diff --git a/integration-tests/src/bindings_tests/task.c b/integration-tests/src/bindings_tests/task.c index 5a0214ce3..0eb1e45c0 100644 --- a/integration-tests/src/bindings_tests/task.c +++ b/integration-tests/src/bindings_tests/task.c @@ -333,6 +333,59 @@ static void test_task_get_tags(void) { tc_replica_free(rep); } +// annotation manipulation (add, remove, list, free) +static void test_task_annotations(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); + + TCAnnotationList anns = tc_task_get_annotations(task); + TEST_ASSERT_EQUAL(0, anns.len); + TEST_ASSERT_NOT_NULL(anns.items); + tc_annotation_list_free(&anns); + + tc_task_to_mut(task, rep); + + 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_NULL(ann.description); + + ann.entry = 1644623422; + ann.description = tc_string_borrow("ann2"); + TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_add_annotation(task, &ann)); + TEST_ASSERT_NULL(ann.description); + + anns = tc_task_get_annotations(task); + + int found1 = false, found2 = false; + for (size_t i = 0; i < anns.len; i++) { + if (0 == strcmp("ann1", tc_string_content(anns.items[i].description))) { + TEST_ASSERT_EQUAL(anns.items[i].entry, 1644623411); + found1 = true; + } + if (0 == strcmp("ann2", tc_string_content(anns.items[i].description))) { + TEST_ASSERT_EQUAL(anns.items[i].entry, 1644623422); + found2 = true; + } + } + TEST_ASSERT_TRUE(found1); + TEST_ASSERT_TRUE(found2); + + tc_annotation_list_free(&anns); + TEST_ASSERT_NULL(anns.items); + + tc_task_free(task); + tc_replica_free(rep); +} + int task_tests(void) { UNITY_BEGIN(); // each test case above should be named here, in order. @@ -347,5 +400,6 @@ int task_tests(void) { RUN_TEST(test_task_done_and_delete); RUN_TEST(test_task_add_remove_has_tag); RUN_TEST(test_task_get_tags); + RUN_TEST(test_task_annotations); return UNITY_END(); } diff --git a/lib/src/annotation.rs b/lib/src/annotation.rs new file mode 100644 index 000000000..8c403327b --- /dev/null +++ b/lib/src/annotation.rs @@ -0,0 +1,127 @@ +use crate::traits::*; +use crate::types::*; +use chrono::prelude::*; +use taskchampion::Annotation; + +/// TCAnnotation contains the details of an annotation. +#[repr(C)] +pub struct TCAnnotation { + /// Time the annotation was made, as a UNIX epoch timestamp + pub entry: i64, + /// Content of the annotation + pub description: *mut TCString<'static>, +} + +impl PassByValue for TCAnnotation { + type RustType = Annotation; + + unsafe fn from_ctype(self) -> Annotation { + let entry = Utc.timestamp(self.entry, 0); + // SAFETY: + // - self is owned, so we can take ownership of this TCString + // - self.description was created from a String so has valid UTF-8 + // - caller did not change it (promised by caller) + let description = unsafe { TCString::take_from_arg(self.description) } + .into_string() + .unwrap(); + Annotation { entry, description } + } + + fn as_ctype(arg: Annotation) -> Self { + let description: TCString = arg.description.into(); + TCAnnotation { + entry: arg.entry.timestamp(), + // SAFETY: caller will later free this value via tc_annotation_free + description: unsafe { description.return_val() }, + } + } +} + +impl Default for TCAnnotation { + fn default() -> Self { + TCAnnotation { + entry: 0, + description: std::ptr::null_mut(), + } + } +} + +/// TCAnnotationList represents a list of annotations. +/// +/// The content of this struct must be treated as read-only. +#[repr(C)] +pub struct TCAnnotationList { + /// number of annotations in items + len: libc::size_t, + + /// total size of items (internal use only) + _capacity: libc::size_t, + + /// array of annotations. these remain owned by the TCAnnotationList instance and will be freed by + /// tc_annotation_list_free. This pointer is never NULL for a valid TCAnnotationList. + items: *const TCAnnotation, +} + +impl CArray for TCAnnotationList { + type Element = TCAnnotation; + + unsafe fn from_raw_parts(items: *const Self::Element, len: usize, cap: usize) -> Self { + TCAnnotationList { + len, + _capacity: cap, + items, + } + } + + fn into_raw_parts(self) -> (*const Self::Element, usize, usize) { + (self.items, self.len, self._capacity) + } +} + +/// Free a TCAnnotation instance. The instance, and the TCString it contains, must not be used +/// after this call. +#[no_mangle] +pub unsafe extern "C" fn tc_annotation_free(tcann: *mut TCAnnotation) { + debug_assert!(!tcann.is_null()); + // SAFETY: + // - *tcann is a valid TCAnnotation (caller promises to treat it as read-only) + let annotation = unsafe { TCAnnotation::take_from_arg(tcann, TCAnnotation::default()) }; + drop(annotation); +} + +/// Free a TCAnnotationList instance. The instance, and all TCAnnotations it contains, must not be used after +/// this call. +/// +/// When this call returns, the `items` pointer will be NULL, signalling an invalid TCAnnotationList. +#[no_mangle] +pub unsafe extern "C" fn tc_annotation_list_free(tcanns: *mut TCAnnotationList) { + debug_assert!(!tcanns.is_null()); + // SAFETY: + // - *tcanns is a valid TCAnnotationList (caller promises to treat it as read-only) + let annotations = + unsafe { TCAnnotationList::take_from_arg(tcanns, TCAnnotationList::null_value()) }; + TCAnnotationList::drop_vector(annotations); +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn empty_array_has_non_null_pointer() { + let tcanns = TCAnnotationList::return_val(Vec::new()); + assert!(!tcanns.items.is_null()); + assert_eq!(tcanns.len, 0); + assert_eq!(tcanns._capacity, 0); + } + + #[test] + fn free_sets_null_pointer() { + let mut tcanns = TCAnnotationList::return_val(Vec::new()); + // SAFETY: testing expected behavior + unsafe { tc_annotation_list_free(&mut tcanns) }; + assert!(tcanns.items.is_null()); + assert_eq!(tcanns.len, 0); + assert_eq!(tcanns._capacity, 0); + } +} diff --git a/lib/src/lib.rs b/lib/src/lib.rs index b1456e73b..f5c43439c 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -5,6 +5,7 @@ mod traits; mod util; +pub mod annotation; pub mod atomic; pub mod replica; pub mod result; @@ -17,6 +18,7 @@ pub mod uuid; pub mod uuidlist; pub(crate) mod types { + pub(crate) use crate::annotation::{TCAnnotation, TCAnnotationList}; pub(crate) use crate::replica::TCReplica; pub(crate) use crate::result::TCResult; pub(crate) use crate::status::TCStatus; diff --git a/lib/src/string.rs b/lib/src/string.rs index da35e1220..257d105eb 100644 --- a/lib/src/string.rs +++ b/lib/src/string.rs @@ -69,6 +69,18 @@ impl<'a> TCString<'a> { } } + /// Consume this TCString and return an equivalent String, or an error if not + /// valid UTF-8. In the error condition, the original data is lost. + pub(crate) fn into_string(self) -> Result { + match self { + TCString::CString(cstring) => cstring.into_string().map_err(|e| e.utf8_error()), + TCString::CStr(cstr) => cstr.to_str().map(|s| s.to_string()), + TCString::String(string) => Ok(string), + TCString::InvalidUtf8(e, _) => Err(e), + TCString::None => unreachable!(), + } + } + fn as_bytes(&self) -> &[u8] { match self { TCString::CString(cstring) => cstring.as_bytes(), diff --git a/lib/src/task.rs b/lib/src/task.rs index 8f4055a8a..2f7da32ef 100644 --- a/lib/src/task.rs +++ b/lib/src/task.rs @@ -305,7 +305,21 @@ pub unsafe extern "C" fn tc_task_get_tags<'a>(task: *mut TCTask) -> TCStringList }) } -// TODO: tc_task_get_annotations +/// Get the annotations for the task. +/// +/// The caller must free the returned TCAnnotationList instance. The TCStringList instance does not +/// reference the task and the two may be freed in any order. +#[no_mangle] +pub unsafe extern "C" fn tc_task_get_annotations<'a>(task: *mut TCTask) -> TCAnnotationList { + wrap(task, |task| { + let vec: Vec = task + .get_annotations() + .map(|a| TCAnnotation::as_ctype(a)) + .collect(); + TCAnnotationList::return_val(vec) + }) +} + // TODO: tc_task_get_uda // TODO: tc_task_get_udas // TODO: tc_task_get_legacy_uda @@ -471,8 +485,37 @@ pub unsafe extern "C" fn tc_task_remove_tag(task: *mut TCTask, tag: *mut TCStrin ) } -// TODO: tc_task_add_annotation -// TODO: tc_task_remove_annotation +/// Add an annotation to a mutable task. +#[no_mangle] +pub unsafe extern "C" fn tc_task_add_annotation( + task: *mut TCTask, + annotation: *mut TCAnnotation, +) -> TCResult { + // SAFETY: see TCAnnotation docstring + let ann = unsafe { TCAnnotation::take_from_arg(annotation, TCAnnotation::default()) }; + wrap_mut( + task, + |task| { + task.add_annotation(ann)?; + Ok(TCResult::Ok) + }, + TCResult::Error, + ) +} + +/// Remove an annotation from a mutable task. +#[no_mangle] +pub unsafe extern "C" fn tc_task_remove_annotation(task: *mut TCTask, entry: i64) -> TCResult { + wrap_mut( + task, + |task| { + task.remove_annotation(Utc.timestamp(entry, 0))?; + Ok(TCResult::Ok) + }, + TCResult::Error, + ) +} + // TODO: tc_task_set_uda // TODO: tc_task_remove_uda // TODO: tc_task_set_legacy_uda diff --git a/lib/taskchampion.h b/lib/taskchampion.h index 0e5753f67..a6ffc7644 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -117,6 +117,41 @@ typedef struct TCString TCString; */ typedef struct TCTask TCTask; +/** + * TCAnnotation contains the details of an annotation. + */ +typedef struct TCAnnotation { + /** + * Time the annotation was made, as a UNIX epoch timestamp + */ + int64_t entry; + /** + * Content of the annotation + */ + struct TCString *description; +} TCAnnotation; + +/** + * TCAnnotationList represents a list of annotations. + * + * The content of this struct must be treated as read-only. + */ +typedef struct TCAnnotationList { + /** + * number of annotations in items + */ + size_t len; + /** + * total size of items (internal use only) + */ + size_t _capacity; + /** + * array of annotations. these remain owned by the TCAnnotationList instance and will be freed by + * tc_annotation_list_free. This pointer is never NULL for a valid TCAnnotationList. + */ + const struct TCAnnotation *items; +} TCAnnotationList; + /** * TCTaskList represents a list of tasks. * @@ -195,6 +230,20 @@ typedef struct TCStringList { extern "C" { #endif // __cplusplus +/** + * Free a TCAnnotation instance. The instance, and the TCString it contains, must not be used + * after this call. + */ +void tc_annotation_free(struct TCAnnotation *tcann); + +/** + * Free a TCAnnotationList instance. The instance, and all TCAnnotations it contains, must not be used after + * this call. + * + * When this call returns, the `items` pointer will be NULL, signalling an invalid TCAnnotationList. + */ +void tc_annotation_list_free(struct TCAnnotationList *tcanns); + /** * Create a new TCReplica with an in-memory database. The contents of the database will be * lost when it is freed. @@ -434,6 +483,14 @@ bool tc_task_has_tag(struct TCTask *task, struct TCString *tag); */ struct TCStringList tc_task_get_tags(struct TCTask *task); +/** + * Get the annotations for the task. + * + * The caller must free the returned TCAnnotationList instance. The TCStringList instance does not + * reference the task and the two may be freed in any order. + */ +struct TCAnnotationList tc_task_get_annotations(struct TCTask *task); + /** * Set a mutable task's status. */ @@ -490,6 +547,16 @@ TCResult tc_task_add_tag(struct TCTask *task, struct TCString *tag); */ TCResult tc_task_remove_tag(struct TCTask *task, struct TCString *tag); +/** + * Add an annotation to a mutable task. + */ +TCResult tc_task_add_annotation(struct TCTask *task, struct TCAnnotation *annotation); + +/** + * Remove an annotation from a mutable task. + */ +TCResult tc_task_remove_annotation(struct TCTask *task, int64_t entry); + /** * 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