fusabi_stdlib_ext/
format.rs

1//! Format module.
2//!
3//! Provides string formatting and templating functions.
4
5use fusabi_host::ExecutionContext;
6use fusabi_host::Value;
7
8/// Sprintf-style string formatting.
9pub fn sprintf(
10    args: &[Value],
11    _ctx: &ExecutionContext,
12) -> fusabi_host::Result<Value> {
13    let format_str = args
14        .first()
15        .and_then(|v| v.as_str())
16        .ok_or_else(|| fusabi_host::Error::host_function("format.sprintf: missing format string"))?;
17
18    let format_args = &args[1..];
19    let result = format_string(format_str, format_args)?;
20
21    Ok(Value::String(result))
22}
23
24/// Simple template string substitution.
25pub fn template(
26    args: &[Value],
27    _ctx: &ExecutionContext,
28) -> fusabi_host::Result<Value> {
29    let template_str = args
30        .first()
31        .and_then(|v| v.as_str())
32        .ok_or_else(|| fusabi_host::Error::host_function("format.template: missing template string"))?;
33
34    let values = args
35        .get(1)
36        .and_then(|v| v.as_map())
37        .ok_or_else(|| fusabi_host::Error::host_function("format.template: missing values map"))?;
38
39    let mut result = template_str.to_string();
40
41    for (key, value) in values {
42        let placeholder = format!("{{{{{}}}}}", key); // {{key}}
43        let replacement = value_to_string(value);
44        result = result.replace(&placeholder, &replacement);
45    }
46
47    Ok(Value::String(result))
48}
49
50/// Encode a value to JSON string.
51pub fn json_encode(
52    args: &[Value],
53    _ctx: &ExecutionContext,
54) -> fusabi_host::Result<Value> {
55    let value = args
56        .first()
57        .ok_or_else(|| fusabi_host::Error::host_function("format.json_encode: missing value"))?;
58
59    #[cfg(feature = "serde-support")]
60    {
61        let json = value.to_json_string();
62        Ok(Value::String(json))
63    }
64
65    #[cfg(not(feature = "serde-support"))]
66    {
67        // Simple serialization without serde
68        let json = value_to_json_simple(value);
69        Ok(Value::String(json))
70    }
71}
72
73/// Decode a JSON string to a value.
74pub fn json_decode(
75    args: &[Value],
76    _ctx: &ExecutionContext,
77) -> fusabi_host::Result<Value> {
78    let json_str = args
79        .first()
80        .and_then(|v| v.as_str())
81        .ok_or_else(|| fusabi_host::Error::host_function("format.json_decode: missing JSON string"))?;
82
83    #[cfg(feature = "serde-support")]
84    {
85        Value::from_json_str(json_str)
86            .map_err(|e| fusabi_host::Error::host_function(format!("format.json_decode: {}", e)))
87    }
88
89    #[cfg(not(feature = "serde-support"))]
90    {
91        // Simple parsing without serde (very limited)
92        Err(fusabi_host::Error::host_function("json_decode requires serde-support feature"))
93    }
94}
95
96// Helper functions
97
98fn format_string(format_str: &str, args: &[Value]) -> fusabi_host::Result<String> {
99    let mut result = String::new();
100    let mut chars = format_str.chars().peekable();
101    let mut arg_index = 0;
102
103    while let Some(c) = chars.next() {
104        if c == '%' {
105            if let Some(&next) = chars.peek() {
106                match next {
107                    '%' => {
108                        result.push('%');
109                        chars.next();
110                    }
111                    's' => {
112                        chars.next();
113                        let arg = args.get(arg_index).ok_or_else(|| {
114                            fusabi_host::Error::host_function("format.sprintf: not enough arguments")
115                        })?;
116                        result.push_str(&value_to_string(arg));
117                        arg_index += 1;
118                    }
119                    'd' | 'i' => {
120                        chars.next();
121                        let arg = args.get(arg_index).ok_or_else(|| {
122                            fusabi_host::Error::host_function("format.sprintf: not enough arguments")
123                        })?;
124                        if let Some(n) = arg.as_int() {
125                            result.push_str(&n.to_string());
126                        } else {
127                            result.push_str(&value_to_string(arg));
128                        }
129                        arg_index += 1;
130                    }
131                    'f' => {
132                        chars.next();
133                        let arg = args.get(arg_index).ok_or_else(|| {
134                            fusabi_host::Error::host_function("format.sprintf: not enough arguments")
135                        })?;
136                        if let Some(f) = arg.as_float() {
137                            result.push_str(&f.to_string());
138                        } else {
139                            result.push_str(&value_to_string(arg));
140                        }
141                        arg_index += 1;
142                    }
143                    _ => {
144                        result.push(c);
145                    }
146                }
147            } else {
148                result.push(c);
149            }
150        } else {
151            result.push(c);
152        }
153    }
154
155    Ok(result)
156}
157
158fn value_to_string(value: &Value) -> String {
159    match value {
160        Value::Null => "null".to_string(),
161        Value::Bool(b) => b.to_string(),
162        Value::Int(i) => i.to_string(),
163        Value::Float(f) => f.to_string(),
164        Value::String(s) => s.clone(),
165        Value::List(l) => {
166            let items: Vec<String> = l.iter().map(value_to_string).collect();
167            format!("[{}]", items.join(", "))
168        }
169        Value::Map(m) => {
170            let items: Vec<String> = m
171                .iter()
172                .map(|(k, v)| format!("{}: {}", k, value_to_string(v)))
173                .collect();
174            format!("{{{}}}", items.join(", "))
175        }
176        _ => format!("{}", value),
177    }
178}
179
180fn value_to_json_simple(value: &Value) -> String {
181    match value {
182        Value::Null => "null".to_string(),
183        Value::Bool(b) => b.to_string(),
184        Value::Int(i) => i.to_string(),
185        Value::Float(f) => f.to_string(),
186        Value::String(s) => format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\"")),
187        Value::List(l) => {
188            let items: Vec<String> = l.iter().map(value_to_json_simple).collect();
189            format!("[{}]", items.join(","))
190        }
191        Value::Map(m) => {
192            let items: Vec<String> = m
193                .iter()
194                .map(|(k, v)| format!("\"{}\":{}", k, value_to_json_simple(v)))
195                .collect();
196            format!("{{{}}}", items.join(","))
197        }
198        _ => "null".to_string(),
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205    use fusabi_host::Capabilities;
206    use fusabi_host::{Sandbox, SandboxConfig};
207    use fusabi_host::Limits;
208
209    fn create_test_ctx() -> ExecutionContext {
210        let sandbox = Sandbox::new(SandboxConfig::default()).unwrap();
211        ExecutionContext::new(1, Capabilities::none(), Limits::default(), sandbox)
212    }
213
214    #[test]
215    fn test_sprintf() {
216        let ctx = create_test_ctx();
217
218        let result = sprintf(&[
219            Value::String("Hello, %s! You have %d messages.".into()),
220            Value::String("Alice".into()),
221            Value::Int(5),
222        ], &ctx).unwrap();
223
224        assert_eq!(result.as_str().unwrap(), "Hello, Alice! You have 5 messages.");
225    }
226
227    #[test]
228    fn test_sprintf_float() {
229        let ctx = create_test_ctx();
230
231        let result = sprintf(&[
232            Value::String("Pi is approximately %f".into()),
233            Value::Float(3.14159),
234        ], &ctx).unwrap();
235
236        assert!(result.as_str().unwrap().contains("3.14"));
237    }
238
239    #[test]
240    fn test_template() {
241        let ctx = create_test_ctx();
242
243        let mut values = std::collections::HashMap::new();
244        values.insert("name".to_string(), Value::String("Bob".into()));
245        values.insert("count".to_string(), Value::Int(3));
246
247        let result = template(&[
248            Value::String("Hello, {{name}}! You have {{count}} items.".into()),
249            Value::Map(values),
250        ], &ctx).unwrap();
251
252        assert_eq!(result.as_str().unwrap(), "Hello, Bob! You have 3 items.");
253    }
254
255    #[test]
256    fn test_json_encode() {
257        let ctx = create_test_ctx();
258
259        let result = json_encode(&[Value::Int(42)], &ctx).unwrap();
260        assert_eq!(result.as_str().unwrap(), "42");
261
262        let result = json_encode(&[Value::String("hello".into())], &ctx).unwrap();
263        assert!(result.as_str().unwrap().contains("hello"));
264    }
265}