Skip to main content

mailang_stdlib/stdlib/
json.rs

1//! Minimal JSON encode/decode (`json_parse` / `json_stringify`).
2//!
3//! Hand-rolled parser — no external crates. Supports objects, arrays, strings
4//! (with common escapes and `\uXXXX`), numbers, `true`/`false`/`null`.
5
6use core::cell::RefCell;
7use mailang_bytecode::Value;
8use std::rc::Rc;
9
10/// `json_parse(s)` → map/array/str/num/bool/null
11pub fn builtin_json_parse(args: &[Value]) -> Result<Value, String> {
12    let s = match args.first() {
13        Some(Value::Str(s)) => s.to_string(),
14        _ => return Err("json_parse(s): s must be string".into()),
15    };
16    let mut p = Parser::new(&s);
17    p.skip_ws();
18    let v = p.parse_value()?;
19    p.skip_ws();
20    if p.pos != p.bytes.len() {
21        return Err(format!("json_parse: trailing data at {}", p.pos));
22    }
23    Ok(v)
24}
25
26/// `json_stringify(v)` → str (compact JSON)
27pub fn builtin_json_stringify(args: &[Value]) -> Result<Value, String> {
28    let v = args.first().ok_or("json_stringify(v): missing value")?;
29    let mut out = String::new();
30    write_json(v, &mut out)?;
31    Ok(Value::Str(out.into()))
32}
33
34fn write_json(v: &Value, out: &mut String) -> Result<(), String> {
35    match v {
36        Value::Null => out.push_str("null"),
37        Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
38        Value::Int(n) => out.push_str(&n.to_string()),
39        Value::Float(n) => {
40            if n.fract() == 0.0 && n.is_finite() && n.abs() < 1e15 {
41                out.push_str(&format!("{}", *n as i64));
42            } else {
43                out.push_str(&n.to_string());
44            }
45        }
46        Value::Str(s) => write_json_string(s, out),
47        Value::Char(c) => write_json_string(&c.to_string(), out),
48        Value::Array(a) => {
49            out.push('[');
50            for (i, item) in a.borrow().iter().enumerate() {
51                if i > 0 {
52                    out.push(',');
53                }
54                write_json(item, out)?;
55            }
56            out.push(']');
57        }
58        Value::Tuple(a) => {
59            out.push('[');
60            for (i, item) in a.borrow().iter().enumerate() {
61                if i > 0 {
62                    out.push(',');
63                }
64                write_json(item, out)?;
65            }
66            out.push(']');
67        }
68        Value::Map(m) => {
69            out.push('{');
70            for (i, (k, val)) in m.borrow().iter().enumerate() {
71                if i > 0 {
72                    out.push(',');
73                }
74                match k {
75                    Value::Str(s) => write_json_string(s, out),
76                    Value::Char(c) => write_json_string(&c.to_string(), out),
77                    Value::Int(n) => write_json_string(&n.to_string(), out),
78                    Value::Bool(b) => write_json_string(&b.to_string(), out),
79                    other => {
80                        return Err(format!("json_stringify: unsupported map key {:?}", other))
81                    }
82                }
83                out.push(':');
84                write_json(val, out)?;
85            }
86            out.push('}');
87        }
88        Value::Ok(inner) | Value::Some(inner) => write_json(inner, out)?,
89        other => return Err(format!("json_stringify: unsupported type {:?}", other)),
90    }
91    Ok(())
92}
93
94fn write_json_string(s: &str, out: &mut String) {
95    out.push('"');
96    for c in s.chars() {
97        match c {
98            '"' => out.push_str("\\\""),
99            '\\' => out.push_str("\\\\"),
100            '\n' => out.push_str("\\n"),
101            '\r' => out.push_str("\\r"),
102            '\t' => out.push_str("\\t"),
103            '\u{08}' => out.push_str("\\b"),
104            '\u{0c}' => out.push_str("\\f"),
105            c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
106            c => out.push(c),
107        }
108    }
109    out.push('"');
110}
111
112struct Parser<'a> {
113    bytes: &'a [u8],
114    pos: usize,
115}
116
117impl<'a> Parser<'a> {
118    fn new(s: &'a str) -> Self {
119        Self {
120            bytes: s.as_bytes(),
121            pos: 0,
122        }
123    }
124
125    fn peek(&self) -> Option<u8> {
126        self.bytes.get(self.pos).copied()
127    }
128
129    fn skip_ws(&mut self) {
130        while matches!(self.peek(), Some(b' ' | b'\t' | b'\n' | b'\r')) {
131            self.pos += 1;
132        }
133    }
134
135    fn expect(&mut self, b: u8) -> Result<(), String> {
136        if self.peek() == Some(b) {
137            self.pos += 1;
138            Ok(())
139        } else {
140            Err(format!(
141                "json_parse: expected '{}' at {}",
142                b as char, self.pos
143            ))
144        }
145    }
146
147    fn parse_value(&mut self) -> Result<Value, String> {
148        match self.peek() {
149            Some(b'{') => self.parse_object(),
150            Some(b'[') => self.parse_array(),
151            Some(b'"') => Ok(Value::Str(self.parse_string()?.into())),
152            Some(b't') => self.parse_lit("true", Value::Bool(true)),
153            Some(b'f') => self.parse_lit("false", Value::Bool(false)),
154            Some(b'n') => self.parse_lit("null", Value::Null),
155            Some(c) if c == b'-' || c.is_ascii_digit() => self.parse_number(),
156            other => Err(format!(
157                "json_parse: unexpected {:?} at {}",
158                other, self.pos
159            )),
160        }
161    }
162
163    fn parse_lit(&mut self, lit: &str, v: Value) -> Result<Value, String> {
164        if self.bytes[self.pos..].starts_with(lit.as_bytes()) {
165            self.pos += lit.len();
166            Ok(v)
167        } else {
168            Err(format!("json_parse: expected '{}' at {}", lit, self.pos))
169        }
170    }
171
172    fn parse_object(&mut self) -> Result<Value, String> {
173        self.expect(b'{')?;
174        let mut entries: Vec<(Value, Value)> = Vec::new();
175        self.skip_ws();
176        if self.peek() == Some(b'}') {
177            self.pos += 1;
178            return Ok(Value::Map(Rc::new(RefCell::new(entries))));
179        }
180        loop {
181            self.skip_ws();
182            let key = Value::Str(self.parse_string()?.into());
183            self.skip_ws();
184            self.expect(b':')?;
185            self.skip_ws();
186            let val = self.parse_value()?;
187            entries.push((key, val));
188            self.skip_ws();
189            match self.peek() {
190                Some(b',') => {
191                    self.pos += 1;
192                }
193                Some(b'}') => {
194                    self.pos += 1;
195                    break;
196                }
197                other => {
198                    return Err(format!(
199                        "json_parse: expected ',' or '}}' at {} (got {:?})",
200                        self.pos, other
201                    ))
202                }
203            }
204        }
205        Ok(Value::Map(Rc::new(RefCell::new(entries))))
206    }
207
208    fn parse_array(&mut self) -> Result<Value, String> {
209        self.expect(b'[')?;
210        let mut items: Vec<Value> = Vec::new();
211        self.skip_ws();
212        if self.peek() == Some(b']') {
213            self.pos += 1;
214            return Ok(Value::Array(Rc::new(RefCell::new(items))));
215        }
216        loop {
217            self.skip_ws();
218            items.push(self.parse_value()?);
219            self.skip_ws();
220            match self.peek() {
221                Some(b',') => {
222                    self.pos += 1;
223                }
224                Some(b']') => {
225                    self.pos += 1;
226                    break;
227                }
228                other => {
229                    return Err(format!(
230                        "json_parse: expected ',' or ']' at {} (got {:?})",
231                        self.pos, other
232                    ))
233                }
234            }
235        }
236        Ok(Value::Array(Rc::new(RefCell::new(items))))
237    }
238
239    fn parse_string(&mut self) -> Result<String, String> {
240        self.expect(b'"')?;
241        let mut s = String::new();
242        loop {
243            let c = self.peek().ok_or("json_parse: unterminated string")?;
244            self.pos += 1;
245            match c {
246                b'"' => break,
247                b'\\' => {
248                    let e = self.peek().ok_or("json_parse: bad escape")?;
249                    self.pos += 1;
250                    match e {
251                        b'"' => s.push('"'),
252                        b'\\' => s.push('\\'),
253                        b'/' => s.push('/'),
254                        b'n' => s.push('\n'),
255                        b'r' => s.push('\r'),
256                        b't' => s.push('\t'),
257                        b'b' => s.push('\u{08}'),
258                        b'f' => s.push('\u{0c}'),
259                        b'u' => {
260                            let hex = self
261                                .bytes
262                                .get(self.pos..self.pos + 4)
263                                .ok_or("json_parse: short \\u")?;
264                            self.pos += 4;
265                            let code = u16::from_str_radix(
266                                std::str::from_utf8(hex).map_err(|e| e.to_string())?,
267                                16,
268                            )
269                            .map_err(|e| e.to_string())?;
270                            // Basic BMP only (no surrogate pairs).
271                            s.push(char::from_u32(code as u32).unwrap_or('\u{fffd}'));
272                        }
273                        other => return Err(format!("json_parse: bad escape \\{}", other as char)),
274                    }
275                }
276                // Multi-byte UTF-8: copy the lead byte and its continuation bytes.
277                c if c >= 0x80 => {
278                    let start = self.pos - 1;
279                    let extra = match c {
280                        0xC0..=0xDF => 1,
281                        0xE0..=0xEF => 2,
282                        _ => 3,
283                    };
284                    self.pos = start + 1 + extra;
285                    let chunk = self
286                        .bytes
287                        .get(start..self.pos)
288                        .ok_or("json_parse: bad utf-8")?;
289                    s.push_str(std::str::from_utf8(chunk).map_err(|e| e.to_string())?);
290                }
291                c => s.push(c as char),
292            }
293        }
294        Ok(s)
295    }
296
297    fn parse_number(&mut self) -> Result<Value, String> {
298        let start = self.pos;
299        if self.peek() == Some(b'-') {
300            self.pos += 1;
301        }
302        while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
303            self.pos += 1;
304        }
305        let mut is_float = false;
306        if self.peek() == Some(b'.') {
307            is_float = true;
308            self.pos += 1;
309            while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
310                self.pos += 1;
311            }
312        }
313        if matches!(self.peek(), Some(b'e' | b'E')) {
314            is_float = true;
315            self.pos += 1;
316            if matches!(self.peek(), Some(b'+' | b'-')) {
317                self.pos += 1;
318            }
319            while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
320                self.pos += 1;
321            }
322        }
323        let text = std::str::from_utf8(&self.bytes[start..self.pos]).map_err(|e| e.to_string())?;
324        if text.is_empty() || text == "-" {
325            return Err(format!("json_parse: bad number at {}", start));
326        }
327        if !is_float {
328            if let Ok(n) = text.parse::<i64>() {
329                return Ok(Value::Int(n));
330            }
331        }
332        text.parse::<f64>()
333            .map(Value::Float)
334            .map_err(|e| e.to_string())
335    }
336}
337
338#[cfg(test)]
339mod tests {
340    use super::*;
341
342    fn s(x: &str) -> Value {
343        Value::Str(x.into())
344    }
345
346    #[test]
347    fn parse_scalars() {
348        assert_eq!(builtin_json_parse(&[s("null")]).unwrap(), Value::Null);
349        assert_eq!(builtin_json_parse(&[s("true")]).unwrap(), Value::Bool(true));
350        assert_eq!(
351            builtin_json_parse(&[s("false")]).unwrap(),
352            Value::Bool(false)
353        );
354        assert_eq!(builtin_json_parse(&[s("42")]).unwrap(), Value::Int(42));
355        assert_eq!(builtin_json_parse(&[s("-7")]).unwrap(), Value::Int(-7));
356        assert_eq!(builtin_json_parse(&[s("3.5")]).unwrap(), Value::Float(3.5));
357        assert_eq!(builtin_json_parse(&[s("\"hi\"")]).unwrap(), s("hi"));
358    }
359
360    #[test]
361    fn parse_nested_and_escapes() {
362        let v = builtin_json_parse(&[s(r#"{"a":[1,2,{"b":"x\ny"}],"c":null}"#)]).unwrap();
363        let out = builtin_json_stringify(std::slice::from_ref(&v)).unwrap();
364        assert_eq!(out, s(r#"{"a":[1,2,{"b":"x\ny"}],"c":null}"#));
365    }
366
367    #[test]
368    fn stringify_roundtrip() {
369        let src = r#"[{"k":1,"ok":true},{"n":-2.5,"s":"q\"z"}]"#;
370        let v = builtin_json_parse(&[s(src)]).unwrap();
371        let out = builtin_json_stringify(&[v]).unwrap();
372        assert_eq!(out, s(src));
373    }
374
375    #[test]
376    fn roundtrip_map_from_rust() {
377        let m = Value::Map(Rc::new(RefCell::new(vec![
378            (s("x"), Value::Int(1)),
379            (
380                s("y"),
381                Value::Array(Rc::new(RefCell::new(vec![Value::Bool(false), Value::Null]))),
382            ),
383        ])));
384        let text = builtin_json_stringify(&[m]).unwrap();
385        assert_eq!(text, s(r#"{"x":1,"y":[false,null]}"#));
386        let back = builtin_json_parse(&[text]).unwrap();
387        let again = builtin_json_stringify(&[back]).unwrap();
388        assert_eq!(again, s(r#"{"x":1,"y":[false,null]}"#));
389    }
390
391    #[test]
392    fn parse_rejects_trailing() {
393        assert!(builtin_json_parse(&[s("1 2")]).is_err());
394    }
395}