taskchampion-rb 0.2.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.ruby-version +1 -0
- data/README.md +9 -1
- data/Rakefile +14 -0
- data/docs/breakthrough.md +1 -1
- data/docs/plan.md +1 -1
- data/examples/basic_usage.rb +160 -15
- data/examples/sync_workflow.rb +2 -2
- data/ext/taskchampion/Cargo.toml +1 -1
- data/ext/taskchampion/src/lib.rs +2 -0
- data/ext/taskchampion/src/replica.rs +6 -5
- data/ext/taskchampion/src/task.rs +10 -1
- data/ext/taskchampion/src/task_data.rs +122 -0
- data/ext/taskchampion/src/util.rs +25 -12
- data/lib/taskchampion/version.rb +1 -1
- metadata +8 -6
- data/.claude/settings.local.json +0 -14
- data/sig/taskchampion.rbs +0 -4
- data/taskchampion-0.2.0.gem +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8b267de15e0004e931e2a46f37f9014afe8fdc52e58e675190dab990cb345e75
|
4
|
+
data.tar.gz: 6a84efc5a165c428c67e6d50f3ce0b001eab4d3fde464ffed123d218aaf04033
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ff860c7dcb85bc6a04aced731b403072a03c3a222f869fec0a25af1436b14fb67e4df0a9dc5cfad4d829b1fe1f39936e6df07c90605eb859202ac8f80fc8a17f
|
7
|
+
data.tar.gz: d826c3704fec6432e872abe24423d92e04da810c0718ce0cc224dbddd780b2279b336c635bd1e285405bd4534710fbd9928bf4ee7c5f225f4118baade8315436
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
3.2.0
|
data/README.md
CHANGED
@@ -2,11 +2,19 @@
|
|
2
2
|
|
3
3
|
Ruby bindings for TaskChampion, the task database that powers Taskwarrior.
|
4
4
|
|
5
|
+
## Ruby Version Support (2025-08-12)
|
6
|
+
|
7
|
+
This gem supports Ruby 3.2 and later. We follow Ruby's end-of-life (EOL) schedule and drop support for Ruby versions that have reached EOL.
|
8
|
+
|
9
|
+
- **Ruby 3.2**: Supported (EOL: March 2026)
|
10
|
+
- **Ruby 3.3**: Supported (Current stable)
|
11
|
+
- **Ruby 3.0-3.1**: Not supported (reached EOL)
|
12
|
+
|
5
13
|
## Installation
|
6
14
|
|
7
15
|
### Prerequisites
|
8
16
|
|
9
|
-
1. Ruby 3.
|
17
|
+
1. Ruby 3.2 or later
|
10
18
|
2. Rust toolchain (install from https://rustup.rs/)
|
11
19
|
|
12
20
|
```bash
|
data/Rakefile
CHANGED
@@ -26,3 +26,17 @@ end
|
|
26
26
|
task test: :compile
|
27
27
|
|
28
28
|
task default: %i[compile test]
|
29
|
+
|
30
|
+
desc "Bump version, create tag, and release gem"
|
31
|
+
task :publish, [:version] do |t, args|
|
32
|
+
version = args[:version] || "patch"
|
33
|
+
|
34
|
+
puts "Bumping version (#{version})..."
|
35
|
+
sh "bump #{version} --tag"
|
36
|
+
|
37
|
+
puts "Pushing to git..."
|
38
|
+
sh "git push && git push --tags"
|
39
|
+
|
40
|
+
puts "Building and releasing gem..."
|
41
|
+
Rake::Task["release"].invoke
|
42
|
+
end
|
data/docs/breakthrough.md
CHANGED
@@ -91,7 +91,7 @@ impl Replica {
|
|
91
91
|
Ok(Replica(ThreadBound::new(replica))) // ✅ Wrapped in ThreadBound
|
92
92
|
}
|
93
93
|
|
94
|
-
fn
|
94
|
+
fn all_tasks(&self) -> Result<RHash, Error> {
|
95
95
|
let mut tc_replica = self.0.get_mut()?; // ✅ Thread check + access
|
96
96
|
let tasks = tc_replica.all_tasks().map_err(into_error)?;
|
97
97
|
// ... process tasks
|
data/docs/plan.md
CHANGED
data/examples/basic_usage.rb
CHANGED
@@ -18,7 +18,7 @@ db_path = File.join(temp_dir, "tasks")
|
|
18
18
|
begin
|
19
19
|
# 1. CREATE TASK DATABASE
|
20
20
|
puts "\n1. Creating task database at #{db_path}"
|
21
|
-
replica = Taskchampion::Replica.new_on_disk(db_path,
|
21
|
+
replica = Taskchampion::Replica.new_on_disk(db_path, true, :read_write)
|
22
22
|
|
23
23
|
# 2. CREATE AND MODIFY TASKS
|
24
24
|
puts "\n2. Creating and modifying tasks"
|
@@ -39,8 +39,7 @@ begin
|
|
39
39
|
task1.add_tag(Taskchampion::Tag.new("ruby"), operations)
|
40
40
|
|
41
41
|
# Add annotation
|
42
|
-
|
43
|
-
task1.add_annotation(annotation, operations)
|
42
|
+
task1.add_annotation("Started learning TaskChampion", operations)
|
44
43
|
|
45
44
|
# Create second task
|
46
45
|
uuid2 = SecureRandom.uuid
|
@@ -61,7 +60,6 @@ begin
|
|
61
60
|
task3.set_description("Read TaskChampion documentation", operations)
|
62
61
|
task3.set_status(Taskchampion::Status.completed, operations)
|
63
62
|
task3.add_tag(Taskchampion::Tag.new("learning"), operations)
|
64
|
-
task3.set_end(Time.now, operations)
|
65
63
|
|
66
64
|
# 3. COMMIT CHANGES TO STORAGE
|
67
65
|
puts "\n3. Committing changes to storage"
|
@@ -88,14 +86,13 @@ begin
|
|
88
86
|
|
89
87
|
# Display tags
|
90
88
|
if !task.tags.empty?
|
91
|
-
tag_names = task.tags.map(&:
|
89
|
+
tag_names = task.tags.map(&:to_s)
|
92
90
|
puts " Tags: #{tag_names.join(', ')}"
|
93
91
|
end
|
94
92
|
|
95
93
|
# Display dates
|
96
94
|
puts " Created: #{task.entry.strftime('%Y-%m-%d %H:%M:%S')}" if task.entry
|
97
95
|
puts " Due: #{task.due.strftime('%Y-%m-%d %H:%M:%S')}" if task.due
|
98
|
-
puts " Completed: #{task.end.strftime('%Y-%m-%d %H:%M:%S')}" if task.end
|
99
96
|
|
100
97
|
# Display annotations
|
101
98
|
if !task.annotations.empty?
|
@@ -136,29 +133,42 @@ begin
|
|
136
133
|
|
137
134
|
# Find tasks due today or tomorrow
|
138
135
|
tomorrow = Time.now + 86400
|
139
|
-
due_soon = all_tasks.select { |t| t.due && t.due <= tomorrow }
|
136
|
+
due_soon = all_tasks.select { |t| t.due && t.due.to_time <= tomorrow }
|
140
137
|
puts "Tasks due soon: #{due_soon.length}"
|
141
138
|
|
142
139
|
# 6. MODIFY EXISTING TASKS
|
143
140
|
puts "\n6. Modifying existing tasks"
|
144
141
|
|
145
|
-
# Complete the first task
|
142
|
+
# Complete the first task using the done method
|
146
143
|
operations2 = Taskchampion::Operations.new
|
147
144
|
|
148
145
|
# Retrieve task fresh from storage
|
149
146
|
task_to_complete = replica.task(uuid1)
|
150
147
|
if task_to_complete && task_to_complete.pending?
|
151
148
|
puts "Completing task: #{task_to_complete.description}"
|
152
|
-
|
153
|
-
|
149
|
+
|
150
|
+
# Use the done() method - a convenience method for marking tasks as completed
|
151
|
+
task_to_complete.done(operations2)
|
154
152
|
|
155
153
|
# Add completion annotation
|
156
|
-
|
157
|
-
task_to_complete.add_annotation(completion_note, operations2)
|
154
|
+
task_to_complete.add_annotation("Completed successfully!", operations2)
|
158
155
|
|
159
156
|
# Commit the changes
|
160
157
|
replica.commit_operations(operations2)
|
161
|
-
puts "Task completed and committed"
|
158
|
+
puts "Task completed using done() method and committed"
|
159
|
+
end
|
160
|
+
|
161
|
+
# Complete the second task using traditional set_status for comparison
|
162
|
+
operations2b = Taskchampion::Operations.new
|
163
|
+
task_to_complete2 = replica.task(uuid2)
|
164
|
+
if task_to_complete2 && task_to_complete2.pending?
|
165
|
+
puts "Completing second task: #{task_to_complete2.description}"
|
166
|
+
|
167
|
+
# Alternative way: using set_status directly
|
168
|
+
task_to_complete2.set_status(Taskchampion::Status.completed, operations2b)
|
169
|
+
|
170
|
+
replica.commit_operations(operations2b)
|
171
|
+
puts "Task completed using set_status() method and committed"
|
162
172
|
end
|
163
173
|
|
164
174
|
# 7. WORKING WITH WORKING SET
|
@@ -233,8 +243,143 @@ begin
|
|
233
243
|
end
|
234
244
|
end
|
235
245
|
|
236
|
-
# 10.
|
237
|
-
puts "\n10.
|
246
|
+
# 10. UPDATING TASKS WITH TASKDATA
|
247
|
+
puts "\n10. Updating Tasks with TaskData"
|
248
|
+
|
249
|
+
# Create a new task to demonstrate TaskData updates
|
250
|
+
operations_update = Taskchampion::Operations.new
|
251
|
+
update_uuid = SecureRandom.uuid
|
252
|
+
|
253
|
+
puts "Creating task for TaskData update demo: #{update_uuid}"
|
254
|
+
|
255
|
+
# Create task using TaskData (low-level API)
|
256
|
+
task_data = Taskchampion::TaskData.create(update_uuid, operations_update)
|
257
|
+
|
258
|
+
# Set initial properties using TaskData
|
259
|
+
task_data.update("description", "Task for update demo", operations_update)
|
260
|
+
task_data.update("status", "pending", operations_update)
|
261
|
+
task_data.update("priority", "L", operations_update) # Low priority
|
262
|
+
task_data.update("project", "examples", operations_update)
|
263
|
+
|
264
|
+
# Commit initial task
|
265
|
+
replica.commit_operations(operations_update)
|
266
|
+
puts "Initial task created with TaskData API"
|
267
|
+
|
268
|
+
# Retrieve and display initial task
|
269
|
+
initial_task_data = replica.task_data(update_uuid)
|
270
|
+
if initial_task_data
|
271
|
+
puts "Initial task properties:"
|
272
|
+
initial_task_data.properties.each do |prop|
|
273
|
+
puts " #{prop}: #{initial_task_data.get(prop)}"
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
# Update the task with new properties
|
278
|
+
update_operations = Taskchampion::Operations.new
|
279
|
+
retrieved_task_data = replica.task_data(update_uuid)
|
280
|
+
|
281
|
+
if retrieved_task_data
|
282
|
+
puts "\nUpdating task properties..."
|
283
|
+
|
284
|
+
# Update existing properties
|
285
|
+
retrieved_task_data.update("description", "Updated task description", update_operations)
|
286
|
+
retrieved_task_data.update("priority", "M", update_operations) # Medium priority
|
287
|
+
|
288
|
+
# Add new properties
|
289
|
+
retrieved_task_data.update("tags", "example,taskdata,demo", update_operations)
|
290
|
+
retrieved_task_data.update("estimate", "2h", update_operations)
|
291
|
+
retrieved_task_data.update("modified", Time.now.to_i.to_s, update_operations)
|
292
|
+
|
293
|
+
# Remove a property by setting it to nil
|
294
|
+
retrieved_task_data.update("project", nil, update_operations)
|
295
|
+
|
296
|
+
# Commit updates
|
297
|
+
replica.commit_operations(update_operations)
|
298
|
+
puts "Task updated successfully"
|
299
|
+
|
300
|
+
# Display updated task
|
301
|
+
updated_task_data = replica.task_data(update_uuid)
|
302
|
+
if updated_task_data
|
303
|
+
puts "Updated task properties:"
|
304
|
+
updated_task_data.properties.each do |prop|
|
305
|
+
puts " #{prop}: #{updated_task_data.get(prop)}"
|
306
|
+
end
|
307
|
+
|
308
|
+
puts "Task hash representation:"
|
309
|
+
puts " #{updated_task_data.to_hash}"
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
# 11. DELETING TASKS WITH TASKDATA
|
314
|
+
puts "\n11. Deleting Tasks with TaskData"
|
315
|
+
|
316
|
+
# Create a task specifically for deletion demo
|
317
|
+
delete_operations = Taskchampion::Operations.new
|
318
|
+
delete_uuid = SecureRandom.uuid
|
319
|
+
|
320
|
+
puts "Creating task for deletion demo: #{delete_uuid}"
|
321
|
+
|
322
|
+
# Create task to be deleted
|
323
|
+
deletable_task_data = Taskchampion::TaskData.create(delete_uuid, delete_operations)
|
324
|
+
deletable_task_data.update("description", "Task to be deleted", delete_operations)
|
325
|
+
deletable_task_data.update("status", "completed", delete_operations)
|
326
|
+
deletable_task_data.update("note", "This task will be deleted", delete_operations)
|
327
|
+
|
328
|
+
# Commit the task
|
329
|
+
replica.commit_operations(delete_operations)
|
330
|
+
puts "Task created for deletion"
|
331
|
+
|
332
|
+
# Verify task exists
|
333
|
+
before_delete = replica.task_data(delete_uuid)
|
334
|
+
if before_delete
|
335
|
+
puts "Task exists before deletion:"
|
336
|
+
puts " Description: #{before_delete.get('description')}"
|
337
|
+
puts " Status: #{before_delete.get('status')}"
|
338
|
+
puts " Note: #{before_delete.get('note')}"
|
339
|
+
puts " Properties: #{before_delete.properties.join(', ')}"
|
340
|
+
else
|
341
|
+
puts "ERROR: Task not found before deletion"
|
342
|
+
end
|
343
|
+
|
344
|
+
# Delete the task using TaskData.delete
|
345
|
+
final_delete_operations = Taskchampion::Operations.new
|
346
|
+
task_to_delete = replica.task_data(delete_uuid)
|
347
|
+
|
348
|
+
if task_to_delete
|
349
|
+
puts "\nDeleting task..."
|
350
|
+
task_to_delete.delete(final_delete_operations)
|
351
|
+
|
352
|
+
# Commit the deletion
|
353
|
+
replica.commit_operations(final_delete_operations)
|
354
|
+
puts "Task deletion committed"
|
355
|
+
|
356
|
+
# Verify task is deleted
|
357
|
+
after_delete = replica.task_data(delete_uuid)
|
358
|
+
if after_delete.nil?
|
359
|
+
puts "✓ Task successfully deleted - no longer exists in database"
|
360
|
+
else
|
361
|
+
puts "✗ Task still exists after deletion attempt"
|
362
|
+
end
|
363
|
+
|
364
|
+
# Also verify it's not in the task list
|
365
|
+
remaining_uuids = replica.task_uuids
|
366
|
+
if remaining_uuids.include?(delete_uuid)
|
367
|
+
puts "✗ Task UUID still found in task list"
|
368
|
+
else
|
369
|
+
puts "✓ Task UUID removed from task list"
|
370
|
+
end
|
371
|
+
else
|
372
|
+
puts "ERROR: Could not retrieve task for deletion"
|
373
|
+
end
|
374
|
+
|
375
|
+
# Show difference between task deletion and status update
|
376
|
+
puts "\nNote: TaskData.delete() completely removes the task from the database."
|
377
|
+
puts "This is different from setting status to 'deleted', which keeps the task"
|
378
|
+
puts "but marks it as deleted. Use TaskData.delete() when you want to permanently"
|
379
|
+
puts "purge a task and all its data."
|
380
|
+
|
381
|
+
# 12. FINAL STATISTICS
|
382
|
+
puts "\n12. Final Statistics"
|
238
383
|
|
239
384
|
# Refresh task list
|
240
385
|
final_uuids = replica.task_uuids
|
data/examples/sync_workflow.rb
CHANGED
@@ -231,10 +231,10 @@ begin
|
|
231
231
|
begin
|
232
232
|
replica.sync_to_local(server_dir, avoid_snapshots: false)
|
233
233
|
puts " ✓ Post-work sync completed"
|
234
|
-
|
234
|
+
true
|
235
235
|
rescue => e
|
236
236
|
puts " ✗ Post-work sync failed: #{e.message}"
|
237
|
-
|
237
|
+
false
|
238
238
|
end
|
239
239
|
end
|
240
240
|
|
data/ext/taskchampion/Cargo.toml
CHANGED
data/ext/taskchampion/src/lib.rs
CHANGED
@@ -8,6 +8,7 @@ mod status;
|
|
8
8
|
mod tag;
|
9
9
|
mod annotation;
|
10
10
|
mod task;
|
11
|
+
mod task_data;
|
11
12
|
mod operation;
|
12
13
|
mod operations;
|
13
14
|
mod replica;
|
@@ -31,6 +32,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
31
32
|
tag::init(&module)?;
|
32
33
|
annotation::init(&module)?;
|
33
34
|
task::init(&module)?;
|
35
|
+
task_data::init(&module)?;
|
34
36
|
operation::init(&module)?;
|
35
37
|
operations::init(&module)?;
|
36
38
|
working_set::init(&module)?;
|
@@ -6,6 +6,7 @@ use taskchampion::{Replica as TCReplica, ServerConfig, StorageConfig};
|
|
6
6
|
use crate::access_mode::AccessMode;
|
7
7
|
use crate::operations::Operations;
|
8
8
|
use crate::task::Task;
|
9
|
+
use crate::task_data::TaskData;
|
9
10
|
use crate::working_set::WorkingSet;
|
10
11
|
use crate::dependency_map::DependencyMap;
|
11
12
|
use crate::thread_check::ThreadBound;
|
@@ -77,7 +78,7 @@ impl Replica {
|
|
77
78
|
Ok(())
|
78
79
|
}
|
79
80
|
|
80
|
-
fn
|
81
|
+
fn all_tasks(&self) -> Result<RHash, Error> {
|
81
82
|
let mut tc_replica = self.0.get_mut()?;
|
82
83
|
|
83
84
|
let tasks = tc_replica.all_tasks().map_err(into_error)?;
|
@@ -99,9 +100,9 @@ impl Replica {
|
|
99
100
|
.get_task_data(uuid2tc(&uuid)?)
|
100
101
|
.map_err(into_error)?;
|
101
102
|
|
102
|
-
option_to_ruby(task_data, |
|
103
|
-
|
104
|
-
Ok(
|
103
|
+
option_to_ruby(task_data, |data| {
|
104
|
+
let ruby_task_data = TaskData::from_tc_task_data(data);
|
105
|
+
Ok(ruby_task_data.into_value())
|
105
106
|
})
|
106
107
|
}
|
107
108
|
|
@@ -271,7 +272,7 @@ pub fn init(module: &RModule) -> Result<(), Error> {
|
|
271
272
|
// Instance methods
|
272
273
|
class.define_method("create_task", method!(Replica::create_task, 2))?;
|
273
274
|
class.define_method("commit_operations", method!(Replica::commit_operations, 1))?;
|
274
|
-
class.define_method("
|
275
|
+
class.define_method("all_tasks", method!(Replica::all_tasks, 0))?;
|
275
276
|
class.define_method("task", method!(Replica::task, 1))?;
|
276
277
|
class.define_method("task_data", method!(Replica::task_data, 1))?;
|
277
278
|
class.define_method("task_uuids", method!(Replica::task_uuids, 0))?;
|
@@ -7,7 +7,7 @@ use crate::annotation::Annotation;
|
|
7
7
|
use crate::status::Status;
|
8
8
|
use crate::tag::Tag;
|
9
9
|
use crate::thread_check::ThreadBound;
|
10
|
-
use crate::util::{datetime_to_ruby,
|
10
|
+
use crate::util::{datetime_to_ruby, option_to_ruby, ruby_to_datetime, ruby_to_option, vec_to_ruby};
|
11
11
|
|
12
12
|
#[magnus::wrap(class = "Taskchampion::Task", free_immediately)]
|
13
13
|
pub struct Task(ThreadBound<TCTask>);
|
@@ -326,6 +326,14 @@ impl Task {
|
|
326
326
|
Ok(())
|
327
327
|
}
|
328
328
|
|
329
|
+
fn done(&self, operations: &crate::operations::Operations) -> Result<(), Error> {
|
330
|
+
let mut task = self.0.get_mut()?;
|
331
|
+
operations.with_inner_mut(|ops| {
|
332
|
+
task.done(ops)
|
333
|
+
})?;
|
334
|
+
Ok(())
|
335
|
+
}
|
336
|
+
|
329
337
|
}
|
330
338
|
|
331
339
|
// Remove AsRef implementation as it doesn't work well with thread bounds
|
@@ -384,5 +392,6 @@ pub fn init(module: &RModule) -> Result<(), Error> {
|
|
384
392
|
class.define_method("set_value", method!(Task::set_value, 3))?;
|
385
393
|
class.define_method("set_uda", method!(Task::set_uda, 4))?;
|
386
394
|
class.define_method("delete_uda", method!(Task::delete_uda, 3))?;
|
395
|
+
class.define_method("done", method!(Task::done, 1))?;
|
387
396
|
Ok(())
|
388
397
|
}
|
@@ -0,0 +1,122 @@
|
|
1
|
+
use magnus::{
|
2
|
+
class, function, method, prelude::*, Error, IntoValue, RArray, RHash, RModule, Value,
|
3
|
+
};
|
4
|
+
use taskchampion::TaskData as TCTaskData;
|
5
|
+
|
6
|
+
use crate::operations::Operations;
|
7
|
+
use crate::thread_check::ThreadBound;
|
8
|
+
use crate::util::{option_to_ruby, uuid2tc, vec_to_ruby};
|
9
|
+
|
10
|
+
#[magnus::wrap(class = "Taskchampion::TaskData", free_immediately)]
|
11
|
+
pub struct TaskData(ThreadBound<TCTaskData>);
|
12
|
+
|
13
|
+
impl TaskData {
|
14
|
+
pub fn from_tc_task_data(tc_task_data: TCTaskData) -> Self {
|
15
|
+
TaskData(ThreadBound::new(tc_task_data))
|
16
|
+
}
|
17
|
+
fn inspect(&self) -> Result<String, Error> {
|
18
|
+
let task_data = self.0.get()?;
|
19
|
+
Ok(format!("#<Taskchampion::TaskData: {}>", task_data.get_uuid()))
|
20
|
+
}
|
21
|
+
|
22
|
+
fn uuid(&self) -> Result<String, Error> {
|
23
|
+
let task_data = self.0.get()?;
|
24
|
+
Ok(task_data.get_uuid().to_string())
|
25
|
+
}
|
26
|
+
|
27
|
+
fn get(&self, property: String) -> Result<Value, Error> {
|
28
|
+
let task_data = self.0.get()?;
|
29
|
+
option_to_ruby(task_data.get(&property), |s| Ok(s.into_value()))
|
30
|
+
}
|
31
|
+
|
32
|
+
fn has(&self, property: String) -> Result<bool, Error> {
|
33
|
+
let task_data = self.0.get()?;
|
34
|
+
Ok(task_data.has(&property))
|
35
|
+
}
|
36
|
+
|
37
|
+
fn properties(&self) -> Result<RArray, Error> {
|
38
|
+
let task_data = self.0.get()?;
|
39
|
+
let props: Vec<String> = task_data.properties().cloned().collect();
|
40
|
+
vec_to_ruby(props, |s| Ok(s.into_value()))
|
41
|
+
}
|
42
|
+
|
43
|
+
fn to_hash(&self) -> Result<RHash, Error> {
|
44
|
+
let task_data = self.0.get()?;
|
45
|
+
let hash = RHash::new();
|
46
|
+
|
47
|
+
for (key, value) in task_data.iter() {
|
48
|
+
hash.aset(key.clone(), value.clone())?;
|
49
|
+
}
|
50
|
+
|
51
|
+
Ok(hash)
|
52
|
+
}
|
53
|
+
|
54
|
+
fn update(&self, property: String, value: Value, operations: &Operations) -> Result<(), Error> {
|
55
|
+
if property.trim().is_empty() {
|
56
|
+
return Err(Error::new(
|
57
|
+
crate::error::validation_error(),
|
58
|
+
"Property name cannot be empty or whitespace-only"
|
59
|
+
));
|
60
|
+
}
|
61
|
+
|
62
|
+
let mut task_data = self.0.get_mut()?;
|
63
|
+
let value_str = if value.is_nil() {
|
64
|
+
None
|
65
|
+
} else {
|
66
|
+
Some(value.to_string())
|
67
|
+
};
|
68
|
+
|
69
|
+
operations.with_inner_mut(|ops| {
|
70
|
+
task_data.update(&property, value_str, ops);
|
71
|
+
Ok(())
|
72
|
+
})?;
|
73
|
+
|
74
|
+
Ok(())
|
75
|
+
}
|
76
|
+
|
77
|
+
fn delete(&self, operations: &Operations) -> Result<(), Error> {
|
78
|
+
let mut task_data = self.0.get_mut()?;
|
79
|
+
|
80
|
+
operations.with_inner_mut(|ops| {
|
81
|
+
task_data.delete(ops);
|
82
|
+
Ok(())
|
83
|
+
})?;
|
84
|
+
|
85
|
+
Ok(())
|
86
|
+
}
|
87
|
+
}
|
88
|
+
|
89
|
+
fn create_task_data(uuid: String, operations: &Operations) -> Result<TaskData, Error> {
|
90
|
+
let tc_uuid = uuid2tc(&uuid)?;
|
91
|
+
|
92
|
+
// Create operations for TaskChampion
|
93
|
+
let mut tc_ops = taskchampion::Operations::new();
|
94
|
+
|
95
|
+
// Create the TaskData
|
96
|
+
let tc_task_data = TCTaskData::create(tc_uuid, &mut tc_ops);
|
97
|
+
|
98
|
+
// Add the resulting operations to the provided Operations object
|
99
|
+
operations.extend_from_tc(tc_ops.into_iter().collect())?;
|
100
|
+
|
101
|
+
Ok(TaskData(ThreadBound::new(tc_task_data)))
|
102
|
+
}
|
103
|
+
|
104
|
+
pub fn init(module: &RModule) -> Result<(), Error> {
|
105
|
+
let class = module.define_class("TaskData", class::object())?;
|
106
|
+
|
107
|
+
// Class methods
|
108
|
+
class.define_singleton_method("create", function!(create_task_data, 2))?;
|
109
|
+
|
110
|
+
// Instance methods
|
111
|
+
class.define_method("inspect", method!(TaskData::inspect, 0))?;
|
112
|
+
class.define_method("uuid", method!(TaskData::uuid, 0))?;
|
113
|
+
class.define_method("get", method!(TaskData::get, 1))?;
|
114
|
+
class.define_method("has?", method!(TaskData::has, 1))?;
|
115
|
+
class.define_method("properties", method!(TaskData::properties, 0))?;
|
116
|
+
class.define_method("to_hash", method!(TaskData::to_hash, 0))?;
|
117
|
+
class.define_method("to_h", method!(TaskData::to_hash, 0))?;
|
118
|
+
class.define_method("update", method!(TaskData::update, 3))?;
|
119
|
+
class.define_method("delete", method!(TaskData::delete, 1))?;
|
120
|
+
|
121
|
+
Ok(())
|
122
|
+
}
|
@@ -45,21 +45,34 @@ pub fn ruby_to_datetime(value: Value) -> Result<DateTime<Utc>, Error> {
|
|
45
45
|
format!("Invalid datetime format: '{}'. Expected ISO 8601 format (e.g., '2023-01-01T12:00:00Z') or '%Y-%m-%d %H:%M:%S %z'", s)
|
46
46
|
))
|
47
47
|
} else {
|
48
|
-
//
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
48
|
+
// Check if it's a Time object first (Time doesn't have iso8601 method)
|
49
|
+
let class = value.class();
|
50
|
+
let class_name = unsafe { class.name() };
|
51
|
+
let iso_string = if class_name == "Time" {
|
52
|
+
// For Time objects, use strftime to get ISO 8601-like format
|
53
|
+
value.funcall::<_, (&str,), String>("strftime", ("%Y-%m-%dT%H:%M:%S%z",))?
|
54
|
+
} else {
|
55
|
+
// For DateTime objects, use iso8601 method
|
56
|
+
match value.funcall::<_, (), String>("iso8601", ()) {
|
57
|
+
Ok(s) => s,
|
58
|
+
Err(_) => return Err(Error::new(
|
59
|
+
validation_error(),
|
60
|
+
format!("Cannot convert value to datetime. Expected Time, DateTime, or String, got: {}", class_name)
|
61
|
+
))
|
57
62
|
}
|
58
|
-
|
63
|
+
};
|
64
|
+
|
65
|
+
DateTime::parse_from_rfc3339(&iso_string)
|
66
|
+
.map(|dt| dt.with_timezone(&Utc))
|
67
|
+
.or_else(|_| {
|
68
|
+
// Try parsing the Time strftime format (%z gives +HHMM instead of +HH:MM)
|
69
|
+
DateTime::parse_from_str(&iso_string, "%Y-%m-%dT%H:%M:%S%z")
|
70
|
+
.map(|dt| dt.with_timezone(&Utc))
|
71
|
+
})
|
72
|
+
.map_err(|_| Error::new(
|
59
73
|
validation_error(),
|
60
|
-
format!("
|
74
|
+
format!("Invalid datetime from Ruby object: '{}'. Unable to parse as ISO 8601", iso_string)
|
61
75
|
))
|
62
|
-
}
|
63
76
|
}
|
64
77
|
}
|
65
78
|
|
data/lib/taskchampion/version.rb
CHANGED
metadata
CHANGED
@@ -1,13 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: taskchampion-rb
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tim Case
|
8
|
+
autorequire:
|
8
9
|
bindir: exe
|
9
10
|
cert_chain: []
|
10
|
-
date:
|
11
|
+
date: 2025-08-14 00:00:00.000000000 Z
|
11
12
|
dependencies:
|
12
13
|
- !ruby/object:Gem::Dependency
|
13
14
|
name: rb_sys
|
@@ -32,8 +33,8 @@ extensions:
|
|
32
33
|
- ext/taskchampion/extconf.rb
|
33
34
|
extra_rdoc_files: []
|
34
35
|
files:
|
35
|
-
- ".claude/settings.local.json"
|
36
36
|
- ".rubocop.yml"
|
37
|
+
- ".ruby-version"
|
37
38
|
- CHANGELOG.md
|
38
39
|
- Cargo.lock
|
39
40
|
- Cargo.toml
|
@@ -61,13 +62,12 @@ files:
|
|
61
62
|
- ext/taskchampion/src/status.rs
|
62
63
|
- ext/taskchampion/src/tag.rs
|
63
64
|
- ext/taskchampion/src/task.rs
|
65
|
+
- ext/taskchampion/src/task_data.rs
|
64
66
|
- ext/taskchampion/src/thread_check.rs
|
65
67
|
- ext/taskchampion/src/util.rs
|
66
68
|
- ext/taskchampion/src/working_set.rs
|
67
69
|
- lib/taskchampion.rb
|
68
70
|
- lib/taskchampion/version.rb
|
69
|
-
- sig/taskchampion.rbs
|
70
|
-
- taskchampion-0.2.0.gem
|
71
71
|
homepage: https://github.com/timcase/taskchampion-rb
|
72
72
|
licenses:
|
73
73
|
- MIT
|
@@ -76,6 +76,7 @@ metadata:
|
|
76
76
|
homepage_uri: https://github.com/timcase/taskchampion-rb
|
77
77
|
source_code_uri: https://github.com/timcase/taskchampion-rb
|
78
78
|
changelog_uri: https://github.com/timcase/taskchampion-rb/blob/main/CHANGELOG.md
|
79
|
+
post_install_message:
|
79
80
|
rdoc_options: []
|
80
81
|
require_paths:
|
81
82
|
- lib
|
@@ -90,7 +91,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
90
91
|
- !ruby/object:Gem::Version
|
91
92
|
version: 3.3.11
|
92
93
|
requirements: []
|
93
|
-
rubygems_version: 3.
|
94
|
+
rubygems_version: 3.4.1
|
95
|
+
signing_key:
|
94
96
|
specification_version: 4
|
95
97
|
summary: Ruby bindings for TaskChampion
|
96
98
|
test_files: []
|
data/.claude/settings.local.json
DELETED
@@ -1,14 +0,0 @@
|
|
1
|
-
{
|
2
|
-
"permissions": {
|
3
|
-
"allow": [
|
4
|
-
"Bash(bundle exec rake:*)",
|
5
|
-
"mcp__zen__chat",
|
6
|
-
"mcp__zen__thinkdeep",
|
7
|
-
"WebFetch(domain:github.com)",
|
8
|
-
"WebFetch(domain:docs.taskwarrior.org)",
|
9
|
-
"Bash(chmod:*)",
|
10
|
-
"Bash(gem build:*)"
|
11
|
-
],
|
12
|
-
"deny": []
|
13
|
-
}
|
14
|
-
}
|
data/sig/taskchampion.rbs
DELETED
data/taskchampion-0.2.0.gem
DELETED
Binary file
|