From 213da88b27d8a7a5fa33a2c21f2a21468cffda3a Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sun, 13 Feb 2022 02:05:05 +0000 Subject: [PATCH] add tc_task_get_taskmap --- integration-tests/src/bindings_tests/task.c | 42 ++++++++ lib/src/kv.rs | 103 ++++++++++++++++++++ lib/src/lib.rs | 2 + lib/src/task.rs | 19 +++- lib/taskchampion.h | 47 +++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 lib/src/kv.rs diff --git a/integration-tests/src/bindings_tests/task.c b/integration-tests/src/bindings_tests/task.c index 5dab6c510..7f89ddc51 100644 --- a/integration-tests/src/bindings_tests/task.c +++ b/integration-tests/src/bindings_tests/task.c @@ -520,6 +520,47 @@ static void test_task_udas(void) { 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++) { + if (0 == strcmp(tc_string_content(list->items[i].key), key)) { + TEST_ASSERT_EQUAL_STRING(value, tc_string_content(list->items[i].value)); + return; + } + } + TEST_FAIL_MESSAGE("key not found"); +} + +// get_tags returns the list of tags +static void test_task_taskmap(void) { + TCReplica *rep = tc_replica_new_in_memory(); + TEST_ASSERT_NULL(tc_replica_error(rep)); + + TCTask *task = tc_replica_new_task(rep, TC_STATUS_PENDING, tc_string_borrow("my task")); + TEST_ASSERT_NOT_NULL(task); + + tc_task_to_mut(task, rep); + + TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_add_tag(task, tc_string_borrow("next"))); + + TCAnnotation ann; + ann.entry = 1644623411; + ann.description = tc_string_borrow("ann1"); + TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_add_annotation(task, &ann)); + + TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_set_wait(task, 3643679997)); // 2085 + + TCKVList taskmap = tc_task_get_taskmap(task); + tckvlist_assert_key(&taskmap, "annotation_1644623411", "ann1"); + tckvlist_assert_key(&taskmap, "tag_next", ""); + tckvlist_assert_key(&taskmap, "status", "pending"); + tckvlist_assert_key(&taskmap, "description", "my task"); + tc_kv_list_free(&taskmap); + + tc_task_free(task); + tc_replica_free(rep); +} + int task_tests(void) { UNITY_BEGIN(); // each test case above should be named here, in order. @@ -536,5 +577,6 @@ int task_tests(void) { RUN_TEST(test_task_get_tags); RUN_TEST(test_task_annotations); RUN_TEST(test_task_udas); + RUN_TEST(test_task_taskmap); return UNITY_END(); } diff --git a/lib/src/kv.rs b/lib/src/kv.rs new file mode 100644 index 000000000..3b24e25cf --- /dev/null +++ b/lib/src/kv.rs @@ -0,0 +1,103 @@ +use crate::traits::*; +use crate::types::*; + +/// TCKV contains a key/value pair that is part of a task. +/// +/// Neither key nor value are ever NULL. They remain owned by the TCKV and +/// will be freed when it is freed with tc_kv_list_free. +#[repr(C)] +pub struct TCKV { + pub key: *mut TCString<'static>, + pub value: *mut TCString<'static>, +} + +impl PassByValue for TCKV { + type RustType = (TCString<'static>, TCString<'static>); + + unsafe fn from_ctype(self) -> Self::RustType { + // SAFETY: + // - self is owned, so we can take ownership of this TCString + // - self.key is a valid, non-null TCString (see type docstring) + let key = unsafe { TCString::take_from_arg(self.key) }; + // SAFETY: (same) + let value = unsafe { TCString::take_from_arg(self.value) }; + (key, value) + } + + fn as_ctype((key, value): Self::RustType) -> Self { + TCKV { + // SAFETY: caller assumes ownership of this value + key: unsafe { key.return_val() }, + // SAFETY: caller assumes ownership of this value + value: unsafe { value.return_val() }, + } + } +} + +/// TCKVList represents a list of key/value pairs. +/// +/// The content of this struct must be treated as read-only. +#[repr(C)] +pub struct TCKVList { + /// number of key/value pairs in items + len: libc::size_t, + + /// total size of items (internal use only) + _capacity: libc::size_t, + + /// array of TCKV's. these remain owned by the TCKVList instance and will be freed by + /// tc_kv_list_free. This pointer is never NULL for a valid TCKVList. + items: *const TCKV, +} + +impl CList for TCKVList { + type Element = TCKV; + + unsafe fn from_raw_parts(items: *const Self::Element, len: usize, cap: usize) -> Self { + TCKVList { + len, + _capacity: cap, + items, + } + } + + fn into_raw_parts(self) -> (*const Self::Element, usize, usize) { + (self.items, self.len, self._capacity) + } +} + +/// Free a TCKVList instance. The instance, and all TCKVs it contains, must not be used after +/// this call. +/// +/// When this call returns, the `items` pointer will be NULL, signalling an invalid TCKVList. +#[no_mangle] +pub unsafe extern "C" fn tc_kv_list_free(tckvs: *mut TCKVList) { + // SAFETY: + // - tckvs is not NULL and points to a valid TCKVList (caller is not allowed to + // modify the list) + // - caller promises not to use the value after return + unsafe { drop_value_list(tckvs) } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn empty_list_has_non_null_pointer() { + let tckvs = TCKVList::return_val(Vec::new()); + assert!(!tckvs.items.is_null()); + assert_eq!(tckvs.len, 0); + assert_eq!(tckvs._capacity, 0); + } + + #[test] + fn free_sets_null_pointer() { + let mut tckvs = TCKVList::return_val(Vec::new()); + // SAFETY: testing expected behavior + unsafe { tc_kv_list_free(&mut tckvs) }; + assert!(tckvs.items.is_null()); + assert_eq!(tckvs.len, 0); + assert_eq!(tckvs._capacity, 0); + } +} diff --git a/lib/src/lib.rs b/lib/src/lib.rs index b49acf77c..4c66ec76d 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -7,6 +7,7 @@ mod util; pub mod annotation; pub mod atomic; +pub mod kv; pub mod replica; pub mod result; pub mod status; @@ -18,6 +19,7 @@ pub mod workingset; pub(crate) mod types { pub(crate) use crate::annotation::{TCAnnotation, TCAnnotationList}; + pub(crate) use crate::kv::{TCKVList, TCKV}; pub(crate) use crate::replica::TCReplica; pub(crate) use crate::result::TCResult; pub(crate) use crate::status::TCStatus; diff --git a/lib/src/task.rs b/lib/src/task.rs index a20b2049f..421072373 100644 --- a/lib/src/task.rs +++ b/lib/src/task.rs @@ -243,7 +243,24 @@ pub unsafe extern "C" fn tc_task_get_status<'a>(task: *mut TCTask) -> TCStatus { wrap(task, |task| task.get_status().into()) } -// TODO: tc_task_get_taskmap (?? then we have to wrap a map..) +/// Get the underlying key/value pairs for this task. The returned TCKVList is +/// a "snapshot" of the task and will not be updated if the task is subsequently +/// modified. +#[no_mangle] +pub unsafe extern "C" fn tc_task_get_taskmap(task: *mut TCTask) -> TCKVList { + wrap(task, |task| { + let vec: Vec = task + .get_taskmap() + .iter() + .map(|(k, v)| { + let key = TCString::from(k.as_ref()); + let value = TCString::from(v.as_ref()); + TCKV::as_ctype((key, value)) + }) + .collect(); + TCKVList::return_val(vec) + }) +} /// Get a task's description, or NULL if the task cannot be represented as a C string (e.g., if it /// contains embedded NUL characters). diff --git a/lib/taskchampion.h b/lib/taskchampion.h index 79b25ba35..bb41d4e5a 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -161,6 +161,38 @@ typedef struct TCAnnotationList { const struct TCAnnotation *items; } TCAnnotationList; +/** + * TCKV contains a key/value pair that is part of a task. + * + * Neither key nor value are ever NULL. They remain owned by the TCKV and + * will be freed when it is freed with tc_kv_list_free. + */ +typedef struct TCKV { + struct TCString *key; + struct TCString *value; +} TCKV; + +/** + * TCKVList represents a list of key/value pairs. + * + * The content of this struct must be treated as read-only. + */ +typedef struct TCKVList { + /** + * number of key/value pairs in items + */ + size_t len; + /** + * total size of items (internal use only) + */ + size_t _capacity; + /** + * array of TCKV's. these remain owned by the TCKVList instance and will be freed by + * tc_kv_list_free. This pointer is never NULL for a valid TCKVList. + */ + const struct TCKV *items; +} TCKVList; + /** * TCTaskList represents a list of tasks. * @@ -292,6 +324,14 @@ void tc_annotation_free(struct TCAnnotation *tcann); */ void tc_annotation_list_free(struct TCAnnotationList *tcanns); +/** + * Free a TCKVList instance. The instance, and all TCKVs it contains, must not be used after + * this call. + * + * When this call returns, the `items` pointer will be NULL, signalling an invalid TCKVList. + */ +void tc_kv_list_free(struct TCKVList *tckvs); + /** * Create a new TCReplica with an in-memory database. The contents of the database will be * lost when it is freed. @@ -493,6 +533,13 @@ struct TCUuid tc_task_get_uuid(struct TCTask *task); */ enum TCStatus tc_task_get_status(struct TCTask *task); +/** + * Get the underlying key/value pairs for this task. The returned TCKVList is + * a "snapshot" of the task and will not be updated if the task is subsequently + * modified. + */ +struct TCKVList tc_task_get_taskmap(struct TCTask *task); + /** * Get a task's description, or NULL if the task cannot be represented as a C string (e.g., if it * contains embedded NUL characters).