diff --git a/Makefile b/Makefile index d9da9b2..58c3415 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,10 @@ +changeset-dev: + cd changeset && make dev + +changeset-test: + cd changeset && make test + emseries-dev: cd emseries && make dev diff --git a/changeset/Cargo.lock b/changeset/Cargo.lock new file mode 100644 index 0000000..2bddc66 --- /dev/null +++ b/changeset/Cargo.lock @@ -0,0 +1,48 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "changeset" +version = "0.1.0" +dependencies = [ + "uuid", +] + +[[package]] +name = "getrandom" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "libc" +version = "0.2.139" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" + +[[package]] +name = "uuid" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" diff --git a/changeset/Cargo.toml b/changeset/Cargo.toml new file mode 100644 index 0000000..bf7b094 --- /dev/null +++ b/changeset/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "changeset" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] + +[dev-dependencies] +uuid = { version = "*", features = [ "v4" ] } diff --git a/changeset/Makefile b/changeset/Makefile new file mode 100644 index 0000000..1bd40ea --- /dev/null +++ b/changeset/Makefile @@ -0,0 +1,9 @@ + +dev: + cargo watch -x build + +test: + cargo watch -x test + +test-once: + cargo test diff --git a/changeset/src/lib.rs b/changeset/src/lib.rs new file mode 100644 index 0000000..203c4fb --- /dev/null +++ b/changeset/src/lib.rs @@ -0,0 +1,192 @@ +use std::{ + collections::{HashMap, HashSet}, + hash::Hash, +}; + +pub trait Constructable { + fn new() -> Self; +} + +#[derive(Clone, Debug, PartialEq)] +pub enum Change { + DeleteRecord(Key), + UpdateRecord((Key, Value)), + NewRecord(Value), +} + +#[derive(Clone, Debug)] +pub struct Changeset { + delete: HashSet, + update: HashMap, + new: HashMap, +} + +impl Changeset { + pub fn new() -> Self { + Self { + delete: HashSet::new(), + update: HashMap::new(), + new: HashMap::new(), + } + } + + pub fn add(&mut self, r: Value) -> Key { + let k = Key::new(); + self.new.insert(k.clone(), r); + k.clone() + } + + pub fn update(&mut self, k: Key, v: Value) { + match self.new.get_mut(&k) { + Some(record) => *record = v, + None => { + let _ = self.update.insert(k, v); + } + } + } + + pub fn delete(&mut self, k: Key) { + let new_v = self.new.remove(&k); + let updated_v = self.update.remove(&k); + match (new_v.is_some(), updated_v.is_some()) { + (false, false) => { + let _ = self.delete.insert(k); + } + (true, false) => (), + (false, true) => { + let _ = self.delete.insert(k); + } + (true, true) => { + panic!("state error: key exists in the new and updtade columns at once") + } + } + } +} + +impl From> for Vec> { + fn from(changes: Changeset) -> Self { + let Changeset { + delete, + update, + new, + } = changes; + delete + .into_iter() + .map(|k| Change::DeleteRecord(k)) + .chain( + update + .into_iter() + .map(|(k, v)| Change::UpdateRecord((k, v))), + ) + .chain(new.into_iter().map(|(_, v)| Change::NewRecord(v))) + .collect() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use uuid::Uuid; + + #[derive(Clone, PartialEq, Eq, Hash)] + struct Id(Uuid); + impl Constructable for Id { + fn new() -> Self { + Id(Uuid::new_v4()) + } + } + + #[test] + fn it_generates_a_new_record() { + let mut set: Changeset = Changeset::new(); + set.add("efgh".to_string()); + let changes = Vec::from(set.clone()); + assert_eq!(changes.len(), 1); + assert!(changes.contains(&Change::NewRecord("efgh".to_string()))); + + set.add("wxyz".to_string()); + let changes = Vec::from(set); + assert_eq!(changes.len(), 2); + assert!(changes.contains(&Change::NewRecord("efgh".to_string()))); + assert!(changes.contains(&Change::NewRecord("wxyz".to_string()))); + } + + #[test] + fn it_generates_a_delete_record() { + let mut set: Changeset = Changeset::new(); + let id1 = Id::new(); + set.delete(id1.clone()); + let changes = Vec::from(set.clone()); + assert_eq!(changes.len(), 1); + assert!(changes.contains(&Change::DeleteRecord(id1.clone()))); + + let id2 = Id::new(); + set.delete(id2.clone()); + let changes = Vec::from(set); + assert_eq!(changes.len(), 2); + assert!(changes.contains(&Change::DeleteRecord(id1))); + assert!(changes.contains(&Change::DeleteRecord(id2))); + } + + #[test] + fn update_unrelated_records() { + let mut set: Changeset = Changeset::new(); + let id1 = Id::new(); + let id2 = Id::new(); + set.update(id1.clone(), "abcd".to_owned()); + set.update(id2.clone(), "efgh".to_owned()); + let changes = Vec::from(set); + assert_eq!(changes.len(), 2); + assert!(changes.contains(&Change::UpdateRecord((id1, "abcd".to_owned())))); + assert!(changes.contains(&Change::UpdateRecord((id2, "efgh".to_owned())))); + } + + #[test] + fn delete_cancels_new() { + let mut set: Changeset = Changeset::new(); + let key = set.add("efgh".to_string()); + set.delete(key); + let changes = Vec::from(set); + assert_eq!(changes.len(), 0); + } + + #[test] + fn delete_cancels_update() { + let mut set: Changeset = Changeset::new(); + let id = Id::new(); + set.update(id.clone(), "efgh".to_owned()); + set.delete(id.clone()); + let changes = Vec::from(set); + assert_eq!(changes.len(), 1); + assert!(changes.contains(&Change::DeleteRecord(id))); + } + + #[test] + fn update_atop_new_is_new() { + let mut set: Changeset = Changeset::new(); + let key = set.add("efgh".to_owned()); + set.update(key, "wxyz".to_owned()); + let changes = Vec::from(set); + assert_eq!(changes.len(), 1); + assert!(changes.contains(&Change::NewRecord("wxyz".to_string()))); + } + + #[test] + fn updates_get_squashed() { + let mut set: Changeset = Changeset::new(); + let id1 = Id::new(); + let id2 = Id::new(); + set.update(id1.clone(), "efgh".to_owned()); + set.update(id2.clone(), "efgh".to_owned()); + let changes = Vec::from(set.clone()); + assert_eq!(changes.len(), 2); + assert!(changes.contains(&Change::UpdateRecord((id1.clone(), "efgh".to_string())))); + assert!(changes.contains(&Change::UpdateRecord((id2.clone(), "efgh".to_string())))); + + set.update(id1.clone(), "wxyz".to_owned()); + let changes = Vec::from(set); + assert_eq!(changes.len(), 2); + assert!(changes.contains(&Change::UpdateRecord((id1, "wxyz".to_string())))); + assert!(changes.contains(&Change::UpdateRecord((id2, "efgh".to_string())))); + } +}