diff --git a/Cargo.lock b/Cargo.lock index cba00081d..d33d9d69f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -544,7 +544,7 @@ dependencies = [ [[package]] name = "atomic-cli" -version = "0.12.1" +version = "0.13.0" dependencies = [ "atomic_lib", "clap", @@ -556,7 +556,7 @@ dependencies = [ [[package]] name = "atomic-server" -version = "0.12.1" +version = "0.13.0" dependencies = [ "acme-lib", "actix-files", @@ -582,7 +582,7 @@ dependencies = [ [[package]] name = "atomic_lib" -version = "0.12.1" +version = "0.13.0" dependencies = [ "bincode", "regex", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 076a700f5..285c07bfc 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "atomic-cli" -version = "0.12.1" +version = "0.13.0" authors = ["Joep Meindertsma "] edition = "2018" license = "MIT" @@ -14,6 +14,6 @@ repository = "https://github.com/joepio/atomic" promptly = "0.3.0" clap = "2.33.1" colored = "1.9.3" -atomic_lib = { version = "0.12.1", path = "../lib", features = ["db", "rdf"] } +atomic_lib = { version = "0.13.0", path = "../lib", features = ["db", "rdf"] } dirs = "3.0.1" regex = "1.3.9" diff --git a/cli/src/delta.rs b/cli/src/delta.rs index 453124c27..a06a64bd1 100644 --- a/cli/src/delta.rs +++ b/cli/src/delta.rs @@ -38,7 +38,7 @@ pub fn subcommand_to_url(context: &Context, subcommand: &str) -> AtomicResult { base_url: String, store: atomic_lib::Store, - mapping: Mapping, + mapping: Mutex, matches: ArgMatches<'a>, config_folder: PathBuf, user_mapping_path: PathBuf, @@ -136,7 +136,7 @@ fn main() -> AtomicResult<()> { let mut context = Context { // TODO: This should be configurable base_url: "http://localhost/".into(), - mapping, + mapping: Mutex::new(mapping), store, matches, config_folder, @@ -179,7 +179,7 @@ fn exec_command(context: &mut Context) -> AtomicResult<()>{ /// List all bookmarks fn list(context: &mut Context) { let mut string = String::new(); - for (shortname, url) in context.mapping.clone().into_iter() { + for (shortname, url) in context.mapping.lock().unwrap().clone().into_iter() { string.push_str(&*format!( "{0: <15}{1: <10} \n", shortname.blue().bold(), @@ -239,7 +239,7 @@ fn populate(context: &mut Context) -> AtomicResult<()> { /// Validates the store fn validate(context: &mut Context) -> AtomicResult<()> { - context.store.validate_store()?; - println!("Store is valid!"); + let reportstring = context.store.validate().to_string(); + println!("{}", reportstring); Ok(()) } diff --git a/cli/src/new.rs b/cli/src/new.rs index 5228cffad..2a8775c9b 100644 --- a/cli/src/new.rs +++ b/cli/src/new.rs @@ -21,7 +21,7 @@ pub fn new(context: &mut Context) -> AtomicResult<()> { .unwrap() .value_of("class") .expect("Add a class value"); - let class_url = context.mapping.try_mapping_or_url(class_input).unwrap(); + let class_url = context.mapping.lock().unwrap().try_mapping_or_url(class_input).unwrap(); let class = context.store.get_class(&class_url)?; println!("Enter a new {}: {}", class.shortname, class.description); let (resource, _bookmark) = prompt_instance(context, &class, None)?; @@ -36,11 +36,11 @@ pub fn new(context: &mut Context) -> AtomicResult<()> { /// Lets the user enter an instance of an Atomic Class through multiple prompts /// Adds the instance to the store, and writes to disk. /// Returns the Resource, its URL and its Bookmark. -fn prompt_instance( - context: &mut Context, +fn prompt_instance<'a>( + context: &'a Context, class: &Class, preffered_shortname: Option, -) -> AtomicResult<(Resource, Option)> { +) -> AtomicResult<(Resource<'a>, Option)> { // Not sure about the best way t // The Path is the thing at the end of the URL, from the domain // Here I set some (kind of) random numbers. @@ -52,7 +52,7 @@ fn prompt_instance( subject = format!("{}/{}-{}", context.base_url, path, preffered_shortname.clone().unwrap()); } - let mut new_resource: Resource = Resource::new(subject.clone()); + let mut new_resource: Resource = Resource::new(subject.clone(), &context.store); new_resource.insert( "https://atomicdata.dev/properties/isA".into(), @@ -64,7 +64,6 @@ fn prompt_instance( new_resource.insert_string( field.subject.clone(), &preffered_shortname.clone().unwrap(), - &mut context.store, )?; println!( "Shortname set to {}", @@ -78,7 +77,7 @@ fn prompt_instance( let mut input = prompt_field(&field, false, context)?; loop { if let Some(i) = input { - new_resource.insert_string(field.subject.clone(), &i, &mut context.store)?; + new_resource.insert_string(field.subject.clone(), &i)?; break; } else { println!("Required field, please enter a value."); @@ -91,20 +90,20 @@ fn prompt_instance( println!("{}: {}", field.shortname.bold().blue(), field.description); let input = prompt_field(&field, true, context)?; if let Some(i) = input { - new_resource.insert_string(field.subject.clone(), &i, &mut context.store)?; + new_resource.insert_string(field.subject.clone(), &i)?; } } println!("{} created with URL: {}", &class.shortname, &subject); - let map = prompt_bookmark(&mut context.mapping, &subject); + let map = prompt_bookmark(&mut context.mapping.lock().unwrap(), &subject); // Add created_instance to store context .store .add_resource_string(new_resource.subject().clone(), &new_resource.to_plain())?; context - .mapping + .mapping.lock().unwrap() .write_mapping_to_disk(&context.user_mapping_path); Ok((new_resource, map)) } @@ -113,7 +112,7 @@ fn prompt_instance( fn prompt_field( property: &Property, optional: bool, - context: &mut Context, + context: &Context, ) -> AtomicResult> { let mut input: Option = None; let msg_appendix; @@ -182,7 +181,7 @@ fn prompt_field( } if let Some(u) = url { // TODO: Check if string or if map - input = context.mapping.try_mapping_or_url(&u); + input = context.mapping.lock().unwrap().try_mapping_or_url(&u); match input { Some(url) => return Ok(Some(url)), None => { @@ -204,7 +203,7 @@ fn prompt_field( let mut urls: Vec = Vec::new(); let length = string_items.clone().count(); for item in string_items.into_iter() { - match context.mapping.try_mapping_or_url(item) { + match context.mapping.lock().unwrap().try_mapping_or_url(item) { Some(url) => { urls.push(url); } diff --git a/cli/src/path.rs b/cli/src/path.rs index 96d25d14b..dfc14ae15 100644 --- a/cli/src/path.rs +++ b/cli/src/path.rs @@ -26,7 +26,7 @@ pub fn get_path(context: &mut Context) -> AtomicResult<()> { }; // Returns a URL or Value - let result = context.store.get_path(path_string, Some(&context.mapping)); + let result = context.store.get_path(path_string, Some(&context.mapping.lock().unwrap())); let store = &mut context.store; match result { Ok(res) => match res { diff --git a/cli/wapm.toml b/cli/wapm.toml index f1820eddc..54b8e13be 100644 --- a/cli/wapm.toml +++ b/cli/wapm.toml @@ -1,6 +1,6 @@ [package] name = "atomic" -version = "0.12.0" +version = "0.13.0" description = "Create, share, fetch and model linked Atomic Data!" license = "MIT" repository = "https://github.com/joepio/atomic" diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 8a1a2da10..f236918c7 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "atomic_lib" -version = "0.12.1" +version = "0.13.0" authors = ["Joep Meindertsma "] edition = "2018" license = "MIT" diff --git a/lib/examples/basic.rs b/lib/examples/basic.rs index 54ec05acc..1e77e6d09 100644 --- a/lib/examples/basic.rs +++ b/lib/examples/basic.rs @@ -4,7 +4,7 @@ fn main() { // Import the `Storelike` trait to get access to most functions use atomic_lib::Storelike; // Start with initializing our store - let mut store = atomic_lib::Store::init(); + let store = atomic_lib::Store::init(); // Load the default Atomic Data Atoms store.populate().unwrap(); // Let's parse this AD3 string. It looks awkward because of the escaped quotes. @@ -21,7 +21,7 @@ fn main() { .unwrap(); assert!(my_value.to_string() == "Test"); // We can also use the shortname of description - let my_value_from_shortname = my_resource.get_shortname("description", &mut store).unwrap(); + let my_value_from_shortname = my_resource.get_shortname("description").unwrap(); assert!(my_value_from_shortname.to_string() == "Test"); // We can find any Atoms matching some value using Triple Pattern Fragments: let found_atoms = store.tpf(None, None, Some("Test")).unwrap(); diff --git a/lib/src/client.rs b/lib/src/client.rs index b92a1c8b1..02b197cd1 100644 --- a/lib/src/client.rs +++ b/lib/src/client.rs @@ -28,8 +28,8 @@ pub fn fetch_resource(subject: &str) -> AtomicResult { } /// Posts a delta to an endpoint -pub fn post_delta(endpoint: &str, delta: Delta) -> AtomicResult<()> { - let resp = ureq::post(&endpoint) +pub fn post_delta(endpoint: &str, _delta: Delta) -> AtomicResult<()> { + let _resp = ureq::post(&endpoint) .set("Accept", crate::parse::AD3_MIME) .timeout_read(500) .call(); @@ -42,5 +42,4 @@ pub fn post_delta(endpoint: &str, delta: Delta) -> AtomicResult<()> { // Another one is to create nested Resources for every deltaline. // I think having JSON compatibility should be top priority. todo!(); - Ok(()) } diff --git a/lib/src/collections.rs b/lib/src/collections.rs index 4e4e22fad..de8e8744b 100644 --- a/lib/src/collections.rs +++ b/lib/src/collections.rs @@ -43,7 +43,7 @@ mod test { #[test] fn create_collection() { - let mut store = crate::Store::init(); + let store = crate::Store::init(); store.populate().unwrap(); let tpf = TPFQuery { subject: None, diff --git a/lib/src/db.rs b/lib/src/db.rs index c4f24c2b1..83987bf92 100644 --- a/lib/src/db.rs +++ b/lib/src/db.rs @@ -51,7 +51,7 @@ impl Db { } impl Storelike for Db { - fn add_atoms(&mut self, atoms: Vec) -> AtomicResult<()> { + fn add_atoms(&self, atoms: Vec) -> AtomicResult<()> { for atom in atoms { self.add_atom(atom)?; } @@ -59,16 +59,12 @@ impl Storelike for Db { Ok(()) } - fn add_resource(&mut self, resource: &Resource) -> AtomicResult<()> { + fn add_resource(&self, resource: &Resource) -> AtomicResult<()> { self.add_resource_string(resource.subject().clone(), &resource.to_plain())?; Ok(()) } - fn add_resource_string( - &mut self, - subject: String, - resource: &ResourceString, - ) -> AtomicResult<()> { + fn add_resource_string(&self, subject: String, resource: &ResourceString) -> AtomicResult<()> { let res_bin = bincode::serialize(resource)?; let sub_bin = bincode::serialize(&subject)?; self.resources.insert(sub_bin, res_bin)?; @@ -76,7 +72,7 @@ impl Storelike for Db { Ok(()) } - fn get_resource_string(&mut self, resource_url: &str) -> AtomicResult { + fn get_resource_string(&self, resource_url: &str) -> AtomicResult { match self .resources // Todo: return some custom error types here @@ -90,46 +86,52 @@ impl Storelike for Db { } None => { if resource_url.starts_with(&self.base_url) { - return Err(format!("Failed to retrieve {}, does not exist locally", resource_url).into()) + return Err(format!( + "Failed to retrieve {}, does not exist locally", + resource_url + ) + .into()); } match self.fetch_resource(resource_url) { - Ok(got) => Ok(got), - Err(e) => { - Err(format!("Failed to retrieve {} from the web: {}", resource_url, e).into()) + Ok(got) => Ok(got), + Err(e) => Err(format!( + "Failed to retrieve {} from the web: {}", + resource_url, e + ) + .into()), } - }}, + } } } - fn all_resources(&self) -> AtomicResult { + fn all_resources(&self) -> ResourceCollection { let mut resources: ResourceCollection = Vec::new(); for item in self.resources.into_iter() { - let (subject_bin, resource_bin) = item?; - let subby: String = bincode::deserialize(&subject_bin)?; - let resource: ResourceString = bincode::deserialize(&resource_bin)?; - resources.push((subby, resource)); + let (subject_bin, resource_bin) = item.unwrap(); + let subject: String = bincode::deserialize(&subject_bin).unwrap(); + let resource: ResourceString = bincode::deserialize(&resource_bin).unwrap(); + resources.push((subject, resource)); } - Ok(resources) + resources } } #[cfg(test)] mod test { use super::*; - use crate::{parse::parse_ad3, Storelike}; // Same as examples/basic.rs #[test] fn basic() { - let string = - String::from("[\"_:test\",\"https://atomicdata.dev/properties/shortname\",\"hi\"]"); - let mut store = Db::init("tmp/db", "localhost".into()).unwrap(); + // Import the `Storelike` trait to get access to most functions + use crate::Storelike; + // Start with initializing our store + let store = Db::init("tmp/db", "localhost".into()).unwrap(); + // Load the default Atomic Data Atoms store.populate().unwrap(); - let atoms = parse_ad3(&string).unwrap(); - store.add_atoms(atoms).unwrap(); // Let's parse this AD3 string. It looks awkward because of the escaped quotes. - let string = "[\"_:test\",\"https://atomicdata.dev/properties/description\",\"Test\"]"; + let string = r#"["_:test","https://atomicdata.dev/properties/description","Test"]"#; // The parser returns a Vector of Atoms let atoms = crate::parse::parse_ad3(&string).unwrap(); // Add the Atoms to the Store @@ -142,7 +144,10 @@ mod test { .unwrap(); assert!(my_value.to_string() == "Test"); // We can also use the shortname of description - let my_value_from_shortname = my_resource.get_shortname("description", &mut store).unwrap(); - assert!(my_value_from_shortname.to_string() == "Test") + let my_value_from_shortname = my_resource.get_shortname("description").unwrap(); + assert!(my_value_from_shortname.to_string() == "Test"); + // We can find any Atoms matching some value using Triple Pattern Fragments: + let found_atoms = store.tpf(None, None, Some("Test")).unwrap(); + assert!(found_atoms.len() == 1); } } diff --git a/lib/src/lib.rs b/lib/src/lib.rs index cfe6be94b..44b6222c3 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -9,7 +9,7 @@ The [Store](struct.Store) contains most of the logic that you need. // Import the `Storelike` trait to get access to most functions use atomic_lib::Storelike; // Start with initializing our store -let mut store = atomic_lib::Store::init(); +let store = atomic_lib::Store::init(); // Load the default Atomic Data Atoms store.populate().unwrap(); // Let's parse this AD3 string. It looks awkward because of the escaped quotes. @@ -26,7 +26,7 @@ let my_value = my_resource .unwrap(); assert!(my_value.to_string() == "Test"); // We can also use the shortname of description -let my_value_from_shortname = my_resource.get_shortname("description", &mut store).unwrap(); +let my_value_from_shortname = my_resource.get_shortname("description").unwrap(); assert!(my_value_from_shortname.to_string() == "Test"); // We can find any Atoms matching some value using Triple Pattern Fragments: let found_atoms = store.tpf(None, None, Some("Test")).unwrap(); @@ -51,6 +51,7 @@ pub mod store; pub mod store_native; pub mod storelike; pub mod urls; +pub mod validate; pub mod values; pub use atoms::Atom; diff --git a/lib/src/resources.rs b/lib/src/resources.rs index 8ef818dc9..991c2772b 100644 --- a/lib/src/resources.rs +++ b/lib/src/resources.rs @@ -7,40 +7,41 @@ use crate::{ storelike::{Class, Property}, Atom, Storelike, }; -use serde::{Deserialize, Serialize}; use std::collections::HashMap; /// A resource is a set of Atoms that shares a single Subject -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Resource { +// #[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Resource<'a> { propvals: PropVals, subject: String, // The isA relationship of the resource // Useful for quick access to shortnames and datatypes // Should be an empty vector if it's checked, should be None if unknown classes: Option>, + store: &'a dyn Storelike, } /// Maps Property URLs to their values type PropVals = HashMap; -impl Resource { +impl <'a> Resource<'a> { /// Create a new, empty Resource. - pub fn new(subject: String) -> Resource { + pub fn new(subject: String, store: &'a dyn Storelike) -> Resource<'a> { let properties: PropVals = HashMap::new(); Resource { propvals: properties, subject, classes: None, + store, } } pub fn new_from_resource_string( subject: String, resource_string: &ResourceString, - store: &mut dyn Storelike, - ) -> AtomicResult { - let mut res = Resource::new(subject); + store: &'a dyn Storelike, + ) -> AtomicResult> { + let mut res = Resource::new(subject, store); for (prop_string, val_string) in resource_string { let propertyfull = store.get_property(prop_string).expect("Prop not found"); let fullvalue = Value::new(val_string, &propertyfull.data_type)?; @@ -62,11 +63,10 @@ impl Resource { pub fn get_shortname( &self, shortname: &str, - store: &mut dyn Storelike, ) -> AtomicResult<&Value> { // If there is a class for (url, _val) in self.propvals.iter() { - if let Ok(prop) = store.get_property(url) { + if let Ok(prop) = self.store.get_property(url) { if prop.shortname == shortname { return Ok(self.get(url)?); } @@ -77,9 +77,9 @@ impl Resource { } /// Checks if the classes are there, if not, fetches them - pub fn get_classes(&mut self, store: &mut dyn Storelike) -> AtomicResult> { + pub fn get_classes(&mut self) -> AtomicResult> { if self.classes.is_none() { - self.classes = Some(store.get_classes_for_subject(self.subject())?); + self.classes = Some(self.store.get_classes_for_subject(self.subject())?); } let classes = self.classes.clone().unwrap(); Ok(classes) @@ -90,9 +90,8 @@ impl Resource { pub fn resolve_shortname( &mut self, shortname: &str, - store: &mut dyn Storelike, ) -> AtomicResult> { - let classes = self.get_classes(store)?; + let classes = self.get_classes()?; // Loop over all Requires and Recommends props for class in classes { for required_prop in class.requires { @@ -116,9 +115,8 @@ impl Resource { &mut self, property_url: String, value: &str, - store: &mut dyn Storelike, ) -> AtomicResult<()> { - let fullprop = &store.get_property(&property_url)?; + let fullprop = &self.store.get_property(&property_url)?; let val = Value::new(value, &fullprop.data_type)?; self.propvals.insert(property_url, val); Ok(()) @@ -138,12 +136,11 @@ impl Resource { &mut self, property: &str, value: &str, - store: &mut dyn Storelike, ) -> AtomicResult<()> { let fullprop = if is_url(property) { - store.get_property(property)? + self.store.get_property(property)? } else { - self.resolve_shortname(property, store)?.unwrap() + self.resolve_shortname(property)?.unwrap() }; let fullval = Value::new(value, &fullprop.data_type)?; self.insert(fullprop.subject, fullval)?; @@ -198,7 +195,7 @@ mod test { fn init_store() -> Store { let string = String::from("[\"_:test\",\"https://atomicdata.dev/properties/shortname\",\"hi\"]"); - let mut store = Store::init(); + let store = Store::init(); store.populate().unwrap(); let atoms = parse_ad3(&string).unwrap(); store.add_atoms(atoms).unwrap(); @@ -207,27 +204,27 @@ mod test { #[test] fn get_and_set_resource_props() { - let mut store = init_store(); + let store = init_store(); let mut resource = store.get_resource(urls::CLASS).unwrap(); assert!( resource - .get_shortname("shortname", &mut store) + .get_shortname("shortname") .unwrap() .to_string() == "class" ); resource - .set_prop("shortname", "something-valid", &mut store) + .set_prop("shortname", "something-valid") .unwrap(); assert!( resource - .get_shortname("shortname", &mut store) + .get_shortname("shortname") .unwrap() .to_string() == "something-valid" ); resource - .set_prop("shortname", "should not contain spaces", &mut store) + .set_prop("shortname", "should not contain spaces") .unwrap_err(); } } diff --git a/lib/src/store.rs b/lib/src/store.rs index 0ea370dff..d7a39552f 100644 --- a/lib/src/store.rs +++ b/lib/src/store.rs @@ -10,13 +10,13 @@ use crate::{ storelike::{ResourceCollection, Storelike}, ResourceString, }; -use std::{collections::HashMap, fs, path::PathBuf}; +use std::{collections::HashMap, fs, path::PathBuf, sync::Arc, sync::Mutex}; /// The in-memory store of data, containing the Resources, Properties and Classes #[derive(Clone)] pub struct Store { // The store currently holds two stores - that is not ideal - hashmap: HashMap, + hashmap: Arc>>, log: mutations::Log, } @@ -27,7 +27,7 @@ impl Store { /// let store = Store::init(); pub fn init() -> Store { Store { - hashmap: HashMap::new(), + hashmap: Arc::new(Mutex::new(HashMap::new())), log: Vec::new(), } } @@ -47,7 +47,7 @@ impl Store { /// Serializes the current store and saves to path pub fn write_store_to_disk(&mut self, path: &PathBuf) -> AtomicResult<()> { let mut file_string: String = String::new(); - for (subject, _) in self.all_resources()? { + for (subject, _) in self.all_resources() { let resourcestring = self.resource_to_ad3(&subject)?; file_string.push_str(&*resourcestring); } @@ -59,16 +59,17 @@ impl Store { } impl Storelike for Store { - fn add_atoms(&mut self, atoms: Vec) -> AtomicResult<()> { + fn add_atoms(&self, atoms: Vec) -> AtomicResult<()> { + let mut hm = self.hashmap.lock().unwrap(); for atom in atoms { - match self.hashmap.get_mut(&atom.subject) { + match hm.get_mut(&atom.subject) { Some(resource) => { resource.insert(atom.property, atom.value); } None => { let mut resource: ResourceString = HashMap::new(); resource.insert(atom.property, atom.value); - self.hashmap.insert(atom.subject, resource); + hm.insert(atom.subject, resource); } } } @@ -76,21 +77,20 @@ impl Storelike for Store { } fn add_resource_string( - &mut self, + &self, subject: String, resource: &ResourceString, ) -> AtomicResult<()> { - self.hashmap.insert(subject, resource.clone()); + self.hashmap.lock().unwrap().insert(subject, resource.clone()); Ok(()) } - fn all_resources(&self) -> AtomicResult { - let res = self.hashmap.clone().into_iter().collect(); - Ok(res) + fn all_resources(&self) -> ResourceCollection { + self.hashmap.lock().unwrap().clone().into_iter().collect() } - fn get_resource_string(&mut self, resource_url: &str) -> AtomicResult { - match self.hashmap.get(resource_url) { + fn get_resource_string(&self, resource_url: &str) -> AtomicResult { + match self.hashmap.lock().unwrap().get(resource_url) { Some(result) => Ok(result.clone()), None => { Ok(self.fetch_resource(resource_url)?) @@ -107,7 +107,7 @@ mod test { fn init_store() -> Store { let string = String::from("[\"_:test\",\"https://atomicdata.dev/properties/shortname\",\"hi\"]"); - let mut store = Store::init(); + let store = Store::init(); store.populate().unwrap(); let atoms = parse_ad3(&string).unwrap(); store.add_atoms(atoms).unwrap(); @@ -116,7 +116,7 @@ mod test { #[test] fn get() { - let mut store = init_store(); + let store = init_store(); let my_resource = store.get_resource_string("_:test").unwrap(); let my_value = my_resource .get("https://atomicdata.dev/properties/shortname") @@ -127,28 +127,28 @@ mod test { #[test] fn validate() { - let mut store = init_store(); - store.validate_store().unwrap(); + let store = init_store(); + assert!(store.validate().is_valid()) } #[test] - #[should_panic] fn validate_invalid() { - let mut store = init_store(); + let store = init_store(); let invalid_ad3 = // 'requires' should be an array, but is a string String::from("[\"_:test\",\"https://atomicdata.dev/properties/requires\",\"Test\"]"); let atoms = parse_ad3(&invalid_ad3).unwrap(); store.add_atoms(atoms).unwrap(); - store.validate_store().unwrap(); + let report = store.validate(); + assert!(!report.is_valid()); } #[test] fn get_full_resource_and_shortname() { - let mut store = init_store(); + let store = init_store(); let resource = store.get_resource(urls::CLASS).unwrap(); let shortname = resource - .get_shortname("shortname", &mut store) + .get_shortname("shortname") .unwrap() .to_string(); assert!(shortname == "class"); @@ -156,7 +156,7 @@ mod test { #[test] fn serialize() { - let mut store = init_store(); + let store = init_store(); store .resource_to_json(&String::from(urls::CLASS), 1, true) .unwrap(); @@ -164,7 +164,7 @@ mod test { #[test] fn tpf() { - let mut store = init_store(); + let store = init_store(); // All atoms let atoms = store .tpf(None, None, None) @@ -196,7 +196,7 @@ mod test { #[test] fn path() { - let mut store = init_store(); + let store = init_store(); let res = store.get_path("https://atomicdata.dev/classes/Class shortname", None).unwrap(); match res { crate::storelike::PathReturn::Subject(_) => { @@ -220,14 +220,14 @@ mod test { #[test] #[should_panic] fn path_fail() { - let mut store = init_store(); + let store = init_store(); store.get_path("https://atomicdata.dev/classes/Class requires isa description", None).unwrap(); } #[test] #[should_panic] fn path_fail2() { - let mut store = init_store(); + let store = init_store(); store.get_path("https://atomicdata.dev/classes/Class requires requires", None).unwrap(); } } diff --git a/lib/src/store_native.rs b/lib/src/store_native.rs index 9fe36f507..e69de29bb 100644 --- a/lib/src/store_native.rs +++ b/lib/src/store_native.rs @@ -1,36 +0,0 @@ -//! Store - this is an in-memory store of Atomic data. -//! This provides many methods for finding, changing, serializing and parsing Atomic Data. -//! Currently, it can only persist its data as .ad3 (Atomic Data Triples) to disk. -//! A more robust persistent storage option will be used later, such as: https://github.com/TheNeikos/rustbreak - -use crate::errors::AtomicResult; -use crate::Resource; -use std::collections::HashMap; - -/// In-memory store of data, containing the Atoms with native, validated Values -#[derive(Clone)] -pub struct StoreNative { - // The store currently holds two stores - that is not ideal - resources: HashMap, -} - -impl StoreNative { - /// Create an empty Store. This is where you start. - /// - /// # Example - /// let store = Store::init(); - pub fn init() -> StoreNative { - StoreNative { - resources: HashMap::new(), - } - } - - pub fn add_resource(&mut self, resource: Resource) -> AtomicResult<()> { - self.resources.insert(resource.subject().clone(), resource); - Ok(()) - } - - pub fn get(&self, resource_url: &str) -> Option<&Resource> { - self.resources.get(resource_url) - } -} diff --git a/lib/src/storelike.rs b/lib/src/storelike.rs index d2cb4a7c6..645136842 100644 --- a/lib/src/storelike.rs +++ b/lib/src/storelike.rs @@ -51,23 +51,23 @@ pub type ResourceCollection = Vec<(String, ResourceString)>; pub trait Storelike { /// Add individual Atoms to the store. /// Will replace existing Atoms that share Subject / Property combination. - fn add_atoms(&mut self, atoms: Vec) -> AtomicResult<()>; + fn add_atoms(&self, atoms: Vec) -> AtomicResult<()>; /// Replaces existing resource with the contents /// Accepts a simple nested string only hashmap /// Adds to hashmap and to the resource store fn add_resource_string( - &mut self, + &self, subject: String, resource: &ResourceString, ) -> AtomicResult<()>; /// Returns a hashmap ResourceString with string Values. /// Fetches the resource if it is not in the store. - fn get_resource_string(&mut self, subject: &str) -> AtomicResult; + fn get_resource_string(&self, subject: &str) -> AtomicResult; /// Adds a Resource to the store - fn add_resource(&mut self, resource: &Resource) -> AtomicResult<()> { + fn add_resource(&self, resource: &Resource) -> AtomicResult<()> { self.add_resource_string(resource.subject().clone(), &resource.to_plain())?; Ok(()) } @@ -75,16 +75,16 @@ pub trait Storelike { /// Fetches a resource, makes sure its subject matches. /// Save to the store. /// Only adds atoms with matching subjects will be added. - fn fetch_resource(&mut self, subject: &str) -> AtomicResult { + fn fetch_resource(&self, subject: &str) -> AtomicResult { let resource: ResourceString = crate::client::fetch_resource(subject)?; self.add_resource_string(subject.into(), &resource)?; Ok(resource) } /// Returns a full Resource with native Values - fn get_resource(&mut self, subject: &str) -> AtomicResult { + fn get_resource(&self, subject: &str) -> AtomicResult where Self: std::marker::Sized { let resource_string = self.get_resource_string(subject)?; - let mut res = Resource::new(subject.into()); + let mut res = Resource::new(subject.into(), self); for (prop_string, val_string) in resource_string { let propertyfull = self.get_property(&prop_string)?; let fullvalue = @@ -98,7 +98,7 @@ pub trait Storelike { } /// Retrieves a Class from the store by subject URL and converts it into a Class useful for forms - fn get_class(&mut self, subject: &str) -> AtomicResult { + fn get_class(&self, subject: &str) -> AtomicResult { // The string representation of the Class let class_strings = self.get_resource_string(subject).expect("Class not found"); let shortname = class_strings @@ -112,7 +112,7 @@ pub trait Storelike { let mut requires: Vec = Vec::new(); let mut recommends: Vec = Vec::new(); - let mut get_properties = |resource_array: &str| -> Vec { + let get_properties = |resource_array: &str| -> Vec { let mut properties: Vec = vec![]; let string_vec: Vec = crate::parse::parse_json_array(&resource_array).unwrap(); for prop_url in string_vec { @@ -139,7 +139,7 @@ pub trait Storelike { /// Finds all classes (isA) for any subject. /// Returns an empty vector if there are none. - fn get_classes_for_subject(&mut self, subject: &str) -> AtomicResult> { + fn get_classes_for_subject(&self, subject: &str) -> AtomicResult> { let resource = self.get_resource_string(subject)?; let classes_array_opt = resource.get(urls::IS_A); let classes_array = match classes_array_opt { @@ -161,7 +161,7 @@ pub trait Storelike { /// Constructs a Collection, which is a paginated list of items with some sorting applied. fn get_collection( - &mut self, + &self, tpf: TPFQuery, sort_by: String, sort_desc: bool, @@ -199,7 +199,7 @@ pub trait Storelike { } /// Fetches a property by URL, returns a Property instance - fn get_property(&mut self, url: &str) -> AtomicResult { + fn get_property(&self, url: &str) -> AtomicResult { let property_resource = self.get_resource_string(url)?; let property = Property { data_type: match_datatype( @@ -224,10 +224,10 @@ pub trait Storelike { /// Returns a collection with all resources in the store. /// WARNING: This could be very expensive! - fn all_resources(&self) -> AtomicResult; + fn all_resources(&self) -> ResourceCollection; /// Adds an atom to the store. Does not do any validations - fn add_atom(&mut self, atom: Atom) -> AtomicResult<()> { + fn add_atom(&self, atom: Atom) -> AtomicResult<()> { match self.get_resource_string(&atom.subject).as_mut() { Ok(resource) => { // Overwrites existing properties @@ -248,7 +248,7 @@ pub trait Storelike { /// Processes a vector of deltas and updates the store. /// Panics if the /// Use this for ALL updates to the store! - fn process_delta(&mut self, delta: Delta) -> AtomicResult<()> { + fn process_delta(&self, delta: Delta) -> AtomicResult<()> { let mut updated_resources = Vec::new(); for deltaline in delta.lines.into_iter() { @@ -275,7 +275,7 @@ pub trait Storelike { /// Finds the URL of a shortname used in the context of a specific Resource. /// The Class, Properties and Shortnames of the Resource are used to find this URL fn property_shortname_to_url( - &mut self, + &self, shortname: &str, resource: &ResourceString, ) -> AtomicResult { @@ -292,7 +292,7 @@ pub trait Storelike { } /// Finds - fn property_url_to_shortname(&mut self, url: &str) -> AtomicResult { + fn property_url_to_shortname(&self, url: &str) -> AtomicResult { let resource = self.get_resource_string(url)?; let property_resource = resource .get(urls::SHORTNAME) @@ -302,7 +302,7 @@ pub trait Storelike { } /// fetches a resource, serializes it to .ad3 - fn resource_to_ad3(&mut self, subject: &str) -> AtomicResult { + fn resource_to_ad3(&self, subject: &str) -> AtomicResult { let mut string = String::new(); let resource = self.get_resource_string(subject)?; @@ -320,7 +320,7 @@ pub trait Storelike { // Todo: // [ ] Resources into objects, if the nesting depth allows it fn resource_to_json( - &mut self, + &self, resource_url: &str, // Not yet used _depth: u8, @@ -438,7 +438,7 @@ pub trait Storelike { // Very costly, slow implementation. // Does not assume any indexing. fn tpf( - &mut self, + &self, q_subject: Option<&str>, q_property: Option<&str>, q_value: Option<&str>, @@ -451,7 +451,7 @@ pub trait Storelike { // Simply return all the atoms if !hassub && !hasprop && !hasval { - for (sub, resource) in self.all_resources()? { + for (sub, resource) in self.all_resources() { for (property, value) in resource { vec.push(Atom::new(sub.clone(), property, value)) } @@ -505,7 +505,7 @@ pub trait Storelike { Err(_) => Ok(vec), }, None => { - for (subj, properties) in self.all_resources()? { + for (subj, properties) in self.all_resources() { find_in_resource(&subj, &properties); } Ok(vec) @@ -518,7 +518,7 @@ pub trait Storelike { /// https://docs.atomicdata.dev/core/paths.html // Todo: return something more useful, give more context. fn get_path( - &mut self, + &self, atomic_path: &str, mapping: Option<&Mapping>, ) -> AtomicResult { @@ -607,55 +607,16 @@ pub trait Storelike { Ok(current) } - /// Checks Atomic Data in the store for validity. - /// Returns an Error if it is not valid. - /// - /// Validates: - /// - /// - [X] If the Values can be parsed using their Datatype (e.g. if Integers are integers) - /// - [X] If all required fields of the class are present - /// - [ ] If the URLs are publicly accessible and return the right type of data - /// - [ ] Returns a report, instead of throws an error - #[allow(dead_code, unreachable_code)] - fn validate_store(&mut self) -> AtomicResult<()> { - for (subject, resource) in self.all_resources()? { - println!("Subject: {:?}", subject); - println!("Resource: {:?}", resource); - - let mut found_props: Vec = Vec::new(); - - for (prop_url, value) in resource { - let property = self.get_property(&prop_url)?; - - Value::new(&value, &property.data_type)?; - found_props.push(prop_url.clone()); - // println!("{:?}: {:?}", prop_url, value); - } - let classes = self.get_classes_for_subject(&subject)?; - for class in classes { - println!("Class: {:?}", class.shortname); - println!("Found: {:?}", found_props); - for required_prop in class.requires { - println!("Required: {:?}", required_prop.shortname); - if !found_props.contains(&required_prop.subject) { - return Err(format!( - "Missing requried property {} in {} because of class {}", - &required_prop.shortname, subject, class.subject, - ) - .into()); - } - } - } - println!("{:?} Valid", subject); - } - Ok(()) - } - /// Loads the default store - fn populate(&mut self) -> AtomicResult<()> { + fn populate(&self) -> AtomicResult<()> { let ad3 = include_str!("../defaults/default_store.ad3"); let atoms = crate::parse::parse_ad3(&String::from(ad3))?; self.add_atoms(atoms)?; Ok(()) } + + /// Performs a light validation, without fetching external data + fn validate(&self) -> crate::validate::ValidationReport where Self: std::marker::Sized { + crate::validate::validate_store(self, false) + } } diff --git a/lib/src/validate.rs b/lib/src/validate.rs new file mode 100644 index 000000000..fe8640221 --- /dev/null +++ b/lib/src/validate.rs @@ -0,0 +1,160 @@ +/// Checks Atomic Data in the store for validity. +/// Returns an Error if it is not valid. +/// +/// Validates: +/// +/// - [X] If the Values can be parsed using their Datatype (e.g. if Integers are integers) +/// - [X] If all required fields of the class are present +/// - [X] If the URLs are publicly accessible +/// - [ ] ..and return the right type of data? +/// - [X] Returns a report, instead of throwing an error +#[allow(dead_code, unreachable_code)] +pub fn validate_store( + store: &dyn crate::Storelike, + fetch_items: bool, +) -> crate::validate::ValidationReport { + type Error = String; + let mut resource_count: u8 = 0; + let mut atom_count: u8 = 0; + let mut unfetchable: Vec<(String, Error)> = Vec::new(); + let mut invalid_value: Vec<(crate::Atom, Error)> = Vec::new(); + let mut unfetchable_props: Vec<(String, Error)> = Vec::new(); + let mut unfetchable_classes: Vec<(String, Error)> = Vec::new(); + // subject, property, class + let mut missing_props: Vec<(String, String, String)> = Vec::new(); + for (subject, resource) in store.all_resources() { + println!("Subject: {:?}", subject); + println!("Resource: {:?}", resource); + resource_count += 1; + + if fetch_items { + match crate::client::fetch_resource(&subject) { + Ok(_) => {}, + Err(e) => unfetchable.push((subject.clone(), e.to_string())), + } + } + + let mut found_props: Vec = Vec::new(); + + for (prop_url, value) in resource { + atom_count += 1; + + let property = match store.get_property(&prop_url) { + Ok(prop) => prop, + Err(e) => { + unfetchable_props.push((prop_url, e.to_string())); + break; + } + }; + + match crate::Value::new(&value, &property.data_type) { + Ok(_) => {} + Err(e) => invalid_value.push(( + crate::Atom::new(subject.clone(), prop_url.clone(), value), + e.to_string(), + )), + }; + found_props.push(prop_url.clone()); + } + let classes = match store.get_classes_for_subject(&subject) { + Ok(classes) => classes, + Err(e) => { + unfetchable_classes.push((subject.clone(), e.to_string())); + break; + } + }; + for class in classes { + println!("Class: {:?}", class.shortname); + println!("Found: {:?}", found_props); + for required_prop in class.requires { + println!("Required: {:?}", required_prop.shortname); + if !found_props.contains(&required_prop.subject) { + missing_props.push(( + subject.clone(), + required_prop.subject.clone(), + class.subject.clone(), + )); + } + } + } + println!("{:?} Valid", subject); + } + crate::validate::ValidationReport { + unfetchable, + unfetchable_classes, + unfetchable_props, + invalid_value, + resource_count, + atom_count, + } +} + +pub struct ValidationReport { + pub resource_count: u8, + pub atom_count: u8, + pub unfetchable: Vec<(String, String)>, + pub invalid_value: Vec<(crate::Atom, String)>, + pub unfetchable_props: Vec<(String, String)>, + pub unfetchable_classes: Vec<(String, String)>, +} + +impl ValidationReport { + pub fn is_valid(&self) -> bool { + self.unfetchable.is_empty() + && self.unfetchable_classes.is_empty() + && self.unfetchable_props.is_empty() + && self.invalid_value.is_empty() + } +} + +impl std::fmt::Display for ValidationReport { + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { + if self.is_valid() { + fmt.write_str("Valid!")?; + return Ok(()); + } + for (subject, error) in &self.unfetchable { + fmt.write_str(&*format!("Cannot fetch Resource {}: {} \n", subject, error))?; + } + for (subject, error) in &self.unfetchable_classes { + fmt.write_str(&*format!("Cannot fetch Class {}: {} \n", subject, error))?; + } + for (subject, error) in &self.unfetchable_props { + fmt.write_str(&*format!("Cannot fetch Property {}: {} \n", subject, error))?; + } + for (atom, error) in &self.invalid_value { + fmt.write_str(&*format!("Invalid value {:?}: {} \n", atom, error))?; + } + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{parse::parse_ad3, Store, Storelike}; + + #[test] + fn validate_populated() { + let store = Store::init(); + store.populate().unwrap(); + let report = store.validate(); + assert!(report.atom_count > 30); + assert!(report.resource_count > 5); + assert!(report.is_valid()); + } + + #[test] + fn invalid_ad3() { + let store = Store::init(); + let ad3 = r#"["https://example.com","https://example.com","[\"https://atomicdata.dev/classes/Class\"]"]"#; + let atoms = parse_ad3(ad3).unwrap(); + store.add_atoms(atoms).unwrap(); + let report = validate_store(&store, false); + println!("resource_count: {}", report.resource_count); + assert!(report.resource_count == 1); + println!("atom_count: {}", report.resource_count); + assert!(report.atom_count == 1); + assert!(!report.is_valid()); + } +} diff --git a/server/Cargo.toml b/server/Cargo.toml index ed5f4622f..c18378d06 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "atomic-server" description = "Create, share and standardize linked atomic data!" -version = "0.12.1" +version = "0.13.0" authors = ["Joep Meindertsma "] edition = "2018" license = "MIT" @@ -11,7 +11,7 @@ repository = "https://github.com/joepio/atomic-cli" [dependencies] # When publishing to cargo this needs to be set, but it needs to be absent when building docker file -atomic_lib = { version = "0.12.1", path = "../lib", features = ["db", "rdf"] } +atomic_lib = { version = "0.13.0", path = "../lib", features = ["db", "rdf"] } serde_json = "1.0.56" serde = { version = "1.0.114", features = ["derive"] } uuid = {version = "0.8.1", features = ["v4"]} diff --git a/server/src/appstate.rs b/server/src/appstate.rs index b88a8c740..044f6647c 100644 --- a/server/src/appstate.rs +++ b/server/src/appstate.rs @@ -19,7 +19,7 @@ pub struct AppState { /// Creates the server context. /// Initializes a store. pub fn init(config: Config) -> BetterResult { - let mut store = atomic_lib::Db::init(&config.store_path, config.local_base_url.clone())?; + let store = atomic_lib::Db::init(&config.store_path, config.local_base_url.clone())?; store.populate()?; let mapping = Mapping::init(); diff --git a/server/src/errors.rs b/server/src/errors.rs index c6c77d467..f77567262 100644 --- a/server/src/errors.rs +++ b/server/src/errors.rs @@ -9,7 +9,7 @@ pub type BetterResult = std::result::Result; pub enum AppErrorType { // NotFoundError, OtherError, - NotImplementedError, + // NotImplementedError, } // More strict error type, supports HTTP responses @@ -33,7 +33,7 @@ impl ResponseError for AppError { match self.error_type { // AppErrorType::NotFoundError => StatusCode::NOT_FOUND, AppErrorType::OtherError => StatusCode::INTERNAL_SERVER_ERROR, - AppErrorType::NotImplementedError => StatusCode::NOT_IMPLEMENTED + // AppErrorType::NotImplementedError => StatusCode::NOT_IMPLEMENTED } } fn error_response(&self) -> HttpResponse {