diff --git a/Cargo.lock b/Cargo.lock index 1378fb9..c5af46c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2714,6 +2714,7 @@ dependencies = [ "itertools", "log", "parking_lot", + "rand 0.8.3", "serde", "serde_json", "thiserror", diff --git a/yopa-web/src/routes/models/object.rs b/yopa-web/src/routes/models/object.rs index 361257b..d80669c 100644 --- a/yopa-web/src/routes/models/object.rs +++ b/yopa-web/src/routes/models/object.rs @@ -41,7 +41,7 @@ pub(crate) struct ObjectModelForm { pub name: String, #[serde(default)] // #[serde(with="serde_with::rust::default_on_error")] // This is because "" can be selected - #[serde(with="my_string_empty_as_none")] + #[serde(with = "my_string_empty_as_none")] pub name_property: Option, } @@ -56,7 +56,7 @@ pub(crate) async fn create( match wg.define_object(ObjectModel { id: Default::default(), name: form.name.clone(), - name_property: form.name_property + name_property: form.name_property, }) { Ok(_id) => { wg.persist().err_to_500()?; @@ -96,10 +96,13 @@ pub(crate) async fn update_form( context.insert("model", &model); } else { context.insert("model", model); - context.insert("old", &ObjectModelForm { - name: model.name.to_string(), - name_property: model.name_property - }); + context.insert( + "old", + &ObjectModelForm { + name: model.name.to_string(), + name_property: model.name_property, + }, + ); } let properties = rg.get_property_models_for_parent(*model_id).collect_vec(); @@ -162,30 +165,29 @@ pub(crate) async fn delete( } } - pub mod my_string_empty_as_none { - use serde::{Deserializer, Serializer, Serialize}; - use std::str::FromStr; - use std::fmt::{Display, Write}; - use std::marker::PhantomData; - use serde::de::{Visitor, Error}; + use serde::de::{Error, Visitor}; + use serde::{Deserializer, Serialize, Serializer}; use std::fmt; + use std::fmt::Display; + use std::marker::PhantomData; + use std::str::FromStr; use yopa::ID; // FIXME largely copied from serde_with /// Deserialize an `Option` from a string using `FromStr` pub fn deserialize<'de, D, S>(deserializer: D) -> Result, D::Error> - where - D: Deserializer<'de>, - S: FromStr, - S::Err: Display, + where + D: Deserializer<'de>, + S: FromStr, + S::Err: Display, { struct OptionStringEmptyNone(PhantomData); impl<'de, S> Visitor<'de> for OptionStringEmptyNone - where - S: FromStr, - S::Err: Display, + where + S: FromStr, + S::Err: Display, { type Value = Option; @@ -194,8 +196,8 @@ pub mod my_string_empty_as_none { } fn visit_str(self, value: &str) -> Result - where - E: Error, + where + E: Error, { match value { "" => Ok(None), @@ -204,8 +206,8 @@ pub mod my_string_empty_as_none { } fn visit_string(self, value: String) -> Result - where - E: Error, + where + E: Error, { match &*value { "" => Ok(None), @@ -215,16 +217,16 @@ pub mod my_string_empty_as_none { // TODO remove? fn visit_u64(self, v: u64) -> Result - where - E: Error, + where + E: Error, { self.visit_str(&v.to_string()) } // handles the `null` case fn visit_unit(self) -> Result - where - E: Error, + where + E: Error, { Ok(None) } @@ -235,8 +237,8 @@ pub mod my_string_empty_as_none { /// Serialize a string from `Option` using `AsRef` or using the empty string if `None`. pub fn serialize(option: &Option, serializer: S) -> Result - where - S: Serializer, + where + S: Serializer, { option.serialize(serializer) diff --git a/yopa-web/src/routes/objects.rs b/yopa-web/src/routes/objects.rs index 1aafeb3..adcb7c6 100644 --- a/yopa-web/src/routes/objects.rs +++ b/yopa-web/src/routes/objects.rs @@ -1,23 +1,24 @@ -use std::borrow::{Borrow, Cow}; +use std::borrow::Cow; use std::collections::HashMap; use actix_session::Session; -use actix_web::{HttpResponse, Responder, web}; +use actix_web::{web, HttpResponse, Responder}; use heck::TitleCase; use itertools::Itertools; use json_dotpath::DotPaths; use serde::Serialize; -use yopa::{data, ID, model, Storage}; -use yopa::data::Object; +use yopa::{data, model, Storage, ID}; + use yopa::insert::InsertObj; use yopa::model::{ObjectModel, PropertyModel, RelationModel}; use yopa::update::UpdateObj; use crate::session_ext::SessionExt; -use crate::TERA; use crate::tera_ext::TeraExt; use crate::utils::{redirect, StorageErrorIntoResponseError}; +use crate::TERA; +use yopa::helpers::GroupByModel; // we only need references here, Context serializes everything to Value. // cloning would be a waste of cycles @@ -97,13 +98,14 @@ fn prepare_object_create_data(rg: &Storage, model_id: ID) -> actix_web::Result { wg.persist().err_to_500()?; debug!("Object created, redirecting to root"); - session.flash_success(format!("{} \"{}\" updated.", model_name, wg.get_object_name_by_id(id))); + session.flash_success(format!( + "{} \"{}\" updated.", + model_name, + wg.get_object_name_by_id(id) + )); Ok(HttpResponse::Ok().finish()) } Err(e) => { diff --git a/yopa/Cargo.toml b/yopa/Cargo.toml index f546121..cc686f5 100644 --- a/yopa/Cargo.toml +++ b/yopa/Cargo.toml @@ -22,6 +22,7 @@ itertools = "0.10.0" #lazy_static = "1.4.0" bincode = "1.3.1" atomic = "0.5.0" +rand = "0.8.3" [features] default = [] diff --git a/yopa/src/cool.rs b/yopa/src/cool.rs index f7af889..ba0f32c 100644 --- a/yopa/src/cool.rs +++ b/yopa/src/cool.rs @@ -40,18 +40,16 @@ impl KVVecToKeysOrValues for Vec<(K, V)> { } } -pub(crate) trait IsNoneOrElse : Sized { +pub(crate) trait IsNoneOrElse: Sized { //noinspection RsSelfConvention - fn is_none_or_else(&self, test : impl FnOnce(&T) -> bool) -> bool; + fn is_none_or_else(&self, test: impl FnOnce(&T) -> bool) -> bool; } impl IsNoneOrElse for Option { fn is_none_or_else(&self, test: impl FnOnce(&T) -> bool) -> bool { match self { None => true, - Some(value) => { - test(value) - } + Some(value) => test(value), } } } diff --git a/yopa/src/data.rs b/yopa/src/data.rs index b9e2cd5..1c57cf4 100644 --- a/yopa/src/data.rs +++ b/yopa/src/data.rs @@ -1,6 +1,6 @@ //! Data value structs -use std::borrow::{Cow, Borrow}; +use std::borrow::{Borrow, Cow}; use serde::{Deserialize, Serialize}; diff --git a/yopa/src/helpers.rs b/yopa/src/helpers.rs new file mode 100644 index 0000000..0289084 --- /dev/null +++ b/yopa/src/helpers.rs @@ -0,0 +1,200 @@ +//! Helper traits and stuff for working with lists and iterators of objects + +// Re-export itertools for convenience +pub use itertools; + +use crate::{data, insert, model, update, ID}; + +use itertools::Itertools; +use std::collections::HashMap; + +pub trait GetParent { + fn get_parent(&self) -> ID; +} + +macro_rules! impl_get_parent { + ($($struct:ty),+) => { + $( + impl GetParent for $struct { + fn get_parent(&self) -> ID { self.object } + } + impl GetParent for &$struct { + fn get_parent(&self) -> ID { self.object } + } + impl GetParent for &&$struct { + fn get_parent(&self) -> ID { self.object } + } + )+ + } +} + +impl_get_parent!( + data::Value, data::Relation, + model::RelationModel, model::PropertyModel); + +pub trait GetModelID { + fn get_model_id(&self) -> ID; +} + +macro_rules! impl_get_model_id { + ($($struct:ty),+) => { + $( + impl GetModelID for $struct { + fn get_model_id(&self) -> ID { self.model } + } + impl GetModelID for &$struct { + fn get_model_id(&self) -> ID { self.model } + } + impl GetModelID for &&$struct { + fn get_model_id(&self) -> ID { self.model } + } + )+ + } +} + +impl_get_model_id!( + data::Object, data::Value, data::Relation, + insert::InsertRel, insert::InsertValue, insert::InsertObj, + update::UpsertRelation, update::UpsertValue); + +// This would be nice, but doesn't work + +// +// pub struct FilterOfParent<'a, S: 'a> { iter: S, parent: ID, marker : std::marker::PhantomData<&'a S> } +// +// // impl<'a, V: GetParent + 'a, S: Iterator> Iterator for FilterOfParent { +// // type Item = &'a V; +// // +// // #[inline] +// // fn next(&mut self) -> Option { +// // let parent = self.parent; +// // self.iter.find(|x| x.get_parent() == parent) +// // } +// // } +// +// impl<'a, V: GetParent + 'a, S: Iterator> Iterator for FilterOfParent<'a, S> { +// type Item = V; +// +// #[inline] +// fn next(&mut self) -> Option { +// let parent = self.parent; +// self.iter.find(|x| x.get_parent() == parent) +// } +// } +// +// pub trait WhereParent<'a>: Sized { +// fn where_parent(self, id: ID) -> FilterOfParent<'a, Self>; +// } +// +// impl<'a, V : 'a, S : 'a + Iterator> WhereParent<'a> for S { +// fn where_parent(self, id: ID) -> FilterOfParent<'a, Self> { +// FilterOfParent { iter: self, parent: id, marker: std::marker::PhantomData } +// } +// } + +pub trait GroupByParent<'a, T: 'a + GetParent>: Sized { + fn group_by_parent(self) -> HashMap>; +} + +impl<'a, T: 'a + GetParent, S: IntoIterator> GroupByParent<'a, T> for S { + #[inline] + fn group_by_parent(self) -> HashMap> { + self.into_iter().into_group_map_by(|v| v.get_parent()) + } +} + +pub trait GroupByModel<'a, T: 'a + GetModelID>: Sized { + fn group_by_model(self) -> HashMap>; +} + +impl<'a, T: 'a + GetModelID, S: IntoIterator> GroupByModel<'a, T> for S { + #[inline] + fn group_by_model(self) -> HashMap> { + self.into_iter().into_group_map_by(|v| v.get_model_id()) + } +} + +#[cfg(test)] +mod tests { + use crate::helpers::{GetParent, GroupByParent}; + use crate::id::random_id; + use crate::ID; + + #[derive(Debug, PartialEq, Eq)] + struct Child(ID, &'static str); + + impl GetParent for Child { + fn get_parent(&self) -> ID { + self.0 + } + } + + #[test] + fn group_vec() { + let parent1 = random_id(); + let parent2 = random_id(); + + let vec = vec![ + Child(parent1, "A"), + Child(parent2, "C"), + Child(parent1, "B"), + Child(parent2, "D"), + ]; + + let mut grouped = vec.group_by_parent(); + + assert_eq!(2, grouped.len()); + + assert_eq!( + grouped.remove(&parent1).unwrap(), + vec![Child(parent1, "A"), Child(parent1, "B"),] + ); + + assert_eq!( + grouped.remove(&parent2).unwrap(), + vec![Child(parent2, "C"), Child(parent2, "D"),] + ); + } + + #[test] + fn group_iter() { + let parent1 = random_id(); + let parent2 = random_id(); + + let vec = vec![ + Child(parent1, "A"), + Child(parent2, "C"), + Child(parent1, "B"), + Child(parent2, "D"), + ]; + + let mut grouped = vec.into_iter().group_by_parent(); + + assert_eq!(2, grouped.len()); + + assert_eq!( + grouped.remove(&parent1).unwrap(), + vec![Child(parent1, "A"), Child(parent1, "B"),] + ); + + assert_eq!( + grouped.remove(&parent2).unwrap(), + vec![Child(parent2, "C"), Child(parent2, "D"),] + ); + } + + // #[test] + // fn count_where_parent() { + // let parent1 = random_id(); + // let parent2 = random_id(); + // + // let vec = vec![ + // Child(parent1, "A"), + // Child(parent2, "C"), + // Child(parent1, "B"), + // Child(parent2, "D"), + // ]; + // + // let _ = vec.iter().where_parent(parent2).next(); + // } +} diff --git a/yopa/src/id.rs b/yopa/src/id.rs index b405727..0a3a2f7 100644 --- a/yopa/src/id.rs +++ b/yopa/src/id.rs @@ -17,3 +17,16 @@ pub type ID = u32; pub trait HaveId { fn get_id(&self) -> ID; } + +#[cfg(feature = "uuid-ids")] +#[cfg(test)] +pub fn random_id() -> ID { + ID::new_v4() +} + +#[cfg(not(feature = "uuid-ids"))] +#[cfg(test)] +pub fn random_id() -> ID { + use rand::Rng; + rand::thread_rng().gen() +} diff --git a/yopa/src/insert.rs b/yopa/src/insert.rs index 5c08bc3..097c85a 100644 --- a/yopa/src/insert.rs +++ b/yopa/src/insert.rs @@ -48,11 +48,7 @@ pub struct InsertObj { } impl InsertObj { - pub fn new( - model_id: ID, - values: Vec, - relations: Vec, - ) -> Self { + pub fn new(model_id: ID, values: Vec, relations: Vec) -> Self { Self { model: model_id, values, diff --git a/yopa/src/lib.rs b/yopa/src/lib.rs index 70abd26..46b9696 100644 --- a/yopa/src/lib.rs +++ b/yopa/src/lib.rs @@ -1,7 +1,8 @@ #[macro_use] extern crate log; -// #[macro_use] -// extern crate serde_json; +#[cfg(test)] +#[macro_use] +extern crate serde_json; use std::borrow::Cow; use std::collections::HashMap; @@ -21,10 +22,11 @@ use insert::InsertValue; pub use model::DataType; use model::ObjectModel; +use crate::cool::IsNoneOrElse; +use crate::data::Object; +use crate::helpers::{GroupByModel, GroupByParent}; use crate::model::{PropertyModel, RelationModel}; use crate::update::{UpdateObj, UpsertValue}; -use crate::cool::IsNoneOrElse; -use crate::data::{Object, Value}; mod cool; pub mod data; @@ -33,6 +35,8 @@ pub mod insert; pub mod model; pub mod update; +pub mod helpers; + mod serde_atomic_id; mod serde_map_as_list; #[cfg(test)] @@ -40,8 +44,8 @@ mod tests; pub const VERSION: &'static str = env!("CARGO_PKG_VERSION"); -pub const YOPA_MAGIC : &[u8; 4] = b"YOPA"; -pub const BINARY_FORMAT : u8 = 1; +pub const YOPA_MAGIC: &[u8; 4] = b"YOPA"; +pub const BINARY_FORMAT: u8 = 1; /// Stupid storage with naive inefficient file persistence #[derive(Debug, Default, Serialize, Deserialize)] @@ -62,7 +66,7 @@ pub struct Storage { #[cfg(not(feature = "uuid-ids"))] #[serde(with = "serde_atomic_id")] - next_id : atomic::Atomic, + next_id: atomic::Atomic, #[serde(skip)] opts: StoreOpts, @@ -180,7 +184,7 @@ impl Storage { let parsed: Self = match self.opts.file_format { FileEncoding::JSON => serde_json::from_reader(reader)?, FileEncoding::BINCODE => { - let mut magic : [u8; 5] = [0; 5]; + let mut magic: [u8; 5] = [0; 5]; reader.read_exact(&mut magic)?; if &magic[0..4] != YOPA_MAGIC { @@ -193,7 +197,7 @@ impl Storage { } bincode::deserialize_from(reader)? - }, + } }; let opts = std::mem::replace(&mut self.opts, StoreOpts::default()); @@ -231,7 +235,7 @@ impl Storage { writer.write_all(YOPA_MAGIC)?; writer.write_all(&[BINARY_FORMAT])?; bincode::serialize_into(writer, self)? - }, + } }; } } @@ -253,7 +257,7 @@ impl Storage { } /// Get object name, or the ID as string if no name is configured - pub fn get_object_name_by_id<'a>(&'a self, id : ID) -> Cow<'a, str> { + pub fn get_object_name_by_id<'a>(&'a self, id: ID) -> Cow<'a, str> { if let Some(o) = self.get_object(id) { return self.get_object_name(o); } @@ -261,9 +265,17 @@ impl Storage { return id.to_string().into(); } - pub fn get_object_name<'b, 'a: 'b>(&'a self, object : &'a Object) -> Cow<'b, str> { - if let Some(ObjectModel{ name_property: Some(name_property), .. }) = self.get_object_model(object.model) { - if let Some(v) = self.values.values().find(|v| v.object == object.id && &v.model == name_property) { + pub fn get_object_name<'b, 'a: 'b>(&'a self, object: &'a Object) -> Cow<'b, str> { + if let Some(ObjectModel { + name_property: Some(name_property), + .. + }) = self.get_object_model(object.model) + { + if let Some(v) = self + .values + .values() + .find(|v| v.object == object.id && &v.model == name_property) + { return v.value.to_cow(); } } @@ -293,7 +305,9 @@ impl Storage { /// Iterate object models with given IDs pub fn get_object_models_by_ids(&self, ids: Vec) -> impl Iterator { - self.obj_models.values().filter(move |m| ids.contains(&m.id)) + self.obj_models + .values() + .filter(move |m| ids.contains(&m.id)) } /// Get an object model by ID @@ -313,9 +327,7 @@ impl Storage { /// Get all property models grouped by the parent object ID. pub fn get_grouped_prop_models(&self) -> HashMap> { - self.prop_models - .values() - .into_group_map_by(|model| model.object) + self.prop_models.values().group_by_parent() } /// Get property models belonging to a group of parent IDs, @@ -327,7 +339,7 @@ impl Storage { self.prop_models .values() .filter(|p| parent_model_ids.contains(&p.object)) - .into_group_map_by(|model| model.object) + .group_by_parent() } /// Iterate relation models attached to an object model @@ -362,9 +374,7 @@ impl Storage { /// Get all relation models, grouped by their source object model ID pub fn get_grouped_relation_models(&self) -> HashMap> { - self.rel_models - .values() - .into_group_map_by(|model| model.object) + self.rel_models.values().group_by_parent() } /// Get reciprocal relation models, grouped by their destination model ID @@ -411,7 +421,7 @@ impl Storage { self.values .values() .filter(move |prop| parents.contains(&prop.object)) - .into_group_map_by(|model| model.object) + .group_by_parent() } /// Get all objects belonging to a model. @@ -432,9 +442,7 @@ impl Storage { /// Get all objects, grouped by their model ID pub fn get_grouped_objects(&self) -> HashMap> { - self.objects - .values() - .into_group_map_by(|object| object.model) + self.objects.values().group_by_model() } /// Get object by ID @@ -830,7 +838,7 @@ impl Storage { parent_id: ID, parent_model_id: ID| -> Result, StorageError> { - let mut values_by_id = values.into_iter().into_group_map_by(|iv| iv.model); + let mut values_by_id = values.group_by_model(); let mut values_to_insert = vec![]; for (prop_model_id, prop) in self @@ -873,7 +881,7 @@ impl Storage { first.value, self.get_model_name(*prop_model_id), ) - .into(), + .into(), )); } } @@ -908,10 +916,7 @@ impl Storage { let mut values_to_insert = find_values_to_insert(insobj.values, object_id, obj_model_id)?; // And now ..... relations! - let mut relations_by_id = insobj - .relations - .into_iter() - .into_group_map_by(|ir| ir.model); + let mut relations_by_id = insobj.relations.group_by_model(); let mut relations_to_insert = vec![]; for (relation_model_id, relation_model) in self @@ -1020,7 +1025,7 @@ impl Storage { ), StorageError, > { - let mut values_by_model = values.into_iter().into_group_map_by(|iv| iv.model); + let mut values_by_model = values.group_by_model(); let mut values_to_insert = vec![]; let mut ids_to_delete = vec![]; @@ -1028,7 +1033,7 @@ impl Storage { .values .values() .filter(|v| v.object == parent_id) - .into_group_map_by(|v| v.model); + .group_by_model(); for (prop_model_id, prop) in self .prop_models @@ -1071,7 +1076,7 @@ impl Storage { first.value, self.get_model_name(*prop_model_id), ) - .into(), + .into(), )); } } @@ -1113,10 +1118,7 @@ impl Storage { find_values_to_change(updobj.values, updated_object_id, updated_object_model_id)?; // And now ..... relations! - let mut relations_by_model = updobj - .relations - .into_iter() - .into_group_map_by(|ir| ir.model); + let mut relations_by_model = updobj.relations.group_by_model(); let mut relations_to_insert = vec![]; let mut relations_to_delete = vec![]; @@ -1124,7 +1126,7 @@ impl Storage { .relations .values() .filter(|v| v.object == updated_object_id) - .into_group_map_by(|v| v.model); + .group_by_model(); let rel_models_by_id = self .rel_models @@ -1196,8 +1198,6 @@ impl Storage { ); } - let obj_mut = self.objects.get_mut(&updated_object_id).unwrap(); - debug!("Add {} new object relations", relations_to_insert.len()); for rel in relations_to_insert { self.relations.insert(rel.id, rel); diff --git a/yopa/src/serde_atomic_id.rs b/yopa/src/serde_atomic_id.rs index 94c5ef8..9a7f673 100644 --- a/yopa/src/serde_atomic_id.rs +++ b/yopa/src/serde_atomic_id.rs @@ -1,20 +1,19 @@ //! Serialize atomic ID by fetching its value use crate::id::ID; -use serde::{Serialize, Deserializer, Deserialize}; +use serde::{Deserialize, Deserializer, Serialize}; pub fn serialize(x: &atomic::Atomic, s: S) -> Result where S: serde::Serializer, { - x.load(atomic::Ordering::Relaxed) - .serialize(s) + x.load(atomic::Ordering::Relaxed).serialize(s) } pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> where - D: Deserializer<'de> + D: Deserializer<'de>, { - let id : ID = ID::deserialize(deserializer)?; + let id: ID = ID::deserialize(deserializer)?; Ok(atomic::Atomic::new(id)) } diff --git a/yopa/src/tests.rs b/yopa/src/tests.rs index a93a54c..970be0f 100644 --- a/yopa/src/tests.rs +++ b/yopa/src/tests.rs @@ -5,6 +5,7 @@ fn test1() { let model = crate::model::ObjectModel { id: Default::default(), name: "Name".to_string(), + name_property: None, }; println!("{}", serde_json::to_string_pretty(&model).unwrap());