taskchampion-rb 0.5.0 → 0.7.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/CLAUDE.md +67 -0
- data/docs/errors.md +151 -0
- data/examples/pending_tasks.rb +56 -0
- data/ext/taskchampion/src/replica.rs +15 -0
- data/ext/taskchampion/src/task.rs +10 -0
- data/lib/taskchampion/version.rb +1 -1
- 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: cafd2930e70aef0ac31c03c98e3d806cd1fba49408ce69b4bcb5d41074465c3f
|
4
|
+
data.tar.gz: 23b8948a759f8cc7b28e2cb68d65d40750943bd05bf127a87f5e25bf80116900
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2ff558c7674c2ff686e08ec9e220baa966edf438353e42832a566771cbcb2348c329d34fa27937b7bc9b71c0fdd7721f6e3086a675a5baa4e54f2e4e102600a5
|
7
|
+
data.tar.gz: 34dfcc7ea08bc2f90b846d3a19b1526731a0142d1cc6d09a867923bd6620be3abc13602eab12f8022c85dfc10b3f97502c62419808db3b942808489efbd51488
|
data/CLAUDE.md
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
# CLAUDE.md
|
2
|
+
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
4
|
+
|
5
|
+
## Build and Development Commands
|
6
|
+
|
7
|
+
### Building the gem
|
8
|
+
```bash
|
9
|
+
bundle install
|
10
|
+
bundle exec rake compile # Compiles the Rust extension
|
11
|
+
```
|
12
|
+
|
13
|
+
### Running tests
|
14
|
+
```bash
|
15
|
+
bundle exec rake test # Run all tests
|
16
|
+
bundle exec rake test TEST=test/test_replica.rb # Run specific test file
|
17
|
+
```
|
18
|
+
|
19
|
+
### Linting
|
20
|
+
```bash
|
21
|
+
bundle exec rake rubocop # Run RuboCop linter
|
22
|
+
```
|
23
|
+
|
24
|
+
### Publishing new version
|
25
|
+
```bash
|
26
|
+
rake publish[patch] # Bump patch version and release
|
27
|
+
rake publish[minor] # Bump minor version and release
|
28
|
+
rake publish[major] # Bump major version and release
|
29
|
+
```
|
30
|
+
|
31
|
+
## Architecture Overview
|
32
|
+
|
33
|
+
This is a Ruby gem that provides bindings to TaskChampion (Rust library) for task management. The architecture consists of:
|
34
|
+
|
35
|
+
### Key Components
|
36
|
+
|
37
|
+
1. **Rust Extension** (`ext/taskchampion/`)
|
38
|
+
- Written in Rust using Magnus for Ruby-Rust interop
|
39
|
+
- Entry point: `ext/taskchampion/src/lib.rs`
|
40
|
+
- Implements core classes: Replica, Task, WorkingSet, Operations, etc.
|
41
|
+
- Thread safety enforced via `thread_check.rs` - objects can only be accessed from their creation thread
|
42
|
+
|
43
|
+
2. **Ruby Layer** (`lib/taskchampion.rb`)
|
44
|
+
- Thin wrapper that loads the Rust extension
|
45
|
+
- Extends WorkingSet and Replica classes with Ruby-specific convenience methods
|
46
|
+
- Maintains Ruby idioms (snake_case methods, `?` suffix for booleans)
|
47
|
+
|
48
|
+
3. **Core Classes**
|
49
|
+
- `Replica`: Main database interface for task storage (in-memory or on-disk)
|
50
|
+
- `Task`: Individual task with properties and methods
|
51
|
+
- `Operations`: Collection of operations for batch changes
|
52
|
+
- `WorkingSet`: Active subset of tasks
|
53
|
+
- `DependencyMap`: Task dependency management
|
54
|
+
- Error hierarchy: Error → ThreadError, StorageError, ValidationError, ConfigError
|
55
|
+
|
56
|
+
### Testing Structure
|
57
|
+
- Uses Minitest framework
|
58
|
+
- Tests organized in `test/unit/`, `test/integration/`, and `test/performance/`
|
59
|
+
- Base test class: `TaskchampionTest` in `test/test_helper.rb`
|
60
|
+
- Tests use temporary directories for file-based replicas
|
61
|
+
|
62
|
+
### Ruby API Design Principles
|
63
|
+
- Methods use snake_case (not get_/set_ prefixes)
|
64
|
+
- Boolean methods end with `?`
|
65
|
+
- Symbols for enums (`:pending`, `:completed`, `:read_only`)
|
66
|
+
- `nil` instead of None
|
67
|
+
- Keyword arguments for optional parameters
|
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)
|
@@ -0,0 +1,56 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'taskchampion'
|
5
|
+
require 'securerandom'
|
6
|
+
|
7
|
+
# Create an in-memory replica
|
8
|
+
replica = Taskchampion::Replica.new_in_memory
|
9
|
+
|
10
|
+
# Create several tasks with different statuses
|
11
|
+
puts "Creating tasks..."
|
12
|
+
|
13
|
+
# Create pending tasks
|
14
|
+
3.times do |i|
|
15
|
+
ops = Taskchampion::Operations.new
|
16
|
+
task = replica.create_task(SecureRandom.uuid, ops)
|
17
|
+
task.set_description("Pending task #{i + 1}", ops)
|
18
|
+
task.set_status(Taskchampion::PENDING, ops)
|
19
|
+
replica.commit_operations(ops)
|
20
|
+
puts " Created pending task: #{task.description}"
|
21
|
+
end
|
22
|
+
|
23
|
+
# Create completed tasks
|
24
|
+
2.times do |i|
|
25
|
+
ops = Taskchampion::Operations.new
|
26
|
+
task = replica.create_task(SecureRandom.uuid, ops)
|
27
|
+
task.set_description("Completed task #{i + 1}", ops)
|
28
|
+
task.set_status(Taskchampion::COMPLETED, ops)
|
29
|
+
replica.commit_operations(ops)
|
30
|
+
puts " Created completed task: #{task.description}"
|
31
|
+
end
|
32
|
+
|
33
|
+
# Create a deleted task
|
34
|
+
ops = Taskchampion::Operations.new
|
35
|
+
task = replica.create_task(SecureRandom.uuid, ops)
|
36
|
+
task.set_description("Deleted task", ops)
|
37
|
+
task.set_status(Taskchampion::DELETED, ops)
|
38
|
+
replica.commit_operations(ops)
|
39
|
+
puts " Created deleted task: #{task.description}"
|
40
|
+
|
41
|
+
puts "\n" + "=" * 50
|
42
|
+
puts "Getting pending tasks..."
|
43
|
+
puts "=" * 50
|
44
|
+
|
45
|
+
# Use the pending_tasks method to get only pending tasks
|
46
|
+
pending = replica.pending_tasks
|
47
|
+
|
48
|
+
puts "\nFound #{pending.length} pending tasks:"
|
49
|
+
pending.each_with_index do |task, index|
|
50
|
+
puts " #{index + 1}. [#{task.uuid[0..7]}...] #{task.description}"
|
51
|
+
puts " Status: #{task.status}"
|
52
|
+
puts " Active: #{task.active?}"
|
53
|
+
end
|
54
|
+
|
55
|
+
puts "\n" + "=" * 50
|
56
|
+
puts "For comparison, all_tasks returns #{replica.all_tasks.size} tasks total"
|
@@ -260,6 +260,20 @@ impl Replica {
|
|
260
260
|
|
261
261
|
Ok(tc_replica.num_undo_points().map_err(into_error)?)
|
262
262
|
}
|
263
|
+
|
264
|
+
fn pending_tasks(&self) -> Result<RArray, Error> {
|
265
|
+
let mut tc_replica = self.0.get_mut()?;
|
266
|
+
|
267
|
+
let tc_tasks = tc_replica.pending_tasks().map_err(into_error)?;
|
268
|
+
|
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
|
+
}
|
274
|
+
|
275
|
+
Ok(array)
|
276
|
+
}
|
263
277
|
}
|
264
278
|
|
265
279
|
pub fn init(module: &RModule) -> Result<(), Error> {
|
@@ -285,6 +299,7 @@ pub fn init(module: &RModule) -> Result<(), Error> {
|
|
285
299
|
class.define_method("expire_tasks", method!(Replica::expire_tasks, 0))?;
|
286
300
|
class.define_method("num_local_operations", method!(Replica::num_local_operations, 0))?;
|
287
301
|
class.define_method("num_undo_points", method!(Replica::num_undo_points, 0))?;
|
302
|
+
class.define_method("pending_tasks", method!(Replica::pending_tasks, 0))?;
|
288
303
|
|
289
304
|
Ok(())
|
290
305
|
}
|
@@ -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(
|
@@ -389,6 +398,7 @@ pub fn init(module: &RModule) -> Result<(), Error> {
|
|
389
398
|
class.define_method("remove_tag", method!(Task::remove_tag, 2))?;
|
390
399
|
class.define_method("add_annotation", method!(Task::add_annotation, 2))?;
|
391
400
|
class.define_method("set_due", method!(Task::set_due, 2))?;
|
401
|
+
class.define_method("set_entry", method!(Task::set_entry, 2))?;
|
392
402
|
class.define_method("set_value", method!(Task::set_value, 3))?;
|
393
403
|
class.define_method("set_uda", method!(Task::set_uda, 4))?;
|
394
404
|
class.define_method("delete_uda", method!(Task::delete_uda, 3))?;
|
data/lib/taskchampion/version.rb
CHANGED
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.7.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-
|
11
|
+
date: 2025-08-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rb_sys
|
@@ -36,6 +36,7 @@ files:
|
|
36
36
|
- ".rubocop.yml"
|
37
37
|
- ".ruby-version"
|
38
38
|
- CHANGELOG.md
|
39
|
+
- CLAUDE.md
|
39
40
|
- Cargo.lock
|
40
41
|
- Cargo.toml
|
41
42
|
- README.md
|
@@ -44,10 +45,12 @@ files:
|
|
44
45
|
- docs/THREAD_SAFETY.md
|
45
46
|
- docs/breakthrough.md
|
46
47
|
- docs/description.md
|
48
|
+
- docs/errors.md
|
47
49
|
- docs/phase_3_plan.md
|
48
50
|
- docs/plan.md
|
49
51
|
- example.md
|
50
52
|
- examples/basic_usage.rb
|
53
|
+
- examples/pending_tasks.rb
|
51
54
|
- examples/sync_workflow.rb
|
52
55
|
- ext/taskchampion/Cargo.toml
|
53
56
|
- ext/taskchampion/extconf.rb
|