From bf73cc4cc749543272bee15619a6957224d1c724 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Mon, 21 Feb 2022 21:22:18 +0000 Subject: [PATCH] add dependency support to taskchampion --- integration-tests/src/bindings_tests/task.c | 38 +++++++++++++++ lib/src/task.rs | 52 ++++++++++++++++++++- lib/taskchampion.h | 15 ++++++ taskchampion/src/task/task.rs | 50 ++++++++++++++++++++ 4 files changed, 154 insertions(+), 1 deletion(-) diff --git a/integration-tests/src/bindings_tests/task.c b/integration-tests/src/bindings_tests/task.c index 4e20e52b3..4a30f8e1c 100644 --- a/integration-tests/src/bindings_tests/task.c +++ b/integration-tests/src/bindings_tests/task.c @@ -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(); diff --git a/lib/src/task.rs b/lib/src/task.rs index ee4709a38..dce84d7aa 100644 --- a/lib/src/task.rs +++ b/lib/src/task.rs @@ -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 = 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. diff --git a/lib/taskchampion.h b/lib/taskchampion.h index 8c5f02575..a4ec0061e 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -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 diff --git a/taskchampion/src/task/task.rs b/taskchampion/src/task/task.rs index cd8a12f85..fa8700a62 100644 --- a/taskchampion/src/task/task.rs +++ b/taskchampion/src/task/task.rs @@ -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> { 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 + '_ { + 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![]); + let dep1 = Uuid::new_v4(); + let dep2 = Uuid::new_v4(); + + task.add_dependency(dep1).unwrap(); + assert_eq!(task.get_dependencies().collect::>(), vec![dep1]); + + task.add_dependency(dep1).unwrap(); // add twice is ok + task.add_dependency(dep2).unwrap(); + let deps = task.get_dependencies().collect::>(); + assert!(deps.contains(&dep1)); + assert!(deps.contains(&dep2)); + + task.remove_dependency(dep1).unwrap(); + assert_eq!(task.get_dependencies().collect::>(), vec![dep2]); + }) + } }