taskchampion-rb 0.2.0 → 0.3.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: c7110e37a7e2a8af13267d10879260e10b920b1ba49c3d9f5b124cc7d618862b
4
+ data.tar.gz: '029792b10587e8a680b80936e1156ac2e1a15d2ba19970219b4f1d5496570bc7'
5
5
  SHA512:
6
- metadata.gz: 9379b9c99bb21c7a23beeda1f92756f31f22543b05600e11302650575063e52fe23c99a49b8a51c875f602e4d88f022aaf11681232a2a66fe1201f44116d3055
7
- data.tar.gz: 0745ae9e0c8350b27318c7bd741d4dfaf92921c714b396f2bfeb3c277a8d89260aa53fe2a99c63b036e65e36e9e365398ffd0729ca5bc64b209266f80ba49e0f
6
+ metadata.gz: 7e7ba27d8a5202767273e9221d115640493dbba79b8de6563e0cfeb0d9d2c75945244f649d1874745c0c80fe769eb7efd089737843baf843c47dd3e649fa6db4
7
+ data.tar.gz: d75725f6f197a67195d290a44ebd045434c1ada09779551a2e3c08e8430e4cfd5c6266d86d000135bc444aea77ac945034cdb2218835d48f706e4b8961efea5f
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)
@@ -233,8 +233,143 @@ begin
233
233
  end
234
234
  end
235
235
 
236
- # 10. FINAL STATISTICS
237
- puts "\n10. Final Statistics"
236
+ # 10. UPDATING TASKS WITH TASKDATA
237
+ puts "\n10. Updating Tasks with TaskData"
238
+
239
+ # Create a new task to demonstrate TaskData updates
240
+ operations_update = Taskchampion::Operations.new
241
+ update_uuid = SecureRandom.uuid
242
+
243
+ puts "Creating task for TaskData update demo: #{update_uuid}"
244
+
245
+ # Create task using TaskData (low-level API)
246
+ task_data = Taskchampion::TaskData.create(update_uuid, operations_update)
247
+
248
+ # Set initial properties using TaskData
249
+ task_data.update("description", "Task for update demo", operations_update)
250
+ task_data.update("status", "pending", operations_update)
251
+ task_data.update("priority", "L", operations_update) # Low priority
252
+ task_data.update("project", "examples", operations_update)
253
+
254
+ # Commit initial task
255
+ replica.commit_operations(operations_update)
256
+ puts "Initial task created with TaskData API"
257
+
258
+ # Retrieve and display initial task
259
+ initial_task_data = replica.task_data(update_uuid)
260
+ if initial_task_data
261
+ puts "Initial task properties:"
262
+ initial_task_data.properties.each do |prop|
263
+ puts " #{prop}: #{initial_task_data.get(prop)}"
264
+ end
265
+ end
266
+
267
+ # Update the task with new properties
268
+ update_operations = Taskchampion::Operations.new
269
+ retrieved_task_data = replica.task_data(update_uuid)
270
+
271
+ if retrieved_task_data
272
+ puts "\nUpdating task properties..."
273
+
274
+ # Update existing properties
275
+ retrieved_task_data.update("description", "Updated task description", update_operations)
276
+ retrieved_task_data.update("priority", "M", update_operations) # Medium priority
277
+
278
+ # Add new properties
279
+ retrieved_task_data.update("tags", "example,taskdata,demo", update_operations)
280
+ retrieved_task_data.update("estimate", "2h", update_operations)
281
+ retrieved_task_data.update("modified", Time.now.to_i.to_s, update_operations)
282
+
283
+ # Remove a property by setting it to nil
284
+ retrieved_task_data.update("project", nil, update_operations)
285
+
286
+ # Commit updates
287
+ replica.commit_operations(update_operations)
288
+ puts "Task updated successfully"
289
+
290
+ # Display updated task
291
+ updated_task_data = replica.task_data(update_uuid)
292
+ if updated_task_data
293
+ puts "Updated task properties:"
294
+ updated_task_data.properties.each do |prop|
295
+ puts " #{prop}: #{updated_task_data.get(prop)}"
296
+ end
297
+
298
+ puts "Task hash representation:"
299
+ puts " #{updated_task_data.to_hash}"
300
+ end
301
+ end
302
+
303
+ # 11. DELETING TASKS WITH TASKDATA
304
+ puts "\n11. Deleting Tasks with TaskData"
305
+
306
+ # Create a task specifically for deletion demo
307
+ delete_operations = Taskchampion::Operations.new
308
+ delete_uuid = SecureRandom.uuid
309
+
310
+ puts "Creating task for deletion demo: #{delete_uuid}"
311
+
312
+ # Create task to be deleted
313
+ deletable_task_data = Taskchampion::TaskData.create(delete_uuid, delete_operations)
314
+ deletable_task_data.update("description", "Task to be deleted", delete_operations)
315
+ deletable_task_data.update("status", "completed", delete_operations)
316
+ deletable_task_data.update("note", "This task will be deleted", delete_operations)
317
+
318
+ # Commit the task
319
+ replica.commit_operations(delete_operations)
320
+ puts "Task created for deletion"
321
+
322
+ # Verify task exists
323
+ before_delete = replica.task_data(delete_uuid)
324
+ if before_delete
325
+ puts "Task exists before deletion:"
326
+ puts " Description: #{before_delete.get('description')}"
327
+ puts " Status: #{before_delete.get('status')}"
328
+ puts " Note: #{before_delete.get('note')}"
329
+ puts " Properties: #{before_delete.properties.join(', ')}"
330
+ else
331
+ puts "ERROR: Task not found before deletion"
332
+ end
333
+
334
+ # Delete the task using TaskData.delete
335
+ final_delete_operations = Taskchampion::Operations.new
336
+ task_to_delete = replica.task_data(delete_uuid)
337
+
338
+ if task_to_delete
339
+ puts "\nDeleting task..."
340
+ task_to_delete.delete(final_delete_operations)
341
+
342
+ # Commit the deletion
343
+ replica.commit_operations(final_delete_operations)
344
+ puts "Task deletion committed"
345
+
346
+ # Verify task is deleted
347
+ after_delete = replica.task_data(delete_uuid)
348
+ if after_delete.nil?
349
+ puts "✓ Task successfully deleted - no longer exists in database"
350
+ else
351
+ puts "✗ Task still exists after deletion attempt"
352
+ end
353
+
354
+ # Also verify it's not in the task list
355
+ remaining_uuids = replica.task_uuids
356
+ if remaining_uuids.include?(delete_uuid)
357
+ puts "✗ Task UUID still found in task list"
358
+ else
359
+ puts "✓ Task UUID removed from task list"
360
+ end
361
+ else
362
+ puts "ERROR: Could not retrieve task for deletion"
363
+ end
364
+
365
+ # Show difference between task deletion and status update
366
+ puts "\nNote: TaskData.delete() completely removes the task from the database."
367
+ puts "This is different from setting status to 'deleted', which keeps the task"
368
+ puts "but marks it as deleted. Use TaskData.delete() when you want to permanently"
369
+ puts "purge a task and all its data."
370
+
371
+ # 12. FINAL STATISTICS
372
+ puts "\n12. Final Statistics"
238
373
 
239
374
  # Refresh task list
240
375
  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))?;
@@ -0,0 +1,124 @@
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
+
18
+
19
+ fn inspect(&self) -> Result<String, Error> {
20
+ let task_data = self.0.get()?;
21
+ Ok(format!("#<Taskchampion::TaskData: {}>", task_data.get_uuid()))
22
+ }
23
+
24
+ fn uuid(&self) -> Result<String, Error> {
25
+ let task_data = self.0.get()?;
26
+ Ok(task_data.get_uuid().to_string())
27
+ }
28
+
29
+ fn get(&self, property: String) -> Result<Value, Error> {
30
+ let task_data = self.0.get()?;
31
+ option_to_ruby(task_data.get(&property), |s| Ok(s.into_value()))
32
+ }
33
+
34
+ fn has(&self, property: String) -> Result<bool, Error> {
35
+ let task_data = self.0.get()?;
36
+ Ok(task_data.has(&property))
37
+ }
38
+
39
+ fn properties(&self) -> Result<RArray, Error> {
40
+ let task_data = self.0.get()?;
41
+ let props: Vec<String> = task_data.properties().cloned().collect();
42
+ vec_to_ruby(props, |s| Ok(s.into_value()))
43
+ }
44
+
45
+ fn to_hash(&self) -> Result<RHash, Error> {
46
+ let task_data = self.0.get()?;
47
+ let hash = RHash::new();
48
+
49
+ for (key, value) in task_data.iter() {
50
+ hash.aset(key.clone(), value.clone())?;
51
+ }
52
+
53
+ Ok(hash)
54
+ }
55
+
56
+ fn update(&self, property: String, value: Value, operations: &Operations) -> Result<(), Error> {
57
+ if property.trim().is_empty() {
58
+ return Err(Error::new(
59
+ crate::error::validation_error(),
60
+ "Property name cannot be empty or whitespace-only"
61
+ ));
62
+ }
63
+
64
+ let mut task_data = self.0.get_mut()?;
65
+ let value_str = if value.is_nil() {
66
+ None
67
+ } else {
68
+ Some(value.to_string())
69
+ };
70
+
71
+ operations.with_inner_mut(|ops| {
72
+ task_data.update(&property, value_str, ops);
73
+ Ok(())
74
+ })?;
75
+
76
+ Ok(())
77
+ }
78
+
79
+ fn delete(&self, operations: &Operations) -> Result<(), Error> {
80
+ let mut task_data = self.0.get_mut()?;
81
+
82
+ operations.with_inner_mut(|ops| {
83
+ task_data.delete(ops);
84
+ Ok(())
85
+ })?;
86
+
87
+ Ok(())
88
+ }
89
+ }
90
+
91
+ fn create_task_data(uuid: String, operations: &Operations) -> Result<TaskData, Error> {
92
+ let tc_uuid = uuid2tc(&uuid)?;
93
+
94
+ // Create operations for TaskChampion
95
+ let mut tc_ops = taskchampion::Operations::new();
96
+
97
+ // Create the TaskData
98
+ let tc_task_data = TCTaskData::create(tc_uuid, &mut tc_ops);
99
+
100
+ // Add the resulting operations to the provided Operations object
101
+ operations.extend_from_tc(tc_ops.into_iter().collect())?;
102
+
103
+ Ok(TaskData(ThreadBound::new(tc_task_data)))
104
+ }
105
+
106
+ pub fn init(module: &RModule) -> Result<(), Error> {
107
+ let class = module.define_class("TaskData", class::object())?;
108
+
109
+ // Class methods
110
+ class.define_singleton_method("create", function!(create_task_data, 2))?;
111
+
112
+ // Instance methods
113
+ class.define_method("inspect", method!(TaskData::inspect, 0))?;
114
+ class.define_method("uuid", method!(TaskData::uuid, 0))?;
115
+ class.define_method("get", method!(TaskData::get, 1))?;
116
+ class.define_method("has?", method!(TaskData::has, 1))?;
117
+ class.define_method("properties", method!(TaskData::properties, 0))?;
118
+ class.define_method("to_hash", method!(TaskData::to_hash, 0))?;
119
+ class.define_method("to_h", method!(TaskData::to_hash, 0))?;
120
+ class.define_method("update", method!(TaskData::update, 3))?;
121
+ class.define_method("delete", method!(TaskData::delete, 1))?;
122
+
123
+ Ok(())
124
+ }
@@ -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.3.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.3.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