Add support for Delete operations

This commit is contained in:
Dustin J. Mitchell
2019-12-29 13:16:42 -05:00
parent e83bdc28cd
commit 41acb1fa1e
5 changed files with 175 additions and 51 deletions

View File

@@ -5,11 +5,16 @@ use uuid::Uuid;
/// An Operation defines a single change to the task database
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub enum Operation {
/// Create a new task; if the task already exists in the DB.
/// Create a new task.
///
/// On application, if the task already exists, the operation does nothing.
Create { uuid: Uuid },
/// Delete an existing task.
///
/// On application, if the task does not exist, the operation does nothing.
Delete { uuid: Uuid },
/// Update an existing task, setting the given property to the given value. If the value is
/// None, then the corresponding property is deleted.
///
@@ -52,9 +57,36 @@ impl Operation {
operation2: Operation,
) -> (Option<Operation>, Option<Operation>) {
match (&operation1, &operation2) {
// Two creations of the same uuid reach the same state, so there's no need for any
// further operations to bring the state together.
// Two creations or deletions of the same uuid reach the same state, so there's no need
// for any further operations to bring the state together.
(&Create { uuid: uuid1 }, &Create { uuid: uuid2 }) if uuid1 == uuid2 => (None, None),
(&Delete { uuid: uuid1 }, &Delete { uuid: uuid2 }) if uuid1 == uuid2 => (None, None),
// Given a create and a delete of the same task, one of the operations is invalid: the
// create implies the task does not exist, but the delete implies it exists. Somewhat
// arbitrarily, we prefer the Create
(&Create { uuid: uuid1 }, &Delete { uuid: uuid2 }) if uuid1 == uuid2 => {
(Some(operation1), None)
}
(&Delete { uuid: uuid1 }, &Create { uuid: uuid2 }) if uuid1 == uuid2 => {
(None, Some(operation2))
}
// And again from an Update and a Create, prefer the Update
(&Update { uuid: uuid1, .. }, &Create { uuid: uuid2 }) if uuid1 == uuid2 => {
(Some(operation1), None)
}
(&Create { uuid: uuid1 }, &Update { uuid: uuid2, .. }) if uuid1 == uuid2 => {
(None, Some(operation2))
}
// Given a delete and an update, prefer the delete
(&Update { uuid: uuid1, .. }, &Delete { uuid: uuid2 }) if uuid1 == uuid2 => {
(None, Some(operation2))
}
(&Delete { uuid: uuid1 }, &Update { uuid: uuid2, .. }) if uuid1 == uuid2 => {
(Some(operation1), None)
}
// Two updates to the same property of the same task might conflict.
(
@@ -103,6 +135,7 @@ mod test {
// thoroughly, so this testing is light.
fn test_transform(
setup: Option<Operation>,
o1: Operation,
o2: Operation,
exp1p: Option<Operation>,
@@ -114,15 +147,23 @@ mod test {
// check that the two operation sequences have the same effect, enforcing the invariant of
// the transform function.
let mut db1 = DB::new();
db1.apply(o1);
if let Some(ref o) = setup {
db1.apply(o.clone()).unwrap();
}
db1.apply(o1).unwrap();
if let Some(o) = o2p {
db1.apply(o);
db1.apply(o).unwrap();
}
let mut db2 = DB::new();
db2.apply(o2);
if let Some(o) = o1p {
db2.apply(o);
if let Some(ref o) = setup {
db2.apply(o.clone()).unwrap();
}
db2.apply(o2).unwrap();
if let Some(o) = o1p {
db2.apply(o).unwrap();
}
assert_eq!(db1.tasks(), db2.tasks());
}
@@ -132,6 +173,7 @@ mod test {
let uuid2 = Uuid::new_v4();
test_transform(
None,
Create { uuid: uuid1 },
Create { uuid: uuid2 },
Some(Create { uuid: uuid1 }),
@@ -145,6 +187,7 @@ mod test {
let timestamp = Utc::now();
test_transform(
Some(Create { uuid }),
Update {
uuid,
property: "abc".into(),
@@ -179,6 +222,7 @@ mod test {
let timestamp2 = timestamp1 + Duration::seconds(10);
test_transform(
Some(Create { uuid }),
Update {
uuid,
property: "abc".into(),
@@ -207,6 +251,7 @@ mod test {
let timestamp = Utc::now();
test_transform(
Some(Create { uuid }),
Update {
uuid,
property: "abc".into(),

View File

@@ -1,3 +1,4 @@
use crate::errors::Error;
use crate::operation::Operation;
use crate::server::{Server, VersionAdd};
use serde::{Deserialize, Serialize};
@@ -41,15 +42,30 @@ impl DB {
/// Apply an operation to the DB. Aside from synchronization operations, this
/// is the only way to modify the DB. In cases where an operation does not
/// make sense, this function will ignore the operation.
pub fn apply(&mut self, op: Operation) {
pub fn apply(&mut self, op: Operation) -> Result<(), Error> {
if let err @ Err(_) = self.apply_op(&op) {
return err;
}
self.operations.push(op);
Ok(())
}
fn apply_op(&mut self, op: &Operation) -> Result<(), Error> {
match op {
Operation::Create { uuid } => {
&Operation::Create { uuid } => {
// insert if the task does not already exist
if let ent @ Entry::Vacant(_) = self.tasks.entry(uuid) {
ent.or_insert(HashMap::new());
} else {
return Err(Error::DBError(format!("Task {} already exists", uuid)));
}
}
Operation::Update {
&Operation::Delete { ref uuid } => {
if let None = self.tasks.remove(uuid) {
return Err(Error::DBError(format!("Task {} does not exist", uuid)));
}
}
&Operation::Update {
ref uuid,
ref property,
ref value,
@@ -57,18 +73,17 @@ impl DB {
} => {
// update if this task exists, otherwise ignore
if let Some(task) = self.tasks.get_mut(uuid) {
DB::apply_update(task, property, value);
match value {
Some(ref val) => task.insert(property.to_string(), val.clone()),
None => task.remove(property),
};
} else {
return Err(Error::DBError(format!("Task {} does not exist", uuid)));
}
}
};
self.operations.push(op);
}
}
fn apply_update(task: &mut TaskMap, property: &str, value: &Option<String>) {
match value {
Some(ref val) => task.insert(property.to_string(), val.clone()),
None => task.remove(property),
};
Ok(())
}
/// Get a read-only reference to the underlying set of tasks.
@@ -152,7 +167,9 @@ impl DB {
}
}
if let Some(o) = svr_op {
self.apply(o);
if let Err(e) = self.apply_op(&o) {
println!("Invalid operation when syncing: {} (ignored)", e);
}
}
self.operations = new_local_ops;
}
@@ -171,7 +188,7 @@ mod tests {
let mut db = DB::new();
let uuid = Uuid::new_v4();
let op = Operation::Create { uuid };
db.apply(op.clone());
db.apply(op.clone()).unwrap();
let mut exp = HashMap::new();
exp.insert(uuid, HashMap::new());
@@ -184,13 +201,16 @@ mod tests {
let mut db = DB::new();
let uuid = Uuid::new_v4();
let op = Operation::Create { uuid };
db.apply(op.clone());
db.apply(op.clone());
db.apply(op.clone()).unwrap();
assert_eq!(
db.apply(op.clone()).err().unwrap(),
Error::DBError(format!("Task {} already exists", uuid))
);
let mut exp = HashMap::new();
exp.insert(uuid, HashMap::new());
assert_eq!(db.tasks(), &exp);
assert_eq!(db.operations, vec![op.clone(), op]);
assert_eq!(db.operations, vec![op]);
}
#[test]
@@ -198,14 +218,14 @@ mod tests {
let mut db = DB::new();
let uuid = Uuid::new_v4();
let op1 = Operation::Create { uuid };
db.apply(op1.clone());
db.apply(op1.clone()).unwrap();
let op2 = Operation::Update {
uuid,
property: String::from("title"),
value: Some("my task".into()),
timestamp: Utc::now(),
};
db.apply(op2.clone());
db.apply(op2.clone()).unwrap();
let mut exp = HashMap::new();
let mut task = HashMap::new();
@@ -220,7 +240,7 @@ mod tests {
let mut db = DB::new();
let uuid = Uuid::new_v4();
let op1 = Operation::Create { uuid };
db.apply(op1.clone());
db.apply(op1.clone()).unwrap();
let op2 = Operation::Update {
uuid,
@@ -228,7 +248,7 @@ mod tests {
value: Some("my task".into()),
timestamp: Utc::now(),
};
db.apply(op2.clone());
db.apply(op2.clone()).unwrap();
let op3 = Operation::Update {
uuid,
@@ -236,7 +256,7 @@ mod tests {
value: Some("H".into()),
timestamp: Utc::now(),
};
db.apply(op3.clone());
db.apply(op3.clone()).unwrap();
let op4 = Operation::Update {
uuid,
@@ -244,7 +264,7 @@ mod tests {
value: None,
timestamp: Utc::now(),
};
db.apply(op4.clone());
db.apply(op4.clone()).unwrap();
let mut exp = HashMap::new();
let mut task = HashMap::new();
@@ -264,9 +284,41 @@ mod tests {
value: Some("my task".into()),
timestamp: Utc::now(),
};
db.apply(op.clone());
assert_eq!(
db.apply(op).err().unwrap(),
Error::DBError(format!("Task {} does not exist", uuid))
);
assert_eq!(db.tasks(), &HashMap::new());
assert_eq!(db.operations, vec![op]);
assert_eq!(db.operations, vec![]);
}
#[test]
fn test_apply_create_delete() {
let mut db = DB::new();
let uuid = Uuid::new_v4();
let op1 = Operation::Create { uuid };
db.apply(op1.clone()).unwrap();
let op2 = Operation::Delete { uuid };
db.apply(op2.clone()).unwrap();
assert_eq!(db.tasks(), &HashMap::new());
assert_eq!(db.operations, vec![op1, op2]);
}
#[test]
fn test_apply_delete_not_present() {
let mut db = DB::new();
let uuid = Uuid::new_v4();
let op1 = Operation::Delete { uuid };
assert_eq!(
db.apply(op1).err().unwrap(),
Error::DBError(format!("Task {} does not exist", uuid))
);
assert_eq!(db.tasks(), &HashMap::new());
assert_eq!(db.operations, vec![]);
}
}