1use crate::module_exports::{ModuleContext, ModuleExports, ModuleFunction, ModuleParam};
6use serde::Deserialize;
7use shape_value::ValueWord;
8use std::sync::Arc;
9
10fn 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 yaml_value_to_nanboxed(tagged.value)
45 }
46 }
47}
48
49pub 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 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 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 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 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 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}