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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '029205bd9244f841d99e2913b09dd0e631ef11c28e3ddbc4745d54c9731248ad'
4
- data.tar.gz: 45c862f0b82dba0187025aaee04c85be748aa9f24a17f28428dbd9ff9827b710
3
+ metadata.gz: 8b267de15e0004e931e2a46f37f9014afe8fdc52e58e675190dab990cb345e75
4
+ data.tar.gz: 6a84efc5a165c428c67e6d50f3ce0b001eab4d3fde464ffed123d218aaf04033
5
5
  SHA512:
6
- metadata.gz: 9379b9c99bb21c7a23beeda1f92756f31f22543b05600e11302650575063e52fe23c99a49b8a51c875f602e4d88f022aaf11681232a2a66fe1201f44116d3055
7
- data.tar.gz: 0745ae9e0c8350b27318c7bd741d4dfaf92921c714b396f2bfeb3c277a8d89260aa53fe2a99c63b036e65e36e9e365398ffd0729ca5bc64b209266f80ba49e0f
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.0 or later
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 tasks(&self) -> Result<RHash, Error> {
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
@@ -453,7 +453,7 @@ task1.set_status(:pending)
453
453
  replica.commit_operations(operations)
454
454
 
455
455
  # Query tasks
456
- all_tasks = replica.tasks
456
+ all_tasks = replica.all_tasks
457
457
  puts "Total tasks: #{all_tasks.size}"
458
458
 
459
459
  # Sync with server (if configured)
@@ -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, create_if_missing: true)
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
- annotation = Taskchampion::Annotation.new(Time.now, "Started learning TaskChampion")
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(&:name)
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
- task_to_complete.set_status(Taskchampion::Status.completed, operations2)
153
- task_to_complete.set_end(Time.now, operations2)
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
- completion_note = Taskchampion::Annotation.new(Time.now, "Completed successfully!")
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. FINAL STATISTICS
237
- puts "\n10. Final Statistics"
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
@@ -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
- return true
234
+ true
235
235
  rescue => e
236
236
  puts " ✗ Post-work sync failed: #{e.message}"
237
- return false
237
+ false
238
238
  end
239
239
  end
240
240
 
@@ -11,7 +11,7 @@ crate-type = ["cdylib"]
11
11
 
12
12
  [dependencies]
13
13
  magnus = { version = "0.7", features = ["rb-sys"] }
14
- rb-sys = "0.9"
14
+ rb-sys = "0.9.103"
15
15
  taskchampion = "2.0"
16
16
  chrono = "0.4"
17
17
  uuid = "1.0"
@@ -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 tasks(&self) -> Result<RHash, Error> {
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, |_data| {
103
- // TODO: Convert task data to Ruby TaskData object
104
- Ok(().into_value()) // () converts to nil in Magnus
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("tasks", method!(Replica::tasks, 0))?;
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, into_error, option_to_ruby, ruby_to_datetime, ruby_to_option, vec_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
- // Convert Ruby DateTime/Time to ISO string then parse
49
- match value.funcall::<_, (), String>("iso8601", ()) {
50
- Ok(iso_string) => {
51
- DateTime::parse_from_rfc3339(&iso_string)
52
- .map(|dt| dt.with_timezone(&Utc))
53
- .map_err(|_| Error::new(
54
- validation_error(),
55
- format!("Invalid datetime from Ruby object: '{}'. Unable to parse as ISO 8601", iso_string)
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
- Err(_) => Err(Error::new(
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!("Cannot convert value to datetime. Expected Time, DateTime, or String, got: {}", value.class().inspect())
74
+ format!("Invalid datetime from Ruby object: '{}'. Unable to parse as ISO 8601", iso_string)
61
75
  ))
62
- }
63
76
  }
64
77
  }
65
78
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Taskchampion
4
- VERSION = "0.2.0"
4
+ VERSION = "0.5.0"
5
5
  end
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.2.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: 1980-01-02 00:00:00.000000000 Z
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.6.9
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: []
@@ -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
@@ -1,4 +0,0 @@
1
- module Taskchampion
2
- VERSION: String
3
- # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
- end
Binary file