Merge pull request 'Add a module that helps handle a list of record changes.' (#1) from changeset into main
Reviewed-on: savanni/tools#1
This commit is contained in:
commit
be995d2f26
6
Makefile
6
Makefile
|
@ -1,4 +1,10 @@
|
||||||
|
|
||||||
|
changeset-dev:
|
||||||
|
cd changeset && make dev
|
||||||
|
|
||||||
|
changeset-test:
|
||||||
|
cd changeset && make test
|
||||||
|
|
||||||
emseries-dev:
|
emseries-dev:
|
||||||
cd emseries && make dev
|
cd emseries && make dev
|
||||||
|
|
||||||
|
|
|
@ -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"
|
|
@ -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" ] }
|
|
@ -0,0 +1,9 @@
|
||||||
|
|
||||||
|
dev:
|
||||||
|
cargo watch -x build
|
||||||
|
|
||||||
|
test:
|
||||||
|
cargo watch -x test
|
||||||
|
|
||||||
|
test-once:
|
||||||
|
cargo test
|
|
@ -0,0 +1,192 @@
|
||||||
|
use std::{
|
||||||
|
collections::{HashMap, HashSet},
|
||||||
|
hash::Hash,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub trait Constructable {
|
||||||
|
fn new() -> Self;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub enum Change<Key: Eq + Hash, Value> {
|
||||||
|
DeleteRecord(Key),
|
||||||
|
UpdateRecord((Key, Value)),
|
||||||
|
NewRecord(Value),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Changeset<Key: Clone + Eq + Hash, Value> {
|
||||||
|
delete: HashSet<Key>,
|
||||||
|
update: HashMap<Key, Value>,
|
||||||
|
new: HashMap<Key, Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Key: Clone + Constructable + Eq + Hash, Value> Changeset<Key, Value> {
|
||||||
|
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<Key: Clone + Eq + Hash, Value> From<Changeset<Key, Value>> for Vec<Change<Key, Value>> {
|
||||||
|
fn from(changes: Changeset<Key, Value>) -> 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<Id, String> = 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<Id, String> = 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<Id, String> = 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<Id, String> = 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<Id, String> = 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<Id, String> = 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<Id, String> = 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()))));
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue