mirror of
https://github.com/GothenburgBitFactory/taskwarrior.git
synced 2025-07-07 20:06:36 +02:00
use strings as values, with option to allow removing
This commit is contained in:
parent
e5bd258e84
commit
e83bdc28cd
6 changed files with 89 additions and 39 deletions
|
@ -15,8 +15,7 @@ The data model is only seen from the clients' perspective.
|
||||||
## Task Database
|
## Task Database
|
||||||
|
|
||||||
The task database is composed of an un-ordered collection of tasks, each keyed by a UUID.
|
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.
|
Each task has an arbitrary-sized set of key/value properties, with string values.
|
||||||
A property with a `null` value is considered equivalent to that property not being set on the task.
|
|
||||||
|
|
||||||
Tasks are only created, never deleted.
|
Tasks are only created, never deleted.
|
||||||
See below for details on how tasks can "expire" from the task database.
|
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.
|
The former form creates a new task.
|
||||||
It is invalid to create a task that already exists.
|
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.
|
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.
|
The timestamp on updates serves as additional metadata and is used to resolve conflicts.
|
||||||
|
|
||||||
|
|
15
TODO.txt
15
TODO.txt
|
@ -1,13 +1,9 @@
|
||||||
* assign types to properties
|
* assign types to properties
|
||||||
- modifications to types don't commute the same way
|
- db / operation model is just k/v, but formatted names can be used for
|
||||||
- optimize this to simplify the transform function
|
structure:
|
||||||
- types:
|
- dependencies: `dependency.<uuid> = ""`
|
||||||
- dependencies: set of uuids
|
- annotations: `annotation.<epoch time> = "annotation"`
|
||||||
- annotations: set of annotations (incl timestamps for uniqueness)
|
- tags: `tags.<tag> = ""`
|
||||||
- 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
|
|
||||||
* add HTTP API
|
* add HTTP API
|
||||||
* implement snapshot requests
|
* implement snapshot requests
|
||||||
* implement backups
|
* implement backups
|
||||||
|
@ -16,3 +12,4 @@
|
||||||
- need to be sure that create / delete operations don't get reversed
|
- need to be sure that create / delete operations don't get reversed
|
||||||
* cli tools
|
* cli tools
|
||||||
* prop testing for DB modifications
|
* prop testing for DB modifications
|
||||||
|
- 'strict' mode to fail on application of any nonsense operations
|
||||||
|
|
|
@ -1,17 +1,23 @@
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
/// An Operation defines a single change to the task database
|
||||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
|
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
|
||||||
pub enum Operation {
|
pub enum Operation {
|
||||||
Create {
|
/// Create a new task; if the task already exists in the DB.
|
||||||
uuid: Uuid,
|
///
|
||||||
},
|
/// 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 {
|
Update {
|
||||||
uuid: Uuid,
|
uuid: Uuid,
|
||||||
property: String,
|
property: String,
|
||||||
value: Value,
|
value: Option<String>,
|
||||||
timestamp: DateTime<Utc>,
|
timestamp: DateTime<Utc>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -142,25 +148,25 @@ mod test {
|
||||||
Update {
|
Update {
|
||||||
uuid,
|
uuid,
|
||||||
property: "abc".into(),
|
property: "abc".into(),
|
||||||
value: true.into(),
|
value: Some("true".into()),
|
||||||
timestamp,
|
timestamp,
|
||||||
},
|
},
|
||||||
Update {
|
Update {
|
||||||
uuid,
|
uuid,
|
||||||
property: "def".into(),
|
property: "def".into(),
|
||||||
value: false.into(),
|
value: Some("false".into()),
|
||||||
timestamp,
|
timestamp,
|
||||||
},
|
},
|
||||||
Some(Update {
|
Some(Update {
|
||||||
uuid,
|
uuid,
|
||||||
property: "abc".into(),
|
property: "abc".into(),
|
||||||
value: true.into(),
|
value: Some("true".into()),
|
||||||
timestamp,
|
timestamp,
|
||||||
}),
|
}),
|
||||||
Some(Update {
|
Some(Update {
|
||||||
uuid,
|
uuid,
|
||||||
property: "def".into(),
|
property: "def".into(),
|
||||||
value: false.into(),
|
value: Some("false".into()),
|
||||||
timestamp,
|
timestamp,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
@ -176,20 +182,20 @@ mod test {
|
||||||
Update {
|
Update {
|
||||||
uuid,
|
uuid,
|
||||||
property: "abc".into(),
|
property: "abc".into(),
|
||||||
value: true.into(),
|
value: Some("true".into()),
|
||||||
timestamp: timestamp1,
|
timestamp: timestamp1,
|
||||||
},
|
},
|
||||||
Update {
|
Update {
|
||||||
uuid,
|
uuid,
|
||||||
property: "abc".into(),
|
property: "abc".into(),
|
||||||
value: false.into(),
|
value: Some("false".into()),
|
||||||
timestamp: timestamp2,
|
timestamp: timestamp2,
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
Some(Update {
|
Some(Update {
|
||||||
uuid,
|
uuid,
|
||||||
property: "abc".into(),
|
property: "abc".into(),
|
||||||
value: false.into(),
|
value: Some("false".into()),
|
||||||
timestamp: timestamp2,
|
timestamp: timestamp2,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
@ -204,19 +210,19 @@ mod test {
|
||||||
Update {
|
Update {
|
||||||
uuid,
|
uuid,
|
||||||
property: "abc".into(),
|
property: "abc".into(),
|
||||||
value: true.into(),
|
value: Some("true".into()),
|
||||||
timestamp,
|
timestamp,
|
||||||
},
|
},
|
||||||
Update {
|
Update {
|
||||||
uuid,
|
uuid,
|
||||||
property: "abc".into(),
|
property: "abc".into(),
|
||||||
value: false.into(),
|
value: Some("false".into()),
|
||||||
timestamp,
|
timestamp,
|
||||||
},
|
},
|
||||||
Some(Update {
|
Some(Update {
|
||||||
uuid,
|
uuid,
|
||||||
property: "abc".into(),
|
property: "abc".into(),
|
||||||
value: true.into(),
|
value: Some("true".into()),
|
||||||
timestamp,
|
timestamp,
|
||||||
}),
|
}),
|
||||||
None,
|
None,
|
||||||
|
|
|
@ -1,16 +1,17 @@
|
||||||
use crate::operation::Operation;
|
use crate::operation::Operation;
|
||||||
use crate::server::{Server, VersionAdd};
|
use crate::server::{Server, VersionAdd};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
|
||||||
use std::collections::hash_map::Entry;
|
use std::collections::hash_map::Entry;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::str;
|
use std::str;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
type TaskMap = HashMap<String, String>;
|
||||||
|
|
||||||
#[derive(PartialEq, Debug, Clone)]
|
#[derive(PartialEq, Debug, Clone)]
|
||||||
pub struct DB {
|
pub struct DB {
|
||||||
// The current state, with all pending operations applied
|
// The current state, with all pending operations applied
|
||||||
tasks: HashMap<Uuid, HashMap<String, Value>>,
|
tasks: HashMap<Uuid, TaskMap>,
|
||||||
|
|
||||||
// The version at which `operations` begins
|
// The version at which `operations` begins
|
||||||
base_version: u64,
|
base_version: u64,
|
||||||
|
@ -56,17 +57,24 @@ impl DB {
|
||||||
} => {
|
} => {
|
||||||
// update if this task exists, otherwise ignore
|
// update if this task exists, otherwise ignore
|
||||||
if let Some(task) = self.tasks.get_mut(uuid) {
|
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);
|
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),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// Get a read-only reference to the underlying set of tasks.
|
/// Get a read-only reference to the underlying set of tasks.
|
||||||
///
|
///
|
||||||
/// This API is temporary, but provides query access to the DB.
|
/// This API is temporary, but provides query access to the DB.
|
||||||
pub fn tasks(&self) -> &HashMap<Uuid, HashMap<String, Value>> {
|
pub fn tasks(&self) -> &HashMap<Uuid, TaskMap> {
|
||||||
&self.tasks
|
&self.tasks
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -194,19 +202,58 @@ mod tests {
|
||||||
let op2 = Operation::Update {
|
let op2 = Operation::Update {
|
||||||
uuid,
|
uuid,
|
||||||
property: String::from("title"),
|
property: String::from("title"),
|
||||||
value: Value::from("\"my task\""),
|
value: Some("my task".into()),
|
||||||
timestamp: Utc::now(),
|
timestamp: Utc::now(),
|
||||||
};
|
};
|
||||||
db.apply(op2.clone());
|
db.apply(op2.clone());
|
||||||
|
|
||||||
let mut exp = HashMap::new();
|
let mut exp = HashMap::new();
|
||||||
let mut task = 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);
|
exp.insert(uuid, task);
|
||||||
assert_eq!(db.tasks(), &exp);
|
assert_eq!(db.tasks(), &exp);
|
||||||
assert_eq!(db.operations, vec![op1, op2]);
|
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]
|
#[test]
|
||||||
fn test_apply_update_does_not_exist() {
|
fn test_apply_update_does_not_exist() {
|
||||||
let mut db = DB::new();
|
let mut db = DB::new();
|
||||||
|
@ -214,7 +261,7 @@ mod tests {
|
||||||
let op = Operation::Update {
|
let op = Operation::Update {
|
||||||
uuid,
|
uuid,
|
||||||
property: String::from("title"),
|
property: String::from("title"),
|
||||||
value: Value::from("\"my task\""),
|
value: Some("my task".into()),
|
||||||
timestamp: Utc::now(),
|
timestamp: Utc::now(),
|
||||||
};
|
};
|
||||||
db.apply(op.clone());
|
db.apply(op.clone());
|
||||||
|
|
|
@ -20,7 +20,7 @@ fn operation_strategy() -> impl Strategy<Value = Operation> {
|
||||||
Operation::Update {
|
Operation::Update {
|
||||||
uuid,
|
uuid,
|
||||||
property,
|
property,
|
||||||
value: true.into(),
|
value: Some("true".into()),
|
||||||
timestamp: Utc::now(),
|
timestamp: Utc::now(),
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -18,7 +18,7 @@ fn test_sync() {
|
||||||
db1.apply(Operation::Update {
|
db1.apply(Operation::Update {
|
||||||
uuid: uuid1,
|
uuid: uuid1,
|
||||||
property: "title".into(),
|
property: "title".into(),
|
||||||
value: "my first task".into(),
|
value: Some("my first task".into()),
|
||||||
timestamp: Utc::now(),
|
timestamp: Utc::now(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ fn test_sync() {
|
||||||
db2.apply(Operation::Update {
|
db2.apply(Operation::Update {
|
||||||
uuid: uuid2,
|
uuid: uuid2,
|
||||||
property: "title".into(),
|
property: "title".into(),
|
||||||
value: "my second task".into(),
|
value: Some("my second task".into()),
|
||||||
timestamp: Utc::now(),
|
timestamp: Utc::now(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -41,13 +41,13 @@ fn test_sync() {
|
||||||
db1.apply(Operation::Update {
|
db1.apply(Operation::Update {
|
||||||
uuid: uuid2,
|
uuid: uuid2,
|
||||||
property: "priority".into(),
|
property: "priority".into(),
|
||||||
value: "H".into(),
|
value: Some("H".into()),
|
||||||
timestamp: Utc::now(),
|
timestamp: Utc::now(),
|
||||||
});
|
});
|
||||||
db2.apply(Operation::Update {
|
db2.apply(Operation::Update {
|
||||||
uuid: uuid2,
|
uuid: uuid2,
|
||||||
property: "project".into(),
|
property: "project".into(),
|
||||||
value: "personal".into(),
|
value: Some("personal".into()),
|
||||||
timestamp: Utc::now(),
|
timestamp: Utc::now(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue