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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8b267de15e0004e931e2a46f37f9014afe8fdc52e58e675190dab990cb345e75
4
- data.tar.gz: 6a84efc5a165c428c67e6d50f3ce0b001eab4d3fde464ffed123d218aaf04033
3
+ metadata.gz: cafd2930e70aef0ac31c03c98e3d806cd1fba49408ce69b4bcb5d41074465c3f
4
+ data.tar.gz: 23b8948a759f8cc7b28e2cb68d65d40750943bd05bf127a87f5e25bf80116900
5
5
  SHA512:
6
- metadata.gz: ff860c7dcb85bc6a04aced731b403072a03c3a222f869fec0a25af1436b14fb67e4df0a9dc5cfad4d829b1fe1f39936e6df07c90605eb859202ac8f80fc8a17f
7
- data.tar.gz: d826c3704fec6432e872abe24423d92e04da810c0718ce0cc224dbddd780b2279b336c635bd1e285405bd4534710fbd9928bf4ee7c5f225f4118baade8315436
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))?;
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Taskchampion
4
- VERSION = "0.5.0"
4
+ VERSION = "0.7.0"
5
5
  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.5.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-14 00:00:00.000000000 Z
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