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,77 @@
|
|
1
|
+
use magnus::{class, function, method, prelude::*, Error, RModule, Ruby, Value};
|
2
|
+
use taskchampion::Annotation as TCAnnotation;
|
3
|
+
use crate::util::{datetime_to_ruby, ruby_to_datetime};
|
4
|
+
|
5
|
+
#[magnus::wrap(class = "Taskchampion::Annotation", free_immediately)]
|
6
|
+
pub struct Annotation(TCAnnotation);
|
7
|
+
|
8
|
+
impl Annotation {
|
9
|
+
fn new(_ruby: &Ruby, entry: Value, description: String) -> Result<Self, Error> {
|
10
|
+
let entry = ruby_to_datetime(entry)?;
|
11
|
+
Ok(Annotation(TCAnnotation { entry, description }))
|
12
|
+
}
|
13
|
+
|
14
|
+
fn entry(&self) -> Result<Value, Error> {
|
15
|
+
datetime_to_ruby(self.0.entry)
|
16
|
+
}
|
17
|
+
|
18
|
+
fn description(&self) -> String {
|
19
|
+
self.0.description.clone()
|
20
|
+
}
|
21
|
+
|
22
|
+
fn inspect(&self) -> Result<String, Error> {
|
23
|
+
let entry_str = self.0.entry.to_rfc3339();
|
24
|
+
Ok(format!("#<Taskchampion::Annotation: {} \"{}\">", entry_str, self.0.description))
|
25
|
+
}
|
26
|
+
|
27
|
+
fn to_s(&self) -> String {
|
28
|
+
self.0.description.clone()
|
29
|
+
}
|
30
|
+
|
31
|
+
fn eql(&self, other: &Annotation) -> bool {
|
32
|
+
self.0 == other.0
|
33
|
+
}
|
34
|
+
|
35
|
+
fn hash(&self) -> i64 {
|
36
|
+
use std::collections::hash_map::DefaultHasher;
|
37
|
+
use std::hash::{Hash, Hasher};
|
38
|
+
|
39
|
+
let mut hasher = DefaultHasher::new();
|
40
|
+
self.0.entry.hash(&mut hasher);
|
41
|
+
self.0.description.hash(&mut hasher);
|
42
|
+
hasher.finish() as i64
|
43
|
+
}
|
44
|
+
}
|
45
|
+
|
46
|
+
impl AsRef<TCAnnotation> for Annotation {
|
47
|
+
fn as_ref(&self) -> &TCAnnotation {
|
48
|
+
&self.0
|
49
|
+
}
|
50
|
+
}
|
51
|
+
|
52
|
+
impl From<TCAnnotation> for Annotation {
|
53
|
+
fn from(value: TCAnnotation) -> Self {
|
54
|
+
Annotation(value)
|
55
|
+
}
|
56
|
+
}
|
57
|
+
|
58
|
+
impl From<Annotation> for TCAnnotation {
|
59
|
+
fn from(value: Annotation) -> Self {
|
60
|
+
value.0
|
61
|
+
}
|
62
|
+
}
|
63
|
+
|
64
|
+
pub fn init(module: &RModule) -> Result<(), Error> {
|
65
|
+
let class = module.define_class("Annotation", class::object())?;
|
66
|
+
|
67
|
+
class.define_singleton_method("new", function!(Annotation::new, 2))?;
|
68
|
+
class.define_method("entry", method!(Annotation::entry, 0))?;
|
69
|
+
class.define_method("description", method!(Annotation::description, 0))?;
|
70
|
+
class.define_method("inspect", method!(Annotation::inspect, 0))?;
|
71
|
+
class.define_method("to_s", method!(Annotation::to_s, 0))?;
|
72
|
+
class.define_method("eql?", method!(Annotation::eql, 1))?;
|
73
|
+
class.define_method("==", method!(Annotation::eql, 1))?;
|
74
|
+
class.define_method("hash", method!(Annotation::hash, 0))?;
|
75
|
+
|
76
|
+
Ok(())
|
77
|
+
}
|
@@ -0,0 +1,65 @@
|
|
1
|
+
use magnus::{
|
2
|
+
class, method, prelude::*, Error, IntoValue, RArray, RModule,
|
3
|
+
};
|
4
|
+
use std::sync::Arc;
|
5
|
+
use taskchampion::DependencyMap as TCDependencyMap;
|
6
|
+
|
7
|
+
use crate::thread_check::ThreadBound;
|
8
|
+
use crate::util::{uuid2tc, vec_to_ruby};
|
9
|
+
|
10
|
+
#[magnus::wrap(class = "Taskchampion::DependencyMap", free_immediately)]
|
11
|
+
pub struct DependencyMap(ThreadBound<Arc<TCDependencyMap>>);
|
12
|
+
|
13
|
+
impl DependencyMap {
|
14
|
+
pub fn from_tc_dependency_map(tc_dependency_map: Arc<TCDependencyMap>) -> Self {
|
15
|
+
DependencyMap(ThreadBound::new(tc_dependency_map))
|
16
|
+
}
|
17
|
+
|
18
|
+
fn dependencies(&self, uuid: String) -> Result<RArray, Error> {
|
19
|
+
let dep_map = self.0.get()?;
|
20
|
+
let tc_uuid = uuid2tc(&uuid)?;
|
21
|
+
|
22
|
+
let deps: Vec<String> = dep_map
|
23
|
+
.dependencies(tc_uuid)
|
24
|
+
.map(|uuid| uuid.to_string())
|
25
|
+
.collect();
|
26
|
+
|
27
|
+
vec_to_ruby(deps, |s| Ok(s.into_value()))
|
28
|
+
}
|
29
|
+
|
30
|
+
fn dependents(&self, uuid: String) -> Result<RArray, Error> {
|
31
|
+
let dep_map = self.0.get()?;
|
32
|
+
let tc_uuid = uuid2tc(&uuid)?;
|
33
|
+
|
34
|
+
let deps: Vec<String> = dep_map
|
35
|
+
.dependents(tc_uuid)
|
36
|
+
.map(|uuid| uuid.to_string())
|
37
|
+
.collect();
|
38
|
+
|
39
|
+
vec_to_ruby(deps, |s| Ok(s.into_value()))
|
40
|
+
}
|
41
|
+
|
42
|
+
fn has_dependency(&self, uuid: String) -> Result<bool, Error> {
|
43
|
+
let dep_map = self.0.get()?;
|
44
|
+
let tc_uuid = uuid2tc(&uuid)?;
|
45
|
+
|
46
|
+
// Check if this UUID has any dependencies
|
47
|
+
let result = dep_map.dependencies(tc_uuid).next().is_some();
|
48
|
+
Ok(result)
|
49
|
+
}
|
50
|
+
|
51
|
+
fn inspect(&self) -> Result<String, Error> {
|
52
|
+
Ok("#<Taskchampion::DependencyMap>".to_string())
|
53
|
+
}
|
54
|
+
}
|
55
|
+
|
56
|
+
pub fn init(module: &RModule) -> Result<(), Error> {
|
57
|
+
let class = module.define_class("DependencyMap", class::object())?;
|
58
|
+
|
59
|
+
class.define_method("dependencies", method!(DependencyMap::dependencies, 1))?;
|
60
|
+
class.define_method("dependents", method!(DependencyMap::dependents, 1))?;
|
61
|
+
class.define_method("has_dependency?", method!(DependencyMap::has_dependency, 1))?;
|
62
|
+
class.define_method("inspect", method!(DependencyMap::inspect, 0))?;
|
63
|
+
|
64
|
+
Ok(())
|
65
|
+
}
|
@@ -0,0 +1,78 @@
|
|
1
|
+
use magnus::{exception, prelude::*, Error, RModule};
|
2
|
+
|
3
|
+
pub fn init_errors(module: &RModule) -> Result<(), Error> {
|
4
|
+
let error_class = module.define_error("Error", exception::standard_error())?;
|
5
|
+
module.define_error("ThreadError", error_class)?;
|
6
|
+
module.define_error("StorageError", error_class)?;
|
7
|
+
module.define_error("ValidationError", error_class)?;
|
8
|
+
module.define_error("ConfigError", error_class)?;
|
9
|
+
module.define_error("SyncError", error_class)?;
|
10
|
+
Ok(())
|
11
|
+
}
|
12
|
+
|
13
|
+
pub fn thread_error() -> magnus::ExceptionClass {
|
14
|
+
let ruby = magnus::Ruby::get().expect("Ruby not available");
|
15
|
+
let module = ruby.class_object().const_get::<_, RModule>("Taskchampion")
|
16
|
+
.expect("Taskchampion module not found");
|
17
|
+
module.const_get::<_, magnus::ExceptionClass>("ThreadError")
|
18
|
+
.expect("ThreadError class not initialized")
|
19
|
+
}
|
20
|
+
|
21
|
+
pub fn storage_error() -> magnus::ExceptionClass {
|
22
|
+
let ruby = magnus::Ruby::get().expect("Ruby not available");
|
23
|
+
let module = ruby.class_object().const_get::<_, RModule>("Taskchampion")
|
24
|
+
.expect("Taskchampion module not found");
|
25
|
+
module.const_get::<_, magnus::ExceptionClass>("StorageError")
|
26
|
+
.expect("StorageError class not initialized")
|
27
|
+
}
|
28
|
+
|
29
|
+
pub fn validation_error() -> magnus::ExceptionClass {
|
30
|
+
let ruby = magnus::Ruby::get().expect("Ruby not available");
|
31
|
+
let module = ruby.class_object().const_get::<_, RModule>("Taskchampion")
|
32
|
+
.expect("Taskchampion module not found");
|
33
|
+
module.const_get::<_, magnus::ExceptionClass>("ValidationError")
|
34
|
+
.expect("ValidationError class not initialized")
|
35
|
+
}
|
36
|
+
|
37
|
+
pub fn config_error() -> magnus::ExceptionClass {
|
38
|
+
let ruby = magnus::Ruby::get().expect("Ruby not available");
|
39
|
+
let module = ruby.class_object().const_get::<_, RModule>("Taskchampion")
|
40
|
+
.expect("Taskchampion module not found");
|
41
|
+
module.const_get::<_, magnus::ExceptionClass>("ConfigError")
|
42
|
+
.expect("ConfigError class not initialized")
|
43
|
+
}
|
44
|
+
|
45
|
+
pub fn sync_error() -> magnus::ExceptionClass {
|
46
|
+
let ruby = magnus::Ruby::get().expect("Ruby not available");
|
47
|
+
let module = ruby.class_object().const_get::<_, RModule>("Taskchampion")
|
48
|
+
.expect("Taskchampion module not found");
|
49
|
+
module.const_get::<_, magnus::ExceptionClass>("SyncError")
|
50
|
+
.expect("SyncError class not initialized")
|
51
|
+
}
|
52
|
+
|
53
|
+
// Enhanced error mapping function with context-aware error types
|
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))
|
77
|
+
}
|
78
|
+
}
|
@@ -0,0 +1,41 @@
|
|
1
|
+
use magnus::{Error, Ruby};
|
2
|
+
|
3
|
+
mod error;
|
4
|
+
mod thread_check;
|
5
|
+
mod util;
|
6
|
+
mod access_mode;
|
7
|
+
mod status;
|
8
|
+
mod tag;
|
9
|
+
mod annotation;
|
10
|
+
mod task;
|
11
|
+
mod operation;
|
12
|
+
mod operations;
|
13
|
+
mod replica;
|
14
|
+
mod working_set;
|
15
|
+
mod dependency_map;
|
16
|
+
|
17
|
+
use error::init_errors;
|
18
|
+
|
19
|
+
#[magnus::init]
|
20
|
+
fn init(ruby: &Ruby) -> Result<(), Error> {
|
21
|
+
let module = ruby.define_module("Taskchampion")?;
|
22
|
+
|
23
|
+
// Initialize error classes
|
24
|
+
init_errors(&module)?;
|
25
|
+
|
26
|
+
// Initialize constants
|
27
|
+
access_mode::init(&module)?;
|
28
|
+
status::init(&module)?;
|
29
|
+
|
30
|
+
// Initialize classes
|
31
|
+
tag::init(&module)?;
|
32
|
+
annotation::init(&module)?;
|
33
|
+
task::init(&module)?;
|
34
|
+
operation::init(&module)?;
|
35
|
+
operations::init(&module)?;
|
36
|
+
working_set::init(&module)?;
|
37
|
+
dependency_map::init(&module)?;
|
38
|
+
replica::init(&module)?;
|
39
|
+
|
40
|
+
Ok(())
|
41
|
+
}
|
@@ -0,0 +1,234 @@
|
|
1
|
+
use magnus::{
|
2
|
+
class, function, method, prelude::*, Error, IntoValue, RHash, RModule, Value,
|
3
|
+
};
|
4
|
+
use taskchampion::Operation as TCOperation;
|
5
|
+
|
6
|
+
use crate::util::{datetime_to_ruby, ruby_to_datetime, ruby_to_hashmap, uuid2tc};
|
7
|
+
|
8
|
+
#[magnus::wrap(class = "Taskchampion::Operation", free_immediately)]
|
9
|
+
pub struct Operation(TCOperation);
|
10
|
+
|
11
|
+
impl Operation {
|
12
|
+
fn create(uuid: String) -> Result<Self, Error> {
|
13
|
+
Ok(Operation(TCOperation::Create {
|
14
|
+
uuid: uuid2tc(&uuid)?,
|
15
|
+
}))
|
16
|
+
}
|
17
|
+
|
18
|
+
fn delete(uuid: String, old_task: RHash) -> Result<Self, Error> {
|
19
|
+
let old_task = ruby_to_hashmap(old_task)?;
|
20
|
+
Ok(Operation(TCOperation::Delete {
|
21
|
+
uuid: uuid2tc(&uuid)?,
|
22
|
+
old_task,
|
23
|
+
}))
|
24
|
+
}
|
25
|
+
|
26
|
+
fn update(
|
27
|
+
uuid: String,
|
28
|
+
property: String,
|
29
|
+
timestamp: Value,
|
30
|
+
old_value: Option<String>,
|
31
|
+
value: Option<String>,
|
32
|
+
) -> Result<Self, Error> {
|
33
|
+
let timestamp = ruby_to_datetime(timestamp)?;
|
34
|
+
Ok(Operation(TCOperation::Update {
|
35
|
+
uuid: uuid2tc(&uuid)?,
|
36
|
+
property,
|
37
|
+
timestamp,
|
38
|
+
old_value,
|
39
|
+
value,
|
40
|
+
}))
|
41
|
+
}
|
42
|
+
|
43
|
+
fn undo_point() -> Self {
|
44
|
+
Operation(TCOperation::UndoPoint)
|
45
|
+
}
|
46
|
+
|
47
|
+
// Type checking methods
|
48
|
+
fn create_op(&self) -> bool {
|
49
|
+
matches!(self.0, TCOperation::Create { .. })
|
50
|
+
}
|
51
|
+
|
52
|
+
fn delete_op(&self) -> bool {
|
53
|
+
matches!(self.0, TCOperation::Delete { .. })
|
54
|
+
}
|
55
|
+
|
56
|
+
fn update_op(&self) -> bool {
|
57
|
+
matches!(self.0, TCOperation::Update { .. })
|
58
|
+
}
|
59
|
+
|
60
|
+
fn undo_point_op(&self) -> bool {
|
61
|
+
matches!(self.0, TCOperation::UndoPoint)
|
62
|
+
}
|
63
|
+
|
64
|
+
fn operation_type(&self) -> magnus::Symbol {
|
65
|
+
match &self.0 {
|
66
|
+
TCOperation::Create { .. } => magnus::Symbol::new("create"),
|
67
|
+
TCOperation::Delete { .. } => magnus::Symbol::new("delete"),
|
68
|
+
TCOperation::Update { .. } => magnus::Symbol::new("update"),
|
69
|
+
TCOperation::UndoPoint => magnus::Symbol::new("undo_point"),
|
70
|
+
}
|
71
|
+
}
|
72
|
+
|
73
|
+
// Getters for each variant
|
74
|
+
fn uuid(&self) -> Result<String, Error> {
|
75
|
+
match &self.0 {
|
76
|
+
TCOperation::Create { uuid } => Ok(uuid.to_string()),
|
77
|
+
TCOperation::Delete { uuid, .. } => Ok(uuid.to_string()),
|
78
|
+
TCOperation::Update { uuid, .. } => Ok(uuid.to_string()),
|
79
|
+
TCOperation::UndoPoint => Err(Error::new(
|
80
|
+
magnus::exception::arg_error(),
|
81
|
+
"UndoPoint operations do not have a uuid",
|
82
|
+
)),
|
83
|
+
}
|
84
|
+
}
|
85
|
+
|
86
|
+
fn old_task(&self) -> Result<RHash, Error> {
|
87
|
+
match &self.0 {
|
88
|
+
TCOperation::Delete { old_task, .. } => {
|
89
|
+
let hash = RHash::new();
|
90
|
+
for (k, v) in old_task {
|
91
|
+
hash.aset(k.clone(), v.clone())?;
|
92
|
+
}
|
93
|
+
Ok(hash)
|
94
|
+
}
|
95
|
+
_ => Err(Error::new(
|
96
|
+
magnus::exception::arg_error(),
|
97
|
+
"Only Delete operations have old_task",
|
98
|
+
)),
|
99
|
+
}
|
100
|
+
}
|
101
|
+
|
102
|
+
fn property(&self) -> Result<String, Error> {
|
103
|
+
match &self.0 {
|
104
|
+
TCOperation::Update { property, .. } => Ok(property.clone()),
|
105
|
+
_ => Err(Error::new(
|
106
|
+
magnus::exception::arg_error(),
|
107
|
+
"Only Update operations have property",
|
108
|
+
)),
|
109
|
+
}
|
110
|
+
}
|
111
|
+
|
112
|
+
fn timestamp(&self) -> Result<Value, Error> {
|
113
|
+
match &self.0 {
|
114
|
+
TCOperation::Update { timestamp, .. } => datetime_to_ruby(*timestamp),
|
115
|
+
_ => Err(Error::new(
|
116
|
+
magnus::exception::arg_error(),
|
117
|
+
"Only Update operations have timestamp",
|
118
|
+
)),
|
119
|
+
}
|
120
|
+
}
|
121
|
+
|
122
|
+
fn old_value(&self) -> Result<Value, Error> {
|
123
|
+
match &self.0 {
|
124
|
+
TCOperation::Update { old_value, .. } => match old_value {
|
125
|
+
Some(val) => Ok(val.clone().into_value()),
|
126
|
+
None => Ok(().into_value()), // () converts to nil in Magnus
|
127
|
+
},
|
128
|
+
_ => Err(Error::new(
|
129
|
+
magnus::exception::arg_error(),
|
130
|
+
"Only Update operations have old_value",
|
131
|
+
)),
|
132
|
+
}
|
133
|
+
}
|
134
|
+
|
135
|
+
fn value(&self) -> Result<Value, Error> {
|
136
|
+
match &self.0 {
|
137
|
+
TCOperation::Update { value, .. } => match value {
|
138
|
+
Some(val) => Ok(val.clone().into_value()),
|
139
|
+
None => Ok(().into_value()), // () converts to nil in Magnus
|
140
|
+
},
|
141
|
+
_ => Err(Error::new(
|
142
|
+
magnus::exception::arg_error(),
|
143
|
+
"Only Update operations have value",
|
144
|
+
)),
|
145
|
+
}
|
146
|
+
}
|
147
|
+
|
148
|
+
fn to_s(&self) -> String {
|
149
|
+
match &self.0 {
|
150
|
+
TCOperation::Create { uuid } => {
|
151
|
+
format!("Create task {}", uuid)
|
152
|
+
}
|
153
|
+
TCOperation::Delete { uuid, .. } => {
|
154
|
+
format!("Delete task {}", uuid)
|
155
|
+
}
|
156
|
+
TCOperation::Update { uuid, property, value, old_value, .. } => {
|
157
|
+
let value_desc = match (old_value, value) {
|
158
|
+
(Some(old), Some(new)) => format!("from '{}' to '{}'", old, new),
|
159
|
+
(Some(old), None) => format!("from '{}' to nil", old),
|
160
|
+
(None, Some(new)) => format!("to '{}'", new),
|
161
|
+
(None, None) => "to nil".to_string(),
|
162
|
+
};
|
163
|
+
format!("Update task {} property '{}' {}", uuid, property, value_desc)
|
164
|
+
}
|
165
|
+
TCOperation::UndoPoint => {
|
166
|
+
"Undo point".to_string()
|
167
|
+
}
|
168
|
+
}
|
169
|
+
}
|
170
|
+
|
171
|
+
fn inspect(&self) -> String {
|
172
|
+
match &self.0 {
|
173
|
+
TCOperation::Create { uuid } => {
|
174
|
+
format!("#<Taskchampion::Operation::Create uuid={}>", uuid)
|
175
|
+
}
|
176
|
+
TCOperation::Delete { uuid, .. } => {
|
177
|
+
format!("#<Taskchampion::Operation::Delete uuid={}>", uuid)
|
178
|
+
}
|
179
|
+
TCOperation::Update { uuid, property, .. } => {
|
180
|
+
format!("#<Taskchampion::Operation::Update uuid={} property={}>", uuid, property)
|
181
|
+
}
|
182
|
+
TCOperation::UndoPoint => {
|
183
|
+
"#<Taskchampion::Operation::UndoPoint>".to_string()
|
184
|
+
}
|
185
|
+
}
|
186
|
+
}
|
187
|
+
}
|
188
|
+
|
189
|
+
impl AsRef<TCOperation> for Operation {
|
190
|
+
fn as_ref(&self) -> &TCOperation {
|
191
|
+
&self.0
|
192
|
+
}
|
193
|
+
}
|
194
|
+
|
195
|
+
impl From<TCOperation> for Operation {
|
196
|
+
fn from(value: TCOperation) -> Self {
|
197
|
+
Operation(value)
|
198
|
+
}
|
199
|
+
}
|
200
|
+
|
201
|
+
impl From<Operation> for TCOperation {
|
202
|
+
fn from(value: Operation) -> Self {
|
203
|
+
value.0
|
204
|
+
}
|
205
|
+
}
|
206
|
+
|
207
|
+
pub fn init(module: &RModule) -> Result<(), Error> {
|
208
|
+
let class = module.define_class("Operation", class::object())?;
|
209
|
+
|
210
|
+
// Class methods for creating operations
|
211
|
+
class.define_singleton_method("create", function!(Operation::create, 1))?;
|
212
|
+
class.define_singleton_method("delete", function!(Operation::delete, 2))?;
|
213
|
+
class.define_singleton_method("update", function!(Operation::update, 5))?;
|
214
|
+
class.define_singleton_method("undo_point", function!(Operation::undo_point, 0))?;
|
215
|
+
|
216
|
+
// Type checking methods
|
217
|
+
class.define_method("create?", method!(Operation::create_op, 0))?;
|
218
|
+
class.define_method("delete?", method!(Operation::delete_op, 0))?;
|
219
|
+
class.define_method("update?", method!(Operation::update_op, 0))?;
|
220
|
+
class.define_method("undo_point?", method!(Operation::undo_point_op, 0))?;
|
221
|
+
class.define_method("operation_type", method!(Operation::operation_type, 0))?;
|
222
|
+
|
223
|
+
// Getter methods
|
224
|
+
class.define_method("uuid", method!(Operation::uuid, 0))?;
|
225
|
+
class.define_method("old_task", method!(Operation::old_task, 0))?;
|
226
|
+
class.define_method("property", method!(Operation::property, 0))?;
|
227
|
+
class.define_method("timestamp", method!(Operation::timestamp, 0))?;
|
228
|
+
class.define_method("old_value", method!(Operation::old_value, 0))?;
|
229
|
+
class.define_method("value", method!(Operation::value, 0))?;
|
230
|
+
class.define_method("to_s", method!(Operation::to_s, 0))?;
|
231
|
+
class.define_method("inspect", method!(Operation::inspect, 0))?;
|
232
|
+
|
233
|
+
Ok(())
|
234
|
+
}
|
@@ -0,0 +1,180 @@
|
|
1
|
+
use magnus::{
|
2
|
+
class, function, method, prelude::*, Error, IntoValue, RArray, RModule, Ruby, Value,
|
3
|
+
};
|
4
|
+
use std::cell::RefCell;
|
5
|
+
use taskchampion::Operations as TCOperations;
|
6
|
+
|
7
|
+
use crate::operation::Operation;
|
8
|
+
use crate::thread_check::ThreadBound;
|
9
|
+
|
10
|
+
#[magnus::wrap(class = "Taskchampion::Operations", free_immediately)]
|
11
|
+
pub struct Operations(ThreadBound<RefCell<TCOperations>>);
|
12
|
+
|
13
|
+
impl Operations {
|
14
|
+
fn new(_ruby: &Ruby) -> Self {
|
15
|
+
Operations(ThreadBound::new(RefCell::new(TCOperations::new())))
|
16
|
+
}
|
17
|
+
|
18
|
+
fn push(&self, operation: &Operation) -> Result<(), Error> {
|
19
|
+
let ops = self.0.get()?;
|
20
|
+
ops.borrow_mut().push(operation.as_ref().clone());
|
21
|
+
Ok(())
|
22
|
+
}
|
23
|
+
|
24
|
+
fn len(&self) -> Result<usize, Error> {
|
25
|
+
let ops = self.0.get()?;
|
26
|
+
let borrowed = ops.borrow();
|
27
|
+
Ok(borrowed.len())
|
28
|
+
}
|
29
|
+
|
30
|
+
fn empty(&self) -> Result<bool, Error> {
|
31
|
+
let ops = self.0.get()?;
|
32
|
+
let borrowed = ops.borrow();
|
33
|
+
Ok(borrowed.is_empty())
|
34
|
+
}
|
35
|
+
|
36
|
+
fn get(&self, index: isize) -> Result<Value, Error> {
|
37
|
+
let ops = self.0.get()?;
|
38
|
+
let ops = ops.borrow();
|
39
|
+
let len = ops.len() as isize;
|
40
|
+
|
41
|
+
// Handle negative indices (Ruby-style)
|
42
|
+
let actual_index = if index < 0 {
|
43
|
+
len + index
|
44
|
+
} else {
|
45
|
+
index
|
46
|
+
};
|
47
|
+
|
48
|
+
// Check bounds - return nil instead of raising for Ruby compatibility
|
49
|
+
if actual_index < 0 || actual_index >= len {
|
50
|
+
let ruby = magnus::Ruby::get().map_err(|e| Error::new(
|
51
|
+
magnus::exception::runtime_error(),
|
52
|
+
e.to_string(),
|
53
|
+
))?;
|
54
|
+
return Ok(ruby.qnil().into_value()); // Return nil
|
55
|
+
}
|
56
|
+
|
57
|
+
let operation = Operation::from(ops[actual_index as usize].clone());
|
58
|
+
Ok(operation.into_value())
|
59
|
+
}
|
60
|
+
|
61
|
+
fn each(&self) -> Result<Value, Error> {
|
62
|
+
let ruby = magnus::Ruby::get().map_err(|e| Error::new(
|
63
|
+
magnus::exception::runtime_error(),
|
64
|
+
format!("Failed to get Ruby context: {}", e),
|
65
|
+
))?;
|
66
|
+
|
67
|
+
// Check if a block was given
|
68
|
+
if ruby.block_given() {
|
69
|
+
let ops = self.0.get()?;
|
70
|
+
let ops = ops.borrow();
|
71
|
+
let block = ruby.block_proc()?;
|
72
|
+
|
73
|
+
for op in ops.iter() {
|
74
|
+
let operation = Operation::from(op.clone());
|
75
|
+
block.call::<_, Value>((operation,))?;
|
76
|
+
}
|
77
|
+
|
78
|
+
// Ruby's each method returns self when called with a block
|
79
|
+
let ruby = magnus::Ruby::get().unwrap();
|
80
|
+
Ok(ruby.qnil().into_value())
|
81
|
+
} else {
|
82
|
+
// No block given, return an enumerator (or array for simplicity)
|
83
|
+
self.to_array()
|
84
|
+
}
|
85
|
+
}
|
86
|
+
|
87
|
+
fn to_array(&self) -> Result<Value, Error> {
|
88
|
+
let array = RArray::new();
|
89
|
+
let ops = self.0.get()?;
|
90
|
+
let ops = ops.borrow();
|
91
|
+
|
92
|
+
for op in ops.iter() {
|
93
|
+
let operation = Operation::from(op.clone());
|
94
|
+
// Magnus handles wrapping automatically
|
95
|
+
array.push(operation)?;
|
96
|
+
}
|
97
|
+
|
98
|
+
Ok(array.into_value())
|
99
|
+
}
|
100
|
+
|
101
|
+
fn inspect(&self) -> Result<String, Error> {
|
102
|
+
let ops = self.0.get()?;
|
103
|
+
Ok(format!("#<Taskchampion::Operations: {} operations>", ops.borrow().len()))
|
104
|
+
}
|
105
|
+
|
106
|
+
fn clear(&self) -> Result<(), Error> {
|
107
|
+
let ops = self.0.get()?;
|
108
|
+
ops.borrow_mut().clear();
|
109
|
+
Ok(())
|
110
|
+
}
|
111
|
+
|
112
|
+
// Internal method for accessing the operations
|
113
|
+
pub(crate) fn clone_inner(&self) -> Result<TCOperations, Error> {
|
114
|
+
let ops = self.0.get()?;
|
115
|
+
let borrowed = ops.borrow();
|
116
|
+
Ok(borrowed.clone())
|
117
|
+
}
|
118
|
+
|
119
|
+
pub(crate) fn with_inner_mut<T, F>(&self, f: F) -> Result<T, Error>
|
120
|
+
where
|
121
|
+
F: FnOnce(&mut TCOperations) -> Result<T, taskchampion::Error>,
|
122
|
+
{
|
123
|
+
let ops = self.0.get()?;
|
124
|
+
let mut ops = ops.borrow_mut();
|
125
|
+
f(&mut *ops).map_err(|e| Error::new(
|
126
|
+
magnus::exception::runtime_error(),
|
127
|
+
e.to_string(),
|
128
|
+
))
|
129
|
+
}
|
130
|
+
|
131
|
+
// Internal method for pushing operations from TaskChampion
|
132
|
+
pub(crate) fn extend_from_tc(&self, tc_ops: Vec<taskchampion::Operation>) -> Result<(), Error> {
|
133
|
+
let ops = self.0.get()?;
|
134
|
+
let mut ops = ops.borrow_mut();
|
135
|
+
for op in tc_ops {
|
136
|
+
ops.push(op);
|
137
|
+
}
|
138
|
+
Ok(())
|
139
|
+
}
|
140
|
+
}
|
141
|
+
|
142
|
+
// Note: AsRef and AsMut cannot be implemented with RefCell
|
143
|
+
// as they require returning references with the lifetime of self.
|
144
|
+
// Instead, we'll provide methods to work with the inner value.
|
145
|
+
|
146
|
+
impl From<TCOperations> for Operations {
|
147
|
+
fn from(value: TCOperations) -> Self {
|
148
|
+
Operations(ThreadBound::new(RefCell::new(value)))
|
149
|
+
}
|
150
|
+
}
|
151
|
+
|
152
|
+
impl From<Operations> for TCOperations {
|
153
|
+
fn from(value: Operations) -> Self {
|
154
|
+
// This implementation will panic if called from wrong thread
|
155
|
+
// but that's appropriate for ThreadBound behavior
|
156
|
+
let cell = match value.0.into_inner() {
|
157
|
+
Ok(cell) => cell,
|
158
|
+
Err(_) => panic!("Attempted to extract Operations from wrong thread"),
|
159
|
+
};
|
160
|
+
cell.into_inner()
|
161
|
+
}
|
162
|
+
}
|
163
|
+
|
164
|
+
pub fn init(module: &RModule) -> Result<(), Error> {
|
165
|
+
let class = module.define_class("Operations", class::object())?;
|
166
|
+
|
167
|
+
class.define_singleton_method("new", function!(Operations::new, 0))?;
|
168
|
+
class.define_method("push", method!(Operations::push, 1))?;
|
169
|
+
class.define_method("<<", method!(Operations::push, 1))?;
|
170
|
+
class.define_method("length", method!(Operations::len, 0))?;
|
171
|
+
class.define_method("size", method!(Operations::len, 0))?;
|
172
|
+
class.define_method("empty?", method!(Operations::empty, 0))?;
|
173
|
+
class.define_method("[]", method!(Operations::get, 1))?;
|
174
|
+
class.define_method("each", method!(Operations::each, 0))?;
|
175
|
+
class.define_method("to_a", method!(Operations::to_array, 0))?;
|
176
|
+
class.define_method("inspect", method!(Operations::inspect, 0))?;
|
177
|
+
class.define_method("clear", method!(Operations::clear, 0))?;
|
178
|
+
|
179
|
+
Ok(())
|
180
|
+
}
|