From e83bdc28cdb0ec0c8d882920393cdb69ff31dbfa Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sun, 29 Dec 2019 11:50:05 -0500 Subject: [PATCH] use strings as values, with option to allow removing --- README.md | 6 +-- TODO.txt | 15 +++---- src/operation.rs | 36 ++++++++------- src/taskdb.rs | 61 +++++++++++++++++++++++--- tests/operation_transform_invariant.rs | 2 +- tests/sync.rs | 8 ++-- 6 files changed, 89 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 4c857eeb2..3ea1a229a 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,7 @@ The data model is only seen from the clients' perspective. ## Task Database The task database is composed of an un-ordered collection of tasks, each keyed by a UUID. -Each task has an arbitrary-sized set of key/value properties, with JSON values. -A property with a `null` value is considered equivalent to that property not being set on the task. +Each task has an arbitrary-sized set of key/value properties, with string values. Tasks are only created, never deleted. See below for details on how tasks can "expire" from the task database. @@ -31,7 +30,8 @@ Each operation has one of the forms The former form creates a new task. It is invalid to create a task that already exists. -The latter form updates the given property of the given task. +The latter form updates the given property of the given task, where property and value are both strings. +Value can also be `None` to indicate deletion of a property. It is invalid to update a task that does not exist. The timestamp on updates serves as additional metadata and is used to resolve conflicts. diff --git a/TODO.txt b/TODO.txt index 7f07d5a70..cdbf54749 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,13 +1,9 @@ * assign types to properties - - modifications to types don't commute the same way - - optimize this to simplify the transform function - - types: - - dependencies: set of uuids - - annotations: set of annotations (incl timestamps for uniqueness) - - tags: set of tags - - idea: Update takes a dotted path for property; store everything as a map - e.g., {uuid: true}, {timestamp: annotation}, {tag: true}; keep the - set-to-null-to-delete to remove + - db / operation model is just k/v, but formatted names can be used for + structure: + - dependencies: `dependency. = ""` + - annotations: `annotation. = "annotation"` + - tags: `tags. = ""` * add HTTP API * implement snapshot requests * implement backups @@ -16,3 +12,4 @@ - need to be sure that create / delete operations don't get reversed * cli tools * prop testing for DB modifications + - 'strict' mode to fail on application of any nonsense operations diff --git a/src/operation.rs b/src/operation.rs index 29b42a51f..78ac36058 100644 --- a/src/operation.rs +++ b/src/operation.rs @@ -1,17 +1,23 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; -use serde_json::Value; use uuid::Uuid; +/// An Operation defines a single change to the task database #[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] pub enum Operation { - Create { - uuid: Uuid, - }, + /// Create a new task; if the task already exists in the DB. + /// + /// On application, if the task already exists, the operation does nothing. + Create { 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. + /// + /// If the given task does not exist, the operation does nothing. Update { uuid: Uuid, property: String, - value: Value, + value: Option, timestamp: DateTime, }, } @@ -142,25 +148,25 @@ mod test { Update { uuid, property: "abc".into(), - value: true.into(), + value: Some("true".into()), timestamp, }, Update { uuid, property: "def".into(), - value: false.into(), + value: Some("false".into()), timestamp, }, Some(Update { uuid, property: "abc".into(), - value: true.into(), + value: Some("true".into()), timestamp, }), Some(Update { uuid, property: "def".into(), - value: false.into(), + value: Some("false".into()), timestamp, }), ); @@ -176,20 +182,20 @@ mod test { Update { uuid, property: "abc".into(), - value: true.into(), + value: Some("true".into()), timestamp: timestamp1, }, Update { uuid, property: "abc".into(), - value: false.into(), + value: Some("false".into()), timestamp: timestamp2, }, None, Some(Update { uuid, property: "abc".into(), - value: false.into(), + value: Some("false".into()), timestamp: timestamp2, }), ); @@ -204,19 +210,19 @@ mod test { Update { uuid, property: "abc".into(), - value: true.into(), + value: Some("true".into()), timestamp, }, Update { uuid, property: "abc".into(), - value: false.into(), + value: Some("false".into()), timestamp, }, Some(Update { uuid, property: "abc".into(), - value: true.into(), + value: Some("true".into()), timestamp, }), None, diff --git a/src/taskdb.rs b/src/taskdb.rs index 389f1d47c..1cd022a3d 100644 --- a/src/taskdb.rs +++ b/src/taskdb.rs @@ -1,16 +1,17 @@ use crate::operation::Operation; use crate::server::{Server, VersionAdd}; use serde::{Deserialize, Serialize}; -use serde_json::Value; use std::collections::hash_map::Entry; use std::collections::HashMap; use std::str; use uuid::Uuid; +type TaskMap = HashMap; + #[derive(PartialEq, Debug, Clone)] pub struct DB { // The current state, with all pending operations applied - tasks: HashMap>, + tasks: HashMap, // The version at which `operations` begins base_version: u64, @@ -56,17 +57,24 @@ impl DB { } => { // update if this task exists, otherwise ignore if let Some(task) = self.tasks.get_mut(uuid) { - task.insert(property.clone(), value.clone()); + DB::apply_update(task, property, value); } } }; self.operations.push(op); } + fn apply_update(task: &mut TaskMap, property: &str, value: &Option) { + match value { + Some(ref val) => task.insert(property.to_string(), val.clone()), + None => task.remove(property), + }; + } + /// Get a read-only reference to the underlying set of tasks. /// /// This API is temporary, but provides query access to the DB. - pub fn tasks(&self) -> &HashMap> { + pub fn tasks(&self) -> &HashMap { &self.tasks } @@ -194,19 +202,58 @@ mod tests { let op2 = Operation::Update { uuid, property: String::from("title"), - value: Value::from("\"my task\""), + value: Some("my task".into()), timestamp: Utc::now(), }; db.apply(op2.clone()); let mut exp = HashMap::new(); let mut task = HashMap::new(); - task.insert(String::from("title"), Value::from("\"my task\"")); + task.insert(String::from("title"), String::from("my task")); exp.insert(uuid, task); assert_eq!(db.tasks(), &exp); assert_eq!(db.operations, vec![op1, op2]); } + #[test] + fn test_apply_create_update_delete_prop() { + let mut db = DB::new(); + let uuid = Uuid::new_v4(); + let op1 = Operation::Create { uuid }; + db.apply(op1.clone()); + + let op2 = Operation::Update { + uuid, + property: String::from("title"), + value: Some("my task".into()), + timestamp: Utc::now(), + }; + db.apply(op2.clone()); + + let op3 = Operation::Update { + uuid, + property: String::from("priority"), + value: Some("H".into()), + timestamp: Utc::now(), + }; + db.apply(op3.clone()); + + let op4 = Operation::Update { + uuid, + property: String::from("title"), + value: None, + timestamp: Utc::now(), + }; + db.apply(op4.clone()); + + let mut exp = HashMap::new(); + let mut task = HashMap::new(); + task.insert(String::from("priority"), String::from("H")); + exp.insert(uuid, task); + assert_eq!(db.tasks(), &exp); + assert_eq!(db.operations, vec![op1, op2, op3, op4]); + } + #[test] fn test_apply_update_does_not_exist() { let mut db = DB::new(); @@ -214,7 +261,7 @@ mod tests { let op = Operation::Update { uuid, property: String::from("title"), - value: Value::from("\"my task\""), + value: Some("my task".into()), timestamp: Utc::now(), }; db.apply(op.clone()); diff --git a/tests/operation_transform_invariant.rs b/tests/operation_transform_invariant.rs index 61b0d9816..a88fbeb79 100644 --- a/tests/operation_transform_invariant.rs +++ b/tests/operation_transform_invariant.rs @@ -20,7 +20,7 @@ fn operation_strategy() -> impl Strategy { Operation::Update { uuid, property, - value: true.into(), + value: Some("true".into()), timestamp: Utc::now(), } }), diff --git a/tests/sync.rs b/tests/sync.rs index 055f88779..ed71eb7c0 100644 --- a/tests/sync.rs +++ b/tests/sync.rs @@ -18,7 +18,7 @@ fn test_sync() { db1.apply(Operation::Update { uuid: uuid1, property: "title".into(), - value: "my first task".into(), + value: Some("my first task".into()), timestamp: Utc::now(), }); @@ -27,7 +27,7 @@ fn test_sync() { db2.apply(Operation::Update { uuid: uuid2, property: "title".into(), - value: "my second task".into(), + value: Some("my second task".into()), timestamp: Utc::now(), }); @@ -41,13 +41,13 @@ fn test_sync() { db1.apply(Operation::Update { uuid: uuid2, property: "priority".into(), - value: "H".into(), + value: Some("H".into()), timestamp: Utc::now(), }); db2.apply(Operation::Update { uuid: uuid2, property: "project".into(), - value: "personal".into(), + value: Some("personal".into()), timestamp: Utc::now(), });