taskchampion-rb 0.2.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.
data/Cargo.toml ADDED
@@ -0,0 +1,7 @@
1
+ # This Cargo.toml is here to let externals tools (IDEs, etc.) know that this is
2
+ # a Rust project. Your extensions dependencies should be added to the Cargo.toml
3
+ # in the ext/ directory.
4
+
5
+ [workspace]
6
+ members = ["./ext/taskchampion"]
7
+ resolver = "2"
data/README.md ADDED
@@ -0,0 +1,112 @@
1
+ # Taskchampion
2
+
3
+ Ruby bindings for TaskChampion, the task database that powers Taskwarrior.
4
+
5
+ ## Installation
6
+
7
+ ### Prerequisites
8
+
9
+ 1. Ruby 3.0 or later
10
+ 2. Rust toolchain (install from https://rustup.rs/)
11
+
12
+ ```bash
13
+ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
14
+ ```
15
+
16
+ ### Building from source
17
+
18
+ ```bash
19
+ bundle install
20
+ bundle exec rake compile
21
+ ```
22
+
23
+ ### Testing
24
+
25
+ ```bash
26
+ bundle exec rake test
27
+ ```
28
+
29
+ ## Usage
30
+
31
+ ```ruby
32
+ require 'taskchampion'
33
+
34
+ # Create an in-memory replica
35
+ replica = Taskchampion::Replica.new_in_memory
36
+
37
+ # Create an on-disk replica
38
+ replica = Taskchampion::Replica.new_on_disk("/path/to/taskdb", true)
39
+
40
+ # With access mode
41
+ replica = Taskchampion::Replica.new_on_disk("/path/to/taskdb", true, :read_only)
42
+
43
+ # Working with operations
44
+ ops = Taskchampion::Operations.new
45
+ create_op = Taskchampion::Operation.create(SecureRandom.uuid)
46
+ ops << create_op
47
+
48
+ # Task operations (when Operations integration is complete)
49
+ # task = replica.create_task(uuid, ops)
50
+ # task.set_description("New task description", ops)
51
+ # replica.commit_operations(ops)
52
+
53
+ # Working with tags and annotations
54
+ tag = Taskchampion::Tag.new("work")
55
+ puts tag.to_s # => "work"
56
+ puts tag.user? # => true
57
+ puts tag.synthetic? # => false
58
+
59
+ annotation = Taskchampion::Annotation.new(DateTime.now, "This is a note")
60
+ puts annotation.description # => "This is a note"
61
+
62
+ # Status constants
63
+ puts Taskchampion::PENDING # => :pending
64
+ puts Taskchampion::COMPLETED # => :completed
65
+ puts Taskchampion::ACCESS_MODES # => [:read_only, :read_write]
66
+ ```
67
+
68
+ ## Development Status
69
+
70
+ This is a Ruby port of the taskchampion-py Python bindings. The implementation follows the plan outlined in the `ruby_docs/` directory of the Python project.
71
+
72
+ ### Completed
73
+ - ✅ Basic project structure
74
+ - ✅ Magnus and rb-sys configuration
75
+ - ✅ Error hierarchy (Error, ThreadError, StorageError, ValidationError, ConfigError)
76
+ - ✅ Thread safety utilities
77
+ - ✅ Type conversions (DateTime, Option, HashMap, Vec)
78
+ - ✅ Replica class with Ruby idiomatic API
79
+ - ✅ Task class with Ruby idiomatic API
80
+ - ✅ Tag and Annotation classes
81
+ - ✅ Status constants (:pending, :completed, :deleted, etc.)
82
+ - ✅ Operation and Operations classes
83
+ - ✅ Access mode support
84
+ - ✅ Minitest testing infrastructure
85
+ - ✅ GitHub Actions CI/CD
86
+
87
+ ### TODO
88
+ - [ ] WorkingSet class implementation
89
+ - [ ] DependencyMap class implementation
90
+ - [ ] Complete Task mutation methods (requires Operations integration)
91
+ - [ ] Complete test suite porting from Python
92
+ - [ ] Cross-platform compilation
93
+ - [ ] YARD documentation
94
+ - [ ] RubyGems publication
95
+
96
+ ## API Design
97
+
98
+ The Ruby API follows Ruby idioms:
99
+ - Method names use snake_case
100
+ - Boolean methods end with `?` (e.g., `active?`, `waiting?`)
101
+ - Property access doesn't use `get_` prefix (e.g., `task.uuid` not `task.get_uuid`)
102
+ - Keyword arguments for optional parameters
103
+ - `nil` instead of `None`
104
+ - Symbols for enums (e.g., `:read_only`, `:read_write`)
105
+
106
+ ## Thread Safety
107
+
108
+ All TaskChampion objects are thread-local and will raise `Taskchampion::ThreadError` if accessed from a different thread than the one that created them.
109
+
110
+ ## Contributing
111
+
112
+ Bug reports and pull requests are welcome on GitHub at https://github.com/GothenburgBitFactory/taskchampion.
data/Rakefile ADDED
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rubocop/rake_task"
5
+ require "rake/testtask"
6
+
7
+ RuboCop::RakeTask.new
8
+
9
+ require "rb_sys/extensiontask"
10
+
11
+ task build: :compile
12
+
13
+ GEMSPEC = Gem::Specification.load("taskchampion-rb.gemspec")
14
+
15
+ RbSys::ExtensionTask.new("taskchampion", GEMSPEC) do |ext|
16
+ ext.lib_dir = "lib/taskchampion"
17
+ end
18
+
19
+ Rake::TestTask.new(:test) do |t|
20
+ t.libs << "test"
21
+ t.libs << "lib"
22
+ t.test_files = FileList["test/**/test_*.rb"]
23
+ t.verbose = true
24
+ end
25
+
26
+ task test: :compile
27
+
28
+ task default: %i[compile test]
@@ -0,0 +1,419 @@
1
+ # TaskChampion Ruby API Reference
2
+
3
+ This document provides comprehensive API documentation for the TaskChampion Ruby bindings.
4
+
5
+ ## Core Classes
6
+
7
+ ### Taskchampion::Replica
8
+
9
+ The main entry point for TaskChampion functionality. Manages task storage and synchronization.
10
+
11
+ #### Constructor Methods
12
+
13
+ ```ruby
14
+ # Create an in-memory replica (for testing)
15
+ replica = Taskchampion::Replica.new_in_memory
16
+
17
+ # Create a disk-based replica
18
+ replica = Taskchampion::Replica.new_on_disk("/path/to/tasks", create_if_missing: true)
19
+ ```
20
+
21
+ #### Task Management
22
+
23
+ ```ruby
24
+ # Get all task UUIDs
25
+ uuids = replica.task_uuids # => Array of String UUIDs
26
+
27
+ # Get a specific task by UUID
28
+ task = replica.task(uuid) # => Task or nil
29
+
30
+ # Create a new task
31
+ operations = Taskchampion::Operations.new
32
+ task = replica.create_task(uuid, operations) # => Task
33
+
34
+ # Commit operations to storage
35
+ replica.commit_operations(operations)
36
+ ```
37
+
38
+ #### Working Set Management
39
+
40
+ ```ruby
41
+ # Get the working set
42
+ working_set = replica.working_set # => WorkingSet
43
+
44
+ # Rebuild working set indices
45
+ replica.rebuild_working_set
46
+ ```
47
+
48
+ #### Dependency Management
49
+
50
+ ```ruby
51
+ # Get dependency map
52
+ dep_map = replica.dependency_map(rebuild: false) # => DependencyMap
53
+ ```
54
+
55
+ #### Synchronization
56
+
57
+ ```ruby
58
+ # Sync to local directory
59
+ replica.sync_to_local(server_dir, avoid_snapshots: false)
60
+
61
+ # Sync to remote server
62
+ replica.sync_to_remote(
63
+ url: "https://taskserver.example.com",
64
+ client_id: "client-123",
65
+ encryption_secret: "secret",
66
+ avoid_snapshots: false
67
+ )
68
+
69
+ # Sync to Google Cloud Storage
70
+ replica.sync_to_gcp(
71
+ bucket: "my-tasks-bucket",
72
+ credential_path: "/path/to/credentials.json",
73
+ encryption_secret: "secret",
74
+ avoid_snapshots: false
75
+ )
76
+ ```
77
+
78
+ #### Storage Information
79
+
80
+ ```ruby
81
+ # Get number of local operations
82
+ count = replica.num_local_operations # => Integer
83
+
84
+ # Get number of undo points
85
+ count = replica.num_undo_points # => Integer
86
+ ```
87
+
88
+ ### Taskchampion::Task
89
+
90
+ Represents a single task with all its properties.
91
+
92
+ #### Property Access
93
+
94
+ ```ruby
95
+ # Basic properties
96
+ task.uuid # => String
97
+ task.description # => String or nil
98
+ task.status # => Status
99
+ task.priority # => String or nil
100
+
101
+ # Date properties
102
+ task.entry # => Time or nil
103
+ task.modified # => Time or nil
104
+ task.start # => Time or nil
105
+ task.end # => Time or nil
106
+ task.due # => Time or nil
107
+ task.until # => Time or nil
108
+ task.wait # => Time or nil
109
+
110
+ # Collections
111
+ task.tags # => Array of Tag
112
+ task.annotations # => Array of Annotation
113
+ task.dependencies # => Array of String (UUIDs)
114
+
115
+ # User Defined Attributes (UDAs)
116
+ task.uda(namespace, key) # => String or nil
117
+ task.udas # => Hash of all UDAs
118
+
119
+ # Custom properties
120
+ task.value(property) # => String or nil
121
+ ```
122
+
123
+ #### Task Modification
124
+
125
+ All modification methods require an Operations object:
126
+
127
+ ```ruby
128
+ operations = Taskchampion::Operations.new
129
+
130
+ # Basic modifications
131
+ task.set_description("New description", operations)
132
+ task.set_status(Taskchampion::Status.completed, operations)
133
+ task.set_priority("H", operations) # H, M, L, or nil
134
+
135
+ # Date modifications
136
+ task.set_due(Time.now + 86400, operations) # Due tomorrow
137
+ task.set_start(Time.now, operations)
138
+ task.set_end(Time.now, operations)
139
+
140
+ # Tag management
141
+ task.add_tag(Taskchampion::Tag.new("work"), operations)
142
+ task.remove_tag(Taskchampion::Tag.new("work"), operations)
143
+
144
+ # Annotation management
145
+ annotation = Taskchampion::Annotation.new(Time.now, "Added note")
146
+ task.add_annotation(annotation, operations)
147
+
148
+ # UDA management
149
+ task.set_uda("namespace", "key", "value", operations)
150
+ task.delete_uda("namespace", "key", operations)
151
+
152
+ # Custom properties
153
+ task.set_value("custom_property", "value", operations)
154
+
155
+ # Don't forget to commit!
156
+ replica.commit_operations(operations)
157
+ ```
158
+
159
+ #### Status Checking
160
+
161
+ ```ruby
162
+ # Status predicates
163
+ task.active? # => Boolean (pending or recurring)
164
+ task.pending? # => Boolean
165
+ task.completed? # => Boolean
166
+ task.deleted? # => Boolean
167
+ task.recurring? # => Boolean
168
+
169
+ # Tag checking
170
+ task.has_tag?(Taskchampion::Tag.new("work")) # => Boolean
171
+ ```
172
+
173
+ ### Taskchampion::Operations
174
+
175
+ Collects task modifications before committing them to storage.
176
+
177
+ ```ruby
178
+ # Create new operations collection
179
+ operations = Taskchampion::Operations.new
180
+
181
+ # Add operations (done automatically by task modification methods)
182
+ # operations.push(operation) # Usually not called directly
183
+
184
+ # Collection interface
185
+ operations.length # => Integer
186
+ operations[index] # => Operation
187
+ operations.each {|op| ... } # Block iteration
188
+ operations << operation # Append operation
189
+ operations.clear # Remove all operations
190
+ ```
191
+
192
+ ### Taskchampion::Operation
193
+
194
+ Represents a single task modification operation.
195
+
196
+ ```ruby
197
+ # Operation introspection
198
+ operation.operation_type # => Symbol (:create, :update, :delete)
199
+ operation.uuid # => String (task UUID)
200
+ operation.property # => String or nil
201
+ operation.value # => String or nil
202
+ operation.old_value # => String or nil
203
+ operation.timestamp # => Time
204
+
205
+ # String representation
206
+ operation.to_s # => Human readable string
207
+ operation.inspect # => Debug representation
208
+ ```
209
+
210
+ ### Taskchampion::Status
211
+
212
+ Enumeration of task status values.
213
+
214
+ ```ruby
215
+ # Status constants
216
+ status = Taskchampion::Status.pending # => Status
217
+ status = Taskchampion::Status.completed # => Status
218
+ status = Taskchampion::Status.deleted # => Status
219
+ status = Taskchampion::Status.recurring # => Status
220
+
221
+ # Status predicates
222
+ status.pending? # => Boolean
223
+ status.completed? # => Boolean
224
+ status.deleted? # => Boolean
225
+ status.recurring? # => Boolean
226
+
227
+ # String conversion
228
+ status.to_s # => "pending", "completed", etc.
229
+ status.inspect # => "#<Taskchampion::Status:pending>"
230
+ ```
231
+
232
+ ### Taskchampion::AccessMode
233
+
234
+ Enumeration of replica access modes.
235
+
236
+ ```ruby
237
+ # Access mode constants
238
+ mode = Taskchampion::AccessMode.read_only # => AccessMode
239
+ mode = Taskchampion::AccessMode.read_write # => AccessMode
240
+
241
+ # Access mode predicates
242
+ mode.read_only? # => Boolean
243
+ mode.read_write? # => Boolean
244
+
245
+ # String conversion
246
+ mode.to_s # => "read_only" or "read_write"
247
+ ```
248
+
249
+ ### Taskchampion::Tag
250
+
251
+ Represents a task tag.
252
+
253
+ ```ruby
254
+ # Create tags
255
+ tag = Taskchampion::Tag.new("work")
256
+ tag = Taskchampion::Tag.new("project:website")
257
+
258
+ # Access tag name
259
+ tag.name # => String
260
+ tag.to_s # => String (same as name)
261
+
262
+ # Equality
263
+ tag1 == tag2 # => Boolean
264
+ ```
265
+
266
+ ### Taskchampion::Annotation
267
+
268
+ Represents a task annotation with timestamp and description.
269
+
270
+ ```ruby
271
+ # Create annotations
272
+ annotation = Taskchampion::Annotation.new(Time.now, "Added note")
273
+
274
+ # Access properties
275
+ annotation.entry # => Time
276
+ annotation.description # => String
277
+
278
+ # String conversion
279
+ annotation.to_s # => "2024-01-31 12:00:00 Added note"
280
+ ```
281
+
282
+ ### Taskchampion::WorkingSet
283
+
284
+ Manages the current set of tasks being worked on with index-based access.
285
+
286
+ ```ruby
287
+ # Get working set from replica
288
+ working_set = replica.working_set
289
+
290
+ # Index management
291
+ largest = working_set.largest_index # => Integer
292
+
293
+ # Task access by index
294
+ task = working_set.by_index(1) # => Task or nil
295
+
296
+ # UUID to index mapping
297
+ index = working_set.by_uuid(uuid) # => Integer or nil
298
+
299
+ # Renumber tasks
300
+ working_set.renumber
301
+ ```
302
+
303
+ ### Taskchampion::DependencyMap
304
+
305
+ Tracks task dependencies and relationships.
306
+
307
+ ```ruby
308
+ # Get dependency map from replica
309
+ dep_map = replica.dependency_map(rebuild: false)
310
+
311
+ # Get task dependencies (tasks this task depends on)
312
+ deps = dep_map.dependencies(uuid) # => Array of String (UUIDs)
313
+
314
+ # Get task dependents (tasks that depend on this task)
315
+ dependents = dep_map.dependents(uuid) # => Array of String (UUIDs)
316
+
317
+ # Check if task has dependencies
318
+ has_deps = dep_map.has_dependency?(uuid) # => Boolean
319
+ ```
320
+
321
+ ## Error Classes
322
+
323
+ ### Taskchampion::Error
324
+
325
+ Base class for all TaskChampion errors.
326
+
327
+ ### Taskchampion::ThreadError
328
+
329
+ Raised when attempting to access TaskChampion objects from the wrong thread.
330
+
331
+ ```ruby
332
+ begin
333
+ # This will fail if called from wrong thread
334
+ replica.task_uuids
335
+ rescue Taskchampion::ThreadError => e
336
+ puts "Thread safety violation: #{e.message}"
337
+ end
338
+ ```
339
+
340
+ ### Taskchampion::StorageError
341
+
342
+ Raised for file system and storage-related errors.
343
+
344
+ ### Taskchampion::ValidationError
345
+
346
+ Raised for invalid input or parameter validation failures.
347
+
348
+ ### Taskchampion::ConfigError
349
+
350
+ Raised for configuration-related errors.
351
+
352
+ ### Taskchampion::SyncError
353
+
354
+ Raised for synchronization failures.
355
+
356
+ ## Thread Safety
357
+
358
+ **Important**: All TaskChampion objects are thread-bound and can only be used from the thread that created them. Attempting to access objects from other threads will raise `Taskchampion::ThreadError`.
359
+
360
+ See [THREAD_SAFETY.md](THREAD_SAFETY.md) for detailed thread safety guidelines.
361
+
362
+ ## Common Patterns
363
+
364
+ ### Creating and Modifying Tasks
365
+
366
+ ```ruby
367
+ replica = Taskchampion::Replica.new_on_disk("/path/to/tasks")
368
+ operations = Taskchampion::Operations.new
369
+
370
+ # Create task
371
+ uuid = SecureRandom.uuid
372
+ task = replica.create_task(uuid, operations)
373
+ task.set_description("Buy groceries", operations)
374
+ task.set_status(Taskchampion::Status.pending, operations)
375
+ task.add_tag(Taskchampion::Tag.new("errands"), operations)
376
+
377
+ # Commit changes
378
+ replica.commit_operations(operations)
379
+ ```
380
+
381
+ ### Querying Tasks
382
+
383
+ ```ruby
384
+ # Get all tasks
385
+ all_uuids = replica.task_uuids
386
+ tasks = all_uuids.map { |uuid| replica.task(uuid) }.compact
387
+
388
+ # Filter tasks
389
+ pending_tasks = tasks.select(&:pending?)
390
+ work_tasks = tasks.select { |t| t.has_tag?(Taskchampion::Tag.new("work")) }
391
+ ```
392
+
393
+ ### Working with Operations
394
+
395
+ ```ruby
396
+ # Group multiple changes
397
+ operations = Taskchampion::Operations.new
398
+
399
+ tasks.each do |task|
400
+ if task.pending? && task.priority.nil?
401
+ task.set_priority("M", operations)
402
+ end
403
+ end
404
+
405
+ # Commit all changes at once
406
+ replica.commit_operations(operations)
407
+ ```
408
+
409
+ ### Error Handling
410
+
411
+ ```ruby
412
+ begin
413
+ replica = Taskchampion::Replica.new_on_disk("/invalid/path")
414
+ rescue Taskchampion::StorageError => e
415
+ puts "Storage error: #{e.message}"
416
+ rescue Taskchampion::Error => e
417
+ puts "TaskChampion error: #{e.message}"
418
+ end
419
+ ```