From ad560fdb79f9a9cb42cec088ac5b881c03eda106 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sat, 12 Feb 2022 16:22:45 +0000 Subject: [PATCH] add UDA support --- integration-tests/src/bindings_tests/task.c | 135 +++++++++++++++ lib/src/lib.rs | 2 + lib/src/task.rs | 178 +++++++++++++++++++- lib/src/uda.rs | 148 ++++++++++++++++ lib/taskchampion.h | 105 ++++++++++++ 5 files changed, 560 insertions(+), 8 deletions(-) create mode 100644 lib/src/uda.rs diff --git a/integration-tests/src/bindings_tests/task.c b/integration-tests/src/bindings_tests/task.c index 0eb1e45c0..5dab6c510 100644 --- a/integration-tests/src/bindings_tests/task.c +++ b/integration-tests/src/bindings_tests/task.c @@ -386,6 +386,140 @@ static void test_task_annotations(void) { tc_replica_free(rep); } +// UDA manipulation (add, remove, list, free) +static void test_task_udas(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); + + TCString *value; + TCUDAList udas; + + TEST_ASSERT_NULL(tc_task_get_uda(task, tc_string_borrow("ns"), tc_string_borrow("u1"))); + TEST_ASSERT_NULL(tc_task_get_legacy_uda(task, tc_string_borrow("leg1"))); + + udas = tc_task_get_udas(task); + TEST_ASSERT_NOT_NULL(udas.items); + TEST_ASSERT_EQUAL(0, udas.len); + tc_uda_list_free(&udas); + + udas = tc_task_get_legacy_udas(task); + TEST_ASSERT_NOT_NULL(udas.items); + TEST_ASSERT_EQUAL(0, udas.len); + tc_uda_list_free(&udas); + + TEST_ASSERT_EQUAL(TC_RESULT_OK, + tc_task_set_uda(task, + tc_string_borrow("ns"), + tc_string_borrow("u1"), + tc_string_borrow("vvv"))); + + value = tc_task_get_uda(task, tc_string_borrow("ns"), tc_string_borrow("u1")); + TEST_ASSERT_NOT_NULL(value); + TEST_ASSERT_EQUAL_STRING("vvv", tc_string_content(value)); + tc_string_free(value); + TEST_ASSERT_NULL(tc_task_get_legacy_uda(task, tc_string_borrow("leg1"))); + + udas = tc_task_get_udas(task); + TEST_ASSERT_NOT_NULL(udas.items); + TEST_ASSERT_EQUAL(1, udas.len); + TEST_ASSERT_EQUAL_STRING("ns", tc_string_content(udas.items[0].ns)); + TEST_ASSERT_EQUAL_STRING("u1", tc_string_content(udas.items[0].key)); + TEST_ASSERT_EQUAL_STRING("vvv", tc_string_content(udas.items[0].value)); + tc_uda_list_free(&udas); + + udas = tc_task_get_legacy_udas(task); + TEST_ASSERT_NOT_NULL(udas.items); + TEST_ASSERT_EQUAL(1, udas.len); + TEST_ASSERT_NULL(udas.items[0].ns); + TEST_ASSERT_EQUAL_STRING("ns.u1", tc_string_content(udas.items[0].key)); + TEST_ASSERT_EQUAL_STRING("vvv", tc_string_content(udas.items[0].value)); + tc_uda_list_free(&udas); + + TEST_ASSERT_EQUAL(TC_RESULT_OK, + tc_task_set_legacy_uda(task, + tc_string_borrow("leg1"), + tc_string_borrow("legv"))); + + value = tc_task_get_uda(task, tc_string_borrow("ns"), tc_string_borrow("u1")); + TEST_ASSERT_NOT_NULL(value); + TEST_ASSERT_EQUAL_STRING("vvv", tc_string_content(value)); + tc_string_free(value); + + value = tc_task_get_legacy_uda(task, tc_string_borrow("leg1")); + TEST_ASSERT_NOT_NULL(value); + TEST_ASSERT_EQUAL_STRING("legv", tc_string_content(value)); + tc_string_free(value); + + udas = tc_task_get_udas(task); + TEST_ASSERT_NOT_NULL(udas.items); + TEST_ASSERT_EQUAL(2, udas.len); + tc_uda_list_free(&udas); + + udas = tc_task_get_legacy_udas(task); + TEST_ASSERT_NOT_NULL(udas.items); + TEST_ASSERT_EQUAL(2, udas.len); + tc_uda_list_free(&udas); + + TEST_ASSERT_EQUAL(TC_RESULT_OK, + tc_task_remove_uda(task, + tc_string_borrow("ns"), + tc_string_borrow("u1"))); + + TEST_ASSERT_NULL(tc_task_get_uda(task, tc_string_borrow("ns"), tc_string_borrow("u1"))); + + TEST_ASSERT_EQUAL(TC_RESULT_OK, + tc_task_remove_uda(task, + tc_string_borrow("ns"), + tc_string_borrow("u1"))); + + udas = tc_task_get_udas(task); + TEST_ASSERT_NOT_NULL(udas.items); + TEST_ASSERT_EQUAL(1, udas.len); + TEST_ASSERT_EQUAL_STRING("", tc_string_content(udas.items[0].ns)); + TEST_ASSERT_EQUAL_STRING("leg1", tc_string_content(udas.items[0].key)); + TEST_ASSERT_EQUAL_STRING("legv", tc_string_content(udas.items[0].value)); + tc_uda_list_free(&udas); + + udas = tc_task_get_legacy_udas(task); + TEST_ASSERT_NOT_NULL(udas.items); + TEST_ASSERT_EQUAL(1, udas.len); + TEST_ASSERT_NULL(udas.items[0].ns); + TEST_ASSERT_EQUAL_STRING("leg1", tc_string_content(udas.items[0].key)); + TEST_ASSERT_EQUAL_STRING("legv", tc_string_content(udas.items[0].value)); + tc_uda_list_free(&udas); + + TEST_ASSERT_EQUAL(TC_RESULT_OK, + tc_task_remove_legacy_uda(task, + tc_string_borrow("leg1"))); + + TEST_ASSERT_NULL(tc_task_get_legacy_uda(task, tc_string_borrow("leg1"))); + + TEST_ASSERT_EQUAL(TC_RESULT_OK, + tc_task_remove_legacy_uda(task, + tc_string_borrow("leg1"))); + + udas = tc_task_get_udas(task); + TEST_ASSERT_NOT_NULL(udas.items); + TEST_ASSERT_EQUAL(0, udas.len); + tc_uda_list_free(&udas); + + udas = tc_task_get_legacy_udas(task); + TEST_ASSERT_NOT_NULL(udas.items); + TEST_ASSERT_EQUAL(0, udas.len); + tc_uda_list_free(&udas); + + tc_task_free(task); + tc_replica_free(rep); +} + int task_tests(void) { UNITY_BEGIN(); // each test case above should be named here, in order. @@ -401,5 +535,6 @@ int task_tests(void) { RUN_TEST(test_task_add_remove_has_tag); RUN_TEST(test_task_get_tags); RUN_TEST(test_task_annotations); + RUN_TEST(test_task_udas); return UNITY_END(); } diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 41fa16005..5bc7629a3 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -12,6 +12,7 @@ pub mod result; pub mod status; pub mod string; pub mod task; +pub mod uda; pub mod uuid; pub(crate) mod types { @@ -21,5 +22,6 @@ pub(crate) mod types { pub(crate) use crate::status::TCStatus; pub(crate) use crate::string::{TCString, TCStringList}; pub(crate) use crate::task::{TCTask, TCTaskList}; + pub(crate) use crate::uda::{TCUDAList, TCUDA, UDA}; pub(crate) use crate::uuid::{TCUuid, TCUuidList}; } diff --git a/lib/src/task.rs b/lib/src/task.rs index 390386e93..a20b2049f 100644 --- a/lib/src/task.rs +++ b/lib/src/task.rs @@ -340,10 +340,89 @@ pub unsafe extern "C" fn tc_task_get_annotations<'a>(task: *mut TCTask) -> TCAnn }) } -// TODO: tc_task_get_uda -// TODO: tc_task_get_udas -// TODO: tc_task_get_legacy_uda -// TODO: tc_task_get_legacy_udas +/// Get the named UDA from the task. +/// +/// Returns NULL if the UDA does not exist. +#[no_mangle] +pub unsafe extern "C" fn tc_task_get_uda<'a>( + task: *mut TCTask, + ns: *mut TCString<'a>, + key: *mut TCString<'a>, +) -> *mut TCString<'static> { + wrap(task, |task| { + if let Ok(ns) = unsafe { TCString::take_from_arg(ns) }.as_str() { + if let Ok(key) = unsafe { TCString::take_from_arg(key) }.as_str() { + if let Some(value) = task.get_uda(ns, key) { + // SAFETY: + // - caller will free this string (caller promises) + return unsafe { TCString::return_val(value.into()) }; + } + } + } + std::ptr::null_mut() + }) +} + +/// Get the named legacy UDA from the task. +/// +/// Returns NULL if the UDA does not exist. +#[no_mangle] +pub unsafe extern "C" fn tc_task_get_legacy_uda<'a>( + task: *mut TCTask, + key: *mut TCString<'a>, +) -> *mut TCString<'static> { + wrap(task, |task| { + if let Ok(key) = unsafe { TCString::take_from_arg(key) }.as_str() { + if let Some(value) = task.get_legacy_uda(key) { + // SAFETY: + // - caller will free this string (caller promises) + return unsafe { TCString::return_val(value.into()) }; + } + } + std::ptr::null_mut() + }) +} + +/// Get all UDAs for this task. +/// +/// Legacy UDAs are represented with an empty string in the ns field. +#[no_mangle] +pub unsafe extern "C" fn tc_task_get_udas<'a>(task: *mut TCTask) -> TCUDAList { + wrap(task, |task| { + let vec: Vec = task + .get_udas() + .map(|((ns, key), value)| { + TCUDA::return_val(UDA { + ns: Some(ns.into()), + key: key.into(), + value: value.into(), + }) + }) + .collect(); + TCUDAList::return_val(vec) + }) +} + +/// Get all UDAs for this task. +/// +/// All TCUDAs in this list have a NULL ns field. The entire UDA key is +/// included in the key field. +#[no_mangle] +pub unsafe extern "C" fn tc_task_get_legacy_udas<'a>(task: *mut TCTask) -> TCUDAList { + wrap(task, |task| { + let vec: Vec = task + .get_legacy_udas() + .map(|(key, value)| { + TCUDA::return_val(UDA { + ns: None, + key: key.into(), + value: value.into(), + }) + }) + .collect(); + TCUDAList::return_val(vec) + }) +} /// Set a mutable task's status. #[no_mangle] @@ -542,10 +621,93 @@ pub unsafe extern "C" fn tc_task_remove_annotation(task: *mut TCTask, entry: i64 ) } -// TODO: tc_task_set_uda -// TODO: tc_task_remove_uda -// TODO: tc_task_set_legacy_uda -// TODO: tc_task_remove_legacy_uda +/// Set a UDA on a mutable task. +#[no_mangle] +pub unsafe extern "C" fn tc_task_set_uda<'a>( + task: *mut TCTask, + ns: *mut TCString, + key: *mut TCString, + value: *mut TCString, +) -> TCResult { + // SAFETY: see TCString docstring + let ns = unsafe { TCString::take_from_arg(ns) }; + // SAFETY: see TCString docstring + let key = unsafe { TCString::take_from_arg(key) }; + // SAFETY: see TCString docstring + let value = unsafe { TCString::take_from_arg(value) }; + wrap_mut( + task, + |task| { + task.set_uda( + ns.as_str()?.to_string(), + key.as_str()?.to_string(), + value.as_str()?.to_string(), + )?; + Ok(TCResult::Ok) + }, + TCResult::Error, + ) +} + +/// Remove a UDA fraom a mutable task. +#[no_mangle] +pub unsafe extern "C" fn tc_task_remove_uda<'a>( + task: *mut TCTask, + ns: *mut TCString, + key: *mut TCString, +) -> TCResult { + // SAFETY: see TCString docstring + let ns = unsafe { TCString::take_from_arg(ns) }; + // SAFETY: see TCString docstring + let key = unsafe { TCString::take_from_arg(key) }; + wrap_mut( + task, + |task| { + task.remove_uda(ns.as_str()?.to_string(), key.as_str()?.to_string())?; + Ok(TCResult::Ok) + }, + TCResult::Error, + ) +} + +/// Set a legacy UDA on a mutable task. +#[no_mangle] +pub unsafe extern "C" fn tc_task_set_legacy_uda<'a>( + task: *mut TCTask, + key: *mut TCString, + value: *mut TCString, +) -> TCResult { + // SAFETY: see TCString docstring + let key = unsafe { TCString::take_from_arg(key) }; + // SAFETY: see TCString docstring + let value = unsafe { TCString::take_from_arg(value) }; + wrap_mut( + task, + |task| { + task.set_legacy_uda(key.as_str()?.to_string(), value.as_str()?.to_string())?; + Ok(TCResult::Ok) + }, + TCResult::Error, + ) +} + +/// Remove a UDA fraom a mutable task. +#[no_mangle] +pub unsafe extern "C" fn tc_task_remove_legacy_uda<'a>( + task: *mut TCTask, + key: *mut TCString, +) -> TCResult { + // SAFETY: see TCString docstring + let key = unsafe { TCString::take_from_arg(key) }; + wrap_mut( + task, + |task| { + task.remove_legacy_uda(key.as_str()?.to_string())?; + Ok(TCResult::Ok) + }, + TCResult::Error, + ) +} /// Get the latest error for a task, or NULL 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 diff --git a/lib/src/uda.rs b/lib/src/uda.rs new file mode 100644 index 000000000..27134d80e --- /dev/null +++ b/lib/src/uda.rs @@ -0,0 +1,148 @@ +use crate::traits::*; +use crate::types::*; + +/// TCUDA contains the details of a UDA. +#[repr(C)] +pub struct TCUDA { + /// Namespace of the UDA. For legacy UDAs, this is NULL. + pub ns: *mut TCString<'static>, + /// UDA key. Must not be NULL. + pub key: *mut TCString<'static>, + /// Content of the UDA. Must not be NULL. + pub value: *mut TCString<'static>, +} + +pub(crate) struct UDA { + pub ns: Option>, + pub key: TCString<'static>, + pub value: TCString<'static>, +} + +impl PassByValue for TCUDA { + type RustType = UDA; + + unsafe fn from_ctype(self) -> Self::RustType { + UDA { + ns: if self.ns.is_null() { + None + } else { + // SAFETY: + // - self is owned, so we can take ownership of this TCString + // - self.ns is a valid, non-null TCString (NULL just checked) + Some(unsafe { TCString::take_from_arg(self.ns) }) + }, + // SAFETY: + // - self is owned, so we can take ownership of this TCString + // - self.key is a valid, non-null TCString (see type docstring) + key: unsafe { TCString::take_from_arg(self.key) }, + // SAFETY: + // - self is owned, so we can take ownership of this TCString + // - self.value is a valid, non-null TCString (see type docstring) + value: unsafe { TCString::take_from_arg(self.value) }, + } + } + + fn as_ctype(uda: UDA) -> Self { + TCUDA { + // SAFETY: caller assumes ownership of this value + ns: if let Some(ns) = uda.ns { + unsafe { ns.return_val() } + } else { + std::ptr::null_mut() + }, + // SAFETY: caller assumes ownership of this value + key: unsafe { uda.key.return_val() }, + // SAFETY: caller assumes ownership of this value + value: unsafe { uda.value.return_val() }, + } + } +} + +impl Default for TCUDA { + fn default() -> Self { + TCUDA { + ns: std::ptr::null_mut(), + key: std::ptr::null_mut(), + value: std::ptr::null_mut(), + } + } +} + +/// TCUDAList represents a list of UDAs. +/// +/// The content of this struct must be treated as read-only. +#[repr(C)] +pub struct TCUDAList { + /// number of UDAs in items + len: libc::size_t, + + /// total size of items (internal use only) + _capacity: libc::size_t, + + /// array of UDAs. These remain owned by the TCUDAList instance and will be freed by + /// tc_uda_list_free. This pointer is never NULL for a valid TCUDAList. + items: *const TCUDA, +} + +impl CList for TCUDAList { + type Element = TCUDA; + + unsafe fn from_raw_parts(items: *const Self::Element, len: usize, cap: usize) -> Self { + TCUDAList { + len, + _capacity: cap, + items, + } + } + + fn into_raw_parts(self) -> (*const Self::Element, usize, usize) { + (self.items, self.len, self._capacity) + } +} + +/// Free a TCUDA instance. The instance, and the TCStrings it contains, must not be used +/// after this call. +#[no_mangle] +pub unsafe extern "C" fn tc_uda_free(tcuda: *mut TCUDA) { + debug_assert!(!tcuda.is_null()); + // SAFETY: + // - *tcuda is a valid TCUDA (caller promises to treat it as read-only) + let uda = unsafe { TCUDA::take_from_arg(tcuda, TCUDA::default()) }; + drop(uda); +} + +/// Free a TCUDAList instance. The instance, and all TCUDAs it contains, must not be used after +/// this call. +/// +/// When this call returns, the `items` pointer will be NULL, signalling an invalid TCUDAList. +#[no_mangle] +pub unsafe extern "C" fn tc_uda_list_free(tcudas: *mut TCUDAList) { + // SAFETY: + // - tcudas is not NULL and points to a valid TCUDAList (caller is not allowed to + // modify the list) + // - caller promises not to use the value after return + unsafe { drop_value_list(tcudas) } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn empty_list_has_non_null_pointer() { + let tcudas = TCUDAList::return_val(Vec::new()); + assert!(!tcudas.items.is_null()); + assert_eq!(tcudas.len, 0); + assert_eq!(tcudas._capacity, 0); + } + + #[test] + fn free_sets_null_pointer() { + let mut tcudas = TCUDAList::return_val(Vec::new()); + // SAFETY: testing expected behavior + unsafe { tc_uda_list_free(&mut tcudas) }; + assert!(tcudas.items.is_null()); + assert_eq!(tcudas.len, 0); + assert_eq!(tcudas._capacity, 0); + } +} diff --git a/lib/taskchampion.h b/lib/taskchampion.h index 4b993d8ff..bc61b1e40 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -226,6 +226,45 @@ typedef struct TCStringList { struct TCString *const *items; } TCStringList; +/** + * TCUDA contains the details of a UDA. + */ +typedef struct TCUDA { + /** + * Namespace of the UDA. For legacy UDAs, this is NULL. + */ + struct TCString *ns; + /** + * UDA key. Must not be NULL. + */ + struct TCString *key; + /** + * Content of the UDA. Must not be NULL. + */ + struct TCString *value; +} TCUDA; + +/** + * TCUDAList represents a list of UDAs. + * + * The content of this struct must be treated as read-only. + */ +typedef struct TCUDAList { + /** + * number of UDAs in items + */ + size_t len; + /** + * total size of items (internal use only) + */ + size_t _capacity; + /** + * array of UDAs. These remain owned by the TCUDAList instance and will be freed by + * tc_uda_list_free. This pointer is never NULL for a valid TCUDAList. + */ + const struct TCUDA *items; +} TCUDAList; + #ifdef __cplusplus extern "C" { #endif // __cplusplus @@ -491,6 +530,35 @@ struct TCStringList tc_task_get_tags(struct TCTask *task); */ struct TCAnnotationList tc_task_get_annotations(struct TCTask *task); +/** + * Get the named UDA from the task. + * + * Returns NULL if the UDA does not exist. + */ +struct TCString *tc_task_get_uda(struct TCTask *task, struct TCString *ns, struct TCString *key); + +/** + * Get the named legacy UDA from the task. + * + * Returns NULL if the UDA does not exist. + */ +struct TCString *tc_task_get_legacy_uda(struct TCTask *task, struct TCString *key); + +/** + * Get all UDAs for this task. + * + * Legacy UDAs are represented with an empty string in the ns field. + */ +struct TCUDAList tc_task_get_udas(struct TCTask *task); + +/** + * Get all UDAs for this task. + * + * All TCUDAs in this list have a NULL ns field. The entire UDA key is + * included in the key field. + */ +struct TCUDAList tc_task_get_legacy_udas(struct TCTask *task); + /** * Set a mutable task's status. */ @@ -557,6 +625,29 @@ TCResult tc_task_add_annotation(struct TCTask *task, struct TCAnnotation *annota */ TCResult tc_task_remove_annotation(struct TCTask *task, int64_t entry); +/** + * Set a UDA on a mutable task. + */ +TCResult tc_task_set_uda(struct TCTask *task, + struct TCString *ns, + struct TCString *key, + struct TCString *value); + +/** + * Remove a UDA fraom a mutable task. + */ +TCResult tc_task_remove_uda(struct TCTask *task, struct TCString *ns, struct TCString *key); + +/** + * Set a legacy UDA on a mutable task. + */ +TCResult tc_task_set_legacy_uda(struct TCTask *task, struct TCString *key, struct TCString *value); + +/** + * Remove a UDA fraom a mutable task. + */ +TCResult tc_task_remove_legacy_uda(struct TCTask *task, struct TCString *key); + /** * Get the latest error for a task, or NULL 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 @@ -580,6 +671,20 @@ void tc_task_free(struct TCTask *task); */ void tc_task_list_free(struct TCTaskList *tctasks); +/** + * Free a TCUDA instance. The instance, and the TCStrings it contains, must not be used + * after this call. + */ +void tc_uda_free(struct TCUDA *tcuda); + +/** + * Free a TCUDAList instance. The instance, and all TCUDAs it contains, must not be used after + * this call. + * + * When this call returns, the `items` pointer will be NULL, signalling an invalid TCUDAList. + */ +void tc_uda_list_free(struct TCUDAList *tcudas); + /** * Create a new, randomly-generated UUID. */