From fcd1f36e0d7bb387db98cdd92b9af312dc577a8a Mon Sep 17 00:00:00 2001 From: Markus Kohlhase Date: Sun, 3 Jul 2016 02:44:42 +0200 Subject: [PATCH] initial commit --- .gitignore | 3 + Cargo.toml | 13 ++ README.md | 20 +++ src/lib.rs | 510 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 546 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 src/lib.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d4f917d --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +target +Cargo.lock +*.swp diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..a0ddc18 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "jfs" +version = "0.1.0" +authors = ["Markus Kohlhase "] +license = "MIT" +homepage = "https://github.com/flosse/rust-json-file-store" +repository = "https://github.com/flosse/rust-json-file-store" +description = "A JSON file store" +keywords = ["json", "file", "store", "db", "database"] + +[dependencies] +uuid = { version = "0.2", features = ["v4"] } +rustc-serialize = "0.3" diff --git a/README.md b/README.md new file mode 100644 index 0000000..e7a36ea --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +# JSON file store + +A simple JSON file store written in Rust. +This is a port and drop-in replacement of the Node.js library +[json-file-store](https://github.com/flosse/json-file-store/). + +WARNING: +Don't use it if you want to persist a large amount of objects. +Use a real DB instead. + +## Usage + +Add the following to your `Cargo.toml` + + [dependencies] + jfs = "0.1" + +## License + +This project is licensed under the MIT License. diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..53e76a6 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,510 @@ +// Copyright (c) 2016 Markus Kohlhase + +//! A simple JSON file store written in Rust. +//! This is a port and drop-in replacement of the Node.js library +//! [json-file-store](https://github.com/flosse/json-file-store/). +//! +//! **WARNING**: +//! Don't use it if you want to persist a large amount of objects. +//! Use a real DB instead. +//! +//! # Example +//! +//! ``` +//! extern crate jfs; +//! extern crate rustc_serialize; +//! use jfs::Store; +//! +//! #[derive(RustcEncodable,RustcDecodable)] +//! struct Foo { +//! foo: String +//! } +//! +//! pub fn main() { +//! let db = Store::new("data").unwrap(); +//! let f = Foo { foo: "bar".to_owned() }; +//! let id = db.save(&f).unwrap(); +//! let obj = db.get::(&id).unwrap(); +//! db.delete(&id).unwrap(); +//! } +//! ``` + +extern crate uuid; +extern crate rustc_serialize; + +use std::io::prelude::*; +use std::io::{Error, ErrorKind, Result}; +use uuid::Uuid; +use rustc_serialize::{Decodable, Encodable}; +use rustc_serialize::json::{self, Json}; +use std::path::PathBuf; +use std::fs::{read_dir, rename, create_dir_all, remove_file, File, metadata}; +use std::collections::BTreeMap; + +#[derive(Clone,Copy)] +pub struct Config { + pretty: bool, + indent: u32, + single: bool, +} + +impl Default for Config { + fn default() -> Config { + Config { + indent: 2, + pretty: false, + single: false, + } + } +} + +pub struct Store { + path: PathBuf, + cfg: Config, +} + +impl Store { + fn id_to_path(&self, id: &str) -> PathBuf { + if self.cfg.single { + self.path.clone() + } else { + self.path.join(id).with_extension("json") + } + } + + fn path_buf_to_id(&self, p: PathBuf) -> Result { + p.file_stem() + .and_then(|n| n.to_os_string().into_string().ok()) + .ok_or(Error::new(ErrorKind::Other, "invalid id")) + } + + fn save_object_to_file(&self, obj: &T, file_name: &PathBuf) -> Result<()> { + let json_string = if self.cfg.pretty { + json::as_pretty_json(&obj).indent(self.cfg.indent).to_string() + } else { + try!(json::encode(&obj).map_err(|err| Error::new(ErrorKind::InvalidData, err))) + }; + + let tmp_filename = file_name.with_extension("tmp"); + + let mut file = try!(File::create(&tmp_filename)); + + match Write::write_all(&mut file, json_string.as_bytes()) { + Err(err) => Err(err), + Ok(_) => { + try!(rename(tmp_filename, file_name)); + Ok(()) + } + } + } + + fn get_json_from_file(file_name: &PathBuf) -> Result { + let mut f = try!(File::open(file_name)); + let mut buffer = String::new(); + try!(f.read_to_string(&mut buffer)); + json::Json::from_str(&buffer).map_err(|err| Error::new(ErrorKind::Other, err)) + } + + fn get_object_from_json(json: &Json) -> Result<&json::Object> { + json.as_object().ok_or(Error::new(ErrorKind::InvalidData, "invalid file content")) + } + + pub fn new(name: &str) -> Result { + Store::new_with_cfg(name, Config::default()) + } + + pub fn new_with_cfg(name: &str, cfg: Config) -> Result { + + let mut s = Store { + path: name.into(), + cfg: cfg, + }; + + if cfg.single { + s.path = s.path.with_extension("json"); + let o = json::Object::new(); + try!(s.save_object_to_file(&o, &s.path)); + } else { + try!(create_dir_all(&s.path)); + } + Ok(s) + } + + pub fn save(&self, obj: &T) -> Result { + self.save_with_id(obj, &Uuid::new_v4().to_string()) + } + + pub fn save_with_id(&self, obj: &T, id: &str) -> Result { + if self.cfg.single { + + let json = try!(Store::get_json_from_file(&self.path)); + let o = try!(Store::get_object_from_json(&json)); + let mut x = o.clone(); + + // start dirty + let s = try!(json::encode(&obj).map_err(|err| Error::new(ErrorKind::InvalidData, err))); + let j = try!(Json::from_str(&s).map_err(|err| Error::new(ErrorKind::InvalidData, err))); + // end dirty + + x.insert(id.to_string(), j); + try!(self.save_object_to_file(&x, &self.path)); + + } else { + try!(self.save_object_to_file(obj, &self.id_to_path(id))); + } + Ok(id.to_owned()) + } + + pub fn get(&self, id: &str) -> Result { + let json = try!(Store::get_json_from_file(&self.id_to_path(id))); + let o = if self.cfg.single { + let x = try!(json.find(id).ok_or(Error::new(ErrorKind::NotFound, "no such object"))); + x.clone() + } else { + json + }; + + T::decode(&mut json::Decoder::new(o)).map_err(|err| Error::new(ErrorKind::Other, err)) + } + + pub fn get_all(&self) -> Result> { + if self.cfg.single { + let json = try!(Store::get_json_from_file(&self.id_to_path(""))); + let o = try!(Store::get_object_from_json(&json)); + let mut result = BTreeMap::new(); + for x in o.iter() { + let (k, v) = x; + match T::decode(&mut json::Decoder::new(v.clone())) { + Ok(r) => { + result.insert(k.clone(), r); + } + Err(_) => {} + } + } + Ok(result) + } else { + let meta = try!(metadata(&self.path)); + if !meta.is_dir() { + Err(Error::new(ErrorKind::NotFound, "invalid path")) + } else { + let entries = try!(read_dir(&self.path)); + Ok(entries.filter_map(|e| { + e.and_then(|x| { + x.metadata().and_then(|m| { + if m.is_file() { + self.path_buf_to_id(x.path()) + } else { + Err(Error::new(ErrorKind::Other, "not a file")) + } + }) + }) + .ok() + }) + .filter_map(|id| match self.get(&id) { + Ok(x) => Some((id.clone(), x)), + _ => None, + }) + .collect::>()) + } + } + } + + pub fn delete(&self, id: &str) -> Result<()> { + if self.cfg.single { + let json = try!(Store::get_json_from_file(&self.path)); + let o = try!(Store::get_object_from_json(&json)); + let mut x = o.clone(); + if x.contains_key(id) { + x.remove(id); + } else { + return Err(Error::new(ErrorKind::NotFound, "no such object")); + } + self.save_object_to_file(&x, &self.path) + } else { + remove_file(self.id_to_path(id)) + } + } +} + +#[cfg(test)] +mod tests { + use std::io::prelude::*; + use std::fs::{remove_dir_all, File, remove_file}; + use Store; + use Config; + use std::collections::BTreeMap; + use std::io::{Result, ErrorKind}; + use std::path::Path; + + static TEST_DIR_NAME: &'static str = ".specTests"; + static TEST_FILE_NAME: &'static str = ".specTests.json"; + + #[derive(RustcEncodable,RustcDecodable)] + struct X { + x: u32, + } + + #[derive(RustcEncodable,RustcDecodable)] + struct Y { + y: u32, + } + + fn write_to_test_file(content: &str) { + let mut file = File::create(&TEST_FILE_NAME).unwrap(); + Write::write_all(&mut file, content.as_bytes()).unwrap(); + } + + fn read_from_test_file() -> String { + let mut f = File::open(TEST_FILE_NAME).unwrap(); + let mut buffer = String::new(); + f.read_to_string(&mut buffer).unwrap(); + buffer + } + + fn teardown() -> Result<()> { + try!(match remove_file(TEST_FILE_NAME) { + Err(err) => { + match err.kind() { + ErrorKind::NotFound => Ok(()), + _ => Err(err), + } + } + Ok(_) => Ok(()), + }); + match remove_dir_all(TEST_DIR_NAME) { + Err(err) => { + match err.kind() { + ErrorKind::NotFound => Ok(()), + _ => Err(err), + } + } + Ok(_) => Ok(()), + } + } + + #[test] + fn save() { + let db = Store::new(TEST_DIR_NAME).unwrap(); + #[derive(RustcEncodable)] + struct MyData { + x: u32, + }; + let data = MyData { x: 56 }; + let id = db.save(&data).unwrap(); + let mut f = File::open(format!("{}/{}.json", TEST_DIR_NAME, id)).unwrap(); + let mut buffer = String::new(); + f.read_to_string(&mut buffer).unwrap(); + assert_eq!(buffer, "{\"x\":56}"); + assert!(teardown().is_ok()); + } + + #[test] + fn save_empty_obj() { + let db = Store::new(TEST_DIR_NAME).unwrap(); + #[derive(RustcEncodable)] + struct Empty {}; + let id = db.save(&Empty {}).unwrap(); + let mut f = File::open(format!("{}/{}.json", TEST_DIR_NAME, id)).unwrap(); + let mut buffer = String::new(); + f.read_to_string(&mut buffer).unwrap(); + assert_eq!(buffer, "{}"); + assert!(teardown().is_ok()); + } + + #[test] + fn save_with_id() { + let db = Store::new(TEST_DIR_NAME).unwrap(); + #[derive(RustcEncodable)] + struct MyData { + y: i32, + }; + let data = MyData { y: -7 }; + db.save_with_id(&data, "foo").unwrap(); + let mut f = File::open(format!("{}/foo.json", TEST_DIR_NAME)).unwrap(); + let mut buffer = String::new(); + f.read_to_string(&mut buffer).unwrap(); + assert_eq!(buffer, "{\"y\":-7}"); + assert!(teardown().is_ok()); + } + + #[test] + fn pretty_print_file_content() { + let mut cfg = Config::default(); + cfg.pretty = true; + let db = Store::new_with_cfg(TEST_DIR_NAME, cfg).unwrap(); + + #[derive(RustcEncodable)] + struct SubStruct { + c: u32, + }; + + #[derive(RustcEncodable)] + struct MyData { + a: String, + b: SubStruct, + }; + + let data = MyData { + a: "foo".to_string(), + b: SubStruct { c: 33 }, + }; + + let id = db.save(&data).unwrap(); + let mut f = File::open(format!("{}/{}.json", TEST_DIR_NAME, id)).unwrap(); + let mut buffer = String::new(); + f.read_to_string(&mut buffer).unwrap(); + let expected = "{\n \"a\": \"foo\",\n \"b\": {\n \"c\": 33\n }\n}"; + assert_eq!(buffer, expected); + assert!(teardown().is_ok()); + } + + #[test] + fn get() { + let db = Store::new(TEST_DIR_NAME).unwrap(); + #[derive(RustcDecodable)] + struct MyData { + z: f32, + }; + let mut file = File::create(format!("{}/foo.json", TEST_DIR_NAME)).unwrap(); + Write::write_all(&mut file, "{\"z\":9.9}".as_bytes()).unwrap(); + let obj: MyData = db.get("foo").unwrap(); + assert_eq!(obj.z, 9.9); + assert!(teardown().is_ok()); + } + + #[test] + fn get_non_existent() { + let db = Store::new(TEST_DIR_NAME).unwrap(); + let res = db.get::("foobarobject"); + assert!(res.is_err()); + assert_eq!(res.err().unwrap().kind(), ErrorKind::NotFound); + + } + + #[test] + fn get_all() { + let db = Store::new(TEST_DIR_NAME).unwrap(); + #[derive(RustcEncodable,RustcDecodable)] + struct X { + x: u32, + y: u32, + }; + + let mut file = File::create(format!("{}/foo.json", TEST_DIR_NAME)).unwrap(); + Write::write_all(&mut file, "{\"x\":1, \"y\":0}".as_bytes()).unwrap(); + + let mut file = File::create(format!("{}/bar.json", TEST_DIR_NAME)).unwrap(); + Write::write_all(&mut file, "{\"y\":2}".as_bytes()).unwrap(); + + let all_x: BTreeMap = db.get_all().unwrap(); + let all_y: BTreeMap = db.get_all().unwrap(); + assert_eq!(all_x.get("foo").unwrap().x, 1); + assert!(all_x.get("bar").is_none()); + assert_eq!(all_y.get("bar").unwrap().y, 2); + assert!(teardown().is_ok()); + } + + #[test] + fn delete() { + let db = Store::new(TEST_DIR_NAME).unwrap(); + let data = Y { y: 88 }; + let id = db.save(&data).unwrap(); + let f_name = format!("{}/{}.json", TEST_DIR_NAME, id); + db.get::(&id).unwrap(); + assert_eq!(Path::new(&f_name).exists(), true); + db.delete(&id).unwrap(); + assert_eq!(Path::new(&f_name).exists(), false); + assert!(db.get::(&id).is_err()); + assert!(db.delete(&id).is_err()); + assert!(teardown().is_ok()); + } + + #[test] + fn delete_non_existent() { + let db = Store::new(TEST_DIR_NAME).unwrap(); + let res = db.delete("blabla"); + assert!(res.is_err()); + assert_eq!(res.err().unwrap().kind(), ErrorKind::NotFound); + } + + #[test] + fn single_save() { + let mut cfg = Config::default(); + cfg.single = true; + let db = Store::new_with_cfg(TEST_FILE_NAME, cfg).unwrap(); + assert_eq!(read_from_test_file(), "{}"); + let x = X { x: 3 }; + let y = Y { y: 4 }; + db.save_with_id(&x, "x").unwrap(); + db.save_with_id(&y, "y").unwrap(); + assert_eq!(read_from_test_file(), "{\"x\":{\"x\":3},\"y\":{\"y\":4}}"); + assert!(teardown().is_ok()); + } + + #[test] + fn single_save_without_file_name_ext() { + let mut cfg = Config::default(); + cfg.single = true; + Store::new_with_cfg(TEST_DIR_NAME, cfg).unwrap(); + assert!(Path::new(&format!("{}.json", TEST_DIR_NAME)).exists()); + assert!(teardown().is_ok()); + } + + #[test] + fn single_get() { + let mut cfg = Config::default(); + cfg.single = true; + let db = Store::new_with_cfg(TEST_FILE_NAME, cfg).unwrap(); + write_to_test_file("{\"x\":{\"x\":8},\"y\":{\"y\":9}}"); + let y = db.get::("y").unwrap(); + assert_eq!(y.y, 9); + assert!(teardown().is_ok()); + } + + #[test] + fn single_get_non_existent() { + let mut cfg = Config::default(); + cfg.single = true; + let db = Store::new_with_cfg(TEST_FILE_NAME, cfg).unwrap(); + let res = db.get::("foobarobject"); + assert!(res.is_err()); + assert_eq!(res.err().unwrap().kind(), ErrorKind::NotFound); + } + + #[test] + fn single_get_all() { + let mut cfg = Config::default(); + cfg.single = true; + let db = Store::new_with_cfg(TEST_FILE_NAME, cfg).unwrap(); + write_to_test_file("{\"foo\":{\"x\":8},\"bar\":{\"x\":9}}"); + let all: BTreeMap = db.get_all().unwrap(); + assert_eq!(all.get("foo").unwrap().x, 8); + assert_eq!(all.get("bar").unwrap().x, 9); + assert!(teardown().is_ok()); + } + + + #[test] + fn single_delete() { + let mut cfg = Config::default(); + cfg.single = true; + let db = Store::new_with_cfg(TEST_FILE_NAME, cfg).unwrap(); + write_to_test_file("{\"foo\":{\"x\":8},\"bar\":{\"x\":9}}"); + db.delete("bar").unwrap(); + assert_eq!(read_from_test_file(), "{\"foo\":{\"x\":8}}"); + db.delete("foo").unwrap(); + assert_eq!(read_from_test_file(), "{}"); + assert!(teardown().is_ok()); + } + + #[test] + fn single_delete_non_existent() { + let mut cfg = Config::default(); + cfg.single = true; + let db = Store::new_with_cfg(TEST_FILE_NAME, cfg).unwrap(); + let res = db.delete("blabla"); + assert!(res.is_err()); + assert_eq!(res.err().unwrap().kind(), ErrorKind::NotFound); + } + +}