add dependency support to taskchampion

This commit is contained in:
Dustin J. Mitchell
2022-02-21 21:22:18 +00:00
parent a030053dae
commit bf73cc4cc7
4 changed files with 154 additions and 1 deletions

View File

@@ -520,6 +520,43 @@ static void test_task_udas(void) {
tc_replica_free(rep);
}
// dependency manipulation
static void test_task_dependencies(void) {
TCReplica *rep = tc_replica_new_in_memory();
TEST_ASSERT_NULL(tc_replica_error(rep).ptr);
TCTask *task1 = tc_replica_new_task(rep, TC_STATUS_PENDING, tc_string_borrow("task 1"));
TEST_ASSERT_NOT_NULL(task1);
TCTask *task2 = tc_replica_new_task(rep, TC_STATUS_PENDING, tc_string_borrow("task 2"));
TEST_ASSERT_NOT_NULL(task2);
TCUuidList deps;
deps = tc_task_get_dependencies(task1);
TEST_ASSERT_EQUAL(0, deps.len);
tc_uuid_list_free(&deps);
tc_task_to_mut(task1, rep);
TEST_ASSERT_EQUAL(TC_RESULT_OK,
tc_task_add_dependency(task1, tc_task_get_uuid(task2)));
deps = tc_task_get_dependencies(task1);
TEST_ASSERT_EQUAL(1, deps.len);
TEST_ASSERT_EQUAL_MEMORY(tc_task_get_uuid(task2).bytes, deps.items[0].bytes, 16);
tc_uuid_list_free(&deps);
TEST_ASSERT_EQUAL(TC_RESULT_OK,
tc_task_remove_dependency(task1, tc_task_get_uuid(task2)));
deps = tc_task_get_dependencies(task1);
TEST_ASSERT_EQUAL(0, deps.len);
tc_uuid_list_free(&deps);
tc_task_free(task1);
tc_task_free(task2);
tc_replica_free(rep);
}
static void tckvlist_assert_key(TCKVList *list, char *key, char *value) {
TEST_ASSERT_NOT_NULL(list);
for (size_t i = 0; i < list->len; i++) {
@@ -624,6 +661,7 @@ int task_tests(void) {
RUN_TEST(test_task_get_tags);
RUN_TEST(test_task_annotations);
RUN_TEST(test_task_udas);
RUN_TEST(test_task_dependencies);
RUN_TEST(test_task_taskmap);
RUN_TEST(test_task_list_take);
return UNITY_END();

View File

@@ -6,7 +6,7 @@ use std::ops::Deref;
use std::ptr::NonNull;
use std::str::FromStr;
use taskchampion::chrono::{TimeZone, Utc};
use taskchampion::{Annotation, Tag, Task, TaskMut};
use taskchampion::{Annotation, Tag, Task, TaskMut, Uuid};
/// A task, as publicly exposed by this library.
///
@@ -790,6 +790,56 @@ pub unsafe extern "C" fn tc_task_remove_legacy_uda(task: *mut TCTask, key: TCStr
)
}
/// Get all dependencies for a task.
#[no_mangle]
pub unsafe extern "C" fn tc_task_get_dependencies(task: *mut TCTask) -> TCUuidList {
wrap(task, |task| {
let vec: Vec<TCUuid> = task
.get_dependencies()
.map(|u| {
// SAFETY:
// - value is not allocated
unsafe { TCUuid::return_val(u) }
})
.collect();
// SAFETY:
// - caller will free this list
unsafe { TCUuidList::return_val(vec) }
})
}
/// Add a dependency.
#[no_mangle]
pub unsafe extern "C" fn tc_task_add_dependency(task: *mut TCTask, dep: TCUuid) -> TCResult {
// SAFETY:
// - tcuuid is a valid TCUuid (all byte patterns are valid)
let dep: Uuid = unsafe { TCUuid::val_from_arg(dep) };
wrap_mut(
task,
|task| {
task.add_dependency(dep)?;
Ok(TCResult::Ok)
},
TCResult::Error,
)
}
/// Remove a dependency.
#[no_mangle]
pub unsafe extern "C" fn tc_task_remove_dependency(task: *mut TCTask, dep: TCUuid) -> TCResult {
// SAFETY:
// - tcuuid is a valid TCUuid (all byte patterns are valid)
let dep: Uuid = unsafe { TCUuid::val_from_arg(dep) };
wrap_mut(
task,
|task| {
task.remove_dependency(dep)?;
Ok(TCResult::Ok)
},
TCResult::Error,
)
}
/// Get the latest error for a task, or a string NULL ptr field if the last operation succeeded.
/// Subsequent calls to this function will return NULL. The task pointer must not be NULL. The
/// caller must free the returned string.

View File

@@ -911,6 +911,21 @@ TCResult tc_task_set_legacy_uda(struct TCTask *task, struct TCString key, struct
*/
TCResult tc_task_remove_legacy_uda(struct TCTask *task, struct TCString key);
/**
* Get all dependencies for a task.
*/
struct TCUuidList tc_task_get_dependencies(struct TCTask *task);
/**
* Add a dependency.
*/
TCResult tc_task_add_dependency(struct TCTask *task, struct TCUuid dep);
/**
* Remove a dependency.
*/
TCResult tc_task_remove_dependency(struct TCTask *task, struct TCUuid dep);
/**
* Get the latest error for a task, or a string NULL ptr field if the last operation succeeded.
* Subsequent calls to this function will return NULL. The task pointer must not be NULL. The

View File

@@ -243,10 +243,27 @@ impl Task {
.map(|(p, v)| (p.as_ref(), v.as_ref()))
}
/// Get the modification time for this task.
pub fn get_modified(&self) -> Option<DateTime<Utc>> {
self.get_timestamp(Prop::Modified.as_ref())
}
/// Get the UUIDs of tasks on which this task depends.
///
/// This includes all dependencies, regardless of their status. In fact, it may include
/// dependencies that do not exist.
pub fn get_dependencies(&self) -> impl Iterator<Item = Uuid> + '_ {
self.taskmap.iter().filter_map(|(p, _)| {
if let Some(dep_str) = p.strip_prefix("dep_") {
if let Ok(u) = Uuid::parse_str(dep_str) {
return Some(u);
}
// (un-parseable dep_.. properties are ignored)
}
None
})
}
// -- utility functions
fn is_known_key(key: &str) -> bool {
@@ -423,6 +440,18 @@ impl<'r> TaskMut<'r> {
self.set_string(key, None)
}
/// Add a dependency.
pub fn add_dependency(&mut self, dep: Uuid) -> anyhow::Result<()> {
let key = format!("dep_{}", dep);
self.set_string(key, Some("".to_string()))
}
/// Remove a dependency.
pub fn remove_dependency(&mut self, dep: Uuid) -> anyhow::Result<()> {
let key = format!("dep_{}", dep);
self.set_string(key, None)
}
// -- utility functions
fn update_modified(&mut self) -> anyhow::Result<()> {
@@ -1011,4 +1040,25 @@ mod test {
assert!(task.remove_legacy_uda("tag_abc").is_err());
})
}
#[test]
fn test_dependencies() {
with_mut_task(|mut task| {
assert_eq!(task.get_dependencies().collect::<Vec<_>>(), vec![]);
let dep1 = Uuid::new_v4();
let dep2 = Uuid::new_v4();
task.add_dependency(dep1).unwrap();
assert_eq!(task.get_dependencies().collect::<Vec<_>>(), vec![dep1]);
task.add_dependency(dep1).unwrap(); // add twice is ok
task.add_dependency(dep2).unwrap();
let deps = task.get_dependencies().collect::<Vec<_>>();
assert!(deps.contains(&dep1));
assert!(deps.contains(&dep2));
task.remove_dependency(dep1).unwrap();
assert_eq!(task.get_dependencies().collect::<Vec<_>>(), vec![dep2]);
})
}
}