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.
- checksums.yaml +7 -0
- data/.claude/settings.local.json +14 -0
- data/.rubocop.yml +21 -0
- data/CHANGELOG.md +15 -0
- data/Cargo.lock +3671 -0
- data/Cargo.toml +7 -0
- data/README.md +112 -0
- data/Rakefile +28 -0
- data/docs/API_REFERENCE.md +419 -0
- data/docs/THREAD_SAFETY.md +370 -0
- data/docs/breakthrough.md +246 -0
- data/docs/description.md +3 -0
- data/docs/phase_3_plan.md +482 -0
- data/docs/plan.md +612 -0
- data/example.md +465 -0
- data/examples/basic_usage.rb +278 -0
- data/examples/sync_workflow.rb +480 -0
- data/ext/taskchampion/Cargo.toml +20 -0
- data/ext/taskchampion/extconf.rb +6 -0
- data/ext/taskchampion/src/access_mode.rs +132 -0
- data/ext/taskchampion/src/annotation.rs +77 -0
- data/ext/taskchampion/src/dependency_map.rs +65 -0
- data/ext/taskchampion/src/error.rs +78 -0
- data/ext/taskchampion/src/lib.rs +41 -0
- data/ext/taskchampion/src/operation.rs +234 -0
- data/ext/taskchampion/src/operations.rs +180 -0
- data/ext/taskchampion/src/replica.rs +289 -0
- data/ext/taskchampion/src/status.rs +186 -0
- data/ext/taskchampion/src/tag.rs +77 -0
- data/ext/taskchampion/src/task.rs +388 -0
- data/ext/taskchampion/src/thread_check.rs +61 -0
- data/ext/taskchampion/src/util.rs +131 -0
- data/ext/taskchampion/src/working_set.rs +72 -0
- data/lib/taskchampion/version.rb +5 -0
- data/lib/taskchampion.rb +41 -0
- data/sig/taskchampion.rbs +4 -0
- data/taskchampion-0.2.0.gem +0 -0
- metadata +96 -0
@@ -0,0 +1,289 @@
|
|
1
|
+
use magnus::{
|
2
|
+
class, function, method, prelude::*, Error, IntoValue, RArray, RHash, RModule, Symbol, TryConvert, Value,
|
3
|
+
};
|
4
|
+
use taskchampion::{Replica as TCReplica, ServerConfig, StorageConfig};
|
5
|
+
|
6
|
+
use crate::access_mode::AccessMode;
|
7
|
+
use crate::operations::Operations;
|
8
|
+
use crate::task::Task;
|
9
|
+
use crate::working_set::WorkingSet;
|
10
|
+
use crate::dependency_map::DependencyMap;
|
11
|
+
use crate::thread_check::ThreadBound;
|
12
|
+
use crate::util::{into_error, option_to_ruby, uuid2tc, vec_to_ruby};
|
13
|
+
|
14
|
+
#[magnus::wrap(class = "Taskchampion::Replica", free_immediately)]
|
15
|
+
pub struct Replica(ThreadBound<TCReplica>);
|
16
|
+
|
17
|
+
impl Replica {
|
18
|
+
fn new_on_disk(
|
19
|
+
path: String,
|
20
|
+
create_if_missing: bool,
|
21
|
+
access_mode: Option<Symbol>,
|
22
|
+
) -> Result<Self, Error> {
|
23
|
+
let access_mode = match access_mode {
|
24
|
+
Some(sym) => AccessMode::from_symbol(sym)?,
|
25
|
+
None => AccessMode::from_symbol(Symbol::new("read_write"))?,
|
26
|
+
};
|
27
|
+
|
28
|
+
let replica = TCReplica::new(
|
29
|
+
StorageConfig::OnDisk {
|
30
|
+
taskdb_dir: path.into(),
|
31
|
+
create_if_missing,
|
32
|
+
access_mode: access_mode.into(),
|
33
|
+
}
|
34
|
+
.into_storage()
|
35
|
+
.map_err(into_error)?,
|
36
|
+
);
|
37
|
+
Ok(Replica(ThreadBound::new(replica)))
|
38
|
+
}
|
39
|
+
|
40
|
+
fn new_in_memory() -> Result<Self, Error> {
|
41
|
+
let replica = TCReplica::new(
|
42
|
+
StorageConfig::InMemory
|
43
|
+
.into_storage()
|
44
|
+
.map_err(into_error)?,
|
45
|
+
);
|
46
|
+
Ok(Replica(ThreadBound::new(replica)))
|
47
|
+
}
|
48
|
+
|
49
|
+
fn create_task(&self, uuid: String, operations: &Operations) -> Result<Value, Error> {
|
50
|
+
let mut tc_replica = self.0.get_mut()?;
|
51
|
+
let tc_uuid = uuid2tc(&uuid)?;
|
52
|
+
|
53
|
+
// Create mutable operations vector for TaskChampion
|
54
|
+
let mut tc_ops = vec![];
|
55
|
+
|
56
|
+
// Create the task in TaskChampion
|
57
|
+
let tc_task = tc_replica.create_task(tc_uuid, &mut tc_ops).map_err(into_error)?;
|
58
|
+
|
59
|
+
// Add the resulting operations to the provided Operations object
|
60
|
+
operations.extend_from_tc(tc_ops);
|
61
|
+
|
62
|
+
// Convert to Ruby Task object
|
63
|
+
let task = Task::from_tc_task(tc_task);
|
64
|
+
|
65
|
+
Ok(task.into_value())
|
66
|
+
}
|
67
|
+
|
68
|
+
fn commit_operations(&self, operations: &Operations) -> Result<(), Error> {
|
69
|
+
let mut tc_replica = self.0.get_mut()?;
|
70
|
+
|
71
|
+
// Convert Operations to TaskChampion Operations
|
72
|
+
let tc_operations = operations.clone_inner()?;
|
73
|
+
|
74
|
+
// Commit the operations
|
75
|
+
tc_replica.commit_operations(tc_operations).map_err(into_error)?;
|
76
|
+
|
77
|
+
Ok(())
|
78
|
+
}
|
79
|
+
|
80
|
+
fn tasks(&self) -> Result<RHash, Error> {
|
81
|
+
let mut tc_replica = self.0.get_mut()?;
|
82
|
+
|
83
|
+
let tasks = tc_replica.all_tasks().map_err(into_error)?;
|
84
|
+
let hash = RHash::new();
|
85
|
+
|
86
|
+
for (uuid, task) in tasks {
|
87
|
+
let ruby_task = Task::from_tc_task(task);
|
88
|
+
// Magnus automatically wraps ruby_task as a Taskchampion::Task Ruby object
|
89
|
+
hash.aset(uuid.to_string(), ruby_task)?;
|
90
|
+
}
|
91
|
+
|
92
|
+
Ok(hash)
|
93
|
+
}
|
94
|
+
|
95
|
+
fn task_data(&self, uuid: String) -> Result<Value, Error> {
|
96
|
+
let mut tc_replica = self.0.get_mut()?;
|
97
|
+
|
98
|
+
let task_data = tc_replica
|
99
|
+
.get_task_data(uuid2tc(&uuid)?)
|
100
|
+
.map_err(into_error)?;
|
101
|
+
|
102
|
+
option_to_ruby(task_data, |_data| {
|
103
|
+
// TODO: Convert task data to Ruby TaskData object
|
104
|
+
Ok(().into_value()) // () converts to nil in Magnus
|
105
|
+
})
|
106
|
+
}
|
107
|
+
|
108
|
+
fn task(&self, uuid: String) -> Result<Value, Error> {
|
109
|
+
let mut tc_replica = self.0.get_mut()?;
|
110
|
+
|
111
|
+
let task = tc_replica
|
112
|
+
.get_task(uuid2tc(&uuid)?)
|
113
|
+
.map_err(into_error)?;
|
114
|
+
|
115
|
+
option_to_ruby(task, |task| {
|
116
|
+
let ruby_task = Task::from_tc_task(task);
|
117
|
+
Ok(ruby_task.into_value()) // Convert to Value
|
118
|
+
})
|
119
|
+
}
|
120
|
+
|
121
|
+
fn task_uuids(&self) -> Result<RArray, Error> {
|
122
|
+
let mut tc_replica = self.0.get_mut()?;
|
123
|
+
|
124
|
+
let uuids = tc_replica.all_task_uuids().map_err(into_error)?;
|
125
|
+
vec_to_ruby(uuids, |uuid| Ok(uuid.to_string().into_value()))
|
126
|
+
}
|
127
|
+
|
128
|
+
fn working_set(&self) -> Result<Value, Error> {
|
129
|
+
let mut tc_replica = self.0.get_mut()?;
|
130
|
+
|
131
|
+
let tc_working_set = tc_replica.working_set().map_err(into_error)?;
|
132
|
+
let working_set = WorkingSet::from_tc_working_set(tc_working_set.into());
|
133
|
+
|
134
|
+
Ok(working_set.into_value())
|
135
|
+
}
|
136
|
+
|
137
|
+
fn dependency_map(&self, force: Option<bool>) -> Result<Value, Error> {
|
138
|
+
let mut tc_replica = self.0.get_mut()?;
|
139
|
+
let force = force.unwrap_or(false);
|
140
|
+
|
141
|
+
let tc_dm = tc_replica.dependency_map(force).map_err(into_error)?;
|
142
|
+
let dependency_map = DependencyMap::from_tc_dependency_map(tc_dm);
|
143
|
+
|
144
|
+
Ok(dependency_map.into_value())
|
145
|
+
}
|
146
|
+
|
147
|
+
fn sync_to_local(&self, server_dir: String, avoid_snapshots: Option<bool>) -> Result<(), Error> {
|
148
|
+
let mut tc_replica = self.0.get_mut()?;
|
149
|
+
let avoid_snapshots = avoid_snapshots.unwrap_or(false);
|
150
|
+
|
151
|
+
let mut server = ServerConfig::Local {
|
152
|
+
server_dir: server_dir.into(),
|
153
|
+
}
|
154
|
+
.into_server()
|
155
|
+
.map_err(into_error)?;
|
156
|
+
|
157
|
+
tc_replica
|
158
|
+
.sync(&mut server, avoid_snapshots)
|
159
|
+
.map_err(into_error)
|
160
|
+
}
|
161
|
+
|
162
|
+
fn sync_to_remote(
|
163
|
+
&self,
|
164
|
+
kwargs: RHash,
|
165
|
+
) -> Result<(), Error> {
|
166
|
+
|
167
|
+
// Extract required keyword arguments with proper exception type
|
168
|
+
let url: String = kwargs.fetch(Symbol::new("url")).map_err(|_| Error::new(
|
169
|
+
magnus::exception::arg_error(),
|
170
|
+
"Missing required parameter: url"
|
171
|
+
))?;
|
172
|
+
let client_id: String = kwargs.fetch(Symbol::new("client_id")).map_err(|_| Error::new(
|
173
|
+
magnus::exception::arg_error(),
|
174
|
+
"Missing required parameter: client_id"
|
175
|
+
))?;
|
176
|
+
let encryption_secret: String = kwargs.fetch(Symbol::new("encryption_secret")).map_err(|_| Error::new(
|
177
|
+
magnus::exception::arg_error(),
|
178
|
+
"Missing required parameter: encryption_secret"
|
179
|
+
))?;
|
180
|
+
let avoid_snapshots: bool = kwargs
|
181
|
+
.fetch::<_, Value>(Symbol::new("avoid_snapshots"))
|
182
|
+
.ok()
|
183
|
+
.and_then(|v| bool::try_convert(v).ok())
|
184
|
+
.unwrap_or(false);
|
185
|
+
|
186
|
+
let mut tc_replica = self.0.get_mut()?;
|
187
|
+
|
188
|
+
let mut server = ServerConfig::Remote {
|
189
|
+
url,
|
190
|
+
client_id: uuid2tc(&client_id)?,
|
191
|
+
encryption_secret: encryption_secret.into(),
|
192
|
+
}
|
193
|
+
.into_server()
|
194
|
+
.map_err(into_error)?;
|
195
|
+
|
196
|
+
tc_replica
|
197
|
+
.sync(&mut server, avoid_snapshots)
|
198
|
+
.map_err(into_error)
|
199
|
+
}
|
200
|
+
|
201
|
+
fn rebuild_working_set(&self, renumber: Option<bool>) -> Result<(), Error> {
|
202
|
+
let mut tc_replica = self.0.get_mut()?;
|
203
|
+
let renumber = renumber.unwrap_or(false);
|
204
|
+
|
205
|
+
tc_replica
|
206
|
+
.rebuild_working_set(renumber)
|
207
|
+
.map_err(into_error)
|
208
|
+
}
|
209
|
+
|
210
|
+
fn expire_tasks(&self) -> Result<(), Error> {
|
211
|
+
let mut tc_replica = self.0.get_mut()?;
|
212
|
+
|
213
|
+
tc_replica.expire_tasks().map_err(into_error)
|
214
|
+
}
|
215
|
+
|
216
|
+
fn sync_to_gcp(&self, kwargs: RHash) -> Result<(), Error> {
|
217
|
+
// Extract required keyword arguments with proper exception type
|
218
|
+
let bucket: String = kwargs.fetch(Symbol::new("bucket")).map_err(|_| Error::new(
|
219
|
+
magnus::exception::arg_error(),
|
220
|
+
"Missing required parameter: bucket"
|
221
|
+
))?;
|
222
|
+
let credential_path: String = kwargs.fetch(Symbol::new("credential_path")).map_err(|_| Error::new(
|
223
|
+
magnus::exception::arg_error(),
|
224
|
+
"Missing required parameter: credential_path"
|
225
|
+
))?;
|
226
|
+
let encryption_secret: String = kwargs.fetch(Symbol::new("encryption_secret")).map_err(|_| Error::new(
|
227
|
+
magnus::exception::arg_error(),
|
228
|
+
"Missing required parameter: encryption_secret"
|
229
|
+
))?;
|
230
|
+
let avoid_snapshots: bool = kwargs
|
231
|
+
.fetch::<_, Value>(Symbol::new("avoid_snapshots"))
|
232
|
+
.ok()
|
233
|
+
.and_then(|v| bool::try_convert(v).ok())
|
234
|
+
.unwrap_or(false);
|
235
|
+
|
236
|
+
let mut tc_replica = self.0.get_mut()?;
|
237
|
+
|
238
|
+
let mut server = ServerConfig::Gcp {
|
239
|
+
bucket,
|
240
|
+
credential_path: credential_path.into(),
|
241
|
+
encryption_secret: encryption_secret.into(),
|
242
|
+
}
|
243
|
+
.into_server()
|
244
|
+
.map_err(into_error)?;
|
245
|
+
|
246
|
+
tc_replica
|
247
|
+
.sync(&mut server, avoid_snapshots)
|
248
|
+
.map_err(into_error)
|
249
|
+
}
|
250
|
+
|
251
|
+
fn num_local_operations(&self) -> Result<usize, Error> {
|
252
|
+
let mut tc_replica = self.0.get_mut()?;
|
253
|
+
|
254
|
+
Ok(tc_replica.num_local_operations().map_err(into_error)?)
|
255
|
+
}
|
256
|
+
|
257
|
+
fn num_undo_points(&self) -> Result<usize, Error> {
|
258
|
+
let mut tc_replica = self.0.get_mut()?;
|
259
|
+
|
260
|
+
Ok(tc_replica.num_undo_points().map_err(into_error)?)
|
261
|
+
}
|
262
|
+
}
|
263
|
+
|
264
|
+
pub fn init(module: &RModule) -> Result<(), Error> {
|
265
|
+
let class = module.define_class("Replica", class::object())?;
|
266
|
+
|
267
|
+
// Class methods
|
268
|
+
class.define_singleton_method("new_on_disk", function!(Replica::new_on_disk, 3))?;
|
269
|
+
class.define_singleton_method("new_in_memory", function!(Replica::new_in_memory, 0))?;
|
270
|
+
|
271
|
+
// Instance methods
|
272
|
+
class.define_method("create_task", method!(Replica::create_task, 2))?;
|
273
|
+
class.define_method("commit_operations", method!(Replica::commit_operations, 1))?;
|
274
|
+
class.define_method("tasks", method!(Replica::tasks, 0))?;
|
275
|
+
class.define_method("task", method!(Replica::task, 1))?;
|
276
|
+
class.define_method("task_data", method!(Replica::task_data, 1))?;
|
277
|
+
class.define_method("task_uuids", method!(Replica::task_uuids, 0))?;
|
278
|
+
class.define_method("working_set", method!(Replica::working_set, 0))?;
|
279
|
+
class.define_method("dependency_map", method!(Replica::dependency_map, 1))?;
|
280
|
+
class.define_method("sync_to_local", method!(Replica::sync_to_local, 2))?;
|
281
|
+
class.define_method("sync_to_remote", method!(Replica::sync_to_remote, 1))?;
|
282
|
+
class.define_method("sync_to_gcp", method!(Replica::sync_to_gcp, 1))?;
|
283
|
+
class.define_method("rebuild_working_set", method!(Replica::rebuild_working_set, 1))?;
|
284
|
+
class.define_method("expire_tasks", method!(Replica::expire_tasks, 0))?;
|
285
|
+
class.define_method("num_local_operations", method!(Replica::num_local_operations, 0))?;
|
286
|
+
class.define_method("num_undo_points", method!(Replica::num_undo_points, 0))?;
|
287
|
+
|
288
|
+
Ok(())
|
289
|
+
}
|
@@ -0,0 +1,186 @@
|
|
1
|
+
use magnus::{class, function, method, prelude::*, Error, RModule, Symbol, TryConvert};
|
2
|
+
pub use taskchampion::Status as TCStatus;
|
3
|
+
use crate::error::validation_error;
|
4
|
+
|
5
|
+
#[magnus::wrap(class = "Taskchampion::Status", free_immediately)]
|
6
|
+
#[derive(Clone, Copy, PartialEq)]
|
7
|
+
pub struct Status(StatusKind);
|
8
|
+
|
9
|
+
#[derive(Clone, Copy, PartialEq, Hash)]
|
10
|
+
enum StatusKind {
|
11
|
+
Pending,
|
12
|
+
Completed,
|
13
|
+
Deleted,
|
14
|
+
Recurring,
|
15
|
+
Unknown,
|
16
|
+
}
|
17
|
+
|
18
|
+
impl Status {
|
19
|
+
// Constructor methods
|
20
|
+
fn pending() -> Self {
|
21
|
+
Status(StatusKind::Pending)
|
22
|
+
}
|
23
|
+
|
24
|
+
fn completed() -> Self {
|
25
|
+
Status(StatusKind::Completed)
|
26
|
+
}
|
27
|
+
|
28
|
+
fn deleted() -> Self {
|
29
|
+
Status(StatusKind::Deleted)
|
30
|
+
}
|
31
|
+
|
32
|
+
fn recurring() -> Self {
|
33
|
+
Status(StatusKind::Recurring)
|
34
|
+
}
|
35
|
+
|
36
|
+
fn unknown() -> Self {
|
37
|
+
Status(StatusKind::Unknown)
|
38
|
+
}
|
39
|
+
|
40
|
+
// Predicate methods
|
41
|
+
fn is_pending(&self) -> bool {
|
42
|
+
matches!(self.0, StatusKind::Pending)
|
43
|
+
}
|
44
|
+
|
45
|
+
fn is_completed(&self) -> bool {
|
46
|
+
matches!(self.0, StatusKind::Completed)
|
47
|
+
}
|
48
|
+
|
49
|
+
fn is_deleted(&self) -> bool {
|
50
|
+
matches!(self.0, StatusKind::Deleted)
|
51
|
+
}
|
52
|
+
|
53
|
+
fn is_recurring(&self) -> bool {
|
54
|
+
matches!(self.0, StatusKind::Recurring)
|
55
|
+
}
|
56
|
+
|
57
|
+
fn is_unknown(&self) -> bool {
|
58
|
+
matches!(self.0, StatusKind::Unknown)
|
59
|
+
}
|
60
|
+
|
61
|
+
// String representations
|
62
|
+
fn to_s(&self) -> &'static str {
|
63
|
+
match self.0 {
|
64
|
+
StatusKind::Pending => "pending",
|
65
|
+
StatusKind::Completed => "completed",
|
66
|
+
StatusKind::Deleted => "deleted",
|
67
|
+
StatusKind::Recurring => "recurring",
|
68
|
+
StatusKind::Unknown => "unknown",
|
69
|
+
}
|
70
|
+
}
|
71
|
+
|
72
|
+
fn inspect(&self) -> String {
|
73
|
+
format!("#<Taskchampion::Status:{}>", self.to_s())
|
74
|
+
}
|
75
|
+
|
76
|
+
// Equality - handles any Ruby object type
|
77
|
+
fn eq(&self, other: magnus::Value) -> bool {
|
78
|
+
match <&Status>::try_convert(other) {
|
79
|
+
Ok(other_status) => self.0 == other_status.0,
|
80
|
+
Err(_) => false, // Not a Status object, so not equal
|
81
|
+
}
|
82
|
+
}
|
83
|
+
|
84
|
+
fn eql(&self, other: magnus::Value) -> bool {
|
85
|
+
match <&Status>::try_convert(other) {
|
86
|
+
Ok(other_status) => self.0 == other_status.0,
|
87
|
+
Err(_) => false, // Not a Status object, so not equal
|
88
|
+
}
|
89
|
+
}
|
90
|
+
|
91
|
+
fn hash(&self) -> u64 {
|
92
|
+
use std::collections::hash_map::DefaultHasher;
|
93
|
+
use std::hash::{Hash, Hasher};
|
94
|
+
|
95
|
+
let mut hasher = DefaultHasher::new();
|
96
|
+
std::mem::discriminant(&self.0).hash(&mut hasher);
|
97
|
+
hasher.finish()
|
98
|
+
}
|
99
|
+
|
100
|
+
// For internal use
|
101
|
+
pub fn from_symbol(sym: Symbol) -> Result<Self, Error> {
|
102
|
+
let sym_str = sym.to_string();
|
103
|
+
match sym_str.as_str() {
|
104
|
+
"pending" => Ok(Status(StatusKind::Pending)),
|
105
|
+
"completed" => Ok(Status(StatusKind::Completed)),
|
106
|
+
"deleted" => Ok(Status(StatusKind::Deleted)),
|
107
|
+
"recurring" => Ok(Status(StatusKind::Recurring)),
|
108
|
+
"unknown" => Ok(Status(StatusKind::Unknown)),
|
109
|
+
_ => Err(Error::new(
|
110
|
+
validation_error(),
|
111
|
+
format!("Invalid status: :{} - Expected one of: :pending, :completed, :deleted, :recurring, :unknown", sym_str),
|
112
|
+
)),
|
113
|
+
}
|
114
|
+
}
|
115
|
+
|
116
|
+
pub fn to_symbol(&self) -> Symbol {
|
117
|
+
match self.0 {
|
118
|
+
StatusKind::Pending => Symbol::new("pending"),
|
119
|
+
StatusKind::Completed => Symbol::new("completed"),
|
120
|
+
StatusKind::Deleted => Symbol::new("deleted"),
|
121
|
+
StatusKind::Recurring => Symbol::new("recurring"),
|
122
|
+
StatusKind::Unknown => Symbol::new("unknown"),
|
123
|
+
}
|
124
|
+
}
|
125
|
+
}
|
126
|
+
|
127
|
+
impl From<TCStatus> for Status {
|
128
|
+
fn from(status: TCStatus) -> Self {
|
129
|
+
match status {
|
130
|
+
TCStatus::Pending => Status(StatusKind::Pending),
|
131
|
+
TCStatus::Completed => Status(StatusKind::Completed),
|
132
|
+
TCStatus::Deleted => Status(StatusKind::Deleted),
|
133
|
+
TCStatus::Recurring => Status(StatusKind::Recurring),
|
134
|
+
_ => Status(StatusKind::Unknown),
|
135
|
+
}
|
136
|
+
}
|
137
|
+
}
|
138
|
+
|
139
|
+
impl From<Status> for TCStatus {
|
140
|
+
fn from(status: Status) -> Self {
|
141
|
+
match status.0 {
|
142
|
+
StatusKind::Pending => TCStatus::Pending,
|
143
|
+
StatusKind::Completed => TCStatus::Completed,
|
144
|
+
StatusKind::Deleted => TCStatus::Deleted,
|
145
|
+
StatusKind::Recurring => TCStatus::Recurring,
|
146
|
+
StatusKind::Unknown => TCStatus::Unknown("unknown status".to_string()),
|
147
|
+
}
|
148
|
+
}
|
149
|
+
}
|
150
|
+
|
151
|
+
pub fn init(module: &RModule) -> Result<(), Error> {
|
152
|
+
let class = module.define_class("Status", class::object())?;
|
153
|
+
|
154
|
+
// Constructor methods
|
155
|
+
class.define_singleton_method("pending", function!(Status::pending, 0))?;
|
156
|
+
class.define_singleton_method("completed", function!(Status::completed, 0))?;
|
157
|
+
class.define_singleton_method("deleted", function!(Status::deleted, 0))?;
|
158
|
+
class.define_singleton_method("recurring", function!(Status::recurring, 0))?;
|
159
|
+
class.define_singleton_method("unknown", function!(Status::unknown, 0))?;
|
160
|
+
class.define_singleton_method("from_symbol", function!(Status::from_symbol, 1))?;
|
161
|
+
|
162
|
+
// Predicate methods
|
163
|
+
class.define_method("pending?", method!(Status::is_pending, 0))?;
|
164
|
+
class.define_method("completed?", method!(Status::is_completed, 0))?;
|
165
|
+
class.define_method("deleted?", method!(Status::is_deleted, 0))?;
|
166
|
+
class.define_method("recurring?", method!(Status::is_recurring, 0))?;
|
167
|
+
class.define_method("unknown?", method!(Status::is_unknown, 0))?;
|
168
|
+
|
169
|
+
// String representations
|
170
|
+
class.define_method("to_s", method!(Status::to_s, 0))?;
|
171
|
+
class.define_method("inspect", method!(Status::inspect, 0))?;
|
172
|
+
|
173
|
+
// Equality
|
174
|
+
class.define_method("==", method!(Status::eq, 1))?;
|
175
|
+
class.define_method("eql?", method!(Status::eql, 1))?;
|
176
|
+
class.define_method("hash", method!(Status::hash, 0))?;
|
177
|
+
|
178
|
+
// Keep the constants for backward compatibility
|
179
|
+
module.const_set("PENDING", Symbol::new("pending"))?;
|
180
|
+
module.const_set("COMPLETED", Symbol::new("completed"))?;
|
181
|
+
module.const_set("DELETED", Symbol::new("deleted"))?;
|
182
|
+
module.const_set("RECURRING", Symbol::new("recurring"))?;
|
183
|
+
module.const_set("UNKNOWN", Symbol::new("unknown"))?;
|
184
|
+
|
185
|
+
Ok(())
|
186
|
+
}
|
@@ -0,0 +1,77 @@
|
|
1
|
+
use magnus::{class, function, method, prelude::*, Error, RModule, Ruby};
|
2
|
+
use taskchampion::Tag as TCTag;
|
3
|
+
use crate::error::validation_error;
|
4
|
+
|
5
|
+
#[magnus::wrap(class = "Taskchampion::Tag", free_immediately)]
|
6
|
+
pub struct Tag(TCTag);
|
7
|
+
|
8
|
+
impl Tag {
|
9
|
+
fn new(_ruby: &Ruby, tag: String) -> Result<Self, Error> {
|
10
|
+
let tc_tag = tag.parse()
|
11
|
+
.map_err(|_| Error::new(validation_error(), "Invalid tag"))?;
|
12
|
+
Ok(Tag(tc_tag))
|
13
|
+
}
|
14
|
+
|
15
|
+
fn to_s(&self) -> String {
|
16
|
+
self.0.to_string()
|
17
|
+
}
|
18
|
+
|
19
|
+
fn inspect(&self) -> String {
|
20
|
+
format!("#<Taskchampion::Tag:{:?}>", self.0)
|
21
|
+
}
|
22
|
+
|
23
|
+
fn synthetic(&self) -> bool {
|
24
|
+
self.0.is_synthetic()
|
25
|
+
}
|
26
|
+
|
27
|
+
fn user(&self) -> bool {
|
28
|
+
self.0.is_user()
|
29
|
+
}
|
30
|
+
|
31
|
+
fn hash(&self) -> i64 {
|
32
|
+
use std::collections::hash_map::DefaultHasher;
|
33
|
+
use std::hash::{Hash, Hasher};
|
34
|
+
|
35
|
+
let mut hasher = DefaultHasher::new();
|
36
|
+
self.0.hash(&mut hasher);
|
37
|
+
hasher.finish() as i64
|
38
|
+
}
|
39
|
+
|
40
|
+
fn eql(&self, other: &Tag) -> bool {
|
41
|
+
self.0 == other.0
|
42
|
+
}
|
43
|
+
}
|
44
|
+
|
45
|
+
impl AsRef<TCTag> for Tag {
|
46
|
+
fn as_ref(&self) -> &TCTag {
|
47
|
+
&self.0
|
48
|
+
}
|
49
|
+
}
|
50
|
+
|
51
|
+
impl From<TCTag> for Tag {
|
52
|
+
fn from(value: TCTag) -> Self {
|
53
|
+
Tag(value)
|
54
|
+
}
|
55
|
+
}
|
56
|
+
|
57
|
+
impl From<Tag> for TCTag {
|
58
|
+
fn from(value: Tag) -> Self {
|
59
|
+
value.0
|
60
|
+
}
|
61
|
+
}
|
62
|
+
|
63
|
+
pub fn init(module: &RModule) -> Result<(), Error> {
|
64
|
+
let class = module.define_class("Tag", class::object())?;
|
65
|
+
|
66
|
+
class.define_singleton_method("new", function!(Tag::new, 1))?;
|
67
|
+
class.define_method("to_s", method!(Tag::to_s, 0))?;
|
68
|
+
class.define_method("to_str", method!(Tag::to_s, 0))?;
|
69
|
+
class.define_method("inspect", method!(Tag::inspect, 0))?;
|
70
|
+
class.define_method("synthetic?", method!(Tag::synthetic, 0))?;
|
71
|
+
class.define_method("user?", method!(Tag::user, 0))?;
|
72
|
+
class.define_method("hash", method!(Tag::hash, 0))?;
|
73
|
+
class.define_method("eql?", method!(Tag::eql, 1))?;
|
74
|
+
class.define_method("==", method!(Tag::eql, 1))?;
|
75
|
+
|
76
|
+
Ok(())
|
77
|
+
}
|