taskchampion-rb 0.9.0 → 0.9.2

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: d661bc07cbe2c900550df0973e28fbc8ca3ed34fb8f607e91bdff8824239a6dc
4
- data.tar.gz: e4c5b2908fcac0ec63dcaa1c9e179a6a0b17701cb9c04c884738b4977f86ed64
3
+ metadata.gz: 6d963e64502ad438957e3345671387a3915de84947a63bbcced83270f5c50848
4
+ data.tar.gz: 19309ecfe6ccf1a5a077a513a7b6ff769109ba7949c0282617b8b74e3b54f116
5
5
  SHA512:
6
- metadata.gz: 7aa047cb7dc2adb780e8c2616f726126df2e07bc53e8a357d507d4742ef099a438ed126a7f5849bb43e86136b14dc8044a6038fe7104e94c9a633db6d61101d1
7
- data.tar.gz: 5fb6d41847c431265d85114505b385e36d05c9e5dc7006c92c3c1e29a89adb015a2486f7620b613b28a8132bb1b30f3e6e4d777ee1ba81e623e17aaae7cc441b
6
+ metadata.gz: '052509a0688871de92bc3d4219d7c257b712b7f4b7f5693f4e051d2ff38f891d5b6095d66aaf0cec8d06466ec272ec1fe5bd7e166dd790fb2a0c649592a29d4b'
7
+ data.tar.gz: f21cca16bd5da5921474361eb81d18937a1e328313a4c3dac2ea521ccc4d7b93cf356816d11e67abfa7292bbcfb76e40548766b94718030e42a315a80c3e5531
@@ -0,0 +1,54 @@
1
+ # Taskchampion Ruby Errors
2
+
3
+ ## Hierarchy
4
+
5
+ ```
6
+ StandardError
7
+ └── Taskchampion::Error
8
+ ├── Taskchampion::ThreadError
9
+ ├── Taskchampion::StorageError
10
+ ├── Taskchampion::ValidationError
11
+ ├── Taskchampion::ConfigError
12
+ └── Taskchampion::SyncError
13
+ └── Taskchampion::OutOfSyncError
14
+ ```
15
+
16
+ ## Error Classes
17
+
18
+ - **`Taskchampion::Error`** — Base class for all Taskchampion errors.
19
+ - **`Taskchampion::ThreadError`** — Raised when an object (Replica, Task, etc.) is accessed from a thread other than the one that created it.
20
+ - **`Taskchampion::StorageError`** — Raised for database and storage failures (`Error::Database`), and for wrapped IO/SQLite/cloud infrastructure errors (`Error::Other`).
21
+ - **`Taskchampion::ValidationError`** — Raised for incorrect API usage (`Error::Usage`): bad tag names, empty descriptions, invalid UUIDs, wrong argument types, invalid status symbols.
22
+ - **`Taskchampion::ConfigError`** — Defined for compatibility; not raised by the current TaskChampion error variants. Missing sync keyword arguments raise Ruby `ArgumentError` instead.
23
+ - **`Taskchampion::SyncError`** — Raised for server communication errors (`Error::Server`).
24
+ - **`Taskchampion::OutOfSyncError`** — Raised when the local replica is irrecoverably out of sync with the server (`Error::OutOfSync`). Subclass of `SyncError`. Signals that re-syncing from scratch is required, not just a retry.
25
+
26
+ ## Mapping from TaskChampion Rust errors
27
+
28
+ | Rust variant | Ruby class |
29
+ |---|---|
30
+ | `Error::Database(msg)` | `StorageError` |
31
+ | `Error::Server(msg)` | `SyncError` |
32
+ | `Error::OutOfSync` | `OutOfSyncError` |
33
+ | `Error::Usage(msg)` | `ValidationError` |
34
+ | `Error::Other(_)` / unknown | `StorageError` |
35
+
36
+ ## Usage
37
+
38
+ Rescue any error via the base class or individually:
39
+
40
+ ```ruby
41
+ begin
42
+ replica.sync(config)
43
+ rescue Taskchampion::OutOfSyncError => e
44
+ # local replica is irrecoverably diverged — must re-sync from scratch
45
+ rescue Taskchampion::SyncError => e
46
+ # transient server/network error — may retry
47
+ rescue Taskchampion::StorageError => e
48
+ # database or IO failure
49
+ rescue Taskchampion::ValidationError => e
50
+ # bad input
51
+ rescue Taskchampion::Error => e
52
+ # catch-all for any Taskchampion error
53
+ end
54
+ ```
@@ -6,7 +6,8 @@ pub fn init_errors(module: &RModule) -> Result<(), Error> {
6
6
  module.define_error("StorageError", error_class)?;
7
7
  module.define_error("ValidationError", error_class)?;
8
8
  module.define_error("ConfigError", error_class)?;
9
- module.define_error("SyncError", error_class)?;
9
+ let sync_error_class = module.define_error("SyncError", error_class)?;
10
+ module.define_error("OutOfSyncError", sync_error_class)?;
10
11
  Ok(())
11
12
  }
12
13
 
@@ -34,45 +35,29 @@ pub fn validation_error() -> magnus::ExceptionClass {
34
35
  .expect("ValidationError class not initialized")
35
36
  }
36
37
 
37
- pub fn config_error() -> magnus::ExceptionClass {
38
+ pub fn sync_error() -> magnus::ExceptionClass {
38
39
  let ruby = magnus::Ruby::get().expect("Ruby not available");
39
40
  let module = ruby.class_object().const_get::<_, RModule>("Taskchampion")
40
41
  .expect("Taskchampion module not found");
41
- module.const_get::<_, magnus::ExceptionClass>("ConfigError")
42
- .expect("ConfigError class not initialized")
42
+ module.const_get::<_, magnus::ExceptionClass>("SyncError")
43
+ .expect("SyncError class not initialized")
43
44
  }
44
45
 
45
- pub fn sync_error() -> magnus::ExceptionClass {
46
+ pub fn out_of_sync_error() -> magnus::ExceptionClass {
46
47
  let ruby = magnus::Ruby::get().expect("Ruby not available");
47
48
  let module = ruby.class_object().const_get::<_, RModule>("Taskchampion")
48
49
  .expect("Taskchampion module not found");
49
- module.const_get::<_, magnus::ExceptionClass>("SyncError")
50
- .expect("SyncError class not initialized")
50
+ module.const_get::<_, magnus::ExceptionClass>("OutOfSyncError")
51
+ .expect("OutOfSyncError class not initialized")
51
52
  }
52
53
 
53
- // Enhanced error mapping function with context-aware error types
54
54
  pub fn map_taskchampion_error(error: taskchampion::Error) -> Error {
55
- let error_msg = error.to_string();
56
-
57
- // Map TaskChampion errors to appropriate Ruby error types based on error content
58
- if error_msg.contains("No such file") || error_msg.contains("Permission denied") ||
59
- error_msg.contains("storage") || error_msg.contains("database") {
60
- Error::new(storage_error(), format!("Storage error: {}", error_msg))
61
- } else if error_msg.contains("sync") || error_msg.contains("server") ||
62
- error_msg.contains("network") || error_msg.contains("remote") {
63
- Error::new(sync_error(), format!("Synchronization error: {}", error_msg))
64
- } else if error_msg.contains("config") || error_msg.contains("invalid config") {
65
- Error::new(config_error(), format!("Configuration error: {}", error_msg))
66
- } else if error_msg.contains("invalid") || error_msg.contains("parse") ||
67
- error_msg.contains("format") || error_msg.contains("validation") {
68
- Error::new(validation_error(), format!("Validation error: {}", error_msg))
69
- } else {
70
- // Generic TaskChampion error for unknown types
71
- let ruby = magnus::Ruby::get().expect("Ruby not available");
72
- let module = ruby.class_object().const_get::<_, RModule>("Taskchampion")
73
- .expect("Taskchampion module not found");
74
- let error_class = module.const_get::<_, magnus::ExceptionClass>("Error")
75
- .expect("Error class not initialized");
76
- Error::new(error_class, format!("TaskChampion error: {}", error_msg))
55
+ match error {
56
+ taskchampion::Error::Database(msg) => Error::new(storage_error(), msg),
57
+ taskchampion::Error::Server(msg) => Error::new(sync_error(), msg),
58
+ taskchampion::Error::OutOfSync => Error::new(out_of_sync_error(),
59
+ "Local replica is out of sync with the server"),
60
+ taskchampion::Error::Usage(msg) => Error::new(validation_error(), msg),
61
+ _ => Error::new(storage_error(), error.to_string()),
77
62
  }
78
63
  }
@@ -58,7 +58,7 @@ impl Replica {
58
58
  let tc_task = tc_replica.create_task(tc_uuid, &mut tc_ops).map_err(into_error)?;
59
59
 
60
60
  // Add the resulting operations to the provided Operations object
61
- operations.extend_from_tc(tc_ops);
61
+ operations.extend_from_tc(tc_ops)?;
62
62
 
63
63
  // Convert to Ruby Task object
64
64
  let task = Task::from_tc_task(tc_task);
@@ -137,24 +137,22 @@ impl Task {
137
137
 
138
138
  fn get_uda(&self, namespace: String, key: String) -> Result<Value, Error> {
139
139
  let task = self.0.get()?;
140
- match task.get_uda(&namespace, &key) {
140
+ let combined_key = if namespace.is_empty() { key } else { format!("{}.{}", namespace, key) };
141
+ match task.get_user_defined_attribute(&combined_key) {
141
142
  Some(value) => Ok(value.into_value()),
142
- None => Ok(().into_value()), // () converts to nil in Magnus
143
+ None => Ok(().into_value()),
143
144
  }
144
145
  }
145
146
 
146
147
  fn udas(&self) -> Result<RArray, Error> {
147
148
  let task = self.0.get()?;
148
- let udas: Vec<((String, String), String)> = task.get_udas()
149
- .map(|((ns, key), value)| ((ns.to_string(), key.to_string()), value.to_string()))
149
+ let udas: Vec<(String, String)> = task.get_user_defined_attributes()
150
+ .map(|(key, value)| (key.to_string(), value.to_string()))
150
151
  .collect();
151
152
 
152
- vec_to_ruby(udas, |(key_tuple, value)| {
153
+ vec_to_ruby(udas, |(key, value)| {
153
154
  let array = RArray::new();
154
- let key_array = RArray::new();
155
- key_array.push(key_tuple.0)?;
156
- key_array.push(key_tuple.1)?;
157
- array.push(key_array)?;
155
+ array.push(key)?;
158
156
  array.push(value)?;
159
157
  Ok(array.into_value())
160
158
  })
@@ -360,7 +358,7 @@ impl Task {
360
358
  Some(timestamp_str) => {
361
359
  // Parse the string as Unix timestamp (seconds since epoch)
362
360
  if let Ok(timestamp_secs) = timestamp_str.parse::<i64>() {
363
- use chrono::{DateTime, Utc};
361
+ use chrono::DateTime;
364
362
  if let Some(dt) = DateTime::from_timestamp(timestamp_secs, 0) {
365
363
  return datetime_to_ruby(dt);
366
364
  }
@@ -387,8 +385,9 @@ impl Task {
387
385
  }
388
386
 
389
387
  let mut task = self.0.get_mut()?;
388
+ let combined_key = format!("{}.{}", namespace, key);
390
389
  operations.with_inner_mut(|ops| {
391
- task.set_uda(&namespace, &key, &value, ops)
390
+ task.set_user_defined_attribute(&combined_key, &value, ops)
392
391
  })?;
393
392
  Ok(())
394
393
  }
@@ -408,8 +407,9 @@ impl Task {
408
407
  }
409
408
 
410
409
  let mut task = self.0.get_mut()?;
410
+ let combined_key = format!("{}.{}", namespace, key);
411
411
  operations.with_inner_mut(|ops| {
412
- task.remove_uda(&namespace, &key, ops)
412
+ task.remove_user_defined_attribute(&combined_key, ops)
413
413
  })?;
414
414
  Ok(())
415
415
  }
@@ -32,12 +32,12 @@ impl<T> ThreadBound<T> {
32
32
  Ok(())
33
33
  }
34
34
 
35
- pub fn get(&self) -> Result<std::cell::Ref<T>, Error> {
35
+ pub fn get(&self) -> Result<std::cell::Ref<'_, T>, Error> {
36
36
  self.check_thread()?;
37
37
  Ok(self.inner.borrow())
38
38
  }
39
39
 
40
- pub fn get_mut(&self) -> Result<std::cell::RefMut<T>, Error> {
40
+ pub fn get_mut(&self) -> Result<std::cell::RefMut<'_, T>, Error> {
41
41
  self.check_thread()?;
42
42
  Ok(self.inner.borrow_mut())
43
43
  }
@@ -31,8 +31,6 @@ pub fn datetime_to_ruby(dt: DateTime<Utc>) -> Result<Value, Error> {
31
31
 
32
32
  /// Convert Ruby DateTime/Time/String to Rust DateTime<Utc> with enhanced validation
33
33
  pub fn ruby_to_datetime(value: Value) -> Result<DateTime<Utc>, Error> {
34
- let ruby = magnus::Ruby::get().map_err(|e| Error::new(magnus::exception::runtime_error(), e.to_string()))?;
35
-
36
34
  // If it's a string, parse it
37
35
  if let Ok(s) = RString::try_convert(value) {
38
36
  let s = unsafe { s.as_str()? };
@@ -99,15 +97,6 @@ where
99
97
  }
100
98
  }
101
99
 
102
- /// Convert HashMap to Ruby Hash
103
- pub fn hashmap_to_ruby(map: HashMap<String, String>) -> Result<RHash, Error> {
104
- let hash = RHash::new();
105
- for (k, v) in map {
106
- hash.aset(k, v)?;
107
- }
108
- Ok(hash)
109
- }
110
-
111
100
  /// Convert Ruby Hash to HashMap
112
101
  pub fn ruby_to_hashmap(hash: RHash) -> Result<HashMap<String, String>, Error> {
113
102
  let mut map = HashMap::new();
@@ -130,15 +119,3 @@ where
130
119
  Ok(array)
131
120
  }
132
121
 
133
- /// Convert Ruby Array to Vec
134
- pub fn ruby_to_vec<T, F>(array: RArray, converter: F) -> Result<Vec<T>, Error>
135
- where
136
- F: Fn(Value) -> Result<T, Error>,
137
- {
138
- let mut vec = Vec::with_capacity(array.len());
139
- for i in 0..array.len() {
140
- let value: Value = array.entry(i as isize)?;
141
- vec.push(converter(value)?);
142
- }
143
- Ok(vec)
144
- }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Taskchampion
4
- VERSION = "0.9.0"
4
+ VERSION = "0.9.2"
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.9.0
4
+ version: 0.9.2
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-10-16 00:00:00.000000000 Z
11
+ date: 2026-05-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rb_sys
@@ -41,6 +41,7 @@ files:
41
41
  - Cargo.toml
42
42
  - README.md
43
43
  - Rakefile
44
+ - TaskchampionRbErrors.md
44
45
  - docs/ANNOTATION_IMPLEMENTATION.md
45
46
  - docs/API_REFERENCE.md
46
47
  - docs/THREAD_SAFETY.md