From b4dbb94e274a830ac37d0683060a47cf4e853b90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Tue, 2 Feb 2021 22:48:36 +0100 Subject: [PATCH] initial --- .gitignore | 2 + Cargo.lock | 222 ++++++++++++++++++++++++++++++++++++++ Cargo.toml | 19 ++++ src/main.rs | 78 ++++++++++++++ src/yopa.rs | 299 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 620 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/main.rs create mode 100644 src/yopa.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c403c34 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +.idea/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..d7b937d --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,222 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "anyhow" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afddf7f520a80dbf76e6f50a35bca42a2331ef227a28b3b6dc5c2e2338d114b1" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[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 = "itoa" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ccac4b00700875e6a07c6cde370d44d32fa01c5a65cdd2fca6858c479d28bb3" + +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "proc-macro2" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "991431c3519a3f36861882da93630ce66b52918dcf1b8e2fd66b397fc96f28df" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" + +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" + +[[package]] +name = "serde" +version = "1.0.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d5161132722baa40d802cc70b15262b98258453e85e5d1d365c757c73869ae" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9391c295d64fc0abb2c556bad848f33cb8296276b1ad2677d1ae1ace4f258f31" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fceb2595057b6891a4ee808f70054bd2d12f0e97f1cbb78689b59f676df325a" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "simple-logging" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00d48e85675326bb182a2286ea7c1a0b264333ae10f27a937a72be08628b542" +dependencies = [ + "lazy_static", + "log", + "thread-id", +] + +[[package]] +name = "syn" +version = "1.0.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c700597eca8a5a762beb35753ef6b94df201c81cca676604f547495a0d7f0081" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "thiserror" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76cc616c6abf8c8928e2fdcc0dbfab37175edd8fb49a4641066ad1364fdab146" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9be73a2caec27583d0046ef3796c3794f868a5bc813db689eed00c7631275cd1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread-id" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7fbf4c9d56b320106cd64fd024dadfa0be7cb4706725fc44a7d7ce952d820c1" +dependencies = [ + "libc", + "redox_syscall", + "winapi", +] + +[[package]] +name = "unicode-xid" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "yopa" +version = "0.1.0" +dependencies = [ + "anyhow", + "log", + "serde", + "serde_json", + "simple-logging", + "thiserror", + "uuid", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..bb80fa3 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "yopa" +version = "0.1.0" +authors = ["Ondřej Hruška "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +log = "0.4.13" +simple-logging = "2.0.2" + +uuid = { version = "0.8", features = ["serde", "v4"] } +serde_json = "1.0.61" +serde = { version = "1.0.120", features = ["derive"] } + +#parking_lot = "0.11.1" +anyhow = "1.0.38" +thiserror = "1.0.23" diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..f1bb308 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,78 @@ +#[macro_use] extern crate log; + +use log::LevelFilter; + +mod yopa; +use yopa::model; +use yopa::model::DataType; +use crate::yopa::model::TypedValue; +use anyhow::Error; + +fn main() { + simple_logging::log_to_stderr(LevelFilter::Debug); + + if let Err(e) = _main() { + error!("{:?}", e); + } +} + +fn _main() -> anyhow::Result<()> { + simple_logging::log_to_stderr(LevelFilter::Debug); + + let mut store = yopa::InMemoryStorage::new(); + + let recipe_tid = store.insert_object_template(model::ObjectTemplate { + id: Default::default(), + name: "recipe".to_string(), + parent_tpl_id: None + })?; + + store.insert_property_template(model::PropertyTemplate { + id: Default::default(), + parent_tpl_id: recipe_tid, + name: "title".to_string(), + optional: false, + multiple: false, + data_type: DataType::String, + default: None + })?; + + store.insert_property_template(model::PropertyTemplate { + id: Default::default(), + parent_tpl_id: recipe_tid, + name: "prep_hours".to_string(), + optional: true, + multiple: false, + data_type: DataType::Decimal, + default: None + })?; + + let book_tid = store.insert_object_template(model::ObjectTemplate { + id: Default::default(), + name: "book".to_string(), + parent_tpl_id: None + })?; + + let br_rid = store.insert_relation_template(model::RelationTemplate { + id: Default::default(), + object_tpl_id: recipe_tid, + name: "book reference".to_string(), + optional: false, + multiple: false, + related_tpl_id: book_tid + })?; + + store.insert_property_template(model::PropertyTemplate { + id: Default::default(), + parent_tpl_id: br_rid, + name: "page".to_string(), + optional: true, + multiple: false, + data_type: DataType::Integer, + default: None + })?; + + debug!("{:#?}", store); + + Ok(()) +} diff --git a/src/yopa.rs b/src/yopa.rs new file mode 100644 index 0000000..4d6f0a8 --- /dev/null +++ b/src/yopa.rs @@ -0,0 +1,299 @@ +use std::collections::{HashMap}; +use std::iter::Extend; +use crate::yopa::model::{ObjectTemplate, ID}; +use thiserror::Error; +use std::hash::Hash; + +/// Data model structs and enums +pub mod model { + use serde::{Serialize, Deserialize}; + use std::borrow::Cow; + + /// Common identifier type + #[allow(non_camel_case_types)] + pub type ID = uuid::Uuid; + + /// Object template + #[derive(Debug,Clone,Serialize,Deserialize)] + pub struct ObjectTemplate { + /// PK + pub id : ID, + /// Template name, unique within the database + pub name: String, + /// Parent object template ID + pub parent_tpl_id: Option, + } + + /// Relation between templates + #[derive(Debug,Clone,Serialize,Deserialize)] + pub struct RelationTemplate { + /// PK + pub id: ID, + /// Object template ID + pub object_tpl_id: ID, + /// Relation name, unique within the parent object + pub name: String, + /// Relation is optional + pub optional: bool, + /// Relation can be multiple + pub multiple: bool, + /// Related object template ID + pub related_tpl_id: ID, + } + + /// Property definition + #[derive(Debug,Clone,Serialize,Deserialize)] + pub struct PropertyTemplate { + /// PK + pub id: ID, + /// Object or Reference template ID + pub parent_tpl_id: ID, + /// Property name, unique within the parent object or reference + pub name: String, + /// Property is optional + pub optional: bool, + /// Property can be multiple + pub multiple: bool, + /// Property data type + pub data_type: DataType, + /// Default value, used for newly created objects + pub default: Option, + } + + /// Value data type + #[derive(Debug,Clone,Serialize,Deserialize)] + pub enum DataType { + /// Text + String, + /// Integer + Integer, + /// Floating point number + Decimal, + /// Boolean yes/no + Boolean, + } + + /// Value of a particular type + #[derive(Debug,Clone,Serialize,Deserialize)] + pub enum TypedValue { + /// Text + String(Cow<'static, str>), + /// Integer + Integer(i64), + /// Floating point number + Decimal(f64), + /// Boolean yes/no + Boolean(bool), + } +} + +/// Data value structs +pub mod data { + use serde::{Serialize, Deserialize}; + use crate::yopa::model::{ID, TypedValue}; + + /// Instance of an object + #[derive(Debug,Clone,Serialize,Deserialize)] + pub struct Object { + /// PK + pub id : ID, + /// Object template ID + pub object_tpl_id: ID, + } + + /// Relation between two objects + #[derive(Debug,Clone,Serialize,Deserialize)] + pub struct Relation { + /// PK + pub id : ID, + /// Source object ID + pub object_id : ID, + /// Relation template ID + pub rel_tpl_id: ID, + /// Related object ID + pub related_id : ID, + } + + /// Value attached to an object + #[derive(Debug,Clone,Serialize,Deserialize)] + pub struct Value { + /// PK + pub id : ID, + /// Owning object ID + pub object_id : ID, + /// Property template ID + pub prop_tpl_id: ID, + /// Property value + pub value : TypedValue, + } +} + +#[derive(Debug, Default)] +pub struct InMemoryStorage { + tpl_objects: HashMap, + tpl_relations: HashMap, + tpl_properties: HashMap, + + data_objects: HashMap, + data_relations: HashMap, + data_properties: HashMap, +} + +fn next_id() -> ID { + uuid::Uuid::new_v4() +} + +pub fn zero_id() -> ID { + uuid::Uuid::nil() +} + +#[derive(Debug,Error)] +pub enum StorageError { + #[error("Referenced {0} does not exist")] + NotExist(&'static str), + #[error("Schema constraint violation: {0}")] + ConstraintViolation(&'static str), +} + +impl InMemoryStorage { + pub fn new() -> Self { + Self::default() + } + + pub fn insert_object_template(&mut self, mut tpl : model::ObjectTemplate) -> Result { + if let Some(pid) = tpl.parent_tpl_id { + if !self.tpl_objects.contains_key(&pid) { + return Err(StorageError::NotExist("parent object template")); + } + } + + if self.tpl_objects.iter().find(|(_, t)| t.name == tpl.name).is_some() { + return Err(StorageError::ConstraintViolation("object template with this name already exists")); + } + + let id = next_id(); + tpl.id = id; + self.tpl_objects.insert(id, tpl); + Ok(id) + } + + pub fn insert_relation_template(&mut self, mut rel: model::RelationTemplate) -> Result { + if !self.tpl_objects.contains_key(&rel.object_tpl_id) { + return Err(StorageError::NotExist("origin object template")); + } + if !self.tpl_objects.contains_key(&rel.related_tpl_id) { + return Err(StorageError::NotExist("related object template")); + } + + if self.tpl_relations.iter().find(|(_, t)| t.name == rel.name && t.object_tpl_id == rel.object_tpl_id).is_some() { + return Err(StorageError::ConstraintViolation("relation with this name and parent already exists")); + } + + let id = next_id(); + rel.id = id; + self.tpl_relations.insert(id, rel); + Ok(id) + } + + pub fn insert_property_template(&mut self, mut prop: model::PropertyTemplate) -> Result { + if !self.tpl_objects.contains_key(&prop.parent_tpl_id) { + // Maybe it's attached to a relation? + if !self.tpl_relations.contains_key(&prop.parent_tpl_id) { + return Err(StorageError::NotExist("object or reference template")); + } + } + + if self.tpl_properties.iter().find(|(_, t)| t.parent_tpl_id == prop.parent_tpl_id && t.name == prop.name).is_some() { + return Err(StorageError::ConstraintViolation("property with this name and parent already exists")); + } + + let id = next_id(); + prop.id = id; + self.tpl_properties.insert(id, prop); + Ok(id) + } + + pub fn delete_object_template(&mut self, id : ID) -> Result { + return if let Some(t) = self.tpl_objects.remove(&id) { + // Remove relation templates + let removed_relation_ids = map_drain_filter(&mut self.tpl_relations, |_k, v| v.object_tpl_id == id || v.related_tpl_id == id) + .keys(); + + // Remove related property templates + let removed_prop_ids = map_drain_filter(&mut self.tpl_properties, |_k, v| v.parent_tpl_id == id || removed_relation_ids.contains(&v.parent_tpl_id)) + .keys(); + + // Remove objects + let _ = map_drain_filter(&mut self.data_objects, |_k, v| v.object_tpl_id == id); + + // Remove property values + let _ = map_drain_filter(&mut self.data_properties, |_k, v| removed_prop_ids.contains(&v.prop_tpl_id)); + + // Remove relations + let _ = map_drain_filter(&mut self.data_relations, |_k, v| removed_relation_ids.contains(&v.rel_tpl_id)); + + // Related object remain untouched, so there can be a problem with orphans. This is up to the application to deal with. + + Ok(t) + } else { + Err(StorageError::NotExist("object template")) + } + } + + pub fn delete_relation_template(&mut self, id : ID) -> Result { + return if let Some(t) = self.tpl_relations.remove(&id) { + // Remove relations + let _ = map_drain_filter(&mut self.data_relations, |_k, v| v.rel_tpl_id == id).keys(); + + // Remove related property templates + let removed_prop_tpl_ids = map_drain_filter(&mut self.tpl_properties, |_k, v| v.parent_tpl_id == id).keys(); + + let _ = map_drain_filter(&mut self.data_properties, |_k, v| removed_prop_tpl_ids.contains(&v.prop_tpl_id)); + + // Related object remain untouched, so there can be a problem with orphans. This is up to the application to deal with. + + Ok(t) + } else { + Err(StorageError::NotExist("relation template")) + } + } + + pub fn delete_property_template(&mut self, id : ID) -> Result { + return if let Some(t) = self.tpl_properties.remove(&id) { + // Remove relations + let _ = map_drain_filter(&mut self.data_properties, |_k, v| v.prop_tpl_id == id); + Ok(t) + } else { + Err(StorageError::NotExist("property template")) + } + } +} + +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() { + if filter(&k, &v) { + removed.push((k, v)); + } else { + retain.push((k, v)); + } + } + map.extend(retain); + removed +} + +trait KVVecToKeysOrValues { + fn keys(self) -> Vec; + fn values(self) -> Vec; +} + +impl KVVecToKeysOrValues for Vec<(K,V)> { + fn keys(self) -> Vec { + self.into_iter().map(|(k, _v)| k).collect() + } + + fn values(self) -> Vec { + self.into_iter().map(|(_k, v)| v).collect() + } +}