diff --git a/Cargo.lock b/Cargo.lock index 5aa6b75..9a8c5b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,6 +18,17 @@ version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +[[package]] +name = "getrandom" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "instant" version = "0.1.9" @@ -229,6 +240,22 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom", + "serde", +] + +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + [[package]] name = "winapi" version = "0.3.9" @@ -263,6 +290,7 @@ dependencies = [ "serde", "serde_json", "thiserror", + "uuid", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 0458bbe..714153c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ edition = "2018" log = "0.4.13" simple-logging = "2.0.2" -yopa = { path = "./yopa" } +yopa = { path = "./yopa", features = [ "uuid-ids" ] } serde_json = "1.0.61" serde = { version = "1.0.120", features = ["derive"] } diff --git a/yopa/src/cool.rs b/yopa/src/cool.rs index 608bad9..3ba0774 100644 --- a/yopa/src/cool.rs +++ b/yopa/src/cool.rs @@ -1,7 +1,10 @@ -use std::hash::Hash; +//! Utilities for internal use and other cool stuff + use std::collections::HashMap; +use std::hash::Hash; -pub fn map_drain_filter(map : &mut HashMap, filter : impl Fn(&K, &V) -> bool) -> Vec<(K, V)> { +/// drain_filter() implemented for HashMap. It returns the removed items as a Vec +pub fn map_drain_filter(map: &mut HashMap, filter: impl Fn(&K, &V) -> bool) -> Vec<(K, V)> { let mut removed = vec![]; let mut retain = vec![]; for (k, v) in map.drain() { @@ -15,12 +18,16 @@ pub fn map_drain_filter(map : &mut HashMap, filter : imp removed } +/// Get the first or second item from a Vec of (Key, Value) pairs. +/// Use when only one part of the pair is needed pub trait KVVecToKeysOrValues { + /// Get the first item of each tuple fn keys(self) -> Vec; + /// Get the second item of each tuple fn values(self) -> Vec; } -impl KVVecToKeysOrValues for Vec<(K,V)> { +impl KVVecToKeysOrValues for Vec<(K, V)> { fn keys(self) -> Vec { self.into_iter().map(|(k, _v)| k).collect() } diff --git a/yopa/src/data.rs b/yopa/src/data.rs index 11b41fc..972ccb9 100644 --- a/yopa/src/data.rs +++ b/yopa/src/data.rs @@ -1,14 +1,14 @@ //! Data value structs -use serde::{Serialize, Deserialize}; -use crate::ID; use std::borrow::Cow; +use serde::{Deserialize, Serialize}; +use crate::ID; use crate::model::DataType; /// Value of a particular type -#[derive(Debug,Clone,Serialize,Deserialize,PartialEq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum TypedValue { /// Text String(Cow<'static, str>), @@ -22,7 +22,7 @@ pub enum TypedValue { impl TypedValue { /// Try ot cast to another type. On error, the original value is returned as Err. - pub fn cast_to(self, ty : DataType) -> Result { + pub fn cast_to(self, ty: DataType) -> Result { match (self, ty) { // to string (s @ TypedValue::String(_), DataType::String) => Ok(s), @@ -35,7 +35,7 @@ impl TypedValue { Ok(i) => Ok(TypedValue::Integer(i)), Err(_) => Err(TypedValue::String(s)) } - }, + } (s @ TypedValue::Integer(_), DataType::Integer) => Ok(s), (TypedValue::Decimal(f), DataType::Integer) => Ok(TypedValue::Integer(f.round() as i64)), (TypedValue::Boolean(b), DataType::Integer) => Ok(TypedValue::Integer(if b { 1 } else { 0 })), @@ -45,7 +45,7 @@ impl TypedValue { Ok(i) => Ok(TypedValue::Decimal(i)), Err(_) => Err(TypedValue::String(s)) } - }, + } (TypedValue::Integer(i), DataType::Decimal) => Ok(TypedValue::Decimal(i as f64)), (d @ TypedValue::Decimal(_), DataType::Decimal) => Ok(d), (e @ TypedValue::Boolean(_), DataType::Decimal) => Err(e), @@ -62,7 +62,7 @@ impl TypedValue { Err(TypedValue::String(s)) } } - }, + } (TypedValue::Integer(i), DataType::Boolean) => Ok(TypedValue::Boolean(i != 0)), (e @ TypedValue::Decimal(_), DataType::Boolean) => Err(e), (b @ TypedValue::Boolean(_), DataType::Boolean) => Ok(b), @@ -155,36 +155,36 @@ mod tests { } /// Instance of an object -#[derive(Debug,Clone,Serialize,Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct Object { /// PK - pub id : ID, + pub id: ID, /// Object template ID pub model_id: ID, } /// Relation between two objects -#[derive(Debug,Clone,Serialize,Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct Relation { /// PK - pub id : ID, + pub id: ID, /// Source object ID - pub object_id : ID, + pub object_id: ID, /// Relation template ID pub model_id: ID, /// Related object ID - pub related_id : ID, + pub related_id: ID, } /// Value attached to an object -#[derive(Debug,Clone,Serialize,Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct Value { /// PK - pub id : ID, + pub id: ID, /// Owning object ID - pub object_id : ID, + pub object_id: ID, /// Property template ID pub model_id: ID, /// Property value - pub value : TypedValue, + pub value: TypedValue, } diff --git a/yopa/src/id.rs b/yopa/src/id.rs new file mode 100644 index 0000000..742cd4d --- /dev/null +++ b/yopa/src/id.rs @@ -0,0 +1,48 @@ +//! IDs used for data objects and models. +//! +//! UUID is always unique; the numeric ID used as a fallback +//! is auto-incrementing, but some values may be skipped. +//! +//! It is better to treat both ID implementations as opaque. + +#[cfg(feature = "uuid-ids")] +mod impl_uuid { + /// Common identifier type + #[allow(non_camel_case_types)] + pub type ID = uuid::Uuid; + + pub fn next_id() -> ID { + uuid::Uuid::new_v4() + } + + pub fn zero_id() -> ID { + uuid::Uuid::nil() + } +} + +#[cfg(not(feature = "uuid-ids"))] +mod impl_u64 { + /// Common identifier type + #[allow(non_camel_case_types)] + pub type ID = u64; + + lazy_static::lazy_static! { + static ref COUNTER: parking_lot::Mutex = parking_lot::Mutex::new(0); + } + + pub fn next_id() -> ID { + let mut m = COUNTER.lock(); + let v = *m; + *m += 1; + v + } + + pub fn zero_id() -> ID { + 0 + } +} + +#[cfg(feature = "uuid-ids")] +pub use impl_uuid::{ID, next_id, zero_id}; +#[cfg(not(feature = "uuid-ids"))] +pub use impl_u64::{ID, next_id, zero_id}; diff --git a/yopa/src/insert.rs b/yopa/src/insert.rs index fb2b5d5..a30ef03 100644 --- a/yopa/src/insert.rs +++ b/yopa/src/insert.rs @@ -1,45 +1,46 @@ //! Helper struct for inserting relational data in the database -use super::ID; +use serde::{Deserialize, Serialize}; + use super::data::TypedValue; -use serde::{Serialize,Deserialize}; +use super::ID; /// Value to insert -#[derive(Debug,Clone,Serialize,Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct InsertValue { pub model_id: ID, - pub value: TypedValue + pub value: TypedValue, } impl InsertValue { - pub fn new(model_id : ID, value : TypedValue) -> Self { + pub fn new(model_id: ID, value: TypedValue) -> Self { Self { model_id, - value + value, } } } /// Info for inserting a relation -#[derive(Debug,Clone,Serialize,Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct InsertRel { pub model_id: ID, pub related_id: ID, - pub values: Vec + pub values: Vec, } impl InsertRel { - pub fn new(model_id : ID, related_id: ID, values : Vec) -> Self { + pub fn new(model_id: ID, related_id: ID, values: Vec) -> Self { Self { model_id, related_id, - values + values, } } } /// Info for inserting a relation -#[derive(Debug,Clone,Serialize,Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct InsertObj { pub model_id: ID, pub values: Vec, @@ -47,11 +48,11 @@ 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_id, values, - relations + relations, } } } diff --git a/yopa/src/lib.rs b/yopa/src/lib.rs index 9c51b78..8199353 100644 --- a/yopa/src/lib.rs +++ b/yopa/src/lib.rs @@ -1,62 +1,25 @@ -use std::collections::{HashMap}; -use thiserror::Error; - -use crate::cool::{map_drain_filter, KVVecToKeysOrValues}; -use model::{ObjectModel}; -use insert::InsertObj; -use itertools::Itertools; use std::borrow::Cow; +use std::collections::HashMap; -use insert::InsertValue; - - -mod cool; - -#[cfg(feature="uuid-ids")] -pub mod id { - /// Common identifier type - #[allow(non_camel_case_types)] - pub type ID = uuid::Uuid; - - pub fn next_id() -> ID { - uuid::Uuid::new_v4() - } - - pub fn zero_id() -> ID { - uuid::Uuid::nil() - } -} - -#[cfg(not(feature="uuid-ids"))] -pub mod id { - /// Common identifier type - #[allow(non_camel_case_types)] - pub type ID = u64; - - lazy_static::lazy_static! { - static ref COUNTER: parking_lot::Mutex = parking_lot::Mutex::new(0); - } - - pub fn next_id() -> ID { - let mut m = COUNTER.lock(); - let v = *m; - *m += 1; - v - } - - pub fn zero_id() -> ID { - 0 - } -} +use itertools::Itertools; +use serde::{Deserialize, Serialize}; +use thiserror::Error; +use cool::{KVVecToKeysOrValues, map_drain_filter}; pub use id::ID; use id::next_id; +use insert::InsertObj; +use insert::InsertValue; +use model::ObjectModel; pub mod model; pub mod data; pub mod insert; +pub mod id; +mod cool; -#[derive(Debug, Default)] +/// Stupid storage with no persistence +#[derive(Debug, Default, Serialize, Deserialize, Clone)] pub struct InMemoryStorage { obj_models: HashMap, rel_models: HashMap, @@ -67,7 +30,7 @@ pub struct InMemoryStorage { properties: HashMap, } -#[derive(Debug,Error)] +#[derive(Debug, Error)] pub enum StorageError { #[error("Referenced {0} does not exist")] NotExist(Cow<'static, str>), @@ -76,11 +39,13 @@ pub enum StorageError { } impl InMemoryStorage { + /// Create empty store pub fn new() -> Self { Self::default() } - pub fn define_object(&mut self, mut tpl : model::ObjectModel) -> Result { + /// Define a data object + pub fn define_object(&mut self, mut tpl: model::ObjectModel) -> Result { if let Some(pid) = tpl.parent_tpl_id { if !self.obj_models.contains_key(&pid) { return Err(StorageError::NotExist(format!("parent object model {}", pid).into())); @@ -97,6 +62,7 @@ impl InMemoryStorage { Ok(id) } + /// Define a relation between two data objects pub fn define_relation(&mut self, mut rel: model::RelationModel) -> Result { if !self.obj_models.contains_key(&rel.object_tpl_id) { return Err(StorageError::NotExist(format!("source object model {}", rel.object_tpl_id).into())); @@ -116,6 +82,7 @@ impl InMemoryStorage { Ok(id) } + /// Define a property attached to an object or a relation pub fn define_property(&mut self, mut prop: model::PropertyModel) -> Result { if !self.obj_models.contains_key(&prop.parent_tpl_id) { // Maybe it's attached to a relation? @@ -143,7 +110,8 @@ impl InMemoryStorage { Ok(id) } - pub fn undefine_object(&mut self, id : ID) -> Result { + /// Delete an object definition and associated data + pub fn undefine_object(&mut self, id: ID) -> Result { return if let Some(t) = self.obj_models.remove(&id) { // Remove relation templates let removed_relation_ids = map_drain_filter(&mut self.rel_models, |_k, v| v.object_tpl_id == id || v.related_tpl_id == id) @@ -167,10 +135,11 @@ impl InMemoryStorage { Ok(t) } else { Err(StorageError::NotExist(format!("object model {}", id).into())) - } + }; } - pub fn undefine_relation(&mut self, id : ID) -> Result { + /// Delete a relation definition and associated data + pub fn undefine_relation(&mut self, id: ID) -> Result { return if let Some(t) = self.rel_models.remove(&id) { // Remove relations let _ = map_drain_filter(&mut self.relations, |_k, v| v.model_id == id).keys(); @@ -185,22 +154,23 @@ impl InMemoryStorage { Ok(t) } else { Err(StorageError::NotExist(format!("relation model {}", id).into())) - } + }; } - pub fn undefine_property(&mut self, id : ID) -> Result { + /// Delete a property definition and associated data + pub fn undefine_property(&mut self, id: ID) -> Result { return if let Some(t) = self.prop_models.remove(&id) { // Remove relations let _ = map_drain_filter(&mut self.properties, |_k, v| v.model_id == id); Ok(t) } else { Err(StorageError::NotExist(format!("property model {}", id).into())) - } + }; } // DATA - /// Insert object with relations, validating the data model + /// Insert object with relations, validating the data model constraints pub fn insert_object(&mut self, insobj: InsertObj) -> Result { let obj_model_id = insobj.model_id; @@ -212,10 +182,10 @@ impl InMemoryStorage { let object_id = next_id(); let object = data::Object { id: object_id, - model_id: obj_model_id + model_id: obj_model_id, }; - let find_values_to_insert = |values : Vec, parent_id : ID| -> Result, StorageError> { + let find_values_to_insert = |values: Vec, parent_id: ID| -> Result, StorageError> { let mut values_by_id = values.into_iter().into_group_map_by(|iv| iv.model_id); let mut values_to_insert = vec![]; @@ -230,7 +200,7 @@ impl InMemoryStorage { object_id, model_id: prop.id, value: val_instance.value.cast_to(prop.data_type) - .map_err(|v| StorageError::ConstraintViolation(format!("{} cannot accept value {:?}", prop, v).into()))? + .map_err(|v| StorageError::ConstraintViolation(format!("{} cannot accept value {:?}", prop, v).into()))?, }); } } else { @@ -240,7 +210,7 @@ impl InMemoryStorage { id: next_id(), object_id, model_id: prop.id, - value: def.clone() + value: def.clone(), }); } else { return Err(StorageError::ConstraintViolation(format!("{} is required for {} (and no default value is defined)", prop, obj_model).into())); @@ -278,7 +248,7 @@ impl InMemoryStorage { id: next_id(), object_id, model_id: rel_instance.model_id, - related_id: rel_instance.related_id + related_id: rel_instance.related_id, }); } } else { diff --git a/yopa/src/model.rs b/yopa/src/model.rs index 28edbca..1997230 100644 --- a/yopa/src/model.rs +++ b/yopa/src/model.rs @@ -1,12 +1,13 @@ //! Data model structs and enums -use serde::{Serialize, Deserialize}; - -use super::ID; -use super::data::TypedValue; use std::fmt::{Display, Formatter}; use std::fmt; +use serde::{Deserialize, Serialize}; + +use super::data::TypedValue; +use super::ID; + /// Get a description of a struct pub trait Describe { /// Short but informative description for error messages @@ -14,10 +15,10 @@ pub trait Describe { } /// Object template -#[derive(Debug,Clone,Serialize,Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct ObjectModel { /// PK - pub id : ID, + pub id: ID, /// Template name, unique within the database pub name: String, /// Parent object template ID @@ -25,7 +26,7 @@ pub struct ObjectModel { } /// Relation between templates -#[derive(Debug,Clone,Serialize,Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct RelationModel { /// PK pub id: ID, @@ -42,7 +43,7 @@ pub struct RelationModel { } /// Property definition -#[derive(Debug,Clone,Serialize,Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct PropertyModel { /// PK pub id: ID, @@ -61,7 +62,7 @@ pub struct PropertyModel { } /// Value data type -#[derive(Debug,Clone,Copy,Serialize,Deserialize,Eq,PartialEq)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, Eq, PartialEq)] pub enum DataType { /// Text String,