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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d8f06f8b7d034756777352dc23a2c6400d8b2d3fe994c2c0ada4b01a9193b984
4
- data.tar.gz: b8fbfc2d175752ba2b556813d3c6d7632aeb16cfc57bf3569eceaa01828b87b3
3
+ metadata.gz: 5017194e4c300b6e147b029385f6d44148e9e64171c35ae9c50c78077c3928c2
4
+ data.tar.gz: 1f8561b60fe2a24d0299f7d80cf359efcf824efd15280e37e3a9f4ed19b76c56
5
5
  SHA512:
6
- metadata.gz: a3c055f446b538eb978a5fb94e78730fcf54aace6670570c2c0805d995c212a6dec3b75c87e2c007e0bdf3e3e37bbe79d6e1812fa822793cb23c994c9403e01d
7
- data.tar.gz: 9877b3169f65cccec7ed32334045cd87ada82070c1270287ec116169a7f6279351873e44754651c06525e43be4c5e2d79e0e07dbf60a95dde3256f314f94af6b
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
+ }
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Amoskeag
4
- VERSION = "0.1.2"
4
+ VERSION = "0.1.3"
5
5
  AMOSKEAG_VERSION = "0.1.1e"
6
6
  end
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.2
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