Skip to main content

shape_runtime/stdlib/
yaml.rs

1//! Native `yaml` module for YAML parsing and serialization.
2//!
3//! Exports: yaml.parse(text), yaml.parse_all(text), yaml.stringify(value), yaml.is_valid(text)
4
5use crate::module_exports::{ModuleContext, ModuleExports, ModuleFunction, ModuleParam};
6use serde::Deserialize;
7use shape_value::ValueWord;
8use std::sync::Arc;
9
10/// Convert a `serde_yaml::Value` into a `ValueWord`.
11fn yaml_value_to_nanboxed(value: serde_yaml::Value) -> ValueWord {
12    match value {
13        serde_yaml::Value::Null => ValueWord::none(),
14        serde_yaml::Value::Bool(b) => ValueWord::from_bool(b),
15        serde_yaml::Value::Number(n) => {
16            if let Some(i) = n.as_i64() {
17                ValueWord::from_i64(i)
18            } else {
19                ValueWord::from_f64(n.as_f64().unwrap_or(0.0))
20            }
21        }
22        serde_yaml::Value::String(s) => ValueWord::from_string(Arc::new(s)),
23        serde_yaml::Value::Sequence(arr) => {
24            let items: Vec<ValueWord> = arr.into_iter().map(yaml_value_to_nanboxed).collect();
25            ValueWord::from_array(Arc::new(items))
26        }
27        serde_yaml::Value::Mapping(map) => {
28            let mut keys = Vec::with_capacity(map.len());
29            let mut values = Vec::with_capacity(map.len());
30            for (k, v) in map.into_iter() {
31                let key_str = match k {
32                    serde_yaml::Value::String(s) => s,
33                    serde_yaml::Value::Number(n) => n.to_string(),
34                    serde_yaml::Value::Bool(b) => b.to_string(),
35                    other => format!("{:?}", other),
36                };
37                keys.push(ValueWord::from_string(Arc::new(key_str)));
38                values.push(yaml_value_to_nanboxed(v));
39            }
40            ValueWord::from_hashmap_pairs(keys, values)
41        }
42        serde_yaml::Value::Tagged(tagged) => {
43            // Unwrap tagged values — preserve the inner value
44            yaml_value_to_nanboxed(tagged.value)
45        }
46    }
47}
48
49/// Create the `yaml` module with YAML parsing and serialization functions.
50pub fn create_yaml_module() -> ModuleExports {
51    let mut module = ModuleExports::new("std::core::yaml");
52    module.description = "YAML parsing and serialization".to_string();
53
54    // yaml.parse(text: string) -> Result<HashMap>
55    module.add_function_with_schema(
56        "parse",
57        |args: &[ValueWord], _ctx: &ModuleContext| {
58            let text = args
59                .first()
60                .and_then(|a| a.as_str())
61                .ok_or_else(|| "yaml.parse() requires a string argument".to_string())?;
62
63            let parsed: serde_yaml::Value =
64                serde_yaml::from_str(text).map_err(|e| format!("yaml.parse() failed: {}", e))?;
65
66            let result = yaml_value_to_nanboxed(parsed);
67            Ok(ValueWord::from_ok(result))
68        },
69        ModuleFunction {
70            description: "Parse a YAML string into Shape values".to_string(),
71            params: vec![ModuleParam {
72                name: "text".to_string(),
73                type_name: "string".to_string(),
74                required: true,
75                description: "YAML string to parse".to_string(),
76                ..Default::default()
77            }],
78            return_type: Some("Result<HashMap>".to_string()),
79        },
80    );
81
82    // yaml.parse_all(text: string) -> Result<Array>
83    module.add_function_with_schema(
84        "parse_all",
85        |args: &[ValueWord], _ctx: &ModuleContext| {
86            let text = args
87                .first()
88                .and_then(|a| a.as_str())
89                .ok_or_else(|| "yaml.parse_all() requires a string argument".to_string())?;
90
91            let mut documents = Vec::new();
92            for document in serde_yaml::Deserializer::from_str(text) {
93                let value: serde_yaml::Value = serde_yaml::Value::deserialize(document)
94                    .map_err(|e| format!("yaml.parse_all() failed: {}", e))?;
95                documents.push(yaml_value_to_nanboxed(value));
96            }
97
98            Ok(ValueWord::from_ok(ValueWord::from_array(Arc::new(
99                documents,
100            ))))
101        },
102        ModuleFunction {
103            description: "Parse a multi-document YAML string into an array of Shape values"
104                .to_string(),
105            params: vec![ModuleParam {
106                name: "text".to_string(),
107                type_name: "string".to_string(),
108                required: true,
109                description: "YAML string with one or more documents".to_string(),
110                ..Default::default()
111            }],
112            return_type: Some("Result<Array>".to_string()),
113        },
114    );
115
116    // yaml.stringify(value: any) -> Result<string>
117    module.add_function_with_schema(
118        "stringify",
119        |args: &[ValueWord], _ctx: &ModuleContext| {
120            let value = args
121                .first()
122                .ok_or_else(|| "yaml.stringify() requires a value argument".to_string())?;
123
124            let json_value = value.to_json_value();
125            let output = serde_yaml::to_string(&json_value)
126                .map_err(|e| format!("yaml.stringify() failed: {}", e))?;
127
128            Ok(ValueWord::from_ok(ValueWord::from_string(Arc::new(output))))
129        },
130        ModuleFunction {
131            description: "Serialize a Shape value to a YAML string".to_string(),
132            params: vec![ModuleParam {
133                name: "value".to_string(),
134                type_name: "any".to_string(),
135                required: true,
136                description: "Value to serialize".to_string(),
137                ..Default::default()
138            }],
139            return_type: Some("Result<string>".to_string()),
140        },
141    );
142
143    // yaml.is_valid(text: string) -> bool
144    module.add_function_with_schema(
145        "is_valid",
146        |args: &[ValueWord], _ctx: &ModuleContext| {
147            let text = args
148                .first()
149                .and_then(|a| a.as_str())
150                .ok_or_else(|| "yaml.is_valid() requires a string argument".to_string())?;
151
152            let valid = serde_yaml::from_str::<serde_yaml::Value>(text).is_ok();
153            Ok(ValueWord::from_bool(valid))
154        },
155        ModuleFunction {
156            description: "Check if a string is valid YAML".to_string(),
157            params: vec![ModuleParam {
158                name: "text".to_string(),
159                type_name: "string".to_string(),
160                required: true,
161                description: "String to validate as YAML".to_string(),
162                ..Default::default()
163            }],
164            return_type: Some("bool".to_string()),
165        },
166    );
167
168    module
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    fn test_ctx() -> crate::module_exports::ModuleContext<'static> {
176        let registry = Box::leak(Box::new(crate::type_schema::TypeSchemaRegistry::new()));
177        crate::module_exports::ModuleContext {
178            schemas: registry,
179            invoke_callable: None,
180            raw_invoker: None,
181            function_hashes: None,
182            vm_state: None,
183            granted_permissions: None,
184            scope_constraints: None,
185            set_pending_resume: None,
186            set_pending_frame_resume: None,
187        }
188    }
189
190    #[test]
191    fn test_yaml_module_creation() {
192        let module = create_yaml_module();
193        assert_eq!(module.name, "std::core::yaml");
194        assert!(module.has_export("parse"));
195        assert!(module.has_export("parse_all"));
196        assert!(module.has_export("stringify"));
197        assert!(module.has_export("is_valid"));
198    }
199
200    #[test]
201    fn test_yaml_parse_mapping() {
202        let module = create_yaml_module();
203        let parse_fn = module.get_export("parse").unwrap();
204        let ctx = test_ctx();
205        let input = ValueWord::from_string(Arc::new(
206            "name: test\nversion: 42\npi: 3.14\nactive: true\n".to_string(),
207        ));
208        let result = parse_fn(&[input], &ctx).unwrap();
209        let inner = result.as_ok_inner().expect("should be Ok");
210        let (keys, _values, _index) = inner.as_hashmap().expect("should be hashmap");
211        assert_eq!(keys.len(), 4);
212    }
213
214    #[test]
215    fn test_yaml_parse_sequence() {
216        let module = create_yaml_module();
217        let parse_fn = module.get_export("parse").unwrap();
218        let ctx = test_ctx();
219        let input = ValueWord::from_string(Arc::new("- 1\n- 2\n- 3\n".to_string()));
220        let result = parse_fn(&[input], &ctx).unwrap();
221        let inner = result.as_ok_inner().expect("should be Ok");
222        let arr = inner.as_any_array().expect("should be array").to_generic();
223        assert_eq!(arr.len(), 3);
224    }
225
226    #[test]
227    fn test_yaml_parse_scalar_string() {
228        let module = create_yaml_module();
229        let parse_fn = module.get_export("parse").unwrap();
230        let ctx = test_ctx();
231        let input = ValueWord::from_string(Arc::new("hello world".to_string()));
232        let result = parse_fn(&[input], &ctx).unwrap();
233        let inner = result.as_ok_inner().expect("should be Ok");
234        assert_eq!(inner.as_str(), Some("hello world"));
235    }
236
237    #[test]
238    fn test_yaml_parse_null() {
239        let module = create_yaml_module();
240        let parse_fn = module.get_export("parse").unwrap();
241        let ctx = test_ctx();
242        let input = ValueWord::from_string(Arc::new("null".to_string()));
243        let result = parse_fn(&[input], &ctx).unwrap();
244        let inner = result.as_ok_inner().expect("should be Ok");
245        assert!(inner.is_none());
246    }
247
248    #[test]
249    fn test_yaml_parse_nested() {
250        let module = create_yaml_module();
251        let parse_fn = module.get_export("parse").unwrap();
252        let ctx = test_ctx();
253        let input = ValueWord::from_string(Arc::new(
254            "server:\n  host: localhost\n  port: 8080\n".to_string(),
255        ));
256        let result = parse_fn(&[input], &ctx).unwrap();
257        let inner = result.as_ok_inner().expect("should be Ok");
258        let (keys, _values, _index) = inner.as_hashmap().expect("should be hashmap");
259        assert_eq!(keys.len(), 1);
260    }
261
262    #[test]
263    fn test_yaml_parse_requires_string() {
264        let module = create_yaml_module();
265        let parse_fn = module.get_export("parse").unwrap();
266        let ctx = test_ctx();
267        let result = parse_fn(&[ValueWord::from_f64(42.0)], &ctx);
268        assert!(result.is_err());
269    }
270
271    #[test]
272    fn test_yaml_parse_all_multi_document() {
273        let module = create_yaml_module();
274        let parse_all_fn = module.get_export("parse_all").unwrap();
275        let ctx = test_ctx();
276        let input = ValueWord::from_string(Arc::new(
277            "---\nname: doc1\n---\nname: doc2\n---\nname: doc3\n".to_string(),
278        ));
279        let result = parse_all_fn(&[input], &ctx).unwrap();
280        let inner = result.as_ok_inner().expect("should be Ok");
281        let arr = inner.as_any_array().expect("should be array").to_generic();
282        assert_eq!(arr.len(), 3);
283    }
284
285    #[test]
286    fn test_yaml_parse_all_single_document() {
287        let module = create_yaml_module();
288        let parse_all_fn = module.get_export("parse_all").unwrap();
289        let ctx = test_ctx();
290        let input = ValueWord::from_string(Arc::new("name: single\n".to_string()));
291        let result = parse_all_fn(&[input], &ctx).unwrap();
292        let inner = result.as_ok_inner().expect("should be Ok");
293        let arr = inner.as_any_array().expect("should be array").to_generic();
294        assert_eq!(arr.len(), 1);
295    }
296
297    #[test]
298    fn test_yaml_stringify_mapping() {
299        let module = create_yaml_module();
300        let stringify_fn = module.get_export("stringify").unwrap();
301        let ctx = test_ctx();
302        let keys = vec![ValueWord::from_string(Arc::new("name".to_string()))];
303        let values = vec![ValueWord::from_string(Arc::new("test".to_string()))];
304        let hm = ValueWord::from_hashmap_pairs(keys, values);
305        let result = stringify_fn(&[hm], &ctx).unwrap();
306        let inner = result.as_ok_inner().expect("should be Ok");
307        let s = inner.as_str().expect("should be string");
308        assert!(s.contains("name"));
309        assert!(s.contains("test"));
310    }
311
312    #[test]
313    fn test_yaml_stringify_number() {
314        let module = create_yaml_module();
315        let stringify_fn = module.get_export("stringify").unwrap();
316        let ctx = test_ctx();
317        let result = stringify_fn(&[ValueWord::from_f64(42.0)], &ctx).unwrap();
318        let inner = result.as_ok_inner().expect("should be Ok");
319        let s = inner.as_str().expect("should be string");
320        assert!(s.contains("42"));
321    }
322
323    #[test]
324    fn test_yaml_stringify_bool() {
325        let module = create_yaml_module();
326        let stringify_fn = module.get_export("stringify").unwrap();
327        let ctx = test_ctx();
328        let result = stringify_fn(&[ValueWord::from_bool(true)], &ctx).unwrap();
329        let inner = result.as_ok_inner().expect("should be Ok");
330        let s = inner.as_str().expect("should be string");
331        assert!(s.contains("true"));
332    }
333
334    #[test]
335    fn test_yaml_is_valid_true() {
336        let module = create_yaml_module();
337        let is_valid_fn = module.get_export("is_valid").unwrap();
338        let ctx = test_ctx();
339        let result = is_valid_fn(
340            &[ValueWord::from_string(Arc::new("key: value\n".to_string()))],
341            &ctx,
342        )
343        .unwrap();
344        assert_eq!(result.as_bool(), Some(true));
345    }
346
347    #[test]
348    fn test_yaml_is_valid_false() {
349        let module = create_yaml_module();
350        let is_valid_fn = module.get_export("is_valid").unwrap();
351        let ctx = test_ctx();
352        let result = is_valid_fn(
353            &[ValueWord::from_string(Arc::new(
354                ":\n  :\n    - : :\n  bad: [".to_string(),
355            ))],
356            &ctx,
357        )
358        .unwrap();
359        // serde_yaml may or may not parse some edge cases; just verify we get a bool
360        assert!(result.as_bool().is_some());
361    }
362
363    #[test]
364    fn test_yaml_is_valid_requires_string() {
365        let module = create_yaml_module();
366        let is_valid_fn = module.get_export("is_valid").unwrap();
367        let ctx = test_ctx();
368        let result = is_valid_fn(&[ValueWord::from_f64(42.0)], &ctx);
369        assert!(result.is_err());
370    }
371
372    #[test]
373    fn test_yaml_roundtrip() {
374        let module = create_yaml_module();
375        let parse_fn = module.get_export("parse").unwrap();
376        let stringify_fn = module.get_export("stringify").unwrap();
377        let ctx = test_ctx();
378
379        let yaml_str = "name: test\nversion: 42\n";
380        let parsed = parse_fn(
381            &[ValueWord::from_string(Arc::new(yaml_str.to_string()))],
382            &ctx,
383        )
384        .unwrap();
385        let inner = parsed.as_ok_inner().expect("should be Ok");
386        let re_stringified = stringify_fn(&[inner.clone()], &ctx).unwrap();
387        let re_str = re_stringified.as_ok_inner().expect("should be Ok");
388        assert!(re_str.as_str().is_some());
389    }
390
391    #[test]
392    fn test_yaml_schemas() {
393        let module = create_yaml_module();
394
395        let parse_schema = module.get_schema("parse").unwrap();
396        assert_eq!(parse_schema.params.len(), 1);
397        assert_eq!(parse_schema.params[0].name, "text");
398        assert!(parse_schema.params[0].required);
399        assert_eq!(parse_schema.return_type.as_deref(), Some("Result<HashMap>"));
400
401        let parse_all_schema = module.get_schema("parse_all").unwrap();
402        assert_eq!(parse_all_schema.params.len(), 1);
403        assert_eq!(
404            parse_all_schema.return_type.as_deref(),
405            Some("Result<Array>")
406        );
407
408        let stringify_schema = module.get_schema("stringify").unwrap();
409        assert_eq!(stringify_schema.params.len(), 1);
410        assert!(stringify_schema.params[0].required);
411
412        let is_valid_schema = module.get_schema("is_valid").unwrap();
413        assert_eq!(is_valid_schema.params.len(), 1);
414        assert_eq!(is_valid_schema.return_type.as_deref(), Some("bool"));
415    }
416}