Skip to main content

nodedb_sql/parser/
object_literal.rs

1//! Parser for `{ key: value }` object literal syntax.
2
3use std::collections::HashMap;
4
5use nodedb_types::Value;
6
7/// Parse a `{ key: value, ... }` object literal into a field map.
8///
9/// Returns `None` if the input doesn't start with `{` (not an object literal).
10/// Returns `Some(Err(msg))` on parse errors (malformed object literal).
11/// Returns `Some(Ok(fields))` on success.
12pub fn parse_object_literal(s: &str) -> Option<Result<HashMap<String, Value>, String>> {
13    let trimmed = s.trim();
14    if !trimmed.starts_with('{') {
15        return None;
16    }
17    let chars: Vec<char> = trimmed.chars().collect();
18    let mut pos = 0;
19    Some(parse_object(&chars, &mut pos))
20}
21
22/// Parse `[{ ... }, { ... }]` — an array of object literals for batch insert.
23///
24/// Returns `None` if the input doesn't start with `[` (not an array literal).
25/// Returns `Some(Err(msg))` on parse errors.
26/// Returns `Some(Ok(vec))` on success — each element must be an object.
27pub fn parse_object_literal_array(s: &str) -> Option<Result<Vec<HashMap<String, Value>>, String>> {
28    let trimmed = s.trim();
29    if !trimmed.starts_with('[') {
30        return None;
31    }
32    let chars: Vec<char> = trimmed.chars().collect();
33    let mut pos = 0;
34
35    // Consume '['
36    pos += 1;
37    let mut objects = Vec::new();
38    loop {
39        skip_ws(&chars, &mut pos);
40        if pos >= chars.len() {
41            return Some(Err("unterminated array of objects".to_string()));
42        }
43        if chars[pos] == ']' {
44            break;
45        }
46        if chars[pos] == ',' {
47            pos += 1;
48            continue;
49        }
50        if chars[pos] != '{' {
51            return Some(Err(format!(
52                "expected '{{' at position {pos}, found '{}'",
53                chars[pos]
54            )));
55        }
56        match parse_object(&chars, &mut pos) {
57            Ok(obj) => objects.push(obj),
58            Err(e) => return Some(Err(e)),
59        }
60        skip_ws(&chars, &mut pos);
61        if pos < chars.len() && chars[pos] == ',' {
62            pos += 1;
63        }
64    }
65    Some(Ok(objects))
66}
67
68fn skip_ws(chars: &[char], pos: &mut usize) {
69    while *pos < chars.len() && chars[*pos].is_ascii_whitespace() {
70        *pos += 1;
71    }
72}
73
74fn parse_ident(chars: &[char], pos: &mut usize) -> String {
75    let mut s = String::new();
76    while *pos < chars.len() {
77        let c = chars[*pos];
78        if c.is_ascii_alphanumeric() || c == '_' || c == '.' {
79            s.push(c);
80            *pos += 1;
81        } else {
82            break;
83        }
84    }
85    s
86}
87
88fn parse_string(chars: &[char], pos: &mut usize) -> Result<String, String> {
89    // Expect opening single-quote
90    if *pos >= chars.len() || chars[*pos] != '\'' {
91        return Err(format!(
92            "expected single quote at position {}, found {:?}",
93            pos,
94            chars.get(*pos)
95        ));
96    }
97    *pos += 1; // consume opening quote
98    let mut s = String::new();
99    loop {
100        if *pos >= chars.len() {
101            return Err("unterminated string literal".to_string());
102        }
103        if chars[*pos] == '\'' {
104            *pos += 1; // consume quote
105            // SQL escaped quote: '' → '
106            if *pos < chars.len() && chars[*pos] == '\'' {
107                s.push('\'');
108                *pos += 1;
109            } else {
110                break; // end of string
111            }
112        } else {
113            s.push(chars[*pos]);
114            *pos += 1;
115        }
116    }
117    Ok(s)
118}
119
120fn parse_number(chars: &[char], pos: &mut usize) -> Result<Value, String> {
121    let start = *pos;
122    if *pos < chars.len() && chars[*pos] == '-' {
123        *pos += 1;
124    }
125    while *pos < chars.len() && chars[*pos].is_ascii_digit() {
126        *pos += 1;
127    }
128    let is_float = *pos < chars.len() && chars[*pos] == '.';
129    if is_float {
130        *pos += 1; // consume '.'
131        while *pos < chars.len() && chars[*pos].is_ascii_digit() {
132            *pos += 1;
133        }
134    }
135    let raw: String = chars[start..*pos].iter().collect();
136    if is_float {
137        raw.parse::<f64>()
138            .map(Value::Float)
139            .map_err(|_| format!("invalid float: {raw}"))
140    } else {
141        raw.parse::<i64>()
142            .map(Value::Integer)
143            .map_err(|_| format!("invalid integer: {raw}"))
144    }
145}
146
147fn parse_array(chars: &[char], pos: &mut usize) -> Result<Vec<Value>, String> {
148    // Expect '['
149    if *pos >= chars.len() || chars[*pos] != '[' {
150        return Err(format!(
151            "expected '[' at position {pos}, found {:?}",
152            chars.get(*pos)
153        ));
154    }
155    *pos += 1; // consume '['
156    let mut items = Vec::new();
157    loop {
158        skip_ws(chars, pos);
159        if *pos >= chars.len() {
160            return Err("unterminated array literal".to_string());
161        }
162        if chars[*pos] == ']' {
163            *pos += 1; // consume ']'
164            break;
165        }
166        // trailing comma already consumed; skip it
167        if chars[*pos] == ',' {
168            *pos += 1;
169            continue;
170        }
171        let val = parse_value(chars, pos)?;
172        items.push(val);
173        skip_ws(chars, pos);
174        if *pos < chars.len() && chars[*pos] == ',' {
175            *pos += 1; // consume ','
176        }
177    }
178    Ok(items)
179}
180
181fn parse_object(chars: &[char], pos: &mut usize) -> Result<HashMap<String, Value>, String> {
182    // Expect '{'
183    if *pos >= chars.len() || chars[*pos] != '{' {
184        return Err(format!(
185            "expected '{{' at position {pos}, found {:?}",
186            chars.get(*pos)
187        ));
188    }
189    *pos += 1; // consume '{'
190    let mut map = HashMap::new();
191    loop {
192        skip_ws(chars, pos);
193        if *pos >= chars.len() {
194            return Err("unterminated object literal".to_string());
195        }
196        if chars[*pos] == '}' {
197            *pos += 1; // consume '}'
198            break;
199        }
200        // Trailing comma: skip and re-check for '}'
201        if chars[*pos] == ',' {
202            *pos += 1;
203            continue;
204        }
205
206        // Parse key (must be an unquoted identifier)
207        skip_ws(chars, pos);
208        if *pos >= chars.len() {
209            return Err("expected key, reached end of input".to_string());
210        }
211        let first = chars[*pos];
212        if !(first.is_ascii_alphabetic() || first == '_') {
213            return Err(format!(
214                "expected identifier key at position {pos}, found '{first}'"
215            ));
216        }
217        let key = parse_ident(chars, pos);
218        if key.is_empty() {
219            return Err(format!("expected non-empty key at position {pos}"));
220        }
221
222        // Expect ':'
223        skip_ws(chars, pos);
224        if *pos >= chars.len() || chars[*pos] != ':' {
225            return Err(format!(
226                "expected ':' after key '{key}' at position {pos}, found {:?}",
227                chars.get(*pos)
228            ));
229        }
230        *pos += 1; // consume ':'
231
232        // Parse value
233        skip_ws(chars, pos);
234        if *pos >= chars.len() {
235            return Err(format!(
236                "expected value for key '{key}', reached end of input"
237            ));
238        }
239        if chars[*pos] == '}' || chars[*pos] == ',' {
240            return Err(format!(
241                "expected value for key '{key}', found '{}'",
242                chars[*pos]
243            ));
244        }
245        let val = parse_value(chars, pos)?;
246        map.insert(key, val);
247
248        // Optional comma
249        skip_ws(chars, pos);
250        if *pos < chars.len() && chars[*pos] == ',' {
251            *pos += 1;
252        }
253    }
254    Ok(map)
255}
256
257fn parse_value(chars: &[char], pos: &mut usize) -> Result<Value, String> {
258    skip_ws(chars, pos);
259    if *pos >= chars.len() {
260        return Err("unexpected end of input while parsing value".to_string());
261    }
262    match chars[*pos] {
263        '\'' => parse_string(chars, pos).map(Value::String),
264        '{' => parse_object(chars, pos).map(Value::Object),
265        '[' => parse_array(chars, pos).map(Value::Array),
266        '-' | '0'..='9' => parse_number(chars, pos),
267        _ => {
268            // bare word: true / false / null / identifier
269            let word = parse_ident(chars, pos);
270            match word.to_lowercase().as_str() {
271                "true" => Ok(Value::Bool(true)),
272                "false" => Ok(Value::Bool(false)),
273                "null" => Ok(Value::Null),
274                _ if word.is_empty() => Err(format!(
275                    "unexpected character '{}' at position {pos}",
276                    chars[*pos]
277                )),
278                _ => Err(format!("unknown bare word: '{word}'")),
279            }
280        }
281    }
282}
283
284#[cfg(test)]
285mod tests {
286    use super::*;
287
288    fn parse(s: &str) -> HashMap<String, Value> {
289        parse_object_literal(s).unwrap().unwrap()
290    }
291
292    #[test]
293    fn simple_string_and_int() {
294        let m = parse("{ name: 'Alice', age: 30 }");
295        assert_eq!(m["name"], Value::String("Alice".to_string()));
296        assert_eq!(m["age"], Value::Integer(30));
297    }
298
299    #[test]
300    fn nested_object() {
301        let m = parse("{ addr: { city: 'NYC' } }");
302        let inner = match &m["addr"] {
303            Value::Object(o) => o,
304            _ => panic!("expected Object"),
305        };
306        assert_eq!(inner["city"], Value::String("NYC".to_string()));
307    }
308
309    #[test]
310    fn array_value() {
311        let m = parse("{ tags: ['a', 'b'] }");
312        assert_eq!(
313            m["tags"],
314            Value::Array(vec![
315                Value::String("a".to_string()),
316                Value::String("b".to_string()),
317            ])
318        );
319    }
320
321    #[test]
322    fn mixed_types() {
323        let m = parse("{ a: 'str', b: 42, c: 2.78, d: true, e: false, f: null }");
324        assert_eq!(m["a"], Value::String("str".to_string()));
325        assert_eq!(m["b"], Value::Integer(42));
326        assert_eq!(m["c"], Value::Float(2.78));
327        assert_eq!(m["d"], Value::Bool(true));
328        assert_eq!(m["e"], Value::Bool(false));
329        assert_eq!(m["f"], Value::Null);
330    }
331
332    #[test]
333    fn escaped_quotes() {
334        let m = parse("{ name: 'O''Brien' }");
335        assert_eq!(m["name"], Value::String("O'Brien".to_string()));
336    }
337
338    #[test]
339    fn empty_object() {
340        let m = parse("{ }");
341        assert!(m.is_empty());
342    }
343
344    #[test]
345    fn trailing_comma() {
346        let m = parse("{ name: 'Alice', }");
347        assert_eq!(m["name"], Value::String("Alice".to_string()));
348    }
349
350    #[test]
351    fn not_an_object_returns_none() {
352        assert!(parse_object_literal("not an object").is_none());
353    }
354
355    #[test]
356    fn missing_value_returns_err() {
357        let result = parse_object_literal("{ name: }");
358        assert!(matches!(result, Some(Err(_))));
359    }
360
361    #[test]
362    fn missing_key_returns_err() {
363        let result = parse_object_literal("{ : 'val' }");
364        assert!(matches!(result, Some(Err(_))));
365    }
366
367    #[test]
368    fn negative_numbers() {
369        let m = parse("{ x: -42, y: -2.78 }");
370        assert_eq!(m["x"], Value::Integer(-42));
371        assert_eq!(m["y"], Value::Float(-2.78));
372    }
373
374    #[test]
375    fn nested_array_in_object() {
376        let m = parse("{ data: { items: [1, 2, 3] } }");
377        let inner = match &m["data"] {
378            Value::Object(o) => o,
379            _ => panic!("expected Object"),
380        };
381        assert_eq!(
382            inner["items"],
383            Value::Array(vec![
384                Value::Integer(1),
385                Value::Integer(2),
386                Value::Integer(3),
387            ])
388        );
389    }
390
391    #[test]
392    fn dotted_key() {
393        let m = parse("{ metadata.source: 'web' }");
394        assert_eq!(m["metadata.source"], Value::String("web".to_string()));
395    }
396
397    #[test]
398    fn parse_array_of_objects() {
399        let result = parse_object_literal_array("[{ name: 'Alice' }, { name: 'Bob' }]")
400            .unwrap()
401            .unwrap();
402        assert_eq!(result.len(), 2);
403        assert_eq!(result[0]["name"], Value::String("Alice".to_string()));
404        assert_eq!(result[1]["name"], Value::String("Bob".to_string()));
405    }
406
407    #[test]
408    fn parse_array_empty() {
409        let result = parse_object_literal_array("[]").unwrap().unwrap();
410        assert!(result.is_empty());
411    }
412
413    #[test]
414    fn parse_array_not_array_returns_none() {
415        assert!(parse_object_literal_array("{ name: 'Alice' }").is_none());
416    }
417
418    #[test]
419    fn parse_array_non_object_element_returns_err() {
420        let result = parse_object_literal_array("[42]");
421        assert!(matches!(result, Some(Err(_))));
422    }
423
424    #[test]
425    fn parse_array_trailing_comma() {
426        let result = parse_object_literal_array("[{ a: 1 }, { b: 2 },]")
427            .unwrap()
428            .unwrap();
429        assert_eq!(result.len(), 2);
430    }
431}