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 +4 -4
- data/README.md +1 -0
- data/docs/date_conversion_analysis.md +113 -0
- data/docs/errors.md +151 -0
- data/examples/pending_tasks.rb +1 -1
- data/examples/undo_demo.rb +104 -0
- data/ext/taskchampion/src/operations.rs +9 -0
- data/ext/taskchampion/src/replica.rs +43 -8
- data/ext/taskchampion/src/task.rs +59 -0
- data/lib/taskchampion/version.rb +1 -1
- data/lib/taskchampion.rb +19 -0
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9c5bf1065611d68b14549f0c6930e630f6e79483699d2ad05d2e1211da8192ac
|
4
|
+
data.tar.gz: f9cd13e961c3b814c4eb8474e22e585803e24f426f92e49dbb24c716640c52ee
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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)
|
data/examples/pending_tasks.rb
CHANGED
@@ -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
|
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
|
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
|
-
|
270
|
-
|
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
|
-
|
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))?;
|
data/lib/taskchampion/version.rb
CHANGED
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.
|
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-
|
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
|