add annotation support

This commit is contained in:
Dustin J. Mitchell 2022-02-11 23:54:52 +00:00
parent b01285d780
commit 7996a98908
6 changed files with 308 additions and 3 deletions

View file

@ -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();
}

127
lib/src/annotation.rs Normal file
View file

@ -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);
}
}

View file

@ -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;

View file

@ -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<String, std::str::Utf8Error> {
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(),

View file

@ -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<TCAnnotation> = 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

View file

@ -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