diff --git a/.travis.yml b/.travis.yml index 1c23677..2c5185c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,21 +4,11 @@ rust: - nightly - beta - stable -before_script: -- | - pip install 'travis-cargo<0.2' --user && - export PATH=$HOME/.local/bin:$PATH script: - | - travis-cargo build && - travis-cargo test && - rm /tmp/* - travis-cargo test -- --no-default-features --features yaml && - travis-cargo bench && - travis-cargo --only stable doc -after_success: -- travis-cargo --only stable doc-upload + cargo build --features yaml_enc,bin_enc,ron_enc && + cargo test --features yaml_enc,bin_enc,ron_enc && + cargo doc --features yaml_enc,bin_enc,ron_enc env: global: - - TRAVIS_CARGO_NIGHTLY_FEATURE=" " - secure: RFX1Ke3uk8ZdrAGwKPP7FXqQfC6bkGMDzV5ut9s5da6ClvoafosJF4RBvXi5j8KhQP9Kyow7Z8IwtTExNHMIjlE0MguHsYdFBdkj9HdjrqBQHThx5vMOq3VY3bNq9ah83Q6idoTTpQvoZ380BSHznHz8E3rmKGbEA0Z1NigWlT8DFfgqI5nTa64/bvD8IzMTjkLax4pq1SjsG8v+UgwNxkuYE6Yl/mpQJ/xTCGYiFHLKk4EW8z2I5DcK/XqY+w42uFDP2BRvXdzuS937A0lGZtAiK3sKJCBDq/2Ulkz4+onkAo9QT0k9Eea9dSoxXDk3RJHzDb5wLANtubxAWyTKiyTlO3I81ogPmHevSv3xDXXwLBjPxwwIRI1qt/01Nod4yIT4fWfIxcW+rY/W5P4v5QbvAJ079v8be1lnwCEFP55WXfTFEypvKUmNpCTeY9nAKiLywpQuibi/TuHmVH+R9xWlJaXrJ8+fM+dkl57V7AnIKs3nnYmE/c5guUakJ46QVZZtSQRcFfD0QeZPM5WaqAjiZ52eP4y/Mi5Rjg4/noLjXoR3YWYxWZsT5b7U2hv/wEMTzERgHev3aPZttO7gr3ug/FDv0TP6PKPCLd/i+g0w4RKRGArcAXIBWHdeEhfOYuC8Q93gl3d/qutvgGdWF4sdBpaPRcwmaWD+FoEWjWQ= diff --git a/Cargo.toml b/Cargo.toml index 3de90ac..1d4c7e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,29 +1,46 @@ [package] authors = ["Marcel Müller "] -description = "A single file database" -documentation = "http://neikos.me/rustbreak/rustbreak/index.html" +description = "A modular and configurable database" +documentation = "https://docs.rs/rustbreak" homepage = "https://github.com/TheNeikos/rustbreak" -keywords = ["database", "simple", "fast", "daybreak", "rustbreak"] +keywords = ["database", "simple", "fast", "rustbreak"] license = "MPL-2.0" name = "rustbreak" readme = "README.md" repository = "https://github.com/TheNeikos/rustbreak" -version = "1.4.0" +version = "2.0.0-rc2" + +[package.metadata.docs.rs] +all-features = true [dependencies] -bincode = { version = "0.8", optional = true } -serde_yaml = { version = "0.7", optional = true } -ron = { version = "0.1.3", optional = true } -fs2 = "0.4" -quick-error = "1.1.0" +failure = "0.1.1" serde = "1" +[dependencies.ron] +optional = true +version = "0.2.0" + +[dependencies.base64] +optional = true +version = "0.9.0" + +[dependencies.bincode] +optional = true +version = "1" + +[dependencies.serde_yaml] +optional = true +version = "0.7" + [dev-dependencies] -tempfile = "2.1" -lazy_static = "0.2.1" +lazy_static = "1" +serde_derive = "1" +tempfile = "3" [features] -default = ["bin"] -bin = ["bincode"] -yaml = ["serde_yaml"] +default = [] ron_enc = ["ron"] +bin_enc = ["bincode", "base64"] +yaml_enc = ["serde_yaml"] + diff --git a/README.md b/README.md index b603043..85cc2d4 100644 --- a/README.md +++ b/README.md @@ -26,73 +26,120 @@ Features - Simple To Use, Fast, Secure - Threadsafe -- Key/Value Storage -- bincode or yaml storage +- Serde compatible storage (ron, bincode, or yaml included) + +Quickstart +---------- + +Add this to your `Cargo.toml`: + +```toml +[dependencies.rustbreak] +version = "2" +features = ["ron_enc"] # You can also use "yaml_enc" or "bin_enc" + # Check the documentation to add your own! +``` + +```rust +# extern crate failure; +# extern crate rustbreak; +# use std::collections::HashMap; +use rustbreak::{MemoryDatabase, deser::Ron}; + +# fn main() { +# let func = || -> Result<(), failure::Error> { +let db = MemoryDatabase::, Ron>::memory(HashMap::new())?; + +println!("Writing to Database"); +db.write(|db| { + db.insert(0, String::from("world")); + db.insert(1, String::from("bar")); +}); + +db.read(|db| { + // db.insert("foo".into(), String::from("bar")); + // The above line will not compile since we are only reading + println!("Hello: {:?}", db.get(&0)); +})?; +# return Ok(()); }; +# func().unwrap(); +# } +``` Usage ----- Usage is quite simple: -- Create/open a database using `Database::open` - - You can specify the kind of Key you want using this Syntax: - `Database::::open` -- `Insert`/`Retrieve` data from the Database -- Don't forget to run `flush` periodically +- Create/open a database using one of the Database constructors: + - Create a `FileDatabase` with `FileDatabase::from_path` + - Create a `MemoryDatabase` with `MemoryDatabase::memory` + - Create a `Database` with `Database::from_parts` +- `Write`/`Read` data from the Database +- Don't forget to run `save` periodically, or whenever it makes sense. + - You can save in parallel to using the Database. However you will lock + write acess while it is being written to storage. ```rust -use rustbreak::Database; +# use std::collections::HashMap; +use rustbreak::{MemoryDatabase, deser::Ron}; -fn main() { - let db = Database::open("my_contacts").unwrap(); +let db = MemoryDatabase::, Ron>::memory(HashMap::new(), Ron); - db.insert("Lapfox", "lapfoxtrax.com").unwrap(); - db.insert("Rust", "github.com/rust-lang/rust").unwrap(); +println!("Writing to Database"); +db.write(|db| { + db.insert("hello".into(), String::from("world")); + db.insert("foo".into(), String::from("bar")); +}); - // we need to be explicit about the kind of type we want as println! is - // generic - let rust : String = db.retrieve("Rust").unwrap(); - println!("You can find Rust at: {}", rust); - db.flush().unwrap(); -} +db.read(|db| { + // db.insert("foo".into(), String::from("bar")); + // The above line will not compile since we are only reading + println!("Hello: {:?}", db.get("hello")); +}); ``` +## Encodings + +The following parts explain how to enable the respective features. You can also +enable several at the same time. + ### Yaml -If you would like to use yaml instead of bincode to perhaps read or modify the -database in an editor you can use it like this: - -- Disable default features -- Specify yaml as a feature +If you would like to use yaml you need to specify `yaml_enc` as a feature: ```toml [dependencies.rustbreak] version = "1" -default-features = false -features = ["yaml"] +features = ["yaml_enc"] ``` +You can now use `rustbreak::deser::Yaml` as deserialization struct. + ### Ron -If you would like to use [`ron`](https://github.com/ron-rs/ron) instead of bincode: - -- Disable default features -- Specify ron_enc as a feature +If you would like to use [`ron`](https://github.com/ron-rs/ron) you need to +specify `ron_enc` as a feature: ```toml [dependencies.rustbreak] version = "1" -default-features = false features = ["ron_enc"] ``` -How it works ------------- +You can now use `rustbreak::deser::Ron` as deserialization struct. -Internally the Database holds a Hashmap behind a RwLock. -This Hashmap is then written to/read from and safely casted to the requested -type. This works thanks to encoding/decoding traits. +### Bincode +If you would like to use bincode you need to specify `bin_enc` as a feature: + +```toml +[dependencies.rustbreak] +version = "1" +features = ["bin_enc"] +``` + +You can now use `rustbreak::deser::Bincode` as deserialization struct. [doc]:http://neikos.me/rustbreak/rustbreak/index.html diff --git a/examples/config.rs b/examples/config.rs new file mode 100644 index 0000000..10a3e43 --- /dev/null +++ b/examples/config.rs @@ -0,0 +1,56 @@ +// This just reads an example configuration. +// If it doesn't find one, it uses your default configuration +// +// You can create one by writing this file to `/tmp/config.ron`: +// ``` +// --- +// user_path: /tmp/nope +// allow_overwrite: true +// ``` +// + +extern crate rustbreak; +#[macro_use] extern crate serde_derive; +#[macro_use] extern crate lazy_static; + +use std::path::PathBuf; +use std::default::Default; +use rustbreak::FileDatabase; +use rustbreak::deser::Ron; + +type DB = FileDatabase; + +lazy_static! { + static ref CONFIG: DB = { + let db = FileDatabase::from_path("/tmp/config.ron", Config::default()) + .expect("Create database from path"); + db.load().expect("Config to load"); + db + }; +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +struct Config { + user_path: PathBuf, + allow_overwrite: bool, +} + +impl Default for Config { + fn default() -> Config { + Config { + user_path: PathBuf::from("/tmp"), + allow_overwrite: false, + } + } +} + +fn main() { + let _conf : Config = CONFIG.read(|conf| { + conf.clone() + }).expect("Reading configuration"); + + let (user_path, allow_overwrite) = + CONFIG.read(|conf| (conf.user_path.clone(), conf.allow_overwrite.clone())).expect("Read config"); + + println!("The current configuration is: {:?} and {}", user_path, allow_overwrite); +} diff --git a/examples/full.rs b/examples/full.rs index 8e0743c..930124f 100644 --- a/examples/full.rs +++ b/examples/full.rs @@ -1,46 +1,57 @@ extern crate rustbreak; -#[macro_use] extern crate lazy_static; +#[macro_use] extern crate serde_derive; +extern crate failure; -use rustbreak::{Database, Result as BreakResult}; +use rustbreak::FileDatabase; +use rustbreak::deser::Ron; -lazy_static! { - static ref DB: Database = { - Database::open("music").unwrap() - }; +#[derive(Eq, PartialEq, Debug, Serialize, Deserialize, Clone)] +enum Country { + Italy, UnitedKingdom } -type Artist = String; -type Album = String; -type AlbumArtist = (Album, Artist); +#[derive(Eq, PartialEq, Debug, Serialize, Deserialize, Clone)] +struct Person { + name: String, + country: Country, +} + +fn do_main() -> Result<(), failure::Error> { + use std::collections::HashMap; + + let db = FileDatabase::, Ron>::from_path("test.ron", HashMap::new())?; + + println!("Writing to Database"); + db.write(|db| { + db.insert("john".into(), Person { + name: String::from("John Andersson"), + country: Country::Italy + }); + db.insert("fred".into(), Person { + name: String::from("Fred Johnson"), + country: Country::UnitedKingdom + }); + println!("Entries: \n{:#?}", db); + })?; + + println!("Syncing Database"); + db.save()?; + + println!("Loading Database"); + db.load()?; + + println!("Reading from Database"); + db.read(|db| { + println!("Results:"); + println!("{:#?}", db); + })?; -fn add_album_to_artist(name: Artist, album: Album) -> BreakResult<()> { - let mut lock = try!(DB.lock()); - let mut map: Vec = lock.retrieve(&name).unwrap_or_else(|_| vec![]); - map.push(album); - try!(lock.insert(&name, map)); Ok(()) } -fn get_albums(name: &str) -> BreakResult> { - DB.retrieve(name) -} - fn main() { - let albums = [ - ("The Queenstons", "What you do EP"), - ("The Queenstons", "Figurehead"), - ("The Queenstons", "Undertones"), - ("System of a Down", "Toxicity II"), - ("System of a Down", "Mezmerize"), - ]; - - for &(artist, album) in albums.iter() { - add_album_to_artist(artist.to_owned(), album.to_owned()).unwrap(); + if let Err(e) = do_main() { + eprintln!("An error has occurred at: \n{}", e.backtrace()); + ::std::process::exit(1); } - - for al in get_albums("The Queenstons").unwrap() { - println!("{}", al); - } - - DB.flush().unwrap(); } diff --git a/examples/migration.rs.incomplete b/examples/migration.rs.incomplete new file mode 100644 index 0000000..d6b0f92 --- /dev/null +++ b/examples/migration.rs.incomplete @@ -0,0 +1,183 @@ +/* This file is a complete work in progress! And is meant to illustrate a possible way of + * migrating databases with Rustbreak. + * + * Currently the major challenge is to statically define the upgrade process. + * + * This is currently done by using the `upgrading_macro`. + * + * The idea is to read the 'version' tag in the file, load it into a simple tag struct and + * read the version from there. This version is then used to dynamically get the corresponding + * struct type. This then gets converted to the latest version. + */ + + +extern crate rustbreak; +#[macro_use] extern crate serde_derive; + +use rustbreak::FileDatabase; +use rustbreak::deser::Ron; + +mod data { + #[derive(Debug, Serialize, Deserialize, Clone, Copy)] + pub enum Version { + First, Second, Third, + } + + #[derive(Debug, Serialize, Deserialize, Clone)] + pub struct Versioning { + pub version: Version + } + + pub mod v1 { + use data::Version; + use std::collections::HashMap; + + #[derive(Debug, Serialize, Deserialize, Clone)] + pub struct Players { + version: Version, + pub players: HashMap, + } + + impl Players { + pub fn new() -> Players { + Players { + version: Version::First, + players: HashMap::new(), + } + } + } + } + + pub mod v2 { + use data::Version; + use std::collections::HashMap; + + #[derive(Debug, Serialize, Deserialize, Clone)] + pub struct Player { + pub name: String, + pub level: i32, + } + + #[derive(Debug, Serialize, Deserialize, Clone)] + pub struct Players { + version: Version, + pub players: HashMap, + } + + impl Players { + pub fn new() -> Players { + Players { + version: Version::Second, + players: HashMap::new(), + } + } + } + } + + pub mod v3 { + use data::Version; + use std::collections::HashMap; + + #[derive(Debug, Serialize, Deserialize, Clone)] + pub struct Player { + pub name: String, + pub score: i64 + } + + #[derive(Debug, Serialize, Deserialize, Clone)] + pub struct Players { + version: Version, + pub players: HashMap, + } + + impl Players { + pub fn new() -> Players { + Players { + version: Version::Third, + players: HashMap::new(), + } + } + } + } + + pub fn convert_v1_v2(input: v1::Players) -> v2::Players { + let mut new = v2::Players::new(); + for (key, player) in input.players { + new.players.insert(key, + v2::Player { + name: player, + level: 1 + }); + } + + new + } + + pub fn convert_v2_v3(input: v2::Players) -> v3::Players { + let mut new = v3::Players::new(); + for (key, player) in input.players { + new.players.insert(key, + v3::Player { + name: player.name, + score: 0 + }); + } + + new + } +} + +macro_rules! migration_rules { + ( $name:ident -> $res:ty { $( $t:ty => $conv:path ),* $(,)* } ) => { + fn $name(input: T) -> Option<$res> { + fn inner(mut input: Box) -> Option<$res> { + println!("We have: {:#?}", input); + $( + { + let mut maybe_in = None; + if let Some(out) = input.downcast_ref::<$t>() { + maybe_in = Some(Box::new($conv(out.clone()))); + } + + if let Some(inp) = maybe_in { + input = inp; + } + } + )* + + if let Ok(out) = input.downcast::<$res>() { + return Some(*out); + } + + None + } + inner(Box::new(input)) + } + } +} + +migration_rules! { + update_database -> data::v3::Players { + data::v1::Players => data::convert_v1_v2, + data::v2::Players => data::convert_v2_v3, + } +} + +fn get_version() -> data::Version { + let db = FileDatabase::::from_path("migration.ron", data::Versioning { version: data::Version::First }).unwrap(); + db.load().unwrap(); + db.read(|ver| ver.version).unwrap() +} + +fn main() { + use data::v3::Players; + + let version = get_version(); + + + // Let's check if there are any updates + let updated_data = update_database(database.get_data(false).unwrap()).unwrap(); + let database = FileDatabase::::from_path("migration.ron", updated_data).unwrap(); + + println!("{:#?}", database); +} diff --git a/examples/server/Cargo.toml b/examples/server/Cargo.toml new file mode 100644 index 0000000..68b9f0a --- /dev/null +++ b/examples/server/Cargo.toml @@ -0,0 +1,19 @@ +[package] +authors = ["Marcel Müller "] +name = "server" +version = "0.1.0" + +[dependencies] +rocket = "0.3.3" +rocket_codegen = "0.3.3" +serde = "1.0.23" +serde_derive = "1.0.23" + +[dependencies.rocket_contrib] +default-features = false +features = ["handlebars_templates"] +version = "0.3.3" + +[dependencies.rustbreak] +path = "../.." +features = ["ron_enc"] diff --git a/examples/server/server.ron b/examples/server/server.ron new file mode 100644 index 0000000..27f0fd0 --- /dev/null +++ b/examples/server/server.ron @@ -0,0 +1,26 @@ +( + pastes: [ + ( + user: "neikos@neikos.email", + body: "Hello :)", + ), + ( + user: "neikos@neikos.email", + body: "This works great!", + ), + ( + user: "neikos@neikos.email", + body: "Hehe", + ), + ( + user: "neikos@neikos.email", + body: "Hello World :)", + ), + ], + users: { + "neikos@neikos.email": ( + username: "neikos@neikos.email", + password: "asdf", + ), + }, +) \ No newline at end of file diff --git a/examples/server/src/main.rs b/examples/server/src/main.rs new file mode 100644 index 0000000..519861a --- /dev/null +++ b/examples/server/src/main.rs @@ -0,0 +1,159 @@ +#![feature(plugin, decl_macro, custom_derive)] +#![plugin(rocket_codegen)] + +extern crate rustbreak; +extern crate rocket; +extern crate rocket_contrib; +#[macro_use] extern crate serde_derive; + +use std::collections::HashMap; + +use rocket::{State, Outcome}; +use rocket::http::{Cookies, Cookie}; +use rocket::request::{self, Request, FromRequest, Form}; +use rocket::response::Redirect; +use rocket_contrib::Template; +use rustbreak::FileDatabase; +use rustbreak::deser::Ron; + +// We create a type alias so that we always associate the same types to it +type DB = FileDatabase; + +#[derive(Debug, Serialize, Deserialize, Clone)] +struct Paste { + user: String, + body: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone, FromForm)] +struct NewPaste { + body: String +} + +#[derive(Debug, Serialize, Deserialize, Clone, FromForm)] +struct User { + username: String, + password: String, +} + +impl <'a, 'r> FromRequest<'a, 'r> for User { + type Error = (); + + fn from_request(request: &'a Request<'r>) -> request::Outcome { + let mut cookies = request.cookies(); + let db = request.guard::>()?; + match cookies.get_private("user_id") { + Some(cookie) => { + let mut outcome = Outcome::Forward(()); + let _ = db.read(|db| { + if db.users.contains_key(cookie.value()) { + outcome = Outcome::Success(db.users.get(cookie.value()).unwrap().clone()); + } + }); + return outcome; + } + None => return Outcome::Forward(()) + } + } +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +struct ServerData { + pastes: Vec, + users: HashMap +} + +#[derive(Debug, Serialize)] +struct TemplateData { + pastes: Vec, + logged_in: bool, + user: String +} + + +// Routing + +#[get("/")] +fn index(db: State, user: Option) -> Template { + let mut data = TemplateData { + logged_in: user.is_some(), + user: user.map(|u| u.username).unwrap_or_else(|| String::new()), + pastes: vec![], + }; + let _ = db.read(|db| { + data.pastes = db.pastes.clone(); + }); + + return Template::render("index", &data); +} + +#[post("/register", data = "")] +fn post_register(db: State, req_user: Form, mut cookies: Cookies) -> Redirect { + let user = req_user.into_inner(); + let _ = db.write(|db| { + if db.users.contains_key(&user.username) { + return; + } + db.users.insert(user.username.clone(), user.clone()); + cookies.add_private( + Cookie::build("user_id", user.username.clone()).http_only(true).finish() + ); + }); + let _ = db.save(); + + Redirect::to("/") +} + +#[post("/login", data = "")] +fn post_login(db: State, req_user: Form, mut cookies: Cookies) -> Redirect { + let user = req_user.into_inner(); + let _ = db.read(|db| { + match db.users.get(&user.username) { + Some(u) => { + if u.password == user.password { + cookies.add_private( + Cookie::build("user_id", user.username.clone()).http_only(true).finish() + ); + } + } + None => () + } + }); + + Redirect::to("/") +} + +#[post("/paste", data = "")] +fn post_paste(db: State, user: User, paste: Form) -> Redirect { + let body : String = paste.into_inner().body.clone(); + let _ = db.write(|db| { + let paste = Paste { + body: body, + user: user.username.clone(), + }; + db.pastes.push(paste); + }); + let _ = db.save(); + + Redirect::to("/") +} + +#[get("/dummy")] +fn get_dummy() -> &'static str { + "Hello World" +} + +fn main() { + let db : DB = FileDatabase::from_path("server.ron", ServerData { + pastes: vec![], + users: HashMap::new(), + }).unwrap(); + let _ = db.load(); + + + rocket::ignite() + .mount("/", routes![index, post_login, post_paste, post_register, get_dummy]) + .attach(Template::fairing()) + .manage(db) + .launch(); +} diff --git a/examples/server/templates/index.hbs b/examples/server/templates/index.hbs new file mode 100644 index 0000000..3f926ad --- /dev/null +++ b/examples/server/templates/index.hbs @@ -0,0 +1,105 @@ + + + + Rustbreak Pastes + + + + + {{#if logged_in }} +
+
+ You are logged in as: {{user}} +
+
+
+ +
+ +
+
+
+
+ +
+
+
+
+ {{else}} +
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ +
+
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ +
+
+
+
+
+ {{/if}} + +
+ +
+

Pastes

+ + {{#each pastes as |p| ~}} +
+
+
+

+ {{p.user}}
+ {{p.body}} +

+

+
+
+ {{/each~}} +
+
+
+
+

+ Made with love in Germany — Created using Rocket, Handlebars and Rustbreak +

+ + Made with Bulma + +
+
+
+ + diff --git a/examples/simple.rs b/examples/simple.rs deleted file mode 100644 index a1e3ee6..0000000 --- a/examples/simple.rs +++ /dev/null @@ -1,15 +0,0 @@ -extern crate rustbreak; - -use rustbreak::Database; - -fn main() { - let db = Database::open("my_contacts").unwrap(); - - db.insert("Lapfox", "lapfoxtrax.com").unwrap(); - db.insert("Rust", "github.com/rust-lang/rust").unwrap(); - - // we need to be explicit about the kind of type we want as println! is generic - let rust : String = db.retrieve("Rust").unwrap(); - println!("You can find Rust at: {}", rust); - db.flush().unwrap(); -} diff --git a/examples/switching.rs b/examples/switching.rs new file mode 100644 index 0000000..0d78275 --- /dev/null +++ b/examples/switching.rs @@ -0,0 +1,54 @@ +extern crate rustbreak; +#[macro_use] extern crate serde_derive; +extern crate failure; + +use rustbreak::{FileDatabase, backend::FileBackend}; +use rustbreak::deser::{Ron, Yaml}; + +#[derive(Eq, PartialEq, Debug, Serialize, Deserialize, Clone)] +enum Country { + Italy, UnitedKingdom +} + +#[derive(Eq, PartialEq, Debug, Serialize, Deserialize, Clone)] +struct Person { + name: String, + country: Country, +} + +fn do_main() -> Result<(), failure::Error> { + use std::collections::HashMap; + + let db = FileDatabase::, Ron>::from_path("test.ron", HashMap::new())?; + + println!("Writing to Database"); + db.write(|db| { + db.insert("john".into(), Person { + name: String::from("John Andersson"), + country: Country::Italy + }); + db.insert("fred".into(), Person { + name: String::from("Fred Johnson"), + country: Country::UnitedKingdom + }); + println!("Entries: \n{:#?}", db); + })?; + + println!("Syncing Database"); + db.save()?; + + // Now lets switch it + + let db = db.with_deser(Yaml).with_backend(FileBackend::open("test.yml")?); + db.save()?; + + Ok(()) +} + +fn main() { + if let Err(e) = do_main() { + eprintln!("An error has occurred at: \n{}", e.backtrace()); + ::std::process::exit(1); + } +} + diff --git a/src/backend.rs b/src/backend.rs new file mode 100644 index 0000000..b475536 --- /dev/null +++ b/src/backend.rs @@ -0,0 +1,121 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +//! The persistence backends of the Database +//! +//! A file is a `Backend` through the `FileBackend`, so is a `Vec` with a `MemoryBackend`. +//! +//! Implementing your own Backend should be straightforward. Check the `Backend` documentation for +//! details. + +use failure::ResultExt; + +use error; + +/// The Backend Trait +/// +/// It should always read and save in full the data that it is passed. This means that a write to +/// the backend followed by a read __must__ return the same dataset. +pub trait Backend { + /// Read the all data from the backend + fn get_data(&mut self) -> error::Result>; + + /// Write the whole slice to the backend + fn put_data(&mut self, data: &[u8]) -> error::Result<()>; +} + +/// A backend using a file +#[derive(Debug)] +pub struct FileBackend(::std::fs::File); + +impl Backend for FileBackend { + fn get_data(&mut self) -> error::Result> { + use ::std::io::{Seek, SeekFrom, Read}; + + let mut buffer = vec![]; + self.0.seek(SeekFrom::Start(0)).context(error::RustbreakErrorKind::Backend)?; + self.0.read_to_end(&mut buffer).context(error::RustbreakErrorKind::Backend)?; + Ok(buffer) + } + + fn put_data(&mut self, data: &[u8]) -> error::Result<()> { + use ::std::io::{Seek, SeekFrom, Write}; + + self.0.seek(SeekFrom::Start(0)).context(error::RustbreakErrorKind::Backend)?; + self.0.set_len(0).context(error::RustbreakErrorKind::Backend)?; + self.0.write_all(data).context(error::RustbreakErrorKind::Backend)?; + self.0.sync_all().context(error::RustbreakErrorKind::Backend)?; + Ok(()) + } +} + +impl FileBackend { + /// Opens a new FileBackend for a given path, will create it if the file doesn't exist. + pub fn open>(path: P) -> error::Result { + use ::std::fs::OpenOptions; + + Ok(FileBackend( + OpenOptions::new().read(true).write(true).create(true).open(path).context(error::RustbreakErrorKind::Backend)?, + )) + } + + /// Use an already open File as the backend + pub fn from_file(file: ::std::fs::File) -> FileBackend { + FileBackend(file) + } + + /// Return the inner File + pub fn into_inner(self) -> ::std::fs::File { + self.0 + } +} + +/// An in memory backend +/// +/// It is backed by a `Vec` +#[derive(Debug)] +pub struct MemoryBackend(Vec); + +impl MemoryBackend { + /// Construct a new Memory Database + pub fn new() -> MemoryBackend { + MemoryBackend(vec![]) + } +} + +impl Backend for MemoryBackend { + fn get_data(&mut self) -> error::Result> { + Ok(self.0.clone()) + } + + fn put_data(&mut self, data: &[u8]) -> error::Result<()> { + self.0 = data.to_owned(); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use tempfile; + use super::{Backend, MemoryBackend, FileBackend}; + + #[test] + fn test_memory_backend() { + let mut backend = MemoryBackend::new(); + let data = [4, 5, 1, 6, 8, 1]; + + backend.put_data(&data).unwrap(); + assert_eq!(backend.get_data().unwrap(), data); + } + + #[test] + fn test_file_backend() { + let file = tempfile::tempfile().unwrap(); + let mut backend = FileBackend::from_file(file); + let data = [4, 5, 1, 6, 8, 1]; + + backend.put_data(&data).unwrap(); + assert_eq!(backend.get_data().unwrap(), data); + } +} diff --git a/src/bincode_enc.rs b/src/bincode_enc.rs deleted file mode 100644 index 3fcd9b0..0000000 --- a/src/bincode_enc.rs +++ /dev/null @@ -1,18 +0,0 @@ -use serde::{Serialize, Deserialize}; -use bincode::{self, serialize as bin_serialize, deserialize as bin_deserialize}; - -pub type Repr = Vec; -pub type SerializeError = bincode::Error; -pub type DeserializeError = (); - -pub fn serialize(value: &T) -> bincode::Result> - where T: Serialize -{ - bin_serialize(value, bincode::Infinite) -} - -pub fn deserialize<'a, T>(bytes: &'a [u8]) -> Result - where T: Deserialize<'a> -{ - bin_deserialize(bytes) -} diff --git a/src/deser.rs b/src/deser.rs new file mode 100644 index 0000000..fcdd319 --- /dev/null +++ b/src/deser.rs @@ -0,0 +1,163 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +use std::io::Read; + +use failure; +use serde::Serialize; +use serde::de::DeserializeOwned; + +#[cfg(feature = "ron_enc")] +pub use self::ron::Ron; + +#[cfg(feature = "yaml_enc")] +pub use self::yaml::Yaml; + +#[cfg(feature = "bin_enc")] +pub use self::bincode::Bincode; + +/// A trait to bundle serializer and deserializer in a simple struct +/// +/// It should preferably be an struct: one that does not have any members. +/// +/// # Example +/// +/// For an imaginary serde compatible encoding scheme 'Frobnar', an example implementation can look +/// like this: +/// +/// ```rust +/// extern crate rustbreak; +/// extern crate serde; +/// #[macro_use] extern crate failure; +/// +/// use std::io::Read; +/// use serde::Serialize; +/// use serde::de::Deserialize; +/// use failure::{Context, Fail, Backtrace}; +/// +/// use rustbreak::error; +/// use rustbreak::deser::DeSerializer; +/// +/// #[derive(Fail, Debug)] +/// #[fail(display = "A FrobnarError ocurred")] +/// struct FrobnarError; +/// +/// fn to_frobnar(input: &T) -> Vec { +/// unimplemented!(); // implementation not specified +/// } +/// +/// fn from_frobnar<'r, T: Deserialize<'r> + 'r, R: Read>(input: &R) -> Result { +/// unimplemented!(); // implementation not specified +/// } +/// +/// #[derive(Debug, Default, Clone)] +/// struct Frobnar; +/// +/// impl DeSerializer for Frobnar +/// where for<'de> T: Deserialize<'de> +/// { +/// fn serialize(&self, val: &T) -> Result, failure::Error> { +/// Ok(to_frobnar(val)) +/// } +/// +/// fn deserialize(&self, s: R) -> Result { +/// Ok(from_frobnar(&s)?) +/// } +/// } +/// +/// fn main() {} +/// ``` +pub trait DeSerializer : ::std::default::Default + Send + Sync + Clone { + /// Serializes a given value to a String + fn serialize(&self, val: &T) -> Result, failure::Error>; + /// Deserializes a String to a value + fn deserialize(&self, s: R) -> Result; +} + + +#[cfg(feature = "ron_enc")] +mod ron { + use std::io::Read; + + use failure; + use serde::Serialize; + use serde::de::DeserializeOwned; + use failure::ResultExt; + + use ron::ser::to_string_pretty as to_ron_string; + use ron::ser::PrettyConfig; + use ron::de::from_reader as from_ron_string; + + use error; + use deser::DeSerializer; + + /// The Struct that allows you to use `ron` the Rusty Object Notation + #[derive(Debug, Default, Clone)] + pub struct Ron; + + impl DeSerializer for Ron { + fn serialize(&self, val: &T) -> Result, failure::Error> { + Ok(to_ron_string(val, PrettyConfig::default()).map(String::into_bytes) + .context(error::RustbreakErrorKind::Serialization)?) + } + fn deserialize(&self, s: R) -> Result { + Ok(from_ron_string(s).context(error::RustbreakErrorKind::Deserialization)?) + } + } +} + +#[cfg(feature = "yaml_enc")] +mod yaml { + use std::io::Read; + + use failure; + use serde_yaml::{to_string as to_yaml_string, from_reader as from_yaml_string}; + use serde::Serialize; + use serde::de::DeserializeOwned; + use failure::ResultExt; + + use error; + use deser::DeSerializer; + + /// The struct that allows you to use yaml + #[derive(Debug, Default, Clone)] + pub struct Yaml; + + impl DeSerializer for Yaml { + fn serialize(&self, val: &T) -> Result, failure::Error> { + Ok(to_yaml_string(val) + .map(String::into_bytes) + .context(error::RustbreakErrorKind::Serialization)?) + } + fn deserialize(&self, s: R) -> Result { + Ok(from_yaml_string(s).context(error::RustbreakErrorKind::Deserialization)?) + } + } +} + +#[cfg(feature = "bin_enc")] +mod bincode { + use std::io::Read; + + use failure; + use bincode::{deserialize_from, serialize}; + use serde::Serialize; + use serde::de::DeserializeOwned; + use failure::ResultExt; + + use error; + use deser::DeSerializer; + + /// The struct that allows you to use bincode + #[derive(Debug, Default, Clone)] + pub struct Bincode; + + impl DeSerializer for Bincode { + fn serialize(&self, val: &T) -> Result, failure::Error> { + Ok(serialize(val).context(error::RustbreakErrorKind::Serialization)?) + } + fn deserialize(&self, s: R) -> Result { + Ok(deserialize_from(s).context(error::RustbreakErrorKind::Deserialization)?) + } + } +} diff --git a/src/error.rs b/src/error.rs index 1f884ca..320f810 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,37 +1,77 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -quick_error! { - /// The Error type exported by BreakError, usually you only need to check against NotFound, - /// however it might be useful sometimes to get other errors. - #[derive(Debug)] - pub enum BreakError { - /// An error returned when doing file operations, this might happen by opening, closing, - /// locking or flushing - Io(err: ::std::io::Error) { - from() - } - /// This error happens if Bincode cannot deserialize a given file. If you get this error - /// check your database is not corrupt. (This includes non-empty files **not** created by - /// RustBreak! - Deserialize(err: ::enc::DeserializeError) { - from() - } - /// This error happens if bincode cannot serialize the given type at runtime - Serialize(err: ::enc::SerializeError) { - from() - } - /// Error when reading a formatted String - Format(err: ::std::string::FromUtf8Error) { - from() - } - /// Poisoned, you can recover from this by running `recover_poison` on the database - Poison {} - /// This simply means your key could not be found in the database - NotFound {} +use std::fmt::{self, Display}; +use failure::{Context, Fail, Backtrace}; + +/// The different kinds of errors that can be returned +#[derive(Copy, Clone, Eq, PartialEq, Debug, Fail)] +pub enum RustbreakErrorKind { + /// A context error when a serialization failed + #[fail(display = "Could not serialize the value")] + Serialization, + /// A context error when a deserialization failed + #[fail(display = "Could not deserialize the value")] + Deserialization, + /// This error is returned if the `Database` is poisoned. See `Database::write` for details + #[fail(display = "The database has been poisoned")] + Poison, + /// An error in the backend happened + #[fail(display = "The backend has encountered an error")] + Backend, + /// If `Database::write_safe` is used and the closure panics, this error is returned + #[fail(display = "The write operation paniced but got caught")] + WritePanic, + /// This variant should never be used. It is meant to keep this enum forward compatible. + #[doc(hidden)] + #[fail(display = "You have found a secret message, please report it to the Rustbreak maintainer")] + __Nonexhaustive, +} + + + +/// The main error type that gets returned for errors that happen while interacting with a +/// `Database`. +#[derive(Debug)] +pub struct RustbreakError { + inner: Context, +} + +impl Fail for RustbreakError { + fn cause(&self) -> Option<&Fail> { + self.inner.cause() + } + + fn backtrace(&self) -> Option<&Backtrace> { + self.inner.backtrace() } } -impl From<::std::sync::PoisonError> for BreakError { - fn from(_: ::std::sync::PoisonError) -> BreakError { - BreakError::Poison +impl Display for RustbreakError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + Display::fmt(&self.inner, f) } } + +impl RustbreakError { + /// Get the kind of this error + pub fn kind(&self) -> RustbreakErrorKind { + *self.inner.get_context() + } +} + +impl From for RustbreakError { + fn from(kind: RustbreakErrorKind) -> RustbreakError { + RustbreakError { inner: Context::new(kind) } + } +} + +impl From> for RustbreakError { + fn from(inner: Context) -> RustbreakError { + RustbreakError { inner: inner } + } +} + +/// A simple type alias for errors +pub type Result = ::std::result::Result; diff --git a/src/lib.rs b/src/lib.rs index fc03d5b..e1a3129 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,538 +21,605 @@ //! # Rustbreak //! -//! Rustbreak is a [Daybreak][daybreak] inspiried single file Database. -//! It uses [bincode][bincode] or yaml to compactly save data. -//! It is thread safe and very fast due to staying in memory until flushed to disk. +//! Rustbreak was a [Daybreak][daybreak] inspired single file Database. +//! It has now since evolved into something else. Please check v1 for a more similar version. //! -//! It can be used for short-lived processes or with long-lived ones: +//! You will find an overview here in the docs, but to give you a more complete tale of how this is +//! used please check the [examples][examples]. //! -//! ```rust -//! use rustbreak::{Database, Result}; +//! At its core, Rustbreak is an attempt at making a configurable general-purpose store Database. +//! It features the possibility of: //! -//! fn get_data(key: &str) -> Result { -//! let db = try!(Database::::open("/tmp/database")); -//! db.retrieve(key) -//! } +//! - Choosing what kind of Data is stored in it +//! - Which kind of Serialization is used for persistence +//! - Which kind of persistence is used +//! +//! This means you can take any struct you can serialize and deserialize and stick it into this +//! Database. It is then encoded with Ron, Yaml, JSON, Bincode, anything really that uses Serde +//! operations! +//! +//! There are two helper type aliases `MemoryDatabase` and `FileDatabase`, each backed by their +//! respective backend. +//! +//! The `MemoryBackend` saves its data into a `Vec`, which is not that useful on its own, but +//! is needed for compatibility with the rest of the Library. +//! +//! The `FileDatabase` is a classical file based database. You give it a path or a file, and it +//! will use it as its storage. You still get to pick what encoding it uses. +//! +//! Using the `with_deser` and `with_backend` one can switch between the representations one needs. +//! Even at runtime! However this is only useful in a few scenarios. +//! +//! If you have any questions feel free to ask at the main [repo][repo]. +//! +//! ## Quickstart +//! +//! Add this to your `Cargo.toml`: +//! +//! ```toml +//! [dependencies.rustbreak] +//! version = "2" +//! features = ["ron_enc"] # You can also use "yaml_enc" or "bin_enc" +//! # Check the documentation to add your own! //! ``` //! //! ```rust -//! # #[macro_use] extern crate lazy_static; +//! # extern crate failure; //! # extern crate rustbreak; -//! use rustbreak::{Database, Result}; +//! # use std::collections::HashMap; +//! use rustbreak::{MemoryDatabase, deser::Ron}; //! -//! lazy_static! { -//! static ref DB: Database = { -//! Database::open("/tmp/more_data").unwrap() -//! }; -//! } +//! # fn main() { +//! # let func = || -> Result<(), failure::Error> { +//! let db = MemoryDatabase::, Ron>::memory(HashMap::new())?; //! -//! fn get_data(key: &str) -> Result { -//! DB.retrieve(key) -//! } +//! println!("Writing to Database"); +//! db.write(|db| { +//! db.insert(0, String::from("world")); +//! db.insert(1, String::from("bar")); +//! }); //! -//! fn set_data(key: &str, d: u64) -> Result<()> { -//! let mut lock = try!(DB.lock()); -//! let old_data : u64 = try!(lock.retrieve(key)); -//! lock.insert(key, d + old_data) -//! } -//! -//! # fn main() {} +//! db.read(|db| { +//! // db.insert("foo".into(), String::from("bar")); +//! // The above line will not compile since we are only reading +//! println!("Hello: {:?}", db.get(&0)); +//! })?; +//! # return Ok(()); }; +//! # func().unwrap(); +//! # } //! ``` //! -//! [daybreak]:https://propublica.github.io/daybreak/ -//! [bincode]:https://github.com/TyOverby/bincode +//! ## Error Handling +//! +//! Handling errors in Rustbreak is straightforward. Every `Result` has as its fail case as +//! `error::RustbreakError`. This means that you can now either continue bubbling up said error +//! case, or handle it yourself. +//! +//! You can simply call its `.kind()` method, giving you all the information you need to continue. +//! +//! ```rust +//! use rustbreak::{ +//! MemoryDatabase, +//! deser::Ron, +//! error::{ +//! RustbreakError, +//! } +//! }; +//! let db = match MemoryDatabase::::memory(0) { +//! Ok(db) => db, +//! Err(e) => { +//! // Do something with `e` here +//! ::std::process::exit(1); +//! } +//! }; +//! ``` +//! +//! ## Panics +//! +//! This Database implementation uses `RwLock` and `Mutex` under the hood. If either the closures +//! given to `Database::write` or any of the Backend implementation methods panic the respective +//! objects are then poisoned. This means that you *cannot panic* under any circumstances in your +//! closures or custom backends. +//! +//! Currently there is no way to recover from a poisoned `Database` other than re-creating it. +//! +//! ## Examples +//! +//! There are several more or less in-depth example programs you can check out! +//! Check them out here: [Examples][examples] +//! +//! - `config.rs` shows you how a possible configuration file could be managed with rustbreak +//! - `full.rs` shows you how the database can be used as a hashmap store +//! - `switching.rs` show you how you can easily swap out different parts of the Database +//! *Note*: To run this example you need to enable the feature `yaml` like so: +//! `cargo run --example switching --features yaml` +//! - `server/` is a fully fledged example app written with the Rocket framework to make a form of +//! micro-blogging website. You will need rust nightly to start it. +//! +//! ## Features +//! +//! Rustbreak comes with three optional features: +//! +//! - `ron_enc` which enables the [Ron][ron] de/serialization +//! - `yaml_enc` which enables the Yaml de/serialization +//! - `bin_enc` which enables the Bincode de/serialization +//! +//! [Enable them in your `Cargo.toml` file to use them.][features] You can safely have them all +//! turned on per-default. The default feature is `ron` +//! +//! +//! [repo]: https://github.com/TheNeikos/rustbreak +//! [daybreak]: https://propublica.github.io/daybreak +//! [examples]: https://github.com/TheNeikos/rustbreak/tree/master/examples +//! [ron]: https://github.com/ron-rs/ron +//! [failure]: https://boats.gitlab.io/failure/intro.html +//! [features]: https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#choosing-features + extern crate serde; -#[macro_use] extern crate quick_error; -extern crate fs2; -#[cfg(feature = "bin")] extern crate bincode; -#[cfg(feature = "yaml")] extern crate serde_yaml; -#[cfg(feature = "ron_enc")] extern crate ron; -#[cfg(test)] extern crate tempfile; +#[macro_use] extern crate failure; -mod error; -#[cfg(feature = "bin")] mod bincode_enc; -#[cfg(feature = "yaml")] mod yaml_enc; -#[cfg(feature = "ron_enc")] mod ron_enc; +#[cfg(feature = "ron_enc")] +extern crate ron; -mod enc { - #[cfg(feature = "bin")] pub use bincode_enc::*; - #[cfg(feature = "yaml")] pub use yaml_enc::*; - #[cfg(feature = "ron_enc")] pub use ron_enc::*; -} +#[cfg(feature = "yaml_enc")] +extern crate serde_yaml; -use std::collections::HashMap; -use std::fs::File; -use std::path::Path; -use std::sync::{RwLock, RwLockWriteGuard, Mutex}; -use std::hash::Hash; -use std::borrow::Borrow; +#[cfg(feature = "bin_enc")] +extern crate bincode; +#[cfg(feature = "bin_enc")] +extern crate base64; + +#[cfg(test)] +extern crate tempfile; + +/// The rustbreak errors that can be returned +pub mod error; +pub mod backend; +/// Different serialization and deserialization methods one can use +pub mod deser; + +/// The `DeSerializer` trait used by serialization structs +pub use deser::DeSerializer; +/// The general error used by the Rustbreak Module +pub use error::RustbreakError; + +use std::sync::{Mutex, RwLock}; +use std::fmt::Debug; use serde::Serialize; use serde::de::DeserializeOwned; +use failure::ResultExt; -pub use error::BreakError; +use backend::{Backend, MemoryBackend, FileBackend}; -/// Alias for our Result Type -pub type Result = ::std::result::Result; - -/// The Database structure +/// The Central Database to RustBreak /// -/// # Notes -/// One should create this once for each Database instance. -/// Subsequent tries to open the same file should fail or worse, could break the database. +/// It has 3 Type Generics: /// -/// # Example +/// - Data: Is the Data, you must specify this +/// - Back: The storage backend. +/// - DeSer: The Serializer/Deserializer or short DeSer. Check the `deser` module for other +/// strategies. /// -/// ``` -/// use rustbreak::Database; +/// # Panics /// -/// let db = Database::open("/tmp/artists").unwrap(); -/// -/// let albums = vec![ -/// ("What you do", "The Queenstons"), -/// ("Experience", "The Prodigy"), -/// ]; -/// -/// for (album, artist) in albums { -/// db.insert(&format!("album_{}",album), artist).unwrap(); -/// } -/// db.flush().unwrap(); -/// ``` +/// If the backend or the de/serialization panics, the database is poisoned. This means that any +/// subsequent writes/reads will fail with an `error::RustbreakErrorKind::PoisonError`. +/// You can only recover from this by re-creating the Database Object. #[derive(Debug)] -pub struct Database { - file: Mutex, - data: RwLock>, +pub struct Database +{ + data: RwLock, + backend: Mutex, + deser: DeSer } -impl Database { - /// Opens a new Database +impl Database + where + Data: Serialize + DeserializeOwned + Debug + Clone + Send, + Back: Backend + Debug, + DeSer: DeSerializer + Debug + Send + Sync + Clone +{ + /// Write lock the database and get write access to the `Data` container /// - /// This might fail if the file is non-empty and was not created by RustBreak, or if the file - /// is already being used by another RustBreak instance. - /// - /// # Example - /// - /// ``` - /// use rustbreak::Database; - /// - /// let db = Database::open("/tmp/more_artists").unwrap(); - /// - /// let albums = vec![ - /// ("What you do", "The Queenstons"), - /// ("Experience", "The Prodigy"), - /// ]; - /// - /// for (album, artist) in albums { - /// db.insert(&format!("album_{}",album), artist).unwrap(); - /// } - /// db.flush().unwrap(); - /// ``` - pub fn open>(path: P) -> Result> { - use std::fs::OpenOptions; - use fs2::FileExt; - use std::io::Read; - use enc::deserialize; - - let mut file = try!(OpenOptions::new().read(true).write(true).create(true).open(path)); - try!(file.try_lock_exclusive()); - - let mut buf = Vec::new(); - try!(file.read_to_end(&mut buf)); - let map : HashMap = if !buf.is_empty() { - try!(deserialize(&buf)) - } else { - HashMap::new() - }; - - Ok(Database { - file: Mutex::new(file), - data: RwLock::new(map), - }) - } - - /// Insert a given Object into the Database at that key - /// - /// This will overwrite any existing objects. - /// - /// The Object has to be serializable. - pub fn insert(&self, key: &K, obj: S) -> Result<()> - where T: Borrow, K: Hash + PartialEq + ToOwned - { - use enc::serialize; - let mut map = try!(self.data.write()); - map.insert(key.to_owned(), try!(serialize(&obj))); - Ok(()) - } - - /// Remove an Object at that key - pub fn delete(&self, key: &K) -> Result<()> - where T: Borrow, K: Hash + Eq - { - let mut map = try!(self.data.write()); - map.remove(key.to_owned()); - Ok(()) - } - - /// Retrieves an Object from the Database - /// - /// # Errors - /// - /// This will return an `Err(BreakError::NotFound)` if there is no key behind the object. - /// If you tried to request something that can't be serialized to then - /// `Err(BreakError::Deserialize)` will be returned. - /// - /// # Example - /// - /// ``` - /// use rustbreak::{Database, BreakError}; - /// - /// let db = Database::open("/tmp/stuff").unwrap(); - /// - /// for i in 0..5i64 { - /// db.insert(&format!("num_{}", i), i*i*i).unwrap(); - /// } - /// - /// let num : i64 = db.retrieve::("num_0").unwrap(); - /// assert_eq!(num, 0); - /// match db.retrieve::("non-existent") { - /// Err(BreakError::NotFound) => {}, - /// _ => panic!("Was still found?"), - /// } - /// - /// match db.retrieve::, str>("num_1") { - /// Err(_) => {}, - /// _ => panic!("Was deserialized?"), - /// } - /// ``` - pub fn retrieve(&self, key: &K) -> Result - where T: Borrow, K: Hash + Eq - { - use enc::deserialize; - let map = try!(self.data.read()); - match map.get(key.borrow()) { - Some(t) => Ok(try!(deserialize(t))), - None => Err(BreakError::NotFound), - } - } - - /// Checks wether a given key exists in the Database - pub fn contains_key(&self, key: &K) -> Result - where T: Borrow, K: Hash + Eq - { - let map = try!(self.data.read()); - Ok(map.get(key.borrow()).is_some()) - } - - /// Flushes the Database to disk - pub fn flush(&self) -> Result<()> { - use enc::serialize; - use std::io::{Write, Seek, SeekFrom}; - - let map = try!(self.data.read()); - - let mut file = try!(self.file.lock()); - - let buf = try!(serialize(&*map)); - try!(file.set_len(0)); - try!(file.seek(SeekFrom::Start(0))); - try!(file.write(&buf.as_ref())); - try!(file.sync_all()); - Ok(()) - } - - /// Starts a transaction - /// - /// A transaction passes through reads but caches writes. This means that if changes do happen - /// they are processed at the same time. To run them you have to call `run` on the - /// `Transaction` object. - pub fn transaction(&self) -> Transaction { - Transaction { - lock: &self.data, - data: RwLock::new(HashMap::new()), - } - } - - /// Locks the Database, making sure only the caller can change it - /// - /// This write-locks the Database until the `Lock` has been dropped. + /// This gives you an exclusive lock on the memory object. Trying to open the database in + /// writing will block if it is currently being written to. /// /// # Panics /// - /// If you panic while holding the lock it will get poisoned and subsequent calls to it will - /// fail. You will have to re-open the Database to be able to continue accessing it. - pub fn lock(&self) -> Result> { - let map = try!(self.data.write()); - Ok(Lock { - lock: map, + /// If you panic in the closure, the database is poisoned. This means that any + /// subsequent writes/reads will fail with an `std::sync::PoisonError`. + /// You can only recover from this by re-creating the Database Object. + /// + /// If you do not have full control over the code being written, and cannot incur the cost of + /// having a single operation panicking then use `Database::write_safe`. + /// + /// # Examples + /// + /// ```rust + /// # #[macro_use] extern crate serde_derive; + /// # extern crate rustbreak; + /// # extern crate serde; + /// # extern crate tempfile; + /// # extern crate failure; + /// use rustbreak::{FileDatabase, deser::Ron}; + /// + /// #[derive(Debug, Serialize, Deserialize, Clone)] + /// struct Data { + /// level: u32, + /// } + /// + /// # fn main() { + /// # let func = || -> Result<(), failure::Error> { + /// # let file = tempfile::tempfile()?; + /// let db = FileDatabase::::from_file(file, Data { level: 0 })?; + /// + /// db.write(|db| { + /// db.level = 42; + /// })?; + /// + /// // You can also return from a `.read()`. But don't forget that you cannot return references + /// // into the structure + /// let value = db.read(|db| db.level)?; + /// assert_eq!(42, value); + /// # return Ok(()); + /// # }; + /// # func().unwrap(); + /// # } + /// ``` + pub fn write(&self, task: T) -> error::Result<()> + where T: FnOnce(&mut Data) + { + let mut lock = self.data.write().map_err(|_| error::RustbreakErrorKind::Poison)?; + task(&mut lock); + Ok(()) + } + + /// Write lock the database and get write access to the `Data` container in a safe way + /// + /// This gives you an exclusive lock on the memory object. Trying to open the database in + /// writing will block if it is currently being written to. + /// + /// This differs to `Database::write` in that a clone of the internal data is made, which is + /// then passed to the closure. Only if the closure doesn't panic is the internal model + /// updated. + /// + /// Depending on the size of the database this can be very costly. This is a tradeoff to make + /// for panic safety. + /// + /// You should read the documentation about this: + /// [UnwindSafe](https://doc.rust-lang.org/std/panic/trait.UnwindSafe.html) + /// + /// # Panics + /// + /// When the closure panics, it is caught and a `error::RustbreakErrorKind::WritePanic` will be + /// returned. + /// + /// # Examples + /// + /// ```rust + /// # #[macro_use] extern crate serde_derive; + /// # extern crate rustbreak; + /// # extern crate serde; + /// # extern crate tempfile; + /// # extern crate failure; + /// use rustbreak::{ + /// FileDatabase, + /// deser::Ron, + /// error::{ + /// RustbreakError, + /// RustbreakErrorKind, + /// } + /// }; + /// + /// #[derive(Debug, Serialize, Deserialize, Clone)] + /// struct Data { + /// level: u32, + /// } + /// + /// # fn main() { + /// # let func = || -> Result<(), failure::Error> { + /// # let file = tempfile::tempfile()?; + /// let db = FileDatabase::::from_file(file, Data { level: 0 })?; + /// + /// let mut level = 0; + /// + /// let result = db.write_safe(|db| { + /// db.level = 42; + /// panic!("We panic inside the write code."); + /// }).expect_err("This should have been caught"); + /// + /// match result.kind() { + /// RustbreakErrorKind::WritePanic => { + /// // We can now handle this, in this example we will just ignore it + /// } + /// e => { + /// println!("{:#?}", e); + /// // You should always have generic error catching here. + /// // This future-proofs your code, and makes your code more robust. + /// // In this example this is unreachable though, and to assert that we have this + /// // macro here + /// unreachable!(); + /// } + /// } + /// + /// // We read it back out again, it has not changed + /// let value = db.read(|db| db.level)?; + /// assert_eq!(0, value); + /// # return Ok(()); + /// # }; + /// # func().unwrap(); + /// # } + /// ``` + pub fn write_safe(&self, task: T) -> error::Result<()> + where T: FnOnce(&mut Data) + std::panic::UnwindSafe, + { + let mut lock = self.data.write().map_err(|_| error::RustbreakErrorKind::Poison)?; + let mut data = lock.clone(); + ::std::panic::catch_unwind(::std::panic::AssertUnwindSafe(|| { + task(&mut data); + })).map_err(|_| error::RustbreakErrorKind::WritePanic)?; + *lock = data; + Ok(()) + } + + /// Read lock the database and get write access to the `Data` container + /// + /// This gives you a read-only lock on the database. You can have as many readers in parallel + /// as you wish. + /// + /// # Errors + /// + /// May return: + /// + /// - `error::RustbreakErrorKind::Backend` + /// + /// # Panics + /// + /// If you panic in the closure, the database is poisoned. This means that any + /// subsequent writes/reads will fail with an `error::RustbreakErrorKind::Poison`. + /// You can only recover from this by re-creating the Database Object. + pub fn read(&self, task: T) -> error::Result + where T: FnOnce(&Data) -> R + { + let mut lock = self.data.read().map_err(|_| error::RustbreakErrorKind::Poison)?; + Ok(task(&mut lock)) + } + + fn inner_load(backend: &mut Back, deser: &DeSer) -> error::Result { + let new_data = deser.deserialize( + &backend.get_data().context(error::RustbreakErrorKind::Backend)?[..] + ).context(error::RustbreakErrorKind::Deserialization)?; + + Ok(new_data) + } + + /// Load the Data from the backend + pub fn load(&self) -> error::Result<()> { + let mut data = self.data.write().map_err(|_| error::RustbreakErrorKind::Poison)?; + let mut backend = self.backend.lock().map_err(|_| error::RustbreakErrorKind::Poison)?; + + *data = Self::inner_load(&mut backend, &self.deser).context(error::RustbreakErrorKind::Backend)?; + Ok(()) + } + + /// Flush the data structure to the backend + pub fn save(&self) -> error::Result<()> { + let mut backend = self.backend.lock().map_err(|_| error::RustbreakErrorKind::Poison)?; + let data = self.data.write().map_err(|_| error::RustbreakErrorKind::Poison)?; + + let ser = self.deser.serialize(&*data) + .context(error::RustbreakErrorKind::Serialization)?; + + backend.put_data(&ser).context(error::RustbreakErrorKind::Backend)?; + Ok(()) + } + + /// Get a clone of the data as it is in memory right now + /// + /// To make sure you have the latest data, call this method with `load` true + pub fn get_data(&self, load: bool) -> error::Result { + let mut backend = self.backend.lock().map_err(|_| error::RustbreakErrorKind::Poison)?; + let mut data = self.data.write().map_err(|_| error::RustbreakErrorKind::Poison)?; + if load { + *data = Self::inner_load(&mut backend, &self.deser).context(error::RustbreakErrorKind::Backend)?; + drop(backend); + } + Ok(data.clone()) + } + + /// Puts the data as is into memory + /// + /// To save the data afterwards, call with `save` true. + pub fn put_data(&self, new_data: Data, save: bool) -> error::Result<()> { + let mut backend = self.backend.lock().map_err(|_| error::RustbreakErrorKind::Poison)?; + let mut data = self.data.write().map_err(|_| error::RustbreakErrorKind::Poison)?; + if save { + // TODO: Spin this into its own method + let ser = self.deser.serialize(&*data) + .context(error::RustbreakErrorKind::Serialization)?; + + backend.put_data(&ser).context(error::RustbreakErrorKind::Backend)?; + drop(backend); + } + *data = new_data; + Ok(()) + } + + /// Create a database from its constituents + pub fn from_parts(data: Data, backend: Back, deser: DeSer) -> Database { + Database { + data: RwLock::new(data), + backend: Mutex::new(backend), + deser: deser, + } + } + + /// Break a database into its individual parts + pub fn into_inner(self) -> error::Result<(Data, Back, DeSer)> { + Ok((self.data.into_inner().map_err(|_| error::RustbreakErrorKind::Poison)?, + self.backend.into_inner().map_err(|_| error::RustbreakErrorKind::Poison)?, + self.deser)) + } + + /// Tries to clone the Data in the Database. + /// + /// This method returns a `MemoryDatabase` which has an empty vector as a + /// backend initially. This means that the user is responsible for assigning a new backend + /// if an alternative is wanted. + /// + /// # Examples + /// + /// ```rust + /// # #[macro_use] extern crate serde_derive; + /// # extern crate rustbreak; + /// # extern crate serde; + /// # extern crate tempfile; + /// # extern crate failure; + /// use rustbreak::{FileDatabase, deser::Ron}; + /// + /// #[derive(Debug, Serialize, Deserialize, Clone)] + /// struct Data { + /// level: u32, + /// } + /// + /// # fn main() { + /// # let func = || -> Result<(), failure::Error> { + /// # let file = tempfile::tempfile()?; + /// let db = FileDatabase::::from_file(file, Data { level: 0 })?; + /// + /// db.write(|db| { + /// db.level = 42; + /// })?; + /// + /// db.save()?; + /// + /// let other_db = db.try_clone()?; + /// + /// // You can also return from a `.read()`. But don't forget that you cannot return references + /// // into the structure + /// let value = other_db.read(|db| db.level)?; + /// assert_eq!(42, value); + /// # return Ok(()); + /// # }; + /// # func().unwrap(); + /// # } + /// ``` + pub fn try_clone(&self) -> error::Result> { + let lock = self.data.write().map_err(|_| error::RustbreakErrorKind::Poison)?; + + Ok(Database { + data: RwLock::new(lock.clone()), + backend: Mutex::new(MemoryBackend::new()), + deser: self.deser.clone(), }) } } -/// Structure representing a lock of the Database -pub struct Lock<'a, T: Serialize + DeserializeOwned + Eq + Hash + 'a> { - lock: RwLockWriteGuard<'a, HashMap>, -} +/// A database backed by a file +pub type FileDatabase = Database; -impl<'a, T: Serialize + DeserializeOwned + Eq + Hash + 'a> Lock<'a, T> { - /// Insert a given Object into the Database at that key - /// - /// See `Database::insert` for details - pub fn insert(&mut self, key: &K, obj: S) -> Result<()> - where T: Borrow, K: Hash + PartialEq + ToOwned +impl Database + where + Data: Serialize + DeserializeOwned + Debug + Clone + Send, + DeSer: DeSerializer + Debug + Send + Sync + Clone +{ + /// Create new FileDatabase from Path + pub fn from_path(path: S, data: Data) + -> error::Result> + where S: AsRef { - use enc::serialize; - self.lock.insert(key.to_owned(), try!(serialize(&obj))); - Ok(()) + let backend = FileBackend::open(path).context(error::RustbreakErrorKind::Backend)?; + + Ok(Database { + data: RwLock::new(data), + backend: Mutex::new(backend), + deser: DeSer::default(), + }) } - /// Retrieves an Object from the Database - /// - /// See `Database::retrieve` for details - pub fn retrieve(&mut self, key: &K) -> Result - where T: Borrow, K: Hash + Eq + /// Create new FileDatabase from a file + pub fn from_file(file: ::std::fs::File, data: Data) -> error::Result> { - use enc::deserialize; - match self.lock.get(key.borrow()) { - Some(t) => Ok(try!(deserialize(t))), - None => Err(BreakError::NotFound), - } - } + let backend = FileBackend::from_file(file); - /// Starts a transaction - /// - /// See `Database::transaction` for details - pub fn transaction<'b>(&'b mut self) -> TransactionLock<'a, 'b, T> { - TransactionLock { - lock: self, - data: RwLock::new(HashMap::new()), - } + Ok(Database { + data: RwLock::new(data), + backend: Mutex::new(backend), + deser: DeSer::default(), + }) } - } -/// A `TransactionLock` that is atomic in writes and defensive -/// -/// You generate this by calling `transaction` on a `Lock` -/// The transactionlock does not get automatically applied when it is dropped, you have to `run` it. -/// This allows for defensive programming where the values are only applied once it is `run`. -pub struct TransactionLock<'a: 'b, 'b, T: Serialize + DeserializeOwned + Eq + Hash + 'a> { - lock: &'b mut Lock<'a, T>, - data: RwLock>, +/// A database backed by a `Vec` +pub type MemoryDatabase = Database; + +impl Database + where + Data: Serialize + DeserializeOwned + Debug + Clone + Send, + DeSer: DeSerializer + Debug + Send + Sync + Clone +{ + /// Create new FileDatabase from Path + pub fn memory(data: Data) -> error::Result> { + let backend = MemoryBackend::new(); + + Ok(Database { + data: RwLock::new(data), + backend: Mutex::new(backend), + deser: DeSer::default(), + }) + } } -impl<'a: 'b, 'b, T: Serialize + DeserializeOwned + Eq + Hash + 'a> TransactionLock<'a, 'b, T> { - /// Insert a given Object into the Database at that key - /// - /// See `Database::insert` for details - pub fn insert(&mut self, key: &K, obj: S) -> Result<()> - where T: Borrow, K: Hash + PartialEq + ToOwned +impl Database { + /// Exchanges the DeSerialization strategy with the new one + pub fn with_deser(self, deser: T) -> Database { - use enc::serialize; - - let mut map = try!(self.data.write()); - - map.insert(key.to_owned(), try!(serialize(&obj))); - - Ok(()) + Database { + backend: self.backend, + data: self.data, + deser: deser, + } } +} - /// Retrieves an Object from the Database +impl Database { + /// Exchanges the Backend with the new one /// - /// See `Database::retrieve` for details - pub fn retrieve(&mut self, key: &K) -> Result - where T: Borrow, K: Hash + Eq + /// The new backend does not necessarily have the latest data saved to it, so a `.save` should + /// be called to make sure that it is saved. + pub fn with_backend(self, backend: T) -> Database { - use enc::deserialize; - let other_map = &mut self.lock.lock; - if other_map.contains_key(key) { - match other_map.get(key.borrow()) { - Some(t) => Ok(try!(deserialize(t))), - None => Err(BreakError::NotFound), - } - } else { - let map = try!(self.data.read()); - match map.get(key.borrow()) { - Some(t) => Ok(try!(deserialize(t))), - None => Err(BreakError::NotFound), - } + Database { + backend: Mutex::new(backend), + data: self.data, + deser: self.deser, } } - - /// Consumes the TransactionLock and runs it - pub fn run(self) -> Result<()> { - let other_map = &mut self.lock.lock; - - let mut map = try!(self.data.write()); - - for (k, v) in map.drain() { - other_map.insert(k, v); - } - - Ok(()) - } } -/// A Transaction that is atomic in writes -/// -/// You generate this by calling `transaction` on a `Database` -/// The transaction does not get automatically applied when it is dropped, you have to `run` it. -/// This allows for defensive programming where the values are only applied once it is `run`. -pub struct Transaction<'a, T: Serialize + DeserializeOwned + Eq + Hash + 'a> { - lock: &'a RwLock>, - data: RwLock>, -} - -impl<'a, T: Serialize + DeserializeOwned + Eq + Hash + 'a> Transaction<'a, T> { - /// Insert a given Object into the Database at that key +impl Database + where + Data: Serialize + DeserializeOwned + Debug + Clone + Send, + Back: Backend + Debug, + DeSer: DeSerializer + Debug + Send + Sync + Clone +{ + /// Converts from one data type to another /// - /// See `Database::insert` for details - pub fn insert(&mut self, key: &K, obj: S) -> Result<()> - where T: Borrow, K: Hash + PartialEq + ToOwned + /// This method is useful to migrate from one datatype to another + pub fn convert_data(self, convert: C) + -> error::Result> + where + OutputData: Serialize + DeserializeOwned + Debug + Clone + Send, + C: FnOnce(Data) -> OutputData, + DeSer: DeSerializer + Debug + Send + Sync, { - use enc::serialize; - - let mut map = try!(self.data.write()); - - map.insert(key.to_owned(), try!(serialize(&obj))); - - Ok(()) - } - - /// Retrieves an Object from the Database - /// - /// See `Database::retrieve` for details - pub fn retrieve(&self, key: &K) -> Result - where T: Borrow, K: Hash + Eq - { - use enc::deserialize; - let other_map = try!(self.lock.read()); - if other_map.contains_key(key) { - match other_map.get(key.borrow()) { - Some(t) => Ok(try!(deserialize(t))), - None => Err(BreakError::NotFound), - } - } else { - let map = try!(self.data.read()); - match map.get(key.borrow()) { - Some(t) => Ok(try!(deserialize(t))), - None => Err(BreakError::NotFound), - } - } - } - - /// Consumes the Transaction and runs it - pub fn run(self) -> Result<()> { - let mut other_map = try!(self.lock.write()); - - let mut map = try!(self.data.write()); - - for (k, v) in map.drain() { - other_map.insert(k, v); - } - - Ok(()) - } -} - -#[cfg(test)] -mod test { - use super::{Database,BreakError}; - use tempfile::NamedTempFile; - - #[test] - fn insert_and_delete() { - let tmpf = NamedTempFile::new().unwrap(); - let db = Database::open(tmpf.path()).unwrap(); - - db.insert("test", "Hello World!").unwrap(); - db.delete("test").unwrap(); - let hello : Result = db.retrieve("test"); - assert!(hello.is_err()) - } - - #[test] - fn simple_insert_and_retrieve() { - let tmpf = NamedTempFile::new().unwrap(); - let db = Database::open(tmpf.path()).unwrap(); - - db.insert("test", "Hello World!").unwrap(); - let hello : String = db.retrieve("test").unwrap(); - assert_eq!(hello, "Hello World!"); - } - - #[test] - fn simple_insert_and_retrieve_borrow() { - let tmpf = NamedTempFile::new().unwrap(); - let db = Database::open(tmpf.path()).unwrap(); - - db.insert("test", &25).unwrap(); - let hello : u32 = db.retrieve("test").unwrap(); - assert_eq!(hello, 25); - } - - #[test] - fn test_persistence() { - let tmpf = NamedTempFile::new().unwrap(); - let db = Database::open(tmpf.path()).unwrap(); - db.insert("test", "Hello World!").unwrap(); - db.flush().unwrap(); - drop(db); - let db : Database = Database::open(tmpf.path()).unwrap(); - let hello : String = db.retrieve("test").unwrap(); - assert_eq!(hello, "Hello World!"); - } - - #[test] - fn simple_transaction() { - let tmpf = NamedTempFile::new().unwrap(); - let db = Database::open(tmpf.path()).unwrap(); - assert!(db.retrieve::("test").is_err()); - { - let mut trans = db.transaction(); - trans.insert("test", "Hello World!").unwrap(); - trans.run().unwrap(); - } - { - let mut trans = db.transaction(); - trans.insert("test", "Hello World too!!").unwrap(); - drop(trans); - } - let hello : String = db.retrieve("test").unwrap(); - assert_eq!(hello, "Hello World!"); - } - - #[test] - fn multithreaded_locking() { - use std::sync::Arc; - let tmpf = NamedTempFile::new().unwrap(); - let db = Arc::new(Database::open(tmpf.path()).unwrap()); - db.insert("value", 0i64).unwrap(); - let mut threads = vec![]; - for _ in 0..10 { - use std::thread; - let a = db.clone(); - threads.push(thread::spawn(move || { - let mut lock = a.lock().unwrap(); - { - let mut trans = lock.transaction(); - let x = trans.retrieve::("value").unwrap(); - trans.insert("value", x + 1).unwrap(); - trans.run().unwrap(); - } - { - let mut trans = lock.transaction(); - let x = trans.retrieve::("value").unwrap(); - trans.insert("value", x - 1).unwrap(); - drop(trans); - } - })); - } - for thr in threads { - thr.join().unwrap(); - } - let x = db.retrieve::("value").unwrap(); - assert_eq!(x, 10); + let (data, backend, deser) = self.into_inner()?; + Ok(Database { + data: RwLock::new(convert(data)), + backend: Mutex::new(backend), + deser: deser, + }) } } diff --git a/src/ron_enc.rs b/src/ron_enc.rs deleted file mode 100644 index d420fb0..0000000 --- a/src/ron_enc.rs +++ /dev/null @@ -1,21 +0,0 @@ -use serde::Serialize; -use serde::de::DeserializeOwned; -use ron::{ser, de}; - -pub type Repr = String; -pub type SerializeError = ser::Error; -pub type DeserializeError = de::Error; - -pub fn serialize(value: &T) -> ser::Result - where T: Serialize -{ - ser::pretty::to_string(value) -} - -pub fn deserialize(bytes: &I) -> Result - where T: DeserializeOwned, - I: AsRef<[u8]> -{ - let string = try!(String::from_utf8(bytes.as_ref().to_vec())); - Ok(de::from_str(&string)?) -} diff --git a/src/yaml_enc.rs b/src/yaml_enc.rs deleted file mode 100644 index 04122fd..0000000 --- a/src/yaml_enc.rs +++ /dev/null @@ -1,23 +0,0 @@ -use serde::Serialize; -use serde::de::DeserializeOwned; -use serde_yaml::Result as YamlResult; -use serde_yaml::Error; - -pub type Repr = String; -pub type SerializeError = Error; -pub type DeserializeError = (); - -pub fn serialize(value: &T) -> YamlResult - where T: Serialize -{ - ::serde_yaml::to_string(value) -} - -pub fn deserialize>(bytes: &I) -> Result - where T: DeserializeOwned -{ - - let string = try!(String::from_utf8(bytes.as_ref().to_vec())); - let des = try!(::serde_yaml::from_str(&string)); - Ok(des) -}