taskchampion-rb 0.3.0 → 0.6.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: c7110e37a7e2a8af13267d10879260e10b920b1ba49c3d9f5b124cc7d618862b
4
- data.tar.gz: '029792b10587e8a680b80936e1156ac2e1a15d2ba19970219b4f1d5496570bc7'
3
+ metadata.gz: 494521791e308dacc686f45ef7ff87937a0038e46729092a4e145952971a5f56
4
+ data.tar.gz: ac3a7b7367b2d8565ac902981fb4c7a9ee45b7d0c825868b15591211debc733b
5
5
  SHA512:
6
- metadata.gz: 7e7ba27d8a5202767273e9221d115640493dbba79b8de6563e0cfeb0d9d2c75945244f649d1874745c0c80fe769eb7efd089737843baf843c47dd3e649fa6db4
7
- data.tar.gz: d75725f6f197a67195d290a44ebd045434c1ada09779551a2e3c08e8430e4cfd5c6266d86d000135bc444aea77ac945034cdb2218835d48f706e4b8961efea5f
6
+ metadata.gz: 3d5c1e59e10ac46d98a8fc30d666dac904fcf2d14742c12713a16050a7a67d87c21e1a42ebaf7d25762541d0335c3c36ab2cf6a93bd3ed778046cd1a739840f4
7
+ data.tar.gz: 8f69921b628b5b1803a940584b05dd46abc854e50d9a3555646a141298a6c2a55008b2c8ca60b508fd53566bc6d0bd28a69f8caf76021e37a56e9fbf69bd362c
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 3.2.0
1
+ 3.2.0
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
@@ -18,7 +18,7 @@ db_path = File.join(temp_dir, "tasks")
18
18
  begin
19
19
  # 1. CREATE TASK DATABASE
20
20
  puts "\n1. Creating task database at #{db_path}"
21
- replica = Taskchampion::Replica.new_on_disk(db_path, create_if_missing: true)
21
+ replica = Taskchampion::Replica.new_on_disk(db_path, true, :read_write)
22
22
 
23
23
  # 2. CREATE AND MODIFY TASKS
24
24
  puts "\n2. Creating and modifying tasks"
@@ -39,8 +39,7 @@ begin
39
39
  task1.add_tag(Taskchampion::Tag.new("ruby"), operations)
40
40
 
41
41
  # Add annotation
42
- annotation = Taskchampion::Annotation.new(Time.now, "Started learning TaskChampion")
43
- task1.add_annotation(annotation, operations)
42
+ task1.add_annotation("Started learning TaskChampion", operations)
44
43
 
45
44
  # Create second task
46
45
  uuid2 = SecureRandom.uuid
@@ -61,7 +60,6 @@ begin
61
60
  task3.set_description("Read TaskChampion documentation", operations)
62
61
  task3.set_status(Taskchampion::Status.completed, operations)
63
62
  task3.add_tag(Taskchampion::Tag.new("learning"), operations)
64
- task3.set_end(Time.now, operations)
65
63
 
66
64
  # 3. COMMIT CHANGES TO STORAGE
67
65
  puts "\n3. Committing changes to storage"
@@ -88,14 +86,13 @@ begin
88
86
 
89
87
  # Display tags
90
88
  if !task.tags.empty?
91
- tag_names = task.tags.map(&:name)
89
+ tag_names = task.tags.map(&:to_s)
92
90
  puts " Tags: #{tag_names.join(', ')}"
93
91
  end
94
92
 
95
93
  # Display dates
96
94
  puts " Created: #{task.entry.strftime('%Y-%m-%d %H:%M:%S')}" if task.entry
97
95
  puts " Due: #{task.due.strftime('%Y-%m-%d %H:%M:%S')}" if task.due
98
- puts " Completed: #{task.end.strftime('%Y-%m-%d %H:%M:%S')}" if task.end
99
96
 
100
97
  # Display annotations
101
98
  if !task.annotations.empty?
@@ -136,29 +133,42 @@ begin
136
133
 
137
134
  # Find tasks due today or tomorrow
138
135
  tomorrow = Time.now + 86400
139
- due_soon = all_tasks.select { |t| t.due && t.due <= tomorrow }
136
+ due_soon = all_tasks.select { |t| t.due && t.due.to_time <= tomorrow }
140
137
  puts "Tasks due soon: #{due_soon.length}"
141
138
 
142
139
  # 6. MODIFY EXISTING TASKS
143
140
  puts "\n6. Modifying existing tasks"
144
141
 
145
- # Complete the first task
142
+ # Complete the first task using the done method
146
143
  operations2 = Taskchampion::Operations.new
147
144
 
148
145
  # Retrieve task fresh from storage
149
146
  task_to_complete = replica.task(uuid1)
150
147
  if task_to_complete && task_to_complete.pending?
151
148
  puts "Completing task: #{task_to_complete.description}"
152
- task_to_complete.set_status(Taskchampion::Status.completed, operations2)
153
- task_to_complete.set_end(Time.now, operations2)
149
+
150
+ # Use the done() method - a convenience method for marking tasks as completed
151
+ task_to_complete.done(operations2)
154
152
 
155
153
  # Add completion annotation
156
- completion_note = Taskchampion::Annotation.new(Time.now, "Completed successfully!")
157
- task_to_complete.add_annotation(completion_note, operations2)
154
+ task_to_complete.add_annotation("Completed successfully!", operations2)
158
155
 
159
156
  # Commit the changes
160
157
  replica.commit_operations(operations2)
161
- puts "Task completed and committed"
158
+ puts "Task completed using done() method and committed"
159
+ end
160
+
161
+ # Complete the second task using traditional set_status for comparison
162
+ operations2b = Taskchampion::Operations.new
163
+ task_to_complete2 = replica.task(uuid2)
164
+ if task_to_complete2 && task_to_complete2.pending?
165
+ puts "Completing second task: #{task_to_complete2.description}"
166
+
167
+ # Alternative way: using set_status directly
168
+ task_to_complete2.set_status(Taskchampion::Status.completed, operations2b)
169
+
170
+ replica.commit_operations(operations2b)
171
+ puts "Task completed using set_status() method and committed"
162
172
  end
163
173
 
164
174
  # 7. WORKING WITH WORKING SET
@@ -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
  }
@@ -7,7 +7,7 @@ use crate::annotation::Annotation;
7
7
  use crate::status::Status;
8
8
  use crate::tag::Tag;
9
9
  use crate::thread_check::ThreadBound;
10
- use crate::util::{datetime_to_ruby, into_error, option_to_ruby, ruby_to_datetime, ruby_to_option, vec_to_ruby};
10
+ use crate::util::{datetime_to_ruby, option_to_ruby, ruby_to_datetime, ruby_to_option, vec_to_ruby};
11
11
 
12
12
  #[magnus::wrap(class = "Taskchampion::Task", free_immediately)]
13
13
  pub struct Task(ThreadBound<TCTask>);
@@ -326,6 +326,14 @@ impl Task {
326
326
  Ok(())
327
327
  }
328
328
 
329
+ fn done(&self, operations: &crate::operations::Operations) -> Result<(), Error> {
330
+ let mut task = self.0.get_mut()?;
331
+ operations.with_inner_mut(|ops| {
332
+ task.done(ops)
333
+ })?;
334
+ Ok(())
335
+ }
336
+
329
337
  }
330
338
 
331
339
  // Remove AsRef implementation as it doesn't work well with thread bounds
@@ -384,5 +392,6 @@ pub fn init(module: &RModule) -> Result<(), Error> {
384
392
  class.define_method("set_value", method!(Task::set_value, 3))?;
385
393
  class.define_method("set_uda", method!(Task::set_uda, 4))?;
386
394
  class.define_method("delete_uda", method!(Task::delete_uda, 3))?;
395
+ class.define_method("done", method!(Task::done, 1))?;
387
396
  Ok(())
388
397
  }
@@ -14,8 +14,6 @@ impl TaskData {
14
14
  pub fn from_tc_task_data(tc_task_data: TCTaskData) -> Self {
15
15
  TaskData(ThreadBound::new(tc_task_data))
16
16
  }
17
-
18
-
19
17
  fn inspect(&self) -> Result<String, Error> {
20
18
  let task_data = self.0.get()?;
21
19
  Ok(format!("#<Taskchampion::TaskData: {}>", task_data.get_uuid()))
@@ -45,11 +43,11 @@ impl TaskData {
45
43
  fn to_hash(&self) -> Result<RHash, Error> {
46
44
  let task_data = self.0.get()?;
47
45
  let hash = RHash::new();
48
-
46
+
49
47
  for (key, value) in task_data.iter() {
50
48
  hash.aset(key.clone(), value.clone())?;
51
49
  }
52
-
50
+
53
51
  Ok(hash)
54
52
  }
55
53
 
@@ -78,7 +76,7 @@ impl TaskData {
78
76
 
79
77
  fn delete(&self, operations: &Operations) -> Result<(), Error> {
80
78
  let mut task_data = self.0.get_mut()?;
81
-
79
+
82
80
  operations.with_inner_mut(|ops| {
83
81
  task_data.delete(ops);
84
82
  Ok(())
@@ -90,16 +88,16 @@ impl TaskData {
90
88
 
91
89
  fn create_task_data(uuid: String, operations: &Operations) -> Result<TaskData, Error> {
92
90
  let tc_uuid = uuid2tc(&uuid)?;
93
-
91
+
94
92
  // Create operations for TaskChampion
95
93
  let mut tc_ops = taskchampion::Operations::new();
96
-
94
+
97
95
  // Create the TaskData
98
96
  let tc_task_data = TCTaskData::create(tc_uuid, &mut tc_ops);
99
-
97
+
100
98
  // Add the resulting operations to the provided Operations object
101
99
  operations.extend_from_tc(tc_ops.into_iter().collect())?;
102
-
100
+
103
101
  Ok(TaskData(ThreadBound::new(tc_task_data)))
104
102
  }
105
103
 
@@ -121,4 +119,4 @@ pub fn init(module: &RModule) -> Result<(), Error> {
121
119
  class.define_method("delete", method!(TaskData::delete, 1))?;
122
120
 
123
121
  Ok(())
124
- }
122
+ }
@@ -61,7 +61,7 @@ pub fn ruby_to_datetime(value: Value) -> Result<DateTime<Utc>, Error> {
61
61
  ))
62
62
  }
63
63
  };
64
-
64
+
65
65
  DateTime::parse_from_rfc3339(&iso_string)
66
66
  .map(|dt| dt.with_timezone(&Utc))
67
67
  .or_else(|_| {
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Taskchampion
4
- VERSION = "0.3.0"
4
+ VERSION = "0.6.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: taskchampion-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tim Case
@@ -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
@@ -48,6 +49,7 @@ files:
48
49
  - docs/plan.md
49
50
  - example.md
50
51
  - examples/basic_usage.rb
52
+ - examples/pending_tasks.rb
51
53
  - examples/sync_workflow.rb
52
54
  - ext/taskchampion/Cargo.toml
53
55
  - ext/taskchampion/extconf.rb