Skip to main content

shape_runtime/stdlib/
toml_module.rs

1//! Native `toml` module for TOML parsing and serialization.
2//!
3//! Exports: toml.parse(text), toml.stringify(value), toml.is_valid(text)
4
5use crate::module_exports::{ModuleContext, ModuleExports, ModuleFunction, ModuleParam};
6use shape_value::ValueWord;
7use std::sync::Arc;
8
9/// Convert a `toml::Value` into a `ValueWord`.
10fn toml_value_to_nanboxed(value: toml::Value) -> ValueWord {
11    match value {
12        toml::Value::Boolean(b) => ValueWord::from_bool(b),
13        toml::Value::Integer(n) => ValueWord::from_i64(n),
14        toml::Value::Float(f) => ValueWord::from_f64(f),
15        toml::Value::String(s) => ValueWord::from_string(Arc::new(s)),
16        toml::Value::Datetime(dt) => ValueWord::from_string(Arc::new(dt.to_string())),
17        toml::Value::Array(arr) => {
18            let items: Vec<ValueWord> = arr.into_iter().map(toml_value_to_nanboxed).collect();
19            ValueWord::from_array(Arc::new(items))
20        }
21        toml::Value::Table(map) => {
22            let mut keys = Vec::with_capacity(map.len());
23            let mut values = Vec::with_capacity(map.len());
24            for (k, v) in map.into_iter() {
25                keys.push(ValueWord::from_string(Arc::new(k)));
26                values.push(toml_value_to_nanboxed(v));
27            }
28            ValueWord::from_hashmap_pairs(keys, values)
29        }
30    }
31}
32
33/// Convert a `ValueWord` into a `toml::Value` for serialization.
34fn nanboxed_to_toml_value(nb: &ValueWord) -> toml::Value {
35    use shape_value::heap_value::HeapValue;
36
37    if nb.is_none() {
38        return toml::Value::String("null".to_string());
39    }
40    if let Some(b) = nb.as_bool() {
41        return toml::Value::Boolean(b);
42    }
43    if let Some(n) = nb.as_i64() {
44        return toml::Value::Integer(n);
45    }
46    if let Some(f) = nb.as_f64() {
47        return toml::Value::Float(f);
48    }
49    if let Some(s) = nb.as_str() {
50        return toml::Value::String(s.to_string());
51    }
52    if let Some(arr) = nb.as_any_array() {
53        let items: Vec<toml::Value> = arr
54            .to_generic()
55            .iter()
56            .map(nanboxed_to_toml_value)
57            .collect();
58        return toml::Value::Array(items);
59    }
60    if let Some((keys, values, _index)) = nb.as_hashmap() {
61        let mut map = toml::map::Map::new();
62        for (k, v) in keys.iter().zip(values.iter()) {
63            if let Some(key) = k.as_str() {
64                map.insert(key.to_string(), nanboxed_to_toml_value(v));
65            }
66        }
67        return toml::Value::Table(map);
68    }
69    // TypedObject — convert via field extraction
70    if let Some(heap) = nb.as_heap_ref() {
71        if let HeapValue::TypedObject { slots, .. } = heap {
72            // Fall back to string representation for complex types
73            let _ = slots;
74            return toml::Value::String(format!("{}", nb));
75        }
76    }
77    toml::Value::String(format!("{}", nb))
78}
79
80/// Create the `toml` module with TOML parsing and serialization functions.
81pub fn create_toml_module() -> ModuleExports {
82    let mut module = ModuleExports::new("std::core::toml");
83    module.description = "TOML parsing and serialization".to_string();
84
85    // toml.parse(text: string) -> Result<HashMap>
86    module.add_function_with_schema(
87        "parse",
88        |args: &[ValueWord], _ctx: &ModuleContext| {
89            let text = args
90                .first()
91                .and_then(|a| a.as_str())
92                .ok_or_else(|| "toml.parse() requires a string argument".to_string())?;
93
94            let parsed: toml::Value =
95                toml::from_str(text).map_err(|e| format!("toml.parse() failed: {}", e))?;
96
97            let result = toml_value_to_nanboxed(parsed);
98            Ok(ValueWord::from_ok(result))
99        },
100        ModuleFunction {
101            description: "Parse a TOML string into Shape values".to_string(),
102            params: vec![ModuleParam {
103                name: "text".to_string(),
104                type_name: "string".to_string(),
105                required: true,
106                description: "TOML string to parse".to_string(),
107                ..Default::default()
108            }],
109            return_type: Some("Result<HashMap>".to_string()),
110        },
111    );
112
113    // toml.stringify(value: any) -> Result<string>
114    module.add_function_with_schema(
115        "stringify",
116        |args: &[ValueWord], _ctx: &ModuleContext| {
117            let value = args
118                .first()
119                .ok_or_else(|| "toml.stringify() requires a value argument".to_string())?;
120
121            let toml_value = nanboxed_to_toml_value(value);
122            let output = toml::to_string(&toml_value)
123                .map_err(|e| format!("toml.stringify() failed: {}", e))?;
124
125            Ok(ValueWord::from_ok(ValueWord::from_string(Arc::new(output))))
126        },
127        ModuleFunction {
128            description: "Serialize a Shape value to a TOML string".to_string(),
129            params: vec![ModuleParam {
130                name: "value".to_string(),
131                type_name: "any".to_string(),
132                required: true,
133                description: "Value to serialize".to_string(),
134                ..Default::default()
135            }],
136            return_type: Some("Result<string>".to_string()),
137        },
138    );
139
140    // toml.is_valid(text: string) -> bool
141    module.add_function_with_schema(
142        "is_valid",
143        |args: &[ValueWord], _ctx: &ModuleContext| {
144            let text = args
145                .first()
146                .and_then(|a| a.as_str())
147                .ok_or_else(|| "toml.is_valid() requires a string argument".to_string())?;
148
149            let valid = toml::from_str::<toml::Value>(text).is_ok();
150            Ok(ValueWord::from_bool(valid))
151        },
152        ModuleFunction {
153            description: "Check if a string is valid TOML".to_string(),
154            params: vec![ModuleParam {
155                name: "text".to_string(),
156                type_name: "string".to_string(),
157                required: true,
158                description: "String to validate as TOML".to_string(),
159                ..Default::default()
160            }],
161            return_type: Some("bool".to_string()),
162        },
163    );
164
165    module
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    fn test_ctx() -> crate::module_exports::ModuleContext<'static> {
173        let registry = Box::leak(Box::new(crate::type_schema::TypeSchemaRegistry::new()));
174        crate::module_exports::ModuleContext {
175            schemas: registry,
176            invoke_callable: None,
177            raw_invoker: None,
178            function_hashes: None,
179            vm_state: None,
180            granted_permissions: None,
181            scope_constraints: None,
182            set_pending_resume: None,
183            set_pending_frame_resume: None,
184        }
185    }
186
187    #[test]
188    fn test_toml_module_creation() {
189        let module = create_toml_module();
190        assert_eq!(module.name, "std::core::toml");
191        assert!(module.has_export("parse"));
192        assert!(module.has_export("stringify"));
193        assert!(module.has_export("is_valid"));
194    }
195
196    #[test]
197    fn test_toml_parse_simple_table() {
198        let module = create_toml_module();
199        let parse_fn = module.get_export("parse").unwrap();
200        let ctx = test_ctx();
201        let input = ValueWord::from_string(Arc::new(
202            r#"
203[server]
204host = "localhost"
205port = 8080
206"#
207            .to_string(),
208        ));
209        let result = parse_fn(&[input], &ctx).unwrap();
210        let inner = result.as_ok_inner().expect("should be Ok");
211        let (keys, _values, _index) = inner.as_hashmap().expect("should be hashmap");
212        assert_eq!(keys.len(), 1); // "server" key
213    }
214
215    #[test]
216    fn test_toml_parse_basic_types() {
217        let module = create_toml_module();
218        let parse_fn = module.get_export("parse").unwrap();
219        let ctx = test_ctx();
220        let input = ValueWord::from_string(Arc::new(
221            r#"
222name = "test"
223version = 42
224pi = 3.14
225active = true
226"#
227            .to_string(),
228        ));
229        let result = parse_fn(&[input], &ctx).unwrap();
230        let inner = result.as_ok_inner().expect("should be Ok");
231        assert!(inner.as_hashmap().is_some());
232    }
233
234    #[test]
235    fn test_toml_parse_array() {
236        let module = create_toml_module();
237        let parse_fn = module.get_export("parse").unwrap();
238        let ctx = test_ctx();
239        let input = ValueWord::from_string(Arc::new(r#"values = [1, 2, 3]"#.to_string()));
240        let result = parse_fn(&[input], &ctx).unwrap();
241        let inner = result.as_ok_inner().expect("should be Ok");
242        assert!(inner.as_hashmap().is_some());
243    }
244
245    #[test]
246    fn test_toml_parse_invalid() {
247        let module = create_toml_module();
248        let parse_fn = module.get_export("parse").unwrap();
249        let ctx = test_ctx();
250        let input = ValueWord::from_string(Arc::new("= invalid toml [".to_string()));
251        let result = parse_fn(&[input], &ctx);
252        assert!(result.is_err());
253    }
254
255    #[test]
256    fn test_toml_parse_requires_string() {
257        let module = create_toml_module();
258        let parse_fn = module.get_export("parse").unwrap();
259        let ctx = test_ctx();
260        let result = parse_fn(&[ValueWord::from_f64(42.0)], &ctx);
261        assert!(result.is_err());
262    }
263
264    #[test]
265    fn test_toml_stringify_table() {
266        let module = create_toml_module();
267        let stringify_fn = module.get_export("stringify").unwrap();
268        let ctx = test_ctx();
269        let keys = vec![ValueWord::from_string(Arc::new("name".to_string()))];
270        let values = vec![ValueWord::from_string(Arc::new("test".to_string()))];
271        let hm = ValueWord::from_hashmap_pairs(keys, values);
272        let result = stringify_fn(&[hm], &ctx).unwrap();
273        let inner = result.as_ok_inner().expect("should be Ok");
274        let s = inner.as_str().expect("should be string");
275        assert!(s.contains("name"));
276        assert!(s.contains("test"));
277    }
278
279    #[test]
280    fn test_toml_is_valid_true() {
281        let module = create_toml_module();
282        let is_valid_fn = module.get_export("is_valid").unwrap();
283        let ctx = test_ctx();
284        let result = is_valid_fn(
285            &[ValueWord::from_string(Arc::new(
286                r#"key = "value""#.to_string(),
287            ))],
288            &ctx,
289        )
290        .unwrap();
291        assert_eq!(result.as_bool(), Some(true));
292    }
293
294    #[test]
295    fn test_toml_is_valid_false() {
296        let module = create_toml_module();
297        let is_valid_fn = module.get_export("is_valid").unwrap();
298        let ctx = test_ctx();
299        let result = is_valid_fn(
300            &[ValueWord::from_string(Arc::new(
301                "= not valid toml".to_string(),
302            ))],
303            &ctx,
304        )
305        .unwrap();
306        assert_eq!(result.as_bool(), Some(false));
307    }
308
309    #[test]
310    fn test_toml_roundtrip() {
311        let module = create_toml_module();
312        let parse_fn = module.get_export("parse").unwrap();
313        let stringify_fn = module.get_export("stringify").unwrap();
314        let ctx = test_ctx();
315
316        let toml_str = r#"name = "test"
317version = 42
318"#;
319        let parsed = parse_fn(
320            &[ValueWord::from_string(Arc::new(toml_str.to_string()))],
321            &ctx,
322        )
323        .unwrap();
324        let inner = parsed.as_ok_inner().expect("should be Ok");
325        let re_stringified = stringify_fn(&[inner.clone()], &ctx).unwrap();
326        let re_str = re_stringified.as_ok_inner().expect("should be Ok");
327        assert!(re_str.as_str().is_some());
328    }
329
330    #[test]
331    fn test_toml_schemas() {
332        let module = create_toml_module();
333
334        let parse_schema = module.get_schema("parse").unwrap();
335        assert_eq!(parse_schema.params.len(), 1);
336        assert_eq!(parse_schema.params[0].name, "text");
337        assert!(parse_schema.params[0].required);
338        assert_eq!(parse_schema.return_type.as_deref(), Some("Result<HashMap>"));
339
340        let stringify_schema = module.get_schema("stringify").unwrap();
341        assert_eq!(stringify_schema.params.len(), 1);
342        assert!(stringify_schema.params[0].required);
343
344        let is_valid_schema = module.get_schema("is_valid").unwrap();
345        assert_eq!(is_valid_schema.params.len(), 1);
346        assert_eq!(is_valid_schema.return_type.as_deref(), Some("bool"));
347    }
348}