205 lines
6.6 KiB
Rust
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()))));
|
|
}
|
|
}
|