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.
@@ -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
+ }