mirror of
https://github.com/GothenburgBitFactory/taskwarrior.git
synced 2025-06-26 10:54:26 +02:00
add annotation support
This commit is contained in:
parent
b01285d780
commit
7996a98908
6 changed files with 308 additions and 3 deletions
|
@ -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
127
lib/src/annotation.rs
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue