From bb722325fe4a23aa24093fc25c0646a10586ad97 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sun, 23 Jan 2022 22:45:57 +0000 Subject: [PATCH] more task functionality --- binding-tests/Makefile | 2 +- binding-tests/task.cpp | 35 ++++++++++++ lib/build.rs | 5 ++ lib/src/lib.rs | 3 ++ lib/src/replica.rs | 86 +++++++++++++++++++++-------- lib/src/status.rs | 36 +++++++++++++ lib/src/string.rs | 48 +++++++++++++++++ lib/src/task.rs | 96 +++++++++++++++++++++++++++++++++ lib/taskchampion.h | 45 ++++++++++++++++ taskchampion/src/task/status.rs | 1 + 10 files changed, 333 insertions(+), 24 deletions(-) create mode 100644 binding-tests/task.cpp create mode 100644 lib/src/status.rs create mode 100644 lib/src/string.rs create mode 100644 lib/src/task.rs diff --git a/binding-tests/Makefile b/binding-tests/Makefile index 620befab5..121d942cc 100644 --- a/binding-tests/Makefile +++ b/binding-tests/Makefile @@ -3,7 +3,7 @@ INC=-I ../lib LIB=-L ../target/debug RPATH=-Wl,-rpath,../target/debug -TESTS = replica.cpp uuid.cpp +TESTS = replica.cpp uuid.cpp task.cpp .PHONY: all test diff --git a/binding-tests/task.cpp b/binding-tests/task.cpp new file mode 100644 index 000000000..0592b6b23 --- /dev/null +++ b/binding-tests/task.cpp @@ -0,0 +1,35 @@ +#include +#include "doctest.h" +#include "taskchampion.h" + +TEST_CASE("creating a Task does not crash") { + TCReplica *rep = tc_replica_new(NULL); + CHECK(tc_replica_error(rep) == NULL); + + TCTask *task = tc_replica_new_task( + rep, + TC_STATUS_PENDING, + tc_string_new("my task")); + REQUIRE(task != NULL); + + CHECK(tc_task_get_status(task) == TC_STATUS_PENDING); + + TCString *desc = tc_task_get_description(task); + REQUIRE(desc != NULL); + CHECK(strcmp(tc_string_content(desc), "my task") == 0); + tc_string_free(desc); + + tc_task_free(task); + + tc_replica_free(rep); +} + +TEST_CASE("undo on an empty in-memory TCReplica does nothing") { + TCReplica *rep = tc_replica_new(NULL); + CHECK(tc_replica_error(rep) == NULL); + int rv = tc_replica_undo(rep); + CHECK(rv == 0); + CHECK(tc_replica_error(rep) == NULL); + tc_replica_free(rep); +} + diff --git a/lib/build.rs b/lib/build.rs index 8d9db2f1f..13eda1bc5 100644 --- a/lib/build.rs +++ b/lib/build.rs @@ -10,6 +10,11 @@ fn main() { .with_language(Language::C) .with_config(Config { cpp_compat: true, + enumeration: EnumConfig { + // this appears to still default to true for C + enum_class: false, + ..Default::default() + }, ..Default::default() }) .generate() diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 98b7c0124..a97d0f732 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -1,2 +1,5 @@ pub mod replica; +pub mod status; +pub mod string; +pub mod task; pub mod uuid; diff --git a/lib/src/replica.rs b/lib/src/replica.rs index 44066507e..81a387ca5 100644 --- a/lib/src/replica.rs +++ b/lib/src/replica.rs @@ -1,3 +1,4 @@ +use crate::{status::TCStatus, string::TCString, task::TCTask}; use libc::c_char; use std::ffi::{CStr, CString, OsStr}; use std::path::PathBuf; @@ -9,11 +10,37 @@ use std::os::unix::ffi::OsStrExt; /// A replica represents an instance of a user's task data, providing an easy interface /// for querying and modifying that data. pub struct TCReplica { - // TODO: make this an option so that it can be take()n when holding a mut ref + // TODO: make this a RefCell so that it can be take()n when holding a mut ref inner: Replica, error: Option, } +/// Utility function to safely convert *mut TCReplica into &mut TCReplica +fn rep_ref(rep: *mut TCReplica) -> &'static mut TCReplica { + debug_assert!(!rep.is_null()); + unsafe { &mut *rep } +} + +/// Utility function to allow using `?` notation to return an error value. +fn wrap<'a, T, F>(rep: *mut TCReplica, f: F, err_value: T) -> T +where + F: FnOnce(&mut Replica) -> anyhow::Result, +{ + let rep: &'a mut TCReplica = rep_ref(rep); + match f(&mut rep.inner) { + Ok(v) => v, + Err(e) => { + let error = e.to_string(); + let error = match CString::new(error.as_bytes()) { + Ok(e) => e, + Err(_) => CString::new("(invalid error message)".as_bytes()).unwrap(), + }; + rep.error = Some(error); + err_value + } + } +} + /// Create a new TCReplica. /// /// If path is NULL, then an in-memory replica is created. Otherwise, path is the path to the @@ -45,30 +72,37 @@ pub extern "C" fn tc_replica_new<'a>(path: *const c_char) -> *mut TCReplica { })) } -/// Utility function to safely convert *mut TCReplica into &mut TCReplica -fn rep_ref(rep: *mut TCReplica) -> &'static mut TCReplica { - debug_assert!(!rep.is_null()); - unsafe { &mut *rep } +/* + * TODO: + * - tc_replica_all_tasks + * - tc_replica_all_task_uuids + * - tc_replica_working_set + * - tc_replica_get_task + */ + +/// Create a new task. The task must not already exist. +/// +/// Returns the task, or NULL on error. +#[no_mangle] +pub extern "C" fn tc_replica_new_task<'a>( + rep: *mut TCReplica, + status: TCStatus, + description: *mut TCString, +) -> *mut TCTask { + wrap( + rep, + |rep| { + let description = TCString::from_arg(description); + let task = rep.new_task(status.into(), description.as_str()?.to_string())?; + Ok(TCTask::as_ptr(task)) + }, + std::ptr::null_mut(), + ) } -fn wrap<'a, T, F>(rep: *mut TCReplica, f: F, err_value: T) -> T -where - F: FnOnce(&mut Replica) -> anyhow::Result, -{ - let rep: &'a mut TCReplica = rep_ref(rep); - match f(&mut rep.inner) { - Ok(v) => v, - Err(e) => { - let error = e.to_string(); - let error = match CString::new(error.as_bytes()) { - Ok(e) => e, - Err(_) => CString::new("(invalid error message)".as_bytes()).unwrap(), - }; - rep.error = Some(error); - err_value - } - } -} +/* - tc_replica_import_task_with_uuid + * - tc_replica_sync + */ /// Undo local operations until the most recent UndoPoint. /// @@ -95,5 +129,11 @@ pub extern "C" fn tc_replica_error<'a>(rep: *mut TCReplica) -> *const c_char { /// Free a TCReplica. #[no_mangle] pub extern "C" fn tc_replica_free(rep: *mut TCReplica) { + debug_assert!(!rep.is_null()); drop(unsafe { Box::from_raw(rep) }); } + +/* + * - tc_replica_rebuild_working_set + * - tc_replica_add_undo_point + */ diff --git a/lib/src/status.rs b/lib/src/status.rs new file mode 100644 index 000000000..306f27630 --- /dev/null +++ b/lib/src/status.rs @@ -0,0 +1,36 @@ +pub use taskchampion::Status; + +/// The status of a task, as defined by the task data model. +/// cbindgen:prefix-with-name +/// cbindgen:rename-all=ScreamingSnakeCase +#[repr(C)] +pub enum TCStatus { + Pending, + Completed, + Deleted, + /// Unknown signifies a status in the task DB that was not + /// recognized. + Unknown, +} + +impl From for Status { + fn from(status: TCStatus) -> Status { + match status { + TCStatus::Pending => Status::Pending, + TCStatus::Completed => Status::Completed, + TCStatus::Deleted => Status::Deleted, + TCStatus::Unknown => Status::Unknown("unknown".to_string()), + } + } +} + +impl From for TCStatus { + fn from(status: Status) -> TCStatus { + match status { + Status::Pending => TCStatus::Pending, + Status::Completed => TCStatus::Completed, + Status::Deleted => TCStatus::Deleted, + Status::Unknown(_) => TCStatus::Unknown, + } + } +} diff --git a/lib/src/string.rs b/lib/src/string.rs new file mode 100644 index 000000000..39e17be13 --- /dev/null +++ b/lib/src/string.rs @@ -0,0 +1,48 @@ +use std::ffi::{CStr, CString, NulError}; + +// thinking: +// - TCString ownership always taken when passed in +// - TCString ownership always falls to C when passed out +// - accept that bytes must be copied to get owned string +// - Can we do this with an enum of some sort? + +/// TCString supports passing strings into and out of the TaskChampion API. +pub struct TCString(CString); + +impl TCString { + /// Take a TCString from C as an argument. + pub(crate) fn from_arg(tcstring: *mut TCString) -> Self { + debug_assert!(!tcstring.is_null()); + *(unsafe { Box::from_raw(tcstring) }) + } + + /// Get a regular Rust &str for this value. + pub(crate) fn as_str(&self) -> Result<&str, std::str::Utf8Error> { + self.0.as_c_str().to_str() + } + + /// Construct a *mut TCString from a string for returning to C. + pub(crate) fn return_string(string: impl Into>) -> Result<*mut TCString, NulError> { + let tcstring = TCString(CString::new(string)?); + Ok(Box::into_raw(Box::new(tcstring))) + } +} + +#[no_mangle] +pub extern "C" fn tc_string_new(cstr: *const libc::c_char) -> *mut TCString { + let cstring = unsafe { CStr::from_ptr(cstr) }.into(); + Box::into_raw(Box::new(TCString(cstring))) +} + +#[no_mangle] +pub extern "C" fn tc_string_content(string: *mut TCString) -> *const libc::c_char { + debug_assert!(!string.is_null()); + let string: &CString = unsafe { &(*string).0 }; + string.as_ptr() +} + +#[no_mangle] +pub extern "C" fn tc_string_free(string: *mut TCString) { + debug_assert!(!string.is_null()); + drop(unsafe { Box::from_raw(string) }); +} diff --git a/lib/src/task.rs b/lib/src/task.rs new file mode 100644 index 000000000..781b5d3d0 --- /dev/null +++ b/lib/src/task.rs @@ -0,0 +1,96 @@ +use crate::{status::TCStatus, string::TCString, uuid::TCUuid}; +use taskchampion::Task; + +/// A task, as publicly exposed by this library. +/// +/// A task carries no reference to the replica that created it, and can +/// be used until it is freed or converted to a TaskMut. +pub struct TCTask { + inner: Task, +} + +impl TCTask { + pub(crate) fn as_ptr(task: Task) -> *mut TCTask { + Box::into_raw(Box::new(TCTask { inner: task })) + } +} + +/// Utility function to allow using `?` notation to return an error value. +fn wrap<'a, T, F>(task: *const TCTask, f: F, err_value: T) -> T +where + F: FnOnce(&Task) -> anyhow::Result, +{ + let task: &'a Task = task_ref(task); + match f(task) { + Ok(v) => v, + Err(e) => { + /* + let error = e.to_string(); + let error = match CString::new(error.as_bytes()) { + Ok(e) => e, + Err(_) => CString::new("(invalid error message)".as_bytes()).unwrap(), + }; + */ + //task.error = Some(error); + err_value + } + } +} + +/// Utility function to safely convert *const TCTask into &Task +fn task_ref(task: *const TCTask) -> &'static Task { + debug_assert!(!task.is_null()); + unsafe { &(*task).inner } +} + +/// Get a task's UUID. +#[no_mangle] +pub extern "C" fn tc_task_get_uuid<'a>(task: *const TCTask) -> TCUuid { + let task: &'a Task = task_ref(task); + let uuid = task.get_uuid(); + uuid.into() +} + +/// Get a task's status. +#[no_mangle] +pub extern "C" fn tc_task_get_status<'a>(task: *const TCTask) -> TCStatus { + let task: &'a Task = task_ref(task); + task.get_status().into() +} + +/* TODO + * into_mut + * get_taskmap + */ + +/// 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). +#[no_mangle] +pub extern "C" fn tc_task_get_description<'a>(task: *const TCTask) -> *mut TCString { + wrap( + task, + |task| Ok(TCString::return_string(task.get_description())?), + std::ptr::null_mut(), + ) +} + +/* TODO + * get_wait + * is_waiting + * is_active + * has_tag + * get_tags + * get_annotations + * get_uda + * get_udas + * get_legacy_uda + * get_legacy_udas + * get_modified + */ + +/// Free a task. +#[no_mangle] +pub extern "C" fn tc_task_free<'a>(task: *mut TCTask) { + debug_assert!(!task.is_null()); + drop(unsafe { Box::from_raw(task) }); +} diff --git a/lib/taskchampion.h b/lib/taskchampion.h index c018143d5..5d9691d70 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -4,11 +4,32 @@ #include #include +/// The status of a task, as defined by the task data model. +enum TCStatus { + TC_STATUS_PENDING, + TC_STATUS_COMPLETED, + TC_STATUS_DELETED, + /// Unknown signifies a status in the task DB that was not + /// recognized. + TC_STATUS_UNKNOWN, +}; + /// A replica represents an instance of a user's task data, providing an easy interface /// for querying and modifying that data. struct TCReplica; +/// TCString supports passing strings into and out of the TaskChampion API. +struct TCString; + +/// A task, as publicly exposed by this library. +/// +/// A task carries no reference to the replica that created it, and can +/// be used until it is freed or converted to a TaskMut. +struct TCTask; + /// TCUuid is used as a task identifier. Uuids do not contain any pointers and need not be freed. +/// Uuids are typically treated as opaque, but the bytes are available in big-endian format. +/// struct TCUuid { uint8_t bytes[16]; }; @@ -27,6 +48,11 @@ extern const uintptr_t TC_UUID_STRING_BYTES; /// TCReplicas are not threadsafe. TCReplica *tc_replica_new(const char *path); +/// Create a new task. The task must not already exist. +/// +/// Returns the task, or NULL on error. +TCTask *tc_replica_new_task(TCReplica *rep, TCStatus status, TCString *description); + /// Undo local operations until the most recent UndoPoint. /// /// Returns -1 on error, 0 if there are no local operations to undo, and 1 if operations were @@ -41,6 +67,25 @@ const char *tc_replica_error(TCReplica *rep); /// Free a TCReplica. void tc_replica_free(TCReplica *rep); +TCString *tc_string_new(const char *cstr); + +const char *tc_string_content(TCString *string); + +void tc_string_free(TCString *string); + +/// Get a task's UUID. +TCUuid tc_task_get_uuid(const TCTask *task); + +/// Get a task's status. +TCStatus tc_task_get_status(const 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). +TCString *tc_task_get_description(const TCTask *task); + +/// Free a task. +void tc_task_free(TCTask *task); + /// Create a new, randomly-generated UUID. TCUuid tc_uuid_new_v4(); diff --git a/taskchampion/src/task/status.rs b/taskchampion/src/task/status.rs index 2b2afb6ba..31fee9cf7 100644 --- a/taskchampion/src/task/status.rs +++ b/taskchampion/src/task/status.rs @@ -1,5 +1,6 @@ /// The status of a task, as defined by the task data model. #[derive(Debug, PartialEq, Clone, strum_macros::Display)] +#[repr(C)] pub enum Status { Pending, Completed,