monorepo/changeset/src/lib.rs

205 lines
6.6 KiB
Rust

/*
Copyright 2023, Savanni D'Gerinel <savanni@luminescent-dreams.com>
This file is part of the Luminescent Dreams Tools.
Luminescent Dreams Tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
Luminescent Dreams Tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with Lumeto. If not, see <https://www.gnu.org/licenses/>.
*/
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()))));
}
}