taskchampion-rb 0.6.0 → 0.8.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: 494521791e308dacc686f45ef7ff87937a0038e46729092a4e145952971a5f56
4
- data.tar.gz: ac3a7b7367b2d8565ac902981fb4c7a9ee45b7d0c825868b15591211debc733b
3
+ metadata.gz: 9c5bf1065611d68b14549f0c6930e630f6e79483699d2ad05d2e1211da8192ac
4
+ data.tar.gz: f9cd13e961c3b814c4eb8474e22e585803e24f426f92e49dbb24c716640c52ee
5
5
  SHA512:
6
- metadata.gz: 3d5c1e59e10ac46d98a8fc30d666dac904fcf2d14742c12713a16050a7a67d87c21e1a42ebaf7d25762541d0335c3c36ab2cf6a93bd3ed778046cd1a739840f4
7
- data.tar.gz: 8f69921b628b5b1803a940584b05dd46abc854e50d9a3555646a141298a6c2a55008b2c8ca60b508fd53566bc6d0bd28a69f8caf76021e37a56e9fbf69bd362c
6
+ metadata.gz: 2724376c451663195d870dedf33dae60e50058909bb35ab89dad2c93b80e30b0aeacb19f0d412c41d2805a5c5fd9c447da5224808d060ddd241c06cabbfa4994
7
+ data.tar.gz: d6f75a71a7e4a040fe471c741c0f1842d09f36101009842896f8c74c9335aea8e5d72cc2fb707cb2088590835b508d632e2adc1c0103e136fc79d8b58b35be93
data/README.md CHANGED
@@ -8,6 +8,7 @@ This gem supports Ruby 3.2 and later. We follow Ruby's end-of-life (EOL) schedul
8
8
 
9
9
  - **Ruby 3.2**: Supported (EOL: March 2026)
10
10
  - **Ruby 3.3**: Supported (Current stable)
11
+ - **Ruby 3.4**: Supported
11
12
  - **Ruby 3.0-3.1**: Not supported (reached EOL)
12
13
 
13
14
  ## Installation
@@ -0,0 +1,113 @@
1
+ # Date Format Conversion in `task.set_due`
2
+
3
+ ## Overview
4
+
5
+ When `task.set_due` is invoked in the TaskChampion Ruby gem, there **is** a format conversion from various Ruby date/time formats to a Unix timestamp for storage.
6
+
7
+ ## Conversion Flow
8
+
9
+ The conversion follows this path: **Ruby date/time → UTC DateTime → Unix timestamp (stored as string)**
10
+
11
+ ### 1. Ruby Input Processing
12
+ **Location**: `ext/taskchampion/src/task.rs:258-264`
13
+
14
+ The `set_due` method accepts a Ruby `Value` which can be:
15
+ - `nil` (to clear the due date)
16
+ - A Ruby `Time` object
17
+ - A Ruby `DateTime` object
18
+ - A String in ISO 8601 format (e.g., "2023-01-01T12:00:00Z")
19
+ - A String in the format "%Y-%m-%d %H:%M:%S %z"
20
+
21
+ ```rust
22
+ fn set_due(&self, due: Value, operations: &crate::operations::Operations) -> Result<(), Error> {
23
+ let mut task = self.0.get_mut()?;
24
+ let due_datetime = ruby_to_option(due, ruby_to_datetime)?;
25
+ operations.with_inner_mut(|ops| {
26
+ task.set_due(due_datetime, ops)
27
+ })?;
28
+ Ok(())
29
+ }
30
+ ```
31
+
32
+ ### 2. Conversion to Rust DateTime
33
+ **Location**: `ext/taskchampion/src/util.rs:33-77`
34
+
35
+ The `ruby_to_datetime` function converts the Ruby value to a Rust `DateTime<Utc>`:
36
+
37
+ - **For `Time` objects**: Uses `strftime("%Y-%m-%dT%H:%M:%S%z")` to get ISO format, then parses it
38
+ - **For `DateTime` objects**: Calls the `iso8601()` method, then parses the result
39
+ - **For Strings**: Attempts to parse as RFC3339 first, then falls back to "%Y-%m-%d %H:%M:%S %z" format
40
+ - **All dates are converted to UTC timezone**
41
+
42
+ ```rust
43
+ pub fn ruby_to_datetime(value: Value) -> Result<DateTime<Utc>, Error> {
44
+ // String parsing
45
+ if let Ok(s) = RString::try_convert(value) {
46
+ let s = unsafe { s.as_str()? };
47
+ DateTime::parse_from_rfc3339(s)
48
+ .map(|dt| dt.with_timezone(&Utc))
49
+ .or_else(|_| DateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S %z")
50
+ .map(|dt| dt.with_timezone(&Utc)))
51
+ // ... error handling
52
+ } else {
53
+ // Ruby Time/DateTime object handling
54
+ let class_name = unsafe { value.class().name() };
55
+ let iso_string = if class_name == "Time" {
56
+ value.funcall::<_, (&str,), String>("strftime", ("%Y-%m-%dT%H:%M:%S%z",))?
57
+ } else {
58
+ value.funcall::<_, (), String>("iso8601", ())?
59
+ };
60
+ // Parse the ISO string...
61
+ }
62
+ }
63
+ ```
64
+
65
+ ### 3. Storage as Unix Timestamp
66
+ **Location**: `/home/tcase/Sites/reference/taskchampion/src/task/task.rs`
67
+
68
+ The underlying TaskChampion library stores the date as a Unix timestamp:
69
+
70
+ ```rust
71
+ pub fn set_due(&mut self, due: Option<Timestamp>, ops: &mut Operations) -> Result<()> {
72
+ self.set_timestamp(Prop::Due.as_ref(), due, ops)
73
+ }
74
+
75
+ pub fn set_timestamp(
76
+ &mut self,
77
+ property: &str,
78
+ value: Option<Timestamp>,
79
+ ops: &mut Operations,
80
+ ) -> Result<()> {
81
+ self.set_value(property, value.map(|v| v.timestamp().to_string()), ops)
82
+ }
83
+ ```
84
+
85
+ Where `Timestamp` is defined as:
86
+ ```rust
87
+ pub(crate) type Timestamp = DateTime<Utc>;
88
+ ```
89
+
90
+ The `.timestamp()` method converts the `DateTime<Utc>` to Unix timestamp (seconds since epoch), which is then converted to a string for storage (e.g., "1704067200").
91
+
92
+ ### 4. Retrieval Process
93
+ **Location**: `ext/taskchampion/src/task.rs`
94
+
95
+ When retrieving the due date, it's converted back from Unix timestamp to a Ruby DateTime object:
96
+
97
+ ```rust
98
+ fn due(&self) -> Result<Value, Error> {
99
+ let task = self.0.get()?;
100
+ option_to_ruby(task.get_due(), datetime_to_ruby)
101
+ }
102
+ ```
103
+
104
+ ## Summary
105
+
106
+ The `task.set_due` method performs comprehensive date format conversion:
107
+
108
+ 1. **Input**: Accepts various Ruby date/time formats and strings
109
+ 2. **Normalization**: Converts all inputs to UTC `DateTime<Utc>`
110
+ 3. **Storage**: Stores as Unix timestamp string in the underlying TaskChampion database
111
+ 4. **Retrieval**: Converts back to Ruby DateTime objects when accessed
112
+
113
+ This ensures consistent date handling across different input formats while maintaining precision and timezone normalization.
data/docs/errors.md ADDED
@@ -0,0 +1,151 @@
1
+ # TaskChampion Ruby Error Reference
2
+
3
+ ## Error Hierarchy
4
+
5
+ All TaskChampion errors inherit from `Taskchampion::Error`, which inherits from Ruby's `StandardError`.
6
+
7
+ ```
8
+ StandardError
9
+ └── Taskchampion::Error
10
+ ├── Taskchampion::ThreadError
11
+ ├── Taskchampion::StorageError
12
+ ├── Taskchampion::ValidationError
13
+ ├── Taskchampion::ConfigError
14
+ └── Taskchampion::SyncError
15
+ ```
16
+
17
+ ## Error Types
18
+
19
+ ### Taskchampion::ValidationError
20
+
21
+ Raised when data validation fails, including:
22
+ - Invalid UUID format
23
+ - Invalid datetime format
24
+ - Parse errors
25
+ - Format validation failures
26
+
27
+ **Example:**
28
+ ```ruby
29
+ # Invalid UUID format
30
+ replica.create_task("bad-uuid", operations)
31
+ # => Taskchampion::ValidationError: Invalid UUID format: 'bad-uuid'. Expected format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
32
+ ```
33
+
34
+ ### Taskchampion::ThreadError
35
+
36
+ Raised when an object is accessed from a different thread than the one that created it. TaskChampion enforces thread safety by requiring objects to be accessed only from their creation thread.
37
+
38
+ **Example:**
39
+ ```ruby
40
+ replica = Taskchampion::Replica.new_in_memory
41
+ Thread.new { replica.all_tasks }.join
42
+ # => Taskchampion::ThreadError: Object accessed from wrong thread
43
+ ```
44
+
45
+ ### Taskchampion::StorageError
46
+
47
+ Raised for database and storage-related issues:
48
+ - File not found
49
+ - Permission denied
50
+ - Database corruption
51
+ - Storage access failures
52
+
53
+ **Example:**
54
+ ```ruby
55
+ Taskchampion::Replica.new_on_disk("/invalid/path", false)
56
+ # => Taskchampion::StorageError: Storage error: No such file or directory
57
+ ```
58
+
59
+ ### Taskchampion::ConfigError
60
+
61
+ Raised when configuration is invalid or missing required parameters:
62
+ - Invalid configuration values
63
+ - Missing required configuration
64
+
65
+ **Example:**
66
+ ```ruby
67
+ # Missing required sync configuration
68
+ replica.sync_to_remote(url: "https://example.com")
69
+ # => Taskchampion::ConfigError: Configuration error: missing client_id
70
+ ```
71
+
72
+ ### Taskchampion::SyncError
73
+
74
+ Raised during synchronization operations:
75
+ - Network failures
76
+ - Server connection issues
77
+ - Remote sync problems
78
+ - Authentication failures
79
+
80
+ **Example:**
81
+ ```ruby
82
+ replica.sync_to_remote(
83
+ url: "https://invalid.server",
84
+ client_id: "...",
85
+ encryption_secret: "..."
86
+ )
87
+ # => Taskchampion::SyncError: Synchronization error: network timeout
88
+ ```
89
+
90
+ ### Taskchampion::Error
91
+
92
+ Generic error class for TaskChampion errors that don't fall into specific categories. This is the base class for all TaskChampion-specific errors.
93
+
94
+ ## Common Error Scenarios
95
+
96
+ ### Creating Tasks
97
+
98
+ ```ruby
99
+ begin
100
+ task = replica.create_task(uuid, operations)
101
+ rescue Taskchampion::ValidationError => e
102
+ # Handle invalid UUID format
103
+ puts "Invalid UUID: #{e.message}"
104
+ rescue Taskchampion::StorageError => e
105
+ # Handle storage issues
106
+ puts "Storage problem: #{e.message}"
107
+ rescue Taskchampion::ThreadError => e
108
+ # Handle thread safety violation
109
+ puts "Thread error: #{e.message}"
110
+ end
111
+ ```
112
+
113
+ ### Synchronization
114
+
115
+ ```ruby
116
+ begin
117
+ replica.sync_to_remote(
118
+ url: server_url,
119
+ client_id: client_id,
120
+ encryption_secret: secret
121
+ )
122
+ rescue Taskchampion::SyncError => e
123
+ # Handle sync failures
124
+ puts "Sync failed: #{e.message}"
125
+ rescue Taskchampion::ConfigError => e
126
+ # Handle configuration issues
127
+ puts "Config error: #{e.message}"
128
+ end
129
+ ```
130
+
131
+ ### File Operations
132
+
133
+ ```ruby
134
+ begin
135
+ replica = Taskchampion::Replica.new_on_disk(path, false, :read_write)
136
+ rescue Taskchampion::StorageError => e
137
+ # Handle file access issues
138
+ puts "Cannot access database: #{e.message}"
139
+ end
140
+ ```
141
+
142
+ ## Error Message Patterns
143
+
144
+ The error mapping system examines error message content to determine the appropriate exception type:
145
+
146
+ - Messages containing "storage", "database", "No such file", or "Permission denied" → `StorageError`
147
+ - Messages containing "sync", "server", "network", or "remote" → `SyncError`
148
+ - Messages containing "config" or "invalid config" → `ConfigError`
149
+ - Messages containing "invalid", "parse", "format", or "validation" → `ValidationError`
150
+ - Thread access violations → `ThreadError`
151
+ - All other TaskChampion errors → `Error` (base class)
@@ -53,4 +53,4 @@ pending.each_with_index do |task, index|
53
53
  end
54
54
 
55
55
  puts "\n" + "=" * 50
56
- puts "For comparison, all_tasks returns #{replica.all_tasks.size} tasks total"
56
+ puts "For comparison, all_tasks returns #{replica.all_tasks.size} tasks total"
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/taskchampion"
5
+
6
+ # Create a replica for this demo
7
+ replica = Taskchampion::Replica.new_in_memory
8
+
9
+ puts "=== TaskChampion Undo/History Demo ==="
10
+
11
+ # Create first task with undo point
12
+ puts "\n1. Creating first task..."
13
+ ops1 = Taskchampion::Operations.new
14
+ ops1.push(Taskchampion::Operation.undo_point)
15
+ task1 = replica.create_task("550e8400-e29b-41d4-a716-446655440000", ops1)
16
+ task1.set_description("First task", ops1)
17
+ replica.commit_operations(ops1)
18
+
19
+ # Create second task with undo point
20
+ puts "2. Creating second task..."
21
+ ops2 = Taskchampion::Operations.new
22
+ ops2.push(Taskchampion::Operation.undo_point)
23
+ task2 = replica.create_task("550e8400-e29b-41d4-a716-446655440001", ops2)
24
+ task2.set_description("Second task", ops2)
25
+ task2.set_value("project", "home", ops2)
26
+ replica.commit_operations(ops2)
27
+
28
+ # Update task2's description to show old_value -> new_value
29
+ puts "\n3. Updating second task's description..."
30
+ ops3 = Taskchampion::Operations.new
31
+ task2.set_description("Updated second task", ops3)
32
+ replica.commit_operations(ops3)
33
+
34
+ # Show current tasks
35
+ puts "\n4. Current tasks:"
36
+ replica.all_tasks.each do |uuid, task|
37
+ puts " - #{task.description} (#{uuid})"
38
+ end
39
+
40
+ # Show task history for task2
41
+ puts "\n5. History for second task:"
42
+ task2_ops = replica.task_operations(task2.uuid)
43
+ puts " Operations count: #{task2_ops.length}"
44
+ task2_ops.to_a.each_with_index do |op, i|
45
+ puts " #{i + 1}. #{op.operation_type}"
46
+ if op.operation_type == :update
47
+ old_val = op.old_value.nil? ? "(none)" : op.old_value
48
+ new_val = op.value.nil? ? "(none)" : op.value
49
+ puts " Details: #{op.property} changed from #{old_val} to #{new_val} at #{op.timestamp}"
50
+ end
51
+ end
52
+
53
+ # Check undo points available
54
+ puts "\n6. Undo points available: #{replica.num_undo_points}"
55
+
56
+ # Show what would be undone
57
+ puts "\n7. Operations that would be undone:"
58
+ undo_ops = replica.undo_operations
59
+ puts " Operations to undo: #{undo_ops.length}"
60
+ undo_ops.to_a.each_with_index do |op, i|
61
+ puts " #{i + 1}. #{op.operation_type}"
62
+ end
63
+
64
+ # Perform undo
65
+ puts "\n8. Performing undo..."
66
+ result = replica.undo!
67
+ puts " Undo successful: #{result}"
68
+
69
+ # Show tasks after undo
70
+ puts "\n9. Tasks after first undo:"
71
+ tasks = replica.all_tasks
72
+ if tasks.empty?
73
+ puts " No tasks"
74
+ else
75
+ tasks.each do |uuid, task|
76
+ puts " - #{task.description} (#{uuid})"
77
+ end
78
+ end
79
+
80
+ # Show remaining undo points
81
+ puts "\n10. Remaining undo points: #{replica.num_undo_points}"
82
+
83
+ # Perform another undo
84
+ if replica.num_undo_points > 0
85
+ puts "\n11. Performing second undo..."
86
+ result = replica.undo!
87
+ puts " Undo successful: #{result}"
88
+
89
+ puts "\n12. Tasks after second undo:"
90
+ tasks = replica.all_tasks
91
+ if tasks.empty?
92
+ puts " No tasks remaining"
93
+ else
94
+ tasks.each do |uuid, task|
95
+ puts " - #{task.description} (#{uuid})"
96
+ end
97
+ end
98
+
99
+ puts "\n13. Final undo points: #{replica.num_undo_points}"
100
+ else
101
+ puts "\n11. No more undo points available"
102
+ end
103
+
104
+ puts "\n=== Demo Complete ==="
@@ -137,6 +137,15 @@ impl Operations {
137
137
  }
138
138
  Ok(())
139
139
  }
140
+
141
+ // Internal method for creating Operations from TaskChampion operations
142
+ pub(crate) fn from_tc_operations(tc_ops: Vec<taskchampion::Operation>) -> Self {
143
+ let mut operations = TCOperations::new();
144
+ for op in tc_ops {
145
+ operations.push(op);
146
+ }
147
+ Operations(ThreadBound::new(RefCell::new(operations)))
148
+ }
140
149
  }
141
150
 
142
151
  // Note: AsRef and AsMut cannot be implemented with RefCell
@@ -93,6 +93,20 @@ impl Replica {
93
93
  Ok(hash)
94
94
  }
95
95
 
96
+ fn pending_tasks(&self) -> Result<RArray, Error> {
97
+ let mut tc_replica = self.0.get_mut()?;
98
+
99
+ let tc_tasks = tc_replica.pending_tasks().map_err(into_error)?;
100
+
101
+ let array = RArray::new();
102
+ for tc_task in tc_tasks {
103
+ let ruby_task = crate::task::Task::from_tc_task(tc_task);
104
+ array.push(ruby_task)?;
105
+ }
106
+
107
+ Ok(array)
108
+ }
109
+
96
110
  fn task_data(&self, uuid: String) -> Result<Value, Error> {
97
111
  let mut tc_replica = self.0.get_mut()?;
98
112
 
@@ -261,19 +275,37 @@ impl Replica {
261
275
  Ok(tc_replica.num_undo_points().map_err(into_error)?)
262
276
  }
263
277
 
264
- fn pending_tasks(&self) -> Result<RArray, Error> {
278
+ fn get_task_operations(&self, uuid: String) -> Result<Value, Error> {
265
279
  let mut tc_replica = self.0.get_mut()?;
280
+ let tc_uuid = uuid2tc(&uuid)?;
266
281
 
267
- let tc_tasks = tc_replica.pending_tasks().map_err(into_error)?;
282
+ let tc_operations = tc_replica.get_task_operations(tc_uuid).map_err(into_error)?;
283
+ let operations = Operations::from_tc_operations(tc_operations);
268
284
 
269
- let array = RArray::new();
270
- for tc_task in tc_tasks {
271
- let ruby_task = crate::task::Task::from_tc_task(tc_task);
272
- array.push(ruby_task)?;
273
- }
285
+ Ok(operations.into_value())
286
+ }
274
287
 
275
- Ok(array)
288
+ fn get_undo_operations(&self) -> Result<Value, Error> {
289
+ let mut tc_replica = self.0.get_mut()?;
290
+
291
+ let tc_operations = tc_replica.get_undo_operations().map_err(into_error)?;
292
+ let operations = Operations::from_tc_operations(tc_operations);
293
+
294
+ Ok(operations.into_value())
276
295
  }
296
+
297
+ fn commit_reversed_operations(&self, operations: &Operations) -> Result<bool, Error> {
298
+ let mut tc_replica = self.0.get_mut()?;
299
+
300
+ // Convert Operations to TaskChampion Operations
301
+ let tc_operations = operations.clone_inner()?;
302
+
303
+ // Commit the reversed operations
304
+ let success = tc_replica.commit_reversed_operations(tc_operations).map_err(into_error)?;
305
+
306
+ Ok(success)
307
+ }
308
+
277
309
  }
278
310
 
279
311
  pub fn init(module: &RModule) -> Result<(), Error> {
@@ -299,6 +331,9 @@ pub fn init(module: &RModule) -> Result<(), Error> {
299
331
  class.define_method("expire_tasks", method!(Replica::expire_tasks, 0))?;
300
332
  class.define_method("num_local_operations", method!(Replica::num_local_operations, 0))?;
301
333
  class.define_method("num_undo_points", method!(Replica::num_undo_points, 0))?;
334
+ class.define_method("get_task_operations", method!(Replica::get_task_operations, 1))?;
335
+ class.define_method("get_undo_operations", method!(Replica::get_undo_operations, 0))?;
336
+ class.define_method("commit_reversed_operations", method!(Replica::commit_reversed_operations, 1))?;
302
337
  class.define_method("pending_tasks", method!(Replica::pending_tasks, 0))?;
303
338
 
304
339
  Ok(())
@@ -264,6 +264,15 @@ impl Task {
264
264
  Ok(())
265
265
  }
266
266
 
267
+ fn set_entry(&self, entry: Value, operations: &crate::operations::Operations) -> Result<(), Error> {
268
+ let mut task = self.0.get_mut()?;
269
+ let entry_datetime = ruby_to_option(entry, ruby_to_datetime)?;
270
+ operations.with_inner_mut(|ops| {
271
+ task.set_entry(entry_datetime, ops)
272
+ })?;
273
+ Ok(())
274
+ }
275
+
267
276
  fn set_value(&self, property: String, value: Value, operations: &crate::operations::Operations) -> Result<(), Error> {
268
277
  if property.trim().is_empty() {
269
278
  return Err(Error::new(
@@ -284,6 +293,53 @@ impl Task {
284
293
  Ok(())
285
294
  }
286
295
 
296
+ fn set_timestamp(&self, property: String, timestamp: Value, operations: &crate::operations::Operations) -> Result<(), Error> {
297
+ if property.trim().is_empty() {
298
+ return Err(Error::new(
299
+ crate::error::validation_error(),
300
+ "Property name cannot be empty or whitespace-only"
301
+ ));
302
+ }
303
+
304
+ let mut task = self.0.get_mut()?;
305
+ let timestamp_datetime = ruby_to_option(timestamp, ruby_to_datetime)?;
306
+
307
+ // Convert timestamp to Unix timestamp string, or None for clearing
308
+ let timestamp_str = timestamp_datetime.map(|dt| dt.timestamp().to_string());
309
+
310
+ operations.with_inner_mut(|ops| {
311
+ task.set_value(&property, timestamp_str, ops)
312
+ })?;
313
+ Ok(())
314
+ }
315
+
316
+ fn get_timestamp(&self, property: String) -> Result<Value, Error> {
317
+ if property.trim().is_empty() {
318
+ return Err(Error::new(
319
+ crate::error::validation_error(),
320
+ "Property name cannot be empty or whitespace-only"
321
+ ));
322
+ }
323
+
324
+ let task = self.0.get()?;
325
+
326
+ // Get the value as string and attempt to parse as Unix timestamp
327
+ match task.get_value(&property) {
328
+ Some(timestamp_str) => {
329
+ // Parse the string as Unix timestamp (seconds since epoch)
330
+ if let Ok(timestamp_secs) = timestamp_str.parse::<i64>() {
331
+ use chrono::{DateTime, Utc};
332
+ if let Some(dt) = DateTime::from_timestamp(timestamp_secs, 0) {
333
+ return datetime_to_ruby(dt);
334
+ }
335
+ }
336
+ // If parsing fails, return nil
337
+ Ok(().into_value())
338
+ },
339
+ None => Ok(().into_value()) // Return nil if property doesn't exist
340
+ }
341
+ }
342
+
287
343
  fn set_uda(&self, namespace: String, key: String, value: String, operations: &crate::operations::Operations) -> Result<(), Error> {
288
344
  if namespace.trim().is_empty() {
289
345
  return Err(Error::new(
@@ -389,7 +445,10 @@ pub fn init(module: &RModule) -> Result<(), Error> {
389
445
  class.define_method("remove_tag", method!(Task::remove_tag, 2))?;
390
446
  class.define_method("add_annotation", method!(Task::add_annotation, 2))?;
391
447
  class.define_method("set_due", method!(Task::set_due, 2))?;
448
+ class.define_method("set_entry", method!(Task::set_entry, 2))?;
392
449
  class.define_method("set_value", method!(Task::set_value, 3))?;
450
+ class.define_method("set_timestamp", method!(Task::set_timestamp, 3))?;
451
+ class.define_method("get_timestamp", method!(Task::get_timestamp, 1))?;
393
452
  class.define_method("set_uda", method!(Task::set_uda, 4))?;
394
453
  class.define_method("delete_uda", method!(Task::delete_uda, 3))?;
395
454
  class.define_method("done", method!(Task::done, 1))?;
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Taskchampion
4
- VERSION = "0.6.0"
4
+ VERSION = "0.8.0"
5
5
  end
data/lib/taskchampion.rb CHANGED
@@ -37,5 +37,24 @@ module Taskchampion
37
37
  ws.replica = self
38
38
  ws
39
39
  end
40
+
41
+ # Ruby-style convenience methods for undo functionality
42
+ def task_operations(uuid)
43
+ get_task_operations(uuid)
44
+ end
45
+
46
+ def undo_operations
47
+ get_undo_operations
48
+ end
49
+
50
+ def commit_undo!(operations)
51
+ commit_reversed_operations(operations)
52
+ end
53
+
54
+ def undo!
55
+ ops = undo_operations
56
+ return false if ops.empty?
57
+ commit_undo!(ops)
58
+ end
40
59
  end
41
60
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: taskchampion-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tim Case
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-08-14 00:00:00.000000000 Z
11
+ date: 2025-09-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rb_sys
@@ -44,13 +44,16 @@ files:
44
44
  - docs/API_REFERENCE.md
45
45
  - docs/THREAD_SAFETY.md
46
46
  - docs/breakthrough.md
47
+ - docs/date_conversion_analysis.md
47
48
  - docs/description.md
49
+ - docs/errors.md
48
50
  - docs/phase_3_plan.md
49
51
  - docs/plan.md
50
52
  - example.md
51
53
  - examples/basic_usage.rb
52
54
  - examples/pending_tasks.rb
53
55
  - examples/sync_workflow.rb
56
+ - examples/undo_demo.rb
54
57
  - ext/taskchampion/Cargo.toml
55
58
  - ext/taskchampion/extconf.rb
56
59
  - ext/taskchampion/src/access_mode.rs