diff --git a/taskchampion/integration-tests/src/bindings_tests/task.c b/taskchampion/integration-tests/src/bindings_tests/task.c index 4a30f8e1c..828775468 100644 --- a/taskchampion/integration-tests/src/bindings_tests/task.c +++ b/taskchampion/integration-tests/src/bindings_tests/task.c @@ -111,6 +111,54 @@ static void test_task_get_set_description(void) { tc_replica_free(rep); } +// updating arbitrary attributes on a task works +static void test_task_get_set_attribute(void) { + TCReplica *rep = tc_replica_new_in_memory(); + TEST_ASSERT_NULL(tc_replica_error(rep).ptr); + + TCTask *task = tc_replica_new_task( + rep, + TC_STATUS_PENDING, + tc_string_borrow("my task")); + TEST_ASSERT_NOT_NULL(task); + + TCString foo; + + foo = tc_task_get_value(task, tc_string_borrow("foo")); + TEST_ASSERT_NULL(foo.ptr); + + tc_task_to_mut(task, rep); + TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_set_value(task, + tc_string_borrow("foo"), + tc_string_borrow("updated"))); + + foo = tc_task_get_value(task, tc_string_borrow("foo")); + TEST_ASSERT_NOT_NULL(foo.ptr); + TEST_ASSERT_EQUAL_STRING("updated", tc_string_content(&foo)); + tc_string_free(&foo); + + tc_task_to_immut(task); + + foo = tc_task_get_value(task, tc_string_borrow("foo")); + TEST_ASSERT_NOT_NULL(foo.ptr); + TEST_ASSERT_EQUAL_STRING("updated", tc_string_content(&foo)); + tc_string_free(&foo); + + TCString null = { .ptr = NULL }; + + tc_task_to_mut(task, rep); + TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_set_value(task, + tc_string_borrow("foo"), + null)); + + foo = tc_task_get_value(task, tc_string_borrow("foo")); + TEST_ASSERT_NULL(foo.ptr); + + tc_task_free(task); + + tc_replica_free(rep); +} + // updating entry on a task works static void test_task_get_set_entry(void) { TCReplica *rep = tc_replica_new_in_memory(); @@ -652,6 +700,7 @@ int task_tests(void) { RUN_TEST(test_task_free_mutable_task); RUN_TEST(test_task_get_set_status); RUN_TEST(test_task_get_set_description); + RUN_TEST(test_task_get_set_attribute); RUN_TEST(test_task_get_set_entry); RUN_TEST(test_task_get_set_modified); RUN_TEST(test_task_get_set_wait_and_is_waiting); diff --git a/taskchampion/lib/src/task.rs b/taskchampion/lib/src/task.rs index dce84d7aa..0adbc50ac 100644 --- a/taskchampion/lib/src/task.rs +++ b/taskchampion/lib/src/task.rs @@ -294,8 +294,7 @@ pub unsafe extern "C" fn tc_task_get_taskmap(task: *mut TCTask) -> TCKVList { }) } -/// 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). +/// Get a task's description. #[no_mangle] pub unsafe extern "C" fn tc_task_get_description(task: *mut TCTask) -> TCString { wrap(task, |task| { @@ -306,6 +305,27 @@ pub unsafe extern "C" fn tc_task_get_description(task: *mut TCTask) -> TCString }) } +/// Get a task property's value, or NULL if the task has no such property, (including if the +/// property name is not valid utf-8). +#[no_mangle] +pub unsafe extern "C" fn tc_task_get_value(task: *mut TCTask, property: TCString) -> TCString { + // SAFETY: + // - property is valid (promised by caller) + // - caller will not use property after this call (convention) + let mut property = unsafe { TCString::val_from_arg(property) }; + wrap(task, |task| { + if let Ok(property) = property.as_str() { + let value = task.get_value(property); + if let Some(value) = value { + // SAFETY: + // - caller promises to free this string + return unsafe { TCString::return_val(value.into()) }; + } + } + TCString::default() // null value + }) +} + /// Get the entry timestamp for a task (when it was created), or 0 if not set. #[no_mangle] pub unsafe extern "C" fn tc_task_get_entry(task: *mut TCTask) -> libc::time_t { @@ -507,6 +527,40 @@ pub unsafe extern "C" fn tc_task_set_status(task: *mut TCTask, status: TCStatus) ) } +/// Set a mutable task's property value by name. If value.ptr is NULL, the property is removed. +#[no_mangle] +pub unsafe extern "C" fn tc_task_set_value( + task: *mut TCTask, + property: TCString, + value: TCString, +) -> TCResult { + // SAFETY: + // - property is valid (promised by caller) + // - caller will not use property after this call (convention) + let mut property = unsafe { TCString::val_from_arg(property) }; + let value = if value.is_null() { + None + } else { + // SAFETY: + // - value is valid (promised by caller, after NULL check) + // - caller will not use value after this call (convention) + Some(unsafe { TCString::val_from_arg(value) }) + }; + wrap_mut( + task, + |task| { + let value_str = if let Some(mut v) = value { + Some(v.as_str()?.to_string()) + } else { + None + }; + task.set_value(property.as_str()?.to_string(), value_str)?; + Ok(TCResult::Ok) + }, + TCResult::Error, + ) +} + /// Set a mutable task's description. #[no_mangle] pub unsafe extern "C" fn tc_task_set_description( diff --git a/taskchampion/lib/taskchampion.h b/taskchampion/lib/taskchampion.h index a40d1ad41..ccf5744be 100644 --- a/taskchampion/lib/taskchampion.h +++ b/taskchampion/lib/taskchampion.h @@ -751,11 +751,16 @@ enum TCStatus tc_task_get_status(struct TCTask *task); struct TCKVList tc_task_get_taskmap(struct 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). + * Get a task's description. */ struct TCString tc_task_get_description(struct TCTask *task); +/** + * Get a task property's value, or NULL if the task has no such property, (including if the + * property name is not valid utf-8). + */ +struct TCString tc_task_get_value(struct TCTask *task, struct TCString property); + /** * Get the entry timestamp for a task (when it was created), or 0 if not set. */ @@ -837,6 +842,11 @@ struct TCUdaList tc_task_get_legacy_udas(struct TCTask *task); */ TCResult tc_task_set_status(struct TCTask *task, enum TCStatus status); +/** + * Set a mutable task's property value by name. If value.ptr is NULL, the property is removed. + */ +TCResult tc_task_set_value(struct TCTask *task, struct TCString property, struct TCString value); + /** * Set a mutable task's description. */