amoskeag-rb 0.1.2 → 0.1.3
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 +4 -4
- data/ext/amoskeag/Cargo.toml +22 -0
- data/ext/amoskeag/src/lib.rs +607 -0
- data/lib/amoskeag-rb/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5017194e4c300b6e147b029385f6d44148e9e64171c35ae9c50c78077c3928c2
|
|
4
|
+
data.tar.gz: 1f8561b60fe2a24d0299f7d80cf359efcf824efd15280e37e3a9f4ed19b76c56
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a1971d8ceef114ed79792a2fec087fbe5c452e0cf8231e9ae17c7ec3213ff3913710729c9246cadcc110c3f56f413f1af5bbd8cca907ae3d93227db85f378206
|
|
7
|
+
data.tar.gz: c87e52d6a38367d0541e25fa541404f0e180f980c8c7c3a156f76efb5b97be9baca9134e8803da5766c167973ab92837b904b5da879a61e4e8cc6782affc0967
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "amoskeag_native"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
|
|
6
|
+
# Exclude from parent workspace
|
|
7
|
+
[workspace]
|
|
8
|
+
|
|
9
|
+
[lib]
|
|
10
|
+
name = "amoskeag_native"
|
|
11
|
+
crate-type = ["cdylib"]
|
|
12
|
+
|
|
13
|
+
[dependencies]
|
|
14
|
+
amoskeag = { git = "https://github.com/durable-oss/amoskeag" }
|
|
15
|
+
amoskeag-stdlib-operators = { git = "https://github.com/durable-oss/amoskeag" }
|
|
16
|
+
serde_json = "1.0"
|
|
17
|
+
magnus = "0.7"
|
|
18
|
+
|
|
19
|
+
[profile.release]
|
|
20
|
+
lto = true
|
|
21
|
+
opt-level = 3
|
|
22
|
+
codegen-units = 1
|
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
use std::collections::HashMap;
|
|
2
|
+
use magnus::{Error, RHash, RString, RArray, Symbol, Value, define_module, function, prelude::*, TryConvert, IntoValue};
|
|
3
|
+
use amoskeag::{compile, evaluate, CompiledProgram};
|
|
4
|
+
use amoskeag_stdlib_operators::Value as AmoskeagValue;
|
|
5
|
+
|
|
6
|
+
// Maximum allowed sizes for defensive programming
|
|
7
|
+
const MAX_SOURCE_SIZE: usize = 10 * 1024 * 1024; // 10MB
|
|
8
|
+
const MAX_SYMBOLS_COUNT: usize = 10_000;
|
|
9
|
+
const MAX_DICT_DEPTH: usize = 100;
|
|
10
|
+
|
|
11
|
+
#[magnus::wrap(class = "Amoskeag::Program", free_immediately, size)]
|
|
12
|
+
struct Program {
|
|
13
|
+
program: CompiledProgram,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
impl Program {
|
|
17
|
+
fn new(program: CompiledProgram) -> Self {
|
|
18
|
+
Self { program }
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Convert Amoskeag Value to Ruby Value
|
|
23
|
+
fn amoskeag_value_to_ruby(value: &AmoskeagValue) -> Result<Value, Error> {
|
|
24
|
+
match value {
|
|
25
|
+
AmoskeagValue::Number(n) => Ok(n.into_value()),
|
|
26
|
+
AmoskeagValue::String(s) => Ok(RString::new(s).into_value()),
|
|
27
|
+
AmoskeagValue::Boolean(b) => Ok(b.into_value()),
|
|
28
|
+
AmoskeagValue::Nil => Ok(().into_value()),
|
|
29
|
+
AmoskeagValue::Array(arr) => {
|
|
30
|
+
let ruby_arr = RArray::new();
|
|
31
|
+
for item in arr {
|
|
32
|
+
ruby_arr.push(amoskeag_value_to_ruby(item)?)?;
|
|
33
|
+
}
|
|
34
|
+
Ok(ruby_arr.into_value())
|
|
35
|
+
}
|
|
36
|
+
AmoskeagValue::Dictionary(dict) => {
|
|
37
|
+
let ruby_hash = RHash::new();
|
|
38
|
+
for (k, v) in dict.iter() {
|
|
39
|
+
ruby_hash.aset(k.as_str(), amoskeag_value_to_ruby(v)?)?;
|
|
40
|
+
}
|
|
41
|
+
Ok(ruby_hash.into_value())
|
|
42
|
+
}
|
|
43
|
+
AmoskeagValue::Symbol(s) => {
|
|
44
|
+
// Wrap symbols in a hash with "__symbol__" key
|
|
45
|
+
let ruby_hash = RHash::new();
|
|
46
|
+
ruby_hash.aset("__symbol__", s.as_str())?;
|
|
47
|
+
Ok(ruby_hash.into_value())
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Convert Ruby Value to Amoskeag Value with depth tracking
|
|
53
|
+
fn ruby_value_to_amoskeag_with_depth(value: Value, depth: usize) -> Result<AmoskeagValue, Error> {
|
|
54
|
+
// Defensive: Prevent stack overflow from deeply nested structures
|
|
55
|
+
if depth > MAX_DICT_DEPTH {
|
|
56
|
+
return Err(Error::new(
|
|
57
|
+
magnus::exception::arg_error(),
|
|
58
|
+
format!("Data structure too deeply nested (max depth: {})", MAX_DICT_DEPTH)
|
|
59
|
+
));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Check for nil first (most specific)
|
|
63
|
+
if value.is_nil() {
|
|
64
|
+
return Ok(AmoskeagValue::Nil);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Check for composite types (Hash and Array) before primitive types
|
|
68
|
+
// This prevents bool::try_convert from converting hashes to true
|
|
69
|
+
if let Some(hash) = RHash::from_value(value) {
|
|
70
|
+
// Defensive: Check hash size
|
|
71
|
+
let len = hash.len();
|
|
72
|
+
if len > 100_000 {
|
|
73
|
+
return Err(Error::new(
|
|
74
|
+
magnus::exception::arg_error(),
|
|
75
|
+
format!("Hash too large: {} keys (max: 100,000)", len)
|
|
76
|
+
));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let mut result = HashMap::new();
|
|
80
|
+
|
|
81
|
+
// Collect entries first to avoid borrowing issues
|
|
82
|
+
let mut entries = Vec::new();
|
|
83
|
+
hash.foreach(|key: Value, val: Value| {
|
|
84
|
+
entries.push((key, val));
|
|
85
|
+
Ok(magnus::r_hash::ForEach::Continue)
|
|
86
|
+
})?;
|
|
87
|
+
|
|
88
|
+
for (key, val) in entries {
|
|
89
|
+
// Defensive: Validate key is a string or symbol
|
|
90
|
+
let key_str = if let Some(s) = RString::from_value(key) {
|
|
91
|
+
unsafe { s.as_str() }?.to_string()
|
|
92
|
+
} else if let Some(sym) = Symbol::from_value(key) {
|
|
93
|
+
sym.name()?.to_string()
|
|
94
|
+
} else {
|
|
95
|
+
return Err(Error::new(
|
|
96
|
+
magnus::exception::arg_error(),
|
|
97
|
+
"Hash key must be String or Symbol"
|
|
98
|
+
));
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// Defensive: Validate key length
|
|
102
|
+
if key_str.len() > 1000 {
|
|
103
|
+
return Err(Error::new(
|
|
104
|
+
magnus::exception::arg_error(),
|
|
105
|
+
format!("Hash key too long: {} bytes (max: 1000)", key_str.len())
|
|
106
|
+
));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
result.insert(key_str, ruby_value_to_amoskeag_with_depth(val, depth + 1)?);
|
|
110
|
+
}
|
|
111
|
+
return Ok(AmoskeagValue::Dictionary(result));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if let Some(arr) = RArray::from_value(value) {
|
|
115
|
+
// Defensive: Check array size
|
|
116
|
+
let len = arr.len();
|
|
117
|
+
if len > 1_000_000 {
|
|
118
|
+
return Err(Error::new(
|
|
119
|
+
magnus::exception::arg_error(),
|
|
120
|
+
format!("Array too large: {} elements (max: 1,000,000)", len)
|
|
121
|
+
));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let mut result = Vec::new();
|
|
125
|
+
for item in arr.into_iter() {
|
|
126
|
+
result.push(ruby_value_to_amoskeag_with_depth(item, depth + 1)?);
|
|
127
|
+
}
|
|
128
|
+
return Ok(AmoskeagValue::Array(result));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Now check primitive types
|
|
132
|
+
if let Ok(n) = f64::try_convert(value) {
|
|
133
|
+
// Defensive: Check for special float values
|
|
134
|
+
if !n.is_finite() {
|
|
135
|
+
return Err(Error::new(
|
|
136
|
+
magnus::exception::arg_error(),
|
|
137
|
+
format!("Invalid number: {} (must be finite)", n)
|
|
138
|
+
));
|
|
139
|
+
}
|
|
140
|
+
Ok(AmoskeagValue::Number(n))
|
|
141
|
+
} else if let Ok(s) = String::try_convert(value) {
|
|
142
|
+
// Defensive: Check string length
|
|
143
|
+
if s.len() > 100 * 1024 * 1024 {
|
|
144
|
+
return Err(Error::new(
|
|
145
|
+
magnus::exception::arg_error(),
|
|
146
|
+
format!("String too large: {} bytes (max: 100MB)", s.len())
|
|
147
|
+
));
|
|
148
|
+
}
|
|
149
|
+
Ok(AmoskeagValue::String(s))
|
|
150
|
+
} else if let Some(sym) = Symbol::from_value(value) {
|
|
151
|
+
let name = sym.name()?.to_string();
|
|
152
|
+
// Defensive: Validate symbol name
|
|
153
|
+
if name.is_empty() {
|
|
154
|
+
return Err(Error::new(
|
|
155
|
+
magnus::exception::arg_error(),
|
|
156
|
+
"Symbol name cannot be empty"
|
|
157
|
+
));
|
|
158
|
+
}
|
|
159
|
+
if name.len() > 1000 {
|
|
160
|
+
return Err(Error::new(
|
|
161
|
+
magnus::exception::arg_error(),
|
|
162
|
+
format!("Symbol name too long: {} bytes (max: 1000)", name.len())
|
|
163
|
+
));
|
|
164
|
+
}
|
|
165
|
+
// Convert Ruby symbol to Amoskeag symbol directly
|
|
166
|
+
Ok(AmoskeagValue::Symbol(name))
|
|
167
|
+
} else if let Ok(b) = bool::try_convert(value) {
|
|
168
|
+
// Check for bool AFTER all other types to avoid converting hashes/arrays to true
|
|
169
|
+
Ok(AmoskeagValue::Boolean(b))
|
|
170
|
+
} else {
|
|
171
|
+
Err(Error::new(
|
|
172
|
+
magnus::exception::arg_error(),
|
|
173
|
+
format!("Unsupported type for conversion: {}", unsafe { value.classname() })
|
|
174
|
+
))
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
fn ruby_value_to_amoskeag(value: Value) -> Result<AmoskeagValue, Error> {
|
|
179
|
+
ruby_value_to_amoskeag_with_depth(value, 0)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Amoskeag.compile(source, symbols = nil) -> Program
|
|
183
|
+
fn amoskeag_compile(args: &[Value]) -> Result<Program, Error> {
|
|
184
|
+
let args = magnus::scan_args::scan_args::<(Value,), (Option<Value>,), (), (), (), ()>(args)?;
|
|
185
|
+
let (source_val,) = args.required;
|
|
186
|
+
let (symbols_val,) = args.optional;
|
|
187
|
+
|
|
188
|
+
// Validate source argument
|
|
189
|
+
if source_val.is_nil() {
|
|
190
|
+
return Err(Error::new(
|
|
191
|
+
magnus::exception::arg_error(),
|
|
192
|
+
"source must be a String, got NilClass"
|
|
193
|
+
));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
let source = RString::from_value(source_val).ok_or_else(|| {
|
|
197
|
+
Error::new(
|
|
198
|
+
magnus::exception::arg_error(),
|
|
199
|
+
format!("source must be a String, got {}", unsafe { source_val.classname() })
|
|
200
|
+
)
|
|
201
|
+
})?;
|
|
202
|
+
|
|
203
|
+
// Defensive: Validate source
|
|
204
|
+
let source_str = unsafe { source.as_str() }?;
|
|
205
|
+
if source_str.is_empty() {
|
|
206
|
+
return Err(Error::new(
|
|
207
|
+
magnus::exception::arg_error(),
|
|
208
|
+
"source cannot be empty"
|
|
209
|
+
));
|
|
210
|
+
}
|
|
211
|
+
if source_str.len() > MAX_SOURCE_SIZE {
|
|
212
|
+
return Err(Error::new(
|
|
213
|
+
magnus::exception::arg_error(),
|
|
214
|
+
format!("source too large: {} bytes (max: {})", source_str.len(), MAX_SOURCE_SIZE)
|
|
215
|
+
));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
let symbol_vec: Vec<String> = if let Some(syms_val) = symbols_val {
|
|
219
|
+
if syms_val.is_nil() {
|
|
220
|
+
Vec::new()
|
|
221
|
+
} else {
|
|
222
|
+
let syms = RArray::from_value(syms_val).ok_or_else(|| {
|
|
223
|
+
Error::new(
|
|
224
|
+
magnus::exception::arg_error(),
|
|
225
|
+
format!("symbols must be an Array, got {}", unsafe { syms_val.classname() })
|
|
226
|
+
)
|
|
227
|
+
})?;
|
|
228
|
+
|
|
229
|
+
// Defensive: Check symbols array size
|
|
230
|
+
let len = syms.len();
|
|
231
|
+
if len > MAX_SYMBOLS_COUNT {
|
|
232
|
+
return Err(Error::new(
|
|
233
|
+
magnus::exception::arg_error(),
|
|
234
|
+
format!("Too many symbols: {} (max: {})", len, MAX_SYMBOLS_COUNT)
|
|
235
|
+
));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
let mut result = Vec::new();
|
|
239
|
+
for val in syms.into_iter() {
|
|
240
|
+
let sym_str = if let Some(s) = RString::from_value(val) {
|
|
241
|
+
unsafe { s.as_str() }?.to_string()
|
|
242
|
+
} else if let Some(sym) = Symbol::from_value(val) {
|
|
243
|
+
sym.name()?.to_string()
|
|
244
|
+
} else {
|
|
245
|
+
return Err(Error::new(
|
|
246
|
+
magnus::exception::arg_error(),
|
|
247
|
+
format!("symbols must contain only Strings or Symbols, got {}", unsafe { val.classname() })
|
|
248
|
+
));
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
// Defensive: Validate symbol
|
|
252
|
+
if sym_str.is_empty() {
|
|
253
|
+
return Err(Error::new(
|
|
254
|
+
magnus::exception::arg_error(),
|
|
255
|
+
"Symbol cannot be empty"
|
|
256
|
+
));
|
|
257
|
+
}
|
|
258
|
+
if sym_str.len() > 1000 {
|
|
259
|
+
return Err(Error::new(
|
|
260
|
+
magnus::exception::arg_error(),
|
|
261
|
+
format!("Symbol too long: {} bytes (max: 1000)", sym_str.len())
|
|
262
|
+
));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
result.push(sym_str);
|
|
266
|
+
}
|
|
267
|
+
result
|
|
268
|
+
}
|
|
269
|
+
} else {
|
|
270
|
+
Vec::new()
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
let symbols_refs: Vec<&str> = symbol_vec.iter().map(|s| s.as_str()).collect();
|
|
274
|
+
|
|
275
|
+
let program = compile(source_str, &symbols_refs).map_err(|e| {
|
|
276
|
+
Error::new(
|
|
277
|
+
get_compile_error_class().unwrap_or_else(|_| magnus::exception::runtime_error()),
|
|
278
|
+
format!("{:?}", e)
|
|
279
|
+
)
|
|
280
|
+
})?;
|
|
281
|
+
|
|
282
|
+
Ok(Program { program })
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Amoskeag.evaluate(program, data) -> Object
|
|
286
|
+
fn amoskeag_evaluate(args: &[Value]) -> Result<Value, Error> {
|
|
287
|
+
let args = magnus::scan_args::scan_args::<(Value, Value), (), (), (), (), ()>(args)?;
|
|
288
|
+
let (program_val, data_val) = args.required;
|
|
289
|
+
|
|
290
|
+
// Validate program argument
|
|
291
|
+
if program_val.is_nil() {
|
|
292
|
+
return Err(Error::new(
|
|
293
|
+
magnus::exception::arg_error(),
|
|
294
|
+
"program must be an Amoskeag::Program, got NilClass"
|
|
295
|
+
));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
let program: &Program = match <&Program>::try_convert(program_val) {
|
|
299
|
+
Ok(p) => p,
|
|
300
|
+
Err(_) => {
|
|
301
|
+
return Err(Error::new(
|
|
302
|
+
magnus::exception::arg_error(),
|
|
303
|
+
format!("program must be an Amoskeag::Program, got {}", unsafe { program_val.classname() })
|
|
304
|
+
));
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
// Validate data argument
|
|
309
|
+
if data_val.is_nil() {
|
|
310
|
+
return Err(Error::new(
|
|
311
|
+
magnus::exception::arg_error(),
|
|
312
|
+
"data must be a Hash, got NilClass"
|
|
313
|
+
));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
let data = RHash::from_value(data_val).ok_or_else(|| {
|
|
317
|
+
Error::new(
|
|
318
|
+
magnus::exception::arg_error(),
|
|
319
|
+
format!("data must be a Hash, got {}", unsafe { data_val.classname() })
|
|
320
|
+
)
|
|
321
|
+
})?;
|
|
322
|
+
|
|
323
|
+
// Defensive: Validate data
|
|
324
|
+
let data_len = data.len();
|
|
325
|
+
if data_len > 100_000 {
|
|
326
|
+
return Err(Error::new(
|
|
327
|
+
magnus::exception::arg_error(),
|
|
328
|
+
format!("data hash too large: {} keys (max: 100,000)", data_len)
|
|
329
|
+
));
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Convert Ruby hash to HashMap
|
|
333
|
+
let mut data_map = HashMap::new();
|
|
334
|
+
|
|
335
|
+
// Collect entries first to avoid borrowing issues
|
|
336
|
+
let mut entries = Vec::new();
|
|
337
|
+
data.foreach(|key: Value, val: Value| {
|
|
338
|
+
entries.push((key, val));
|
|
339
|
+
Ok(magnus::r_hash::ForEach::Continue)
|
|
340
|
+
})?;
|
|
341
|
+
|
|
342
|
+
for (key, val) in entries {
|
|
343
|
+
let key_str = if let Some(s) = RString::from_value(key) {
|
|
344
|
+
unsafe { s.as_str() }?.to_string()
|
|
345
|
+
} else if let Some(sym) = Symbol::from_value(key) {
|
|
346
|
+
sym.name()?.to_string()
|
|
347
|
+
} else {
|
|
348
|
+
return Err(Error::new(
|
|
349
|
+
magnus::exception::arg_error(),
|
|
350
|
+
"data keys must be Strings or Symbols"
|
|
351
|
+
));
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
data_map.insert(key_str, ruby_value_to_amoskeag(val)?);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
let result = evaluate(&program.program, &data_map).map_err(|e| {
|
|
358
|
+
Error::new(
|
|
359
|
+
get_eval_error_class().unwrap_or_else(|_| magnus::exception::runtime_error()),
|
|
360
|
+
format!("{:?}", e)
|
|
361
|
+
)
|
|
362
|
+
})?;
|
|
363
|
+
|
|
364
|
+
amoskeag_value_to_ruby(&result)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Amoskeag.eval_expression(source, data, symbols = nil) -> Object
|
|
368
|
+
fn amoskeag_eval_expression(args: &[Value]) -> Result<Value, Error> {
|
|
369
|
+
let args = magnus::scan_args::scan_args::<(Value, Value), (Option<Value>,), (), (), (), ()>(args)?;
|
|
370
|
+
let (source_val, data_val) = args.required;
|
|
371
|
+
let (symbols_val,) = args.optional;
|
|
372
|
+
|
|
373
|
+
// Validate source
|
|
374
|
+
if source_val.is_nil() {
|
|
375
|
+
return Err(Error::new(
|
|
376
|
+
magnus::exception::arg_error(),
|
|
377
|
+
"source must be a String, got NilClass"
|
|
378
|
+
));
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Validate data
|
|
382
|
+
if data_val.is_nil() {
|
|
383
|
+
return Err(Error::new(
|
|
384
|
+
magnus::exception::arg_error(),
|
|
385
|
+
"data must be a Hash, got NilClass"
|
|
386
|
+
));
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
let compile_args = if let Some(syms) = symbols_val {
|
|
390
|
+
vec![source_val, syms]
|
|
391
|
+
} else {
|
|
392
|
+
vec![source_val]
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
let program = amoskeag_compile(&compile_args)?;
|
|
396
|
+
|
|
397
|
+
let program_val: Value = program.into_value();
|
|
398
|
+
let eval_args = vec![program_val, data_val];
|
|
399
|
+
amoskeag_evaluate(&eval_args)
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Get error classes from module constants
|
|
403
|
+
fn get_compile_error_class() -> Result<magnus::ExceptionClass, Error> {
|
|
404
|
+
let module = define_module("Amoskeag")?;
|
|
405
|
+
let class: magnus::Value = module.const_get("CompileError")?;
|
|
406
|
+
Ok(magnus::ExceptionClass::from_value(class).unwrap())
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
fn get_eval_error_class() -> Result<magnus::ExceptionClass, Error> {
|
|
410
|
+
let module = define_module("Amoskeag")?;
|
|
411
|
+
let class: magnus::Value = module.const_get("EvalError")?;
|
|
412
|
+
Ok(magnus::ExceptionClass::from_value(class).unwrap())
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
#[magnus::init]
|
|
416
|
+
fn init() -> Result<(), Error> {
|
|
417
|
+
let module = define_module("Amoskeag")?;
|
|
418
|
+
|
|
419
|
+
// Define base Error class
|
|
420
|
+
let error_class = module.define_error("Error", magnus::exception::standard_error())?;
|
|
421
|
+
|
|
422
|
+
// Define error subclasses inheriting from Amoskeag::Error
|
|
423
|
+
module.define_error("CompileError", error_class)?;
|
|
424
|
+
module.define_error("EvalError", error_class)?;
|
|
425
|
+
|
|
426
|
+
// Define the Program class - make it non-allocatable from Ruby
|
|
427
|
+
let program_class = module.define_class("Program", magnus::class::object())?;
|
|
428
|
+
program_class.undef_default_alloc_func();
|
|
429
|
+
|
|
430
|
+
module.define_module_function("compile", function!(amoskeag_compile, -1))?;
|
|
431
|
+
module.define_module_function("evaluate", function!(amoskeag_evaluate, -1))?;
|
|
432
|
+
module.define_module_function("eval_expression", function!(amoskeag_eval_expression, -1))?;
|
|
433
|
+
|
|
434
|
+
Ok(())
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
#[cfg(test)]
|
|
438
|
+
mod tests {
|
|
439
|
+
use super::*;
|
|
440
|
+
|
|
441
|
+
// Helper to initialize Ruby for testing
|
|
442
|
+
fn init_ruby() {
|
|
443
|
+
static INIT: std::sync::Once = std::sync::Once::new();
|
|
444
|
+
INIT.call_once(|| {
|
|
445
|
+
magnus::Ruby::init().unwrap();
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
#[test]
|
|
450
|
+
fn test_ruby_hash_converts_to_dictionary_not_boolean() {
|
|
451
|
+
init_ruby();
|
|
452
|
+
|
|
453
|
+
// Create a Ruby hash: {"a" => 1, "b" => 2}
|
|
454
|
+
let hash: Value = magnus::eval(r#"{"a" => 1, "b" => 2}"#).unwrap();
|
|
455
|
+
|
|
456
|
+
let result = ruby_value_to_amoskeag(hash).unwrap();
|
|
457
|
+
|
|
458
|
+
// Should be a Dictionary, not Boolean(true)
|
|
459
|
+
match result {
|
|
460
|
+
AmoskeagValue::Dictionary(map) => {
|
|
461
|
+
assert_eq!(map.len(), 2);
|
|
462
|
+
assert_eq!(map.get("a"), Some(&AmoskeagValue::Number(1.0)));
|
|
463
|
+
assert_eq!(map.get("b"), Some(&AmoskeagValue::Number(2.0)));
|
|
464
|
+
}
|
|
465
|
+
other => panic!("Expected Dictionary, got {:?}", other),
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
#[test]
|
|
470
|
+
fn test_ruby_array_converts_to_array_not_boolean() {
|
|
471
|
+
init_ruby();
|
|
472
|
+
|
|
473
|
+
let array: Value = magnus::eval("[1, 2, 3]").unwrap();
|
|
474
|
+
|
|
475
|
+
let result = ruby_value_to_amoskeag(array).unwrap();
|
|
476
|
+
|
|
477
|
+
match result {
|
|
478
|
+
AmoskeagValue::Array(arr) => {
|
|
479
|
+
assert_eq!(arr.len(), 3);
|
|
480
|
+
assert_eq!(arr[0], AmoskeagValue::Number(1.0));
|
|
481
|
+
assert_eq!(arr[1], AmoskeagValue::Number(2.0));
|
|
482
|
+
assert_eq!(arr[2], AmoskeagValue::Number(3.0));
|
|
483
|
+
}
|
|
484
|
+
other => panic!("Expected Array, got {:?}", other),
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
#[test]
|
|
489
|
+
fn test_ruby_true_converts_to_boolean_true() {
|
|
490
|
+
init_ruby();
|
|
491
|
+
|
|
492
|
+
let val: Value = magnus::eval("true").unwrap();
|
|
493
|
+
|
|
494
|
+
let result = ruby_value_to_amoskeag(val).unwrap();
|
|
495
|
+
|
|
496
|
+
assert_eq!(result, AmoskeagValue::Boolean(true));
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
#[test]
|
|
500
|
+
fn test_ruby_false_converts_to_boolean_false() {
|
|
501
|
+
init_ruby();
|
|
502
|
+
|
|
503
|
+
let val: Value = magnus::eval("false").unwrap();
|
|
504
|
+
|
|
505
|
+
let result = ruby_value_to_amoskeag(val).unwrap();
|
|
506
|
+
|
|
507
|
+
assert_eq!(result, AmoskeagValue::Boolean(false));
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
#[test]
|
|
511
|
+
fn test_ruby_nil_converts_to_nil() {
|
|
512
|
+
init_ruby();
|
|
513
|
+
|
|
514
|
+
let val: Value = magnus::eval("nil").unwrap();
|
|
515
|
+
|
|
516
|
+
let result = ruby_value_to_amoskeag(val).unwrap();
|
|
517
|
+
|
|
518
|
+
assert_eq!(result, AmoskeagValue::Nil);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
#[test]
|
|
522
|
+
fn test_ruby_number_converts_to_number() {
|
|
523
|
+
init_ruby();
|
|
524
|
+
|
|
525
|
+
let val: Value = magnus::eval("42.5").unwrap();
|
|
526
|
+
|
|
527
|
+
let result = ruby_value_to_amoskeag(val).unwrap();
|
|
528
|
+
|
|
529
|
+
assert_eq!(result, AmoskeagValue::Number(42.5));
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
#[test]
|
|
533
|
+
fn test_ruby_string_converts_to_string() {
|
|
534
|
+
init_ruby();
|
|
535
|
+
|
|
536
|
+
let val: Value = magnus::eval(r#""hello""#).unwrap();
|
|
537
|
+
|
|
538
|
+
let result = ruby_value_to_amoskeag(val).unwrap();
|
|
539
|
+
|
|
540
|
+
assert_eq!(result, AmoskeagValue::String("hello".to_string()));
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
#[test]
|
|
544
|
+
fn test_ruby_symbol_converts_to_symbol() {
|
|
545
|
+
init_ruby();
|
|
546
|
+
|
|
547
|
+
let val: Value = magnus::eval(":test").unwrap();
|
|
548
|
+
|
|
549
|
+
let result = ruby_value_to_amoskeag(val).unwrap();
|
|
550
|
+
|
|
551
|
+
assert_eq!(result, AmoskeagValue::Symbol("test".to_string()));
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
#[test]
|
|
555
|
+
fn test_nested_hash_converts_correctly() {
|
|
556
|
+
init_ruby();
|
|
557
|
+
|
|
558
|
+
let hash: Value = magnus::eval(r#"{"user" => {"name" => "Alice", "age" => 30}}"#).unwrap();
|
|
559
|
+
|
|
560
|
+
let result = ruby_value_to_amoskeag(hash).unwrap();
|
|
561
|
+
|
|
562
|
+
match result {
|
|
563
|
+
AmoskeagValue::Dictionary(map) => {
|
|
564
|
+
match map.get("user") {
|
|
565
|
+
Some(AmoskeagValue::Dictionary(inner)) => {
|
|
566
|
+
assert_eq!(inner.get("name"), Some(&AmoskeagValue::String("Alice".to_string())));
|
|
567
|
+
assert_eq!(inner.get("age"), Some(&AmoskeagValue::Number(30.0)));
|
|
568
|
+
}
|
|
569
|
+
other => panic!("Expected nested Dictionary, got {:?}", other),
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
other => panic!("Expected Dictionary, got {:?}", other),
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
#[test]
|
|
577
|
+
fn test_empty_hash_converts_to_empty_dictionary() {
|
|
578
|
+
init_ruby();
|
|
579
|
+
|
|
580
|
+
let hash: Value = magnus::eval("{}").unwrap();
|
|
581
|
+
|
|
582
|
+
let result = ruby_value_to_amoskeag(hash).unwrap();
|
|
583
|
+
|
|
584
|
+
match result {
|
|
585
|
+
AmoskeagValue::Dictionary(map) => {
|
|
586
|
+
assert_eq!(map.len(), 0);
|
|
587
|
+
}
|
|
588
|
+
other => panic!("Expected empty Dictionary, got {:?}", other),
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
#[test]
|
|
593
|
+
fn test_empty_array_converts_to_empty_array() {
|
|
594
|
+
init_ruby();
|
|
595
|
+
|
|
596
|
+
let array: Value = magnus::eval("[]").unwrap();
|
|
597
|
+
|
|
598
|
+
let result = ruby_value_to_amoskeag(array).unwrap();
|
|
599
|
+
|
|
600
|
+
match result {
|
|
601
|
+
AmoskeagValue::Array(arr) => {
|
|
602
|
+
assert_eq!(arr.len(), 0);
|
|
603
|
+
}
|
|
604
|
+
other => panic!("Expected empty Array, got {:?}", other),
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
data/lib/amoskeag-rb/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: amoskeag-rb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Durable Programming
|
|
@@ -112,7 +112,9 @@ extra_rdoc_files: []
|
|
|
112
112
|
files:
|
|
113
113
|
- CHANGELOG.md
|
|
114
114
|
- README.md
|
|
115
|
+
- ext/amoskeag/Cargo.toml
|
|
115
116
|
- ext/amoskeag/extconf.rb
|
|
117
|
+
- ext/amoskeag/src/lib.rs
|
|
116
118
|
- lib/amoskeag-rb.rb
|
|
117
119
|
- lib/amoskeag-rb/version.rb
|
|
118
120
|
homepage: https://github.com/durable-oss/amoskeag-rb
|