1use std::collections::HashMap;
4
5use nodedb_types::Value;
6
7pub 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
22pub 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 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 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; 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; if *pos < chars.len() && chars[*pos] == '\'' {
107 s.push('\'');
108 *pos += 1;
109 } else {
110 break; }
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; 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 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; 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; break;
165 }
166 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; }
177 }
178 Ok(items)
179}
180
181fn parse_object(chars: &[char], pos: &mut usize) -> Result<HashMap<String, Value>, String> {
182 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; 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; break;
199 }
200 if chars[*pos] == ',' {
202 *pos += 1;
203 continue;
204 }
205
206 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 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; 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 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 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}