more task functionality
This commit is contained in:
@@ -3,7 +3,7 @@ INC=-I ../lib
|
|||||||
LIB=-L ../target/debug
|
LIB=-L ../target/debug
|
||||||
RPATH=-Wl,-rpath,../target/debug
|
RPATH=-Wl,-rpath,../target/debug
|
||||||
|
|
||||||
TESTS = replica.cpp uuid.cpp
|
TESTS = replica.cpp uuid.cpp task.cpp
|
||||||
|
|
||||||
.PHONY: all test
|
.PHONY: all test
|
||||||
|
|
||||||
|
|||||||
35
binding-tests/task.cpp
Normal file
35
binding-tests/task.cpp
Normal 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);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -10,6 +10,11 @@ fn main() {
|
|||||||
.with_language(Language::C)
|
.with_language(Language::C)
|
||||||
.with_config(Config {
|
.with_config(Config {
|
||||||
cpp_compat: true,
|
cpp_compat: true,
|
||||||
|
enumeration: EnumConfig {
|
||||||
|
// this appears to still default to true for C
|
||||||
|
enum_class: false,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
.generate()
|
.generate()
|
||||||
|
|||||||
@@ -1,2 +1,5 @@
|
|||||||
pub mod replica;
|
pub mod replica;
|
||||||
|
pub mod status;
|
||||||
|
pub mod string;
|
||||||
|
pub mod task;
|
||||||
pub mod uuid;
|
pub mod uuid;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::{status::TCStatus, string::TCString, task::TCTask};
|
||||||
use libc::c_char;
|
use libc::c_char;
|
||||||
use std::ffi::{CStr, CString, OsStr};
|
use std::ffi::{CStr, CString, OsStr};
|
||||||
use std::path::PathBuf;
|
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
|
/// A replica represents an instance of a user's task data, providing an easy interface
|
||||||
/// for querying and modifying that data.
|
/// for querying and modifying that data.
|
||||||
pub struct TCReplica {
|
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,
|
inner: Replica,
|
||||||
error: Option<CString>,
|
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.
|
/// Create a new TCReplica.
|
||||||
///
|
///
|
||||||
/// If path is NULL, then an in-memory replica is created. Otherwise, path is the path to the
|
/// 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 {
|
* TODO:
|
||||||
debug_assert!(!rep.is_null());
|
* - tc_replica_all_tasks
|
||||||
unsafe { &mut *rep }
|
* - 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
|
/* - tc_replica_import_task_with_uuid
|
||||||
where
|
* - tc_replica_sync
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Undo local operations until the most recent UndoPoint.
|
/// 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.
|
/// Free a TCReplica.
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn tc_replica_free(rep: *mut TCReplica) {
|
pub extern "C" fn tc_replica_free(rep: *mut TCReplica) {
|
||||||
|
debug_assert!(!rep.is_null());
|
||||||
drop(unsafe { Box::from_raw(rep) });
|
drop(unsafe { Box::from_raw(rep) });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* - tc_replica_rebuild_working_set
|
||||||
|
* - tc_replica_add_undo_point
|
||||||
|
*/
|
||||||
|
|||||||
36
lib/src/status.rs
Normal file
36
lib/src/status.rs
Normal 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
48
lib/src/string.rs
Normal 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
96
lib/src/task.rs
Normal 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) });
|
||||||
|
}
|
||||||
@@ -4,11 +4,32 @@
|
|||||||
#include <ostream>
|
#include <ostream>
|
||||||
#include <new>
|
#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
|
/// A replica represents an instance of a user's task data, providing an easy interface
|
||||||
/// for querying and modifying that data.
|
/// for querying and modifying that data.
|
||||||
struct TCReplica;
|
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.
|
/// 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 {
|
struct TCUuid {
|
||||||
uint8_t bytes[16];
|
uint8_t bytes[16];
|
||||||
};
|
};
|
||||||
@@ -27,6 +48,11 @@ extern const uintptr_t TC_UUID_STRING_BYTES;
|
|||||||
/// TCReplicas are not threadsafe.
|
/// TCReplicas are not threadsafe.
|
||||||
TCReplica *tc_replica_new(const char *path);
|
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.
|
/// 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
|
/// 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.
|
/// Free a TCReplica.
|
||||||
void tc_replica_free(TCReplica *rep);
|
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.
|
/// Create a new, randomly-generated UUID.
|
||||||
TCUuid tc_uuid_new_v4();
|
TCUuid tc_uuid_new_v4();
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
/// The status of a task, as defined by the task data model.
|
/// The status of a task, as defined by the task data model.
|
||||||
#[derive(Debug, PartialEq, Clone, strum_macros::Display)]
|
#[derive(Debug, PartialEq, Clone, strum_macros::Display)]
|
||||||
|
#[repr(C)]
|
||||||
pub enum Status {
|
pub enum Status {
|
||||||
Pending,
|
Pending,
|
||||||
Completed,
|
Completed,
|
||||||
|
|||||||
Reference in New Issue
Block a user