more task functionality

This commit is contained in:
Dustin J. Mitchell 2022-01-23 22:45:57 +00:00
parent 821118106a
commit bb722325fe
10 changed files with 333 additions and 24 deletions

View file

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

35
binding-tests/task.cpp Normal file
View file

@ -0,0 +1,35 @@
#include <string.h>
#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);
}

View file

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

View file

@ -1,2 +1,5 @@
pub mod replica;
pub mod status;
pub mod string;
pub mod task;
pub mod uuid;

View file

@ -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<CString>,
}
/// 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<T>,
{
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<T>,
{
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
*/

36
lib/src/status.rs Normal file
View file

@ -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<TCStatus> 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<Status> 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,
}
}
}

48
lib/src/string.rs Normal file
View file

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

96
lib/src/task.rs Normal file
View file

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

View file

@ -4,11 +4,32 @@
#include <ostream>
#include <new>
/// 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();

View file

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