Skip to main content

shape_runtime/stdlib/
json.rs

1//! Native `json` module for JSON parsing and serialization.
2//!
3//! Exports: json.parse(text), json.stringify(value, pretty?), json.is_valid(text)
4
5use crate::module_exports::{ModuleContext, ModuleExports, ModuleFunction, ModuleParam};
6use crate::type_schema::{SchemaId, TypeSchemaRegistry, nb_to_slot};
7use shape_value::heap_value::HeapValue;
8use shape_value::{ValueSlot, ValueWord};
9use std::sync::Arc;
10
11/// Convert a `serde_json::Value` into an untyped `ValueWord` (legacy fallback).
12fn json_value_to_nanboxed(value: serde_json::Value) -> ValueWord {
13    match value {
14        serde_json::Value::Null => ValueWord::none(),
15        serde_json::Value::Bool(b) => ValueWord::from_bool(b),
16        serde_json::Value::Number(n) => ValueWord::from_f64(n.as_f64().unwrap_or(0.0)),
17        serde_json::Value::String(s) => ValueWord::from_string(Arc::new(s)),
18        serde_json::Value::Array(arr) => {
19            let items: Vec<ValueWord> = arr.into_iter().map(json_value_to_nanboxed).collect();
20            ValueWord::from_array(Arc::new(items))
21        }
22        serde_json::Value::Object(map) => {
23            let mut keys = Vec::with_capacity(map.len());
24            let mut values = Vec::with_capacity(map.len());
25            for (k, v) in map.into_iter() {
26                keys.push(ValueWord::from_string(Arc::new(k)));
27                values.push(json_value_to_nanboxed(v));
28            }
29            ValueWord::from_hashmap_pairs(keys, values)
30        }
31    }
32}
33
34// Json enum variant IDs (must match order in json_value.shape)
35const JSON_VARIANT_NULL: i64 = 0;
36const JSON_VARIANT_BOOL: i64 = 1;
37const JSON_VARIANT_NUMBER: i64 = 2;
38const JSON_VARIANT_STR: i64 = 3;
39const JSON_VARIANT_ARRAY: i64 = 4;
40const JSON_VARIANT_OBJECT: i64 = 5;
41
42/// Build a Json enum TypedObject with the given variant and payload.
43fn make_json_enum(schema_id: u64, variant_id: i64, payload: Option<ValueWord>) -> ValueWord {
44    // Json layout: slot 0 = __variant (I64), slot 1 = __payload_0 (Any)
45    let variant_slot = ValueSlot::from_int(variant_id);
46    let (payload_slot, heap_mask) = if let Some(ref p) = payload {
47        let (slot, is_heap) = nb_to_slot(p);
48        (slot, if is_heap { 1u64 << 1 } else { 0u64 })
49    } else {
50        (ValueSlot::none(), 0u64)
51    };
52    let slots = vec![variant_slot, payload_slot].into_boxed_slice();
53    ValueWord::from_heap_value(HeapValue::TypedObject {
54        schema_id,
55        slots,
56        heap_mask,
57    })
58}
59
60/// Convert a `serde_json::Value` into a typed `Json` enum TypedObject.
61fn json_value_to_enum(value: serde_json::Value, schema_id: u64) -> ValueWord {
62    match value {
63        serde_json::Value::Null => make_json_enum(schema_id, JSON_VARIANT_NULL, None),
64        serde_json::Value::Bool(b) => {
65            make_json_enum(schema_id, JSON_VARIANT_BOOL, Some(ValueWord::from_bool(b)))
66        }
67        serde_json::Value::Number(n) => make_json_enum(
68            schema_id,
69            JSON_VARIANT_NUMBER,
70            Some(ValueWord::from_f64(n.as_f64().unwrap_or(0.0))),
71        ),
72        serde_json::Value::String(s) => make_json_enum(
73            schema_id,
74            JSON_VARIANT_STR,
75            Some(ValueWord::from_string(Arc::new(s))),
76        ),
77        serde_json::Value::Array(arr) => {
78            let items: Vec<ValueWord> = arr
79                .into_iter()
80                .map(|v| json_value_to_enum(v, schema_id))
81                .collect();
82            make_json_enum(
83                schema_id,
84                JSON_VARIANT_ARRAY,
85                Some(ValueWord::from_array(Arc::new(items))),
86            )
87        }
88        serde_json::Value::Object(map) => {
89            let mut keys = Vec::with_capacity(map.len());
90            let mut values = Vec::with_capacity(map.len());
91            for (k, v) in map.into_iter() {
92                keys.push(ValueWord::from_string(Arc::new(k)));
93                values.push(json_value_to_enum(v, schema_id));
94            }
95            make_json_enum(
96                schema_id,
97                JSON_VARIANT_OBJECT,
98                Some(ValueWord::from_hashmap_pairs(keys, values)),
99            )
100        }
101    }
102}
103
104/// Convert a `serde_json::Value` into a typed struct `ValueWord` using a schema.
105///
106/// Matches JSON keys to schema fields using `wire_name()` (respects `@alias`).
107fn json_object_to_typed(
108    schema_id: SchemaId,
109    schema: &crate::type_schema::TypeSchema,
110    map: &serde_json::Map<String, serde_json::Value>,
111    registry: &TypeSchemaRegistry,
112) -> Result<ValueWord, String> {
113    use crate::type_schema::FieldType;
114
115    let num_fields = schema.fields.len();
116    let mut slots = vec![ValueSlot::none(); num_fields];
117    let mut heap_mask = 0u64;
118
119    for field in &schema.fields {
120        let wire = field.wire_name();
121        let json_val = map.get(wire);
122        let nb = if let Some(jv) = json_val {
123            json_value_to_typed_nb(jv, &field.field_type, registry)?
124        } else {
125            ValueWord::none()
126        };
127
128        // Convert ValueWord to ValueSlot based on field type
129        let (slot, is_heap) = match &field.field_type {
130            FieldType::I64 => (
131                ValueSlot::from_int(
132                    nb.as_i64()
133                        .or_else(|| nb.as_f64().map(|n| n as i64))
134                        .unwrap_or(0),
135                ),
136                false,
137            ),
138            FieldType::Bool => (ValueSlot::from_bool(nb.as_bool().unwrap_or(false)), false),
139            FieldType::F64 | FieldType::Decimal => (
140                ValueSlot::from_number(nb.as_number_coerce().unwrap_or(0.0)),
141                false,
142            ),
143            _ => nb_to_slot(&nb),
144        };
145
146        slots[field.index as usize] = slot;
147        if is_heap {
148            heap_mask |= 1u64 << field.index;
149        }
150    }
151
152    Ok(ValueWord::from_heap_value(HeapValue::TypedObject {
153        schema_id: schema_id as u64,
154        slots: slots.into_boxed_slice(),
155        heap_mask,
156    }))
157}
158
159/// Convert a single JSON value to a ValueWord according to the field type.
160fn json_value_to_typed_nb(
161    value: &serde_json::Value,
162    field_type: &crate::type_schema::FieldType,
163    registry: &TypeSchemaRegistry,
164) -> Result<ValueWord, String> {
165    use crate::type_schema::FieldType;
166    match (value, field_type) {
167        (serde_json::Value::Null, _) => Ok(ValueWord::none()),
168        (serde_json::Value::Bool(b), _) => Ok(ValueWord::from_bool(*b)),
169        (serde_json::Value::Number(n), FieldType::I64) => {
170            Ok(ValueWord::from_i64(n.as_i64().unwrap_or(0)))
171        }
172        (serde_json::Value::Number(n), _) => Ok(ValueWord::from_f64(n.as_f64().unwrap_or(0.0))),
173        (serde_json::Value::String(s), _) => Ok(ValueWord::from_string(Arc::new(s.clone()))),
174        (serde_json::Value::Array(arr), _) => {
175            let items: Vec<ValueWord> = arr
176                .iter()
177                .map(|v| json_value_to_typed_nb(v, &FieldType::Any, registry))
178                .collect::<Result<_, _>>()?;
179            Ok(ValueWord::from_array(Arc::new(items)))
180        }
181        (serde_json::Value::Object(obj), FieldType::Object(type_name)) => {
182            if let Some(nested_schema) = registry.get(type_name) {
183                json_object_to_typed(nested_schema.id, nested_schema, obj, registry)
184            } else {
185                // Schema not found — fall back to untyped hashmap
186                Ok(json_value_to_nanboxed(serde_json::Value::Object(
187                    obj.clone(),
188                )))
189            }
190        }
191        (serde_json::Value::Object(obj), _) => Ok(json_value_to_nanboxed(
192            serde_json::Value::Object(obj.clone()),
193        )),
194    }
195}
196
197/// Create the `json` module with JSON parsing and serialization functions.
198pub fn create_json_module() -> ModuleExports {
199    let mut module = ModuleExports::new("std::core::json");
200    module.description = "JSON parsing and serialization".to_string();
201
202    // json.parse(text: string) -> Result<Json>
203    // Returns typed Json enum when the schema is registered, otherwise untyped.
204    module.add_function_with_schema(
205        "parse",
206        |args: &[ValueWord], ctx: &ModuleContext| {
207            let text = args
208                .first()
209                .and_then(|a| a.as_str())
210                .ok_or_else(|| "json.parse() requires a string argument".to_string())?;
211
212            let parsed: serde_json::Value =
213                serde_json::from_str(text).map_err(|e| format!("json.parse() failed: {}", e))?;
214
215            let result = if let Some(json_schema) = ctx.schemas.get("Json") {
216                json_value_to_enum(parsed, json_schema.id as u64)
217            } else {
218                json_value_to_nanboxed(parsed)
219            };
220
221            Ok(ValueWord::from_ok(result))
222        },
223        ModuleFunction {
224            description: "Parse a JSON string into Shape values".to_string(),
225            params: vec![ModuleParam {
226                name: "text".to_string(),
227                type_name: "string".to_string(),
228                required: true,
229                description: "JSON string to parse".to_string(),
230                ..Default::default()
231            }],
232            return_type: Some("Result<Json>".to_string()),
233        },
234    );
235
236    // json.__parse_typed(text: string, schema_id: number) -> Result<T>
237    // Internal: deserializes JSON directly into a typed struct using the schema.
238    module.add_function_with_schema(
239        "__parse_typed",
240        |args: &[ValueWord], ctx: &ModuleContext| {
241            let text = args
242                .first()
243                .and_then(|a| a.as_str())
244                .ok_or_else(|| "json.__parse_typed() requires a string argument".to_string())?;
245            let schema_id = args
246                .get(1)
247                .and_then(|a| {
248                    a.as_f64()
249                        .map(|n| n as u32)
250                        .or_else(|| a.as_i64().map(|n| n as u32))
251                })
252                .ok_or_else(|| "json.__parse_typed() requires a schema_id argument".to_string())?;
253
254            let parsed: serde_json::Value = serde_json::from_str(text)
255                .map_err(|e| format!("json.__parse_typed() failed: {}", e))?;
256
257            let map = match parsed {
258                serde_json::Value::Object(m) => m,
259                _ => {
260                    return Err("json.__parse_typed() requires a JSON object".to_string());
261                }
262            };
263
264            let schema = ctx
265                .schemas
266                .get_by_id(schema_id)
267                .ok_or_else(|| format!("json.__parse_typed(): unknown schema id {}", schema_id))?;
268
269            let result = json_object_to_typed(schema_id, schema, &map, ctx.schemas)?;
270            Ok(ValueWord::from_ok(result))
271        },
272        ModuleFunction {
273            description: "Parse a JSON string into a typed struct".to_string(),
274            params: vec![
275                ModuleParam {
276                    name: "text".to_string(),
277                    type_name: "string".to_string(),
278                    required: true,
279                    description: "JSON string to parse".to_string(),
280                    ..Default::default()
281                },
282                ModuleParam {
283                    name: "schema_id".to_string(),
284                    type_name: "number".to_string(),
285                    required: true,
286                    description: "Schema ID of the target type".to_string(),
287                    ..Default::default()
288                },
289            ],
290            return_type: Some("Result<any>".to_string()),
291        },
292    );
293
294    // json.stringify(value: any, pretty?: bool) -> Result<string>
295    module.add_function_with_schema(
296        "stringify",
297        |args: &[ValueWord], _ctx: &ModuleContext| {
298            let value = args
299                .first()
300                .ok_or_else(|| "json.stringify() requires a value argument".to_string())?;
301
302            let pretty = args.get(1).and_then(|a| a.as_bool()).unwrap_or(false);
303
304            let json_value = value.to_json_value();
305
306            let output = if pretty {
307                serde_json::to_string_pretty(&json_value)
308            } else {
309                serde_json::to_string(&json_value)
310            }
311            .map_err(|e| format!("json.stringify() failed: {}", e))?;
312
313            Ok(ValueWord::from_ok(ValueWord::from_string(Arc::new(output))))
314        },
315        ModuleFunction {
316            description: "Serialize a Shape value to a JSON string".to_string(),
317            params: vec![
318                ModuleParam {
319                    name: "value".to_string(),
320                    type_name: "any".to_string(),
321                    required: true,
322                    description: "Value to serialize".to_string(),
323                    ..Default::default()
324                },
325                ModuleParam {
326                    name: "pretty".to_string(),
327                    type_name: "bool".to_string(),
328                    required: false,
329                    description: "Pretty-print with indentation (default: false)".to_string(),
330                    default_snippet: Some("false".to_string()),
331                    ..Default::default()
332                },
333            ],
334            return_type: Some("Result<string>".to_string()),
335        },
336    );
337
338    // json.is_valid(text: string) -> bool
339    module.add_function_with_schema(
340        "is_valid",
341        |args: &[ValueWord], _ctx: &ModuleContext| {
342            let text = args
343                .first()
344                .and_then(|a| a.as_str())
345                .ok_or_else(|| "json.is_valid() requires a string argument".to_string())?;
346
347            let valid = serde_json::from_str::<serde_json::Value>(text).is_ok();
348            Ok(ValueWord::from_bool(valid))
349        },
350        ModuleFunction {
351            description: "Check if a string is valid JSON".to_string(),
352            params: vec![ModuleParam {
353                name: "text".to_string(),
354                type_name: "string".to_string(),
355                required: true,
356                description: "String to validate as JSON".to_string(),
357                ..Default::default()
358            }],
359            return_type: Some("bool".to_string()),
360        },
361    );
362
363    module
364}
365
366#[cfg(test)]
367mod tests {
368    use super::*;
369
370    fn test_ctx() -> crate::module_exports::ModuleContext<'static> {
371        let registry = Box::leak(Box::new(crate::type_schema::TypeSchemaRegistry::new()));
372        crate::module_exports::ModuleContext {
373            schemas: registry,
374            invoke_callable: None,
375            raw_invoker: None,
376            function_hashes: None,
377            vm_state: None,
378            granted_permissions: None,
379            scope_constraints: None,
380            set_pending_resume: None,
381            set_pending_frame_resume: None,
382        }
383    }
384
385    #[test]
386    fn test_json_module_creation() {
387        let module = create_json_module();
388        assert_eq!(module.name, "std::core::json");
389        assert!(module.has_export("parse"));
390        assert!(module.has_export("stringify"));
391        assert!(module.has_export("is_valid"));
392    }
393
394    #[test]
395    fn test_json_parse_string() {
396        let module = create_json_module();
397        let parse_fn = module.get_export("parse").unwrap();
398        let ctx = test_ctx();
399        let input = ValueWord::from_string(Arc::new(r#""hello""#.to_string()));
400        let result = parse_fn(&[input], &ctx).unwrap();
401        // Result is Ok(value)
402        let inner = result.as_ok_inner().expect("should be Ok");
403        assert_eq!(inner.as_str(), Some("hello"));
404    }
405
406    #[test]
407    fn test_json_parse_number() {
408        let module = create_json_module();
409        let parse_fn = module.get_export("parse").unwrap();
410        let ctx = test_ctx();
411        let input = ValueWord::from_string(Arc::new("42.5".to_string()));
412        let result = parse_fn(&[input], &ctx).unwrap();
413        let inner = result.as_ok_inner().expect("should be Ok");
414        assert_eq!(inner.as_f64(), Some(42.5));
415    }
416
417    #[test]
418    fn test_json_parse_bool() {
419        let module = create_json_module();
420        let parse_fn = module.get_export("parse").unwrap();
421        let ctx = test_ctx();
422        let input = ValueWord::from_string(Arc::new("true".to_string()));
423        let result = parse_fn(&[input], &ctx).unwrap();
424        let inner = result.as_ok_inner().expect("should be Ok");
425        assert_eq!(inner.as_bool(), Some(true));
426    }
427
428    #[test]
429    fn test_json_parse_null() {
430        let module = create_json_module();
431        let parse_fn = module.get_export("parse").unwrap();
432        let ctx = test_ctx();
433        let input = ValueWord::from_string(Arc::new("null".to_string()));
434        let result = parse_fn(&[input], &ctx).unwrap();
435        let inner = result.as_ok_inner().expect("should be Ok");
436        assert!(inner.is_none());
437    }
438
439    #[test]
440    fn test_json_parse_array() {
441        let module = create_json_module();
442        let parse_fn = module.get_export("parse").unwrap();
443        let ctx = test_ctx();
444        let input = ValueWord::from_string(Arc::new("[1, 2, 3]".to_string()));
445        let result = parse_fn(&[input], &ctx).unwrap();
446        let inner = result.as_ok_inner().expect("should be Ok");
447        let arr = inner.as_any_array().expect("should be array").to_generic();
448        assert_eq!(arr.len(), 3);
449        assert_eq!(arr[0].as_f64(), Some(1.0));
450        assert_eq!(arr[1].as_f64(), Some(2.0));
451        assert_eq!(arr[2].as_f64(), Some(3.0));
452    }
453
454    #[test]
455    fn test_json_parse_object() {
456        let module = create_json_module();
457        let parse_fn = module.get_export("parse").unwrap();
458        let ctx = test_ctx();
459        let input = ValueWord::from_string(Arc::new(r#"{"a": 1, "b": "two"}"#.to_string()));
460        let result = parse_fn(&[input], &ctx).unwrap();
461        let inner = result.as_ok_inner().expect("should be Ok");
462        let (keys, _values, _index) = inner.as_hashmap().expect("should be hashmap");
463        assert_eq!(keys.len(), 2);
464    }
465
466    #[test]
467    fn test_json_parse_invalid() {
468        let module = create_json_module();
469        let parse_fn = module.get_export("parse").unwrap();
470        let ctx = test_ctx();
471        let input = ValueWord::from_string(Arc::new("{invalid}".to_string()));
472        let result = parse_fn(&[input], &ctx);
473        assert!(result.is_err());
474    }
475
476    #[test]
477    fn test_json_parse_requires_string() {
478        let module = create_json_module();
479        let parse_fn = module.get_export("parse").unwrap();
480        let ctx = test_ctx();
481        let result = parse_fn(&[ValueWord::from_f64(42.0)], &ctx);
482        assert!(result.is_err());
483    }
484
485    #[test]
486    fn test_json_stringify_number() {
487        let module = create_json_module();
488        let stringify_fn = module.get_export("stringify").unwrap();
489        let ctx = test_ctx();
490        let result = stringify_fn(&[ValueWord::from_f64(42.0)], &ctx).unwrap();
491        let inner = result.as_ok_inner().expect("should be Ok");
492        assert_eq!(inner.as_str(), Some("42.0"));
493    }
494
495    #[test]
496    fn test_json_stringify_string() {
497        let module = create_json_module();
498        let stringify_fn = module.get_export("stringify").unwrap();
499        let ctx = test_ctx();
500        let result = stringify_fn(
501            &[ValueWord::from_string(Arc::new("hello".to_string()))],
502            &ctx,
503        )
504        .unwrap();
505        let inner = result.as_ok_inner().expect("should be Ok");
506        assert_eq!(inner.as_str(), Some("\"hello\""));
507    }
508
509    #[test]
510    fn test_json_stringify_bool() {
511        let module = create_json_module();
512        let stringify_fn = module.get_export("stringify").unwrap();
513        let ctx = test_ctx();
514        let result = stringify_fn(&[ValueWord::from_bool(true)], &ctx).unwrap();
515        let inner = result.as_ok_inner().expect("should be Ok");
516        assert_eq!(inner.as_str(), Some("true"));
517    }
518
519    #[test]
520    fn test_json_stringify_none() {
521        let module = create_json_module();
522        let stringify_fn = module.get_export("stringify").unwrap();
523        let ctx = test_ctx();
524        let result = stringify_fn(&[ValueWord::none()], &ctx).unwrap();
525        let inner = result.as_ok_inner().expect("should be Ok");
526        assert_eq!(inner.as_str(), Some("null"));
527    }
528
529    #[test]
530    fn test_json_stringify_array() {
531        let module = create_json_module();
532        let stringify_fn = module.get_export("stringify").unwrap();
533        let ctx = test_ctx();
534        let arr = ValueWord::from_array(Arc::new(vec![
535            ValueWord::from_f64(1.0),
536            ValueWord::from_f64(2.0),
537        ]));
538        let result = stringify_fn(&[arr], &ctx).unwrap();
539        let inner = result.as_ok_inner().expect("should be Ok");
540        assert_eq!(inner.as_str(), Some("[1.0,2.0]"));
541    }
542
543    #[test]
544    fn test_json_stringify_pretty() {
545        let module = create_json_module();
546        let stringify_fn = module.get_export("stringify").unwrap();
547        let ctx = test_ctx();
548        let result = stringify_fn(
549            &[ValueWord::from_f64(42.0), ValueWord::from_bool(true)],
550            &ctx,
551        )
552        .unwrap();
553        let inner = result.as_ok_inner().expect("should be Ok");
554        // Pretty mode with a single number is the same as compact
555        assert_eq!(inner.as_str(), Some("42.0"));
556    }
557
558    #[test]
559    fn test_json_is_valid_true() {
560        let module = create_json_module();
561        let is_valid_fn = module.get_export("is_valid").unwrap();
562        let ctx = test_ctx();
563        let result = is_valid_fn(
564            &[ValueWord::from_string(Arc::new(
565                r#"{"key": "value"}"#.to_string(),
566            ))],
567            &ctx,
568        )
569        .unwrap();
570        assert_eq!(result.as_bool(), Some(true));
571    }
572
573    #[test]
574    fn test_json_is_valid_false() {
575        let module = create_json_module();
576        let is_valid_fn = module.get_export("is_valid").unwrap();
577        let ctx = test_ctx();
578        let result = is_valid_fn(
579            &[ValueWord::from_string(Arc::new(
580                "{not valid json".to_string(),
581            ))],
582            &ctx,
583        )
584        .unwrap();
585        assert_eq!(result.as_bool(), Some(false));
586    }
587
588    #[test]
589    fn test_json_is_valid_requires_string() {
590        let module = create_json_module();
591        let is_valid_fn = module.get_export("is_valid").unwrap();
592        let ctx = test_ctx();
593        let result = is_valid_fn(&[ValueWord::from_f64(42.0)], &ctx);
594        assert!(result.is_err());
595    }
596
597    #[test]
598    fn test_json_schemas() {
599        let module = create_json_module();
600
601        let parse_schema = module.get_schema("parse").unwrap();
602        assert_eq!(parse_schema.params.len(), 1);
603        assert_eq!(parse_schema.params[0].name, "text");
604        assert!(parse_schema.params[0].required);
605        assert_eq!(parse_schema.return_type.as_deref(), Some("Result<Json>"));
606
607        let stringify_schema = module.get_schema("stringify").unwrap();
608        assert_eq!(stringify_schema.params.len(), 2);
609        assert!(stringify_schema.params[0].required);
610        assert!(!stringify_schema.params[1].required);
611
612        let is_valid_schema = module.get_schema("is_valid").unwrap();
613        assert_eq!(is_valid_schema.params.len(), 1);
614        assert_eq!(is_valid_schema.return_type.as_deref(), Some("bool"));
615    }
616
617    #[test]
618    fn test_json_roundtrip_nested() {
619        let module = create_json_module();
620        let parse_fn = module.get_export("parse").unwrap();
621        let stringify_fn = module.get_export("stringify").unwrap();
622        let ctx = test_ctx();
623
624        let json_str = r#"{"name":"test","values":[1,2,3],"active":true,"meta":null}"#;
625        let parsed = parse_fn(
626            &[ValueWord::from_string(Arc::new(json_str.to_string()))],
627            &ctx,
628        )
629        .unwrap();
630        let inner = parsed.as_ok_inner().expect("should be Ok");
631
632        let re_stringified = stringify_fn(&[inner.clone()], &ctx).unwrap();
633        let re_str = re_stringified.as_ok_inner().expect("should be Ok");
634
635        // Re-parse to verify round-trip validity
636        let re_parsed = parse_fn(&[re_str.clone()], &ctx).unwrap();
637        assert!(re_parsed.as_ok_inner().is_some());
638    }
639
640    /// Test json_value_to_enum produces TypedObjects with correct variant IDs.
641    #[test]
642    fn test_json_value_to_enum_variants() {
643        use crate::type_schema::{EnumVariantInfo, TypeSchema};
644        // Register Json enum schema
645        let schema = TypeSchema::new_enum(
646            "Json",
647            vec![
648                EnumVariantInfo::new("Null", 0, 0),
649                EnumVariantInfo::new("Bool", 1, 1),
650                EnumVariantInfo::new("Number", 2, 1),
651                EnumVariantInfo::new("Str", 3, 1),
652                EnumVariantInfo::new("Array", 4, 1),
653                EnumVariantInfo::new("Object", 5, 1),
654            ],
655        );
656        let sid = schema.id as u64;
657
658        // Null
659        let null_nb = json_value_to_enum(serde_json::Value::Null, sid);
660        let (variant, _payload) = extract_enum_variant(&null_nb);
661        assert_eq!(variant, 0, "Null should be variant 0");
662
663        // Bool
664        let bool_nb = json_value_to_enum(serde_json::Value::Bool(true), sid);
665        let (variant, _payload) = extract_enum_variant(&bool_nb);
666        assert_eq!(variant, 1, "Bool should be variant 1");
667
668        // Number
669        let num_nb = json_value_to_enum(serde_json::json!(42.5), sid);
670        let (variant, _payload) = extract_enum_variant(&num_nb);
671        assert_eq!(variant, 2, "Number should be variant 2");
672
673        // String
674        let str_nb = json_value_to_enum(serde_json::json!("hello"), sid);
675        let (variant, _payload) = extract_enum_variant(&str_nb);
676        assert_eq!(variant, 3, "Str should be variant 3");
677
678        // Array
679        let arr_nb = json_value_to_enum(serde_json::json!([1, 2, 3]), sid);
680        let (variant, _payload) = extract_enum_variant(&arr_nb);
681        assert_eq!(variant, 4, "Array should be variant 4");
682
683        // Object
684        let obj_nb = json_value_to_enum(serde_json::json!({"a": 1}), sid);
685        let (variant, _payload) = extract_enum_variant(&obj_nb);
686        assert_eq!(variant, 5, "Object should be variant 5");
687    }
688
689    /// Test that __parse_typed uses @alias annotations to map JSON keys to fields.
690    #[test]
691    fn test_parse_typed_with_alias() {
692        use crate::type_schema::{FieldAnnotation, TypeSchemaBuilder};
693        use shape_value::heap_value::HeapValue;
694
695        let mut registry = crate::type_schema::TypeSchemaRegistry::new();
696        let mut schema = TypeSchemaBuilder::new("Trade")
697            .f64_field("close")
698            .f64_field("volume")
699            .build();
700
701        // Add @alias annotations manually
702        schema.fields[0].annotations.push(FieldAnnotation {
703            name: "alias".to_string(),
704            args: vec!["Close Price".to_string()],
705        });
706        schema.fields[1].annotations.push(FieldAnnotation {
707            name: "alias".to_string(),
708            args: vec!["vol.".to_string()],
709        });
710        let trade_id = schema.id;
711        registry.register(schema);
712
713        let module = create_json_module();
714        let parse_typed_fn = module.get_export("__parse_typed").unwrap();
715        let ctx = crate::module_exports::ModuleContext {
716            schemas: &registry,
717            invoke_callable: None,
718            raw_invoker: None,
719            function_hashes: None,
720            vm_state: None,
721            granted_permissions: None,
722            scope_constraints: None,
723            set_pending_resume: None,
724            set_pending_frame_resume: None,
725        };
726
727        let text = ValueWord::from_string(Arc::new(
728            r#"{"Close Price": 100.5, "vol.": 1000}"#.to_string(),
729        ));
730        let sid = ValueWord::from_f64(trade_id as f64);
731        let result = parse_typed_fn(&[text, sid], &ctx).unwrap();
732        let inner = result.as_ok_inner().expect("should be Ok");
733
734        // Verify it's a TypedObject with correct field values
735        if let Some(HeapValue::TypedObject { slots, .. }) = inner.as_heap_ref() {
736            // Field 0 ("close", aliased from "Close Price") should be 100.5
737            let close_val = f64::from_bits(slots[0].raw());
738            assert!(
739                (close_val - 100.5).abs() < f64::EPSILON,
740                "close field should be 100.5, got {}",
741                close_val
742            );
743            // Field 1 ("volume", aliased from "vol.") should be 1000.0
744            let volume_val = f64::from_bits(slots[1].raw());
745            assert!(
746                (volume_val - 1000.0).abs() < f64::EPSILON,
747                "volume field should be 1000.0, got {}",
748                volume_val
749            );
750        } else {
751            panic!("expected TypedObject, got: {:?}", inner.type_name());
752        }
753    }
754
755    /// Test that register_type_with_annotations propagates @alias to schema.
756    #[test]
757    fn test_register_type_with_annotations_alias() {
758        use crate::type_schema::{FieldAnnotation, FieldType};
759
760        let mut registry = crate::type_schema::TypeSchemaRegistry::new();
761        let annotations = vec![
762            vec![FieldAnnotation {
763                name: "alias".to_string(),
764                args: vec!["user_name".to_string()],
765            }],
766            vec![], // age has no annotations
767        ];
768        registry.register_type_with_annotations(
769            "User",
770            vec![
771                ("name".to_string(), FieldType::String),
772                ("age".to_string(), FieldType::I64),
773            ],
774            annotations,
775        );
776
777        let schema = registry.get("User").expect("schema should exist");
778        assert_eq!(schema.fields[0].wire_name(), "user_name");
779        assert_eq!(schema.fields[1].wire_name(), "age");
780    }
781
782    /// Test that @alias annotations enable JSON deserialization with wire names.
783    #[test]
784    fn test_parse_typed_alias_string_field() {
785        use crate::type_schema::{FieldAnnotation, FieldType};
786        use shape_value::heap_value::HeapValue;
787
788        let mut registry = crate::type_schema::TypeSchemaRegistry::new();
789        let annotations = vec![
790            vec![FieldAnnotation {
791                name: "alias".to_string(),
792                args: vec!["user_name".to_string()],
793            }],
794            vec![],
795        ];
796        let schema_id = registry.register_type_with_annotations(
797            "User",
798            vec![
799                ("name".to_string(), FieldType::String),
800                ("age".to_string(), FieldType::I64),
801            ],
802            annotations,
803        );
804
805        let module = create_json_module();
806        let parse_typed_fn = module.get_export("__parse_typed").unwrap();
807        let ctx = crate::module_exports::ModuleContext {
808            schemas: &registry,
809            invoke_callable: None,
810            raw_invoker: None,
811            function_hashes: None,
812            vm_state: None,
813            granted_permissions: None,
814            scope_constraints: None,
815            set_pending_resume: None,
816            set_pending_frame_resume: None,
817        };
818
819        // JSON uses the wire name "user_name" instead of the field name "name"
820        let text =
821            ValueWord::from_string(Arc::new(r#"{"user_name": "Bob", "age": 30}"#.to_string()));
822        let sid = ValueWord::from_f64(schema_id as f64);
823        let result = parse_typed_fn(&[text, sid], &ctx).unwrap();
824        let inner = result.as_ok_inner().expect("should be Ok");
825
826        // Verify it's a TypedObject and the name field was populated from the aliased key
827        if let Some(HeapValue::TypedObject { slots, .. }) = inner.as_heap_ref() {
828            // Field 0 ("name") should be a heap string "Bob"
829            let name_nb = slots[0].as_heap_nb();
830            assert_eq!(name_nb.as_str(), Some("Bob"), "name field should be 'Bob'");
831            // Field 1 ("age") should be 30
832            let age_val = slots[1].as_i64();
833            assert_eq!(age_val, 30, "age field should be 30");
834        } else {
835            panic!("expected TypedObject, got: {:?}", inner.type_name());
836        }
837    }
838
839    /// Test that without @alias, field name is used as wire name.
840    #[test]
841    fn test_parse_typed_no_alias_uses_field_name() {
842        use crate::type_schema::FieldType;
843        use shape_value::heap_value::HeapValue;
844
845        let mut registry = crate::type_schema::TypeSchemaRegistry::new();
846        let schema_id = registry.register_type(
847            "Simple",
848            vec![
849                ("name".to_string(), FieldType::String),
850                ("value".to_string(), FieldType::F64),
851            ],
852        );
853
854        let module = create_json_module();
855        let parse_typed_fn = module.get_export("__parse_typed").unwrap();
856        let ctx = crate::module_exports::ModuleContext {
857            schemas: &registry,
858            invoke_callable: None,
859            raw_invoker: None,
860            function_hashes: None,
861            vm_state: None,
862            granted_permissions: None,
863            scope_constraints: None,
864            set_pending_resume: None,
865            set_pending_frame_resume: None,
866        };
867
868        let text =
869            ValueWord::from_string(Arc::new(r#"{"name": "test", "value": 42.5}"#.to_string()));
870        let sid = ValueWord::from_f64(schema_id as f64);
871        let result = parse_typed_fn(&[text, sid], &ctx).unwrap();
872        let inner = result.as_ok_inner().expect("should be Ok");
873
874        if let Some(HeapValue::TypedObject { slots, .. }) = inner.as_heap_ref() {
875            let name_nb = slots[0].as_heap_nb();
876            assert_eq!(name_nb.as_str(), Some("test"));
877            let value_val = f64::from_bits(slots[1].raw());
878            assert!((value_val - 42.5).abs() < f64::EPSILON);
879        } else {
880            panic!("expected TypedObject");
881        }
882    }
883
884    /// Extract variant_id from a Json enum TypedObject.
885    fn extract_enum_variant(nb: &ValueWord) -> (i64, Option<ValueWord>) {
886        use shape_value::heap_value::HeapValue;
887        if let Some(HeapValue::TypedObject {
888            slots, heap_mask, ..
889        }) = nb.as_heap_ref()
890        {
891            let variant_id = slots[0].as_i64();
892            let payload = if slots.len() > 1 {
893                // Only dereference as heap pointer if the heap_mask says slot 1 is a pointer
894                if heap_mask & (1u64 << 1) != 0 {
895                    Some(slots[1].as_heap_nb())
896                } else if slots[1].raw() == 0 && variant_id == 0 {
897                    // Null variant has no payload
898                    None
899                } else {
900                    // Non-heap payload (inline ValueWord) — reconstruct from raw bits
901                    // Safety: bits were stored by nb_to_slot from a valid inline ValueWord.
902                    Some(unsafe { ValueWord::clone_from_bits(slots[1].raw()) })
903                }
904            } else {
905                None
906            };
907            (variant_id, payload)
908        } else {
909            panic!("expected TypedObject, got: {:?}", nb.type_name())
910        }
911    }
912}