1use std::collections::HashMap;
6
7use nodedb_types::Value;
8
9use crate::error::SqlError;
10
11pub fn parse_object_literal(s: &str) -> Option<Result<HashMap<String, Value>, SqlError>> {
17 let trimmed = s.trim();
18 if !trimmed.starts_with('{') {
19 return None;
20 }
21 let chars: Vec<char> = trimmed.chars().collect();
22 let mut pos = 0;
23 Some(parse_object(&chars, &mut pos))
24}
25
26pub fn parse_object_literal_array(
32 s: &str,
33) -> Option<Result<Vec<HashMap<String, Value>>, SqlError>> {
34 let trimmed = s.trim();
35 if !trimmed.starts_with('[') {
36 return None;
37 }
38 let chars: Vec<char> = trimmed.chars().collect();
39 let mut pos = 0;
40
41 pos += 1;
43 let mut objects = Vec::new();
44 loop {
45 skip_ws(&chars, &mut pos);
46 if pos >= chars.len() {
47 return Some(Err(SqlError::Parse {
48 detail: "unterminated array of objects".to_string(),
49 }));
50 }
51 if chars[pos] == ']' {
52 break;
53 }
54 if chars[pos] == ',' {
55 pos += 1;
56 continue;
57 }
58 if chars[pos] != '{' {
59 return Some(Err(SqlError::Parse {
60 detail: format!("expected '{{' at position {pos}, found '{}'", chars[pos]),
61 }));
62 }
63 match parse_object(&chars, &mut pos) {
64 Ok(obj) => objects.push(obj),
65 Err(e) => return Some(Err(e)),
66 }
67 skip_ws(&chars, &mut pos);
68 if pos < chars.len() && chars[pos] == ',' {
69 pos += 1;
70 }
71 }
72 Some(Ok(objects))
73}
74
75fn skip_ws(chars: &[char], pos: &mut usize) {
76 while *pos < chars.len() && chars[*pos].is_ascii_whitespace() {
77 *pos += 1;
78 }
79}
80
81fn parse_ident(chars: &[char], pos: &mut usize) -> String {
82 let mut s = String::new();
83 while *pos < chars.len() {
84 let c = chars[*pos];
85 if c.is_ascii_alphanumeric() || c == '_' || c == '.' {
86 s.push(c);
87 *pos += 1;
88 } else {
89 break;
90 }
91 }
92 s
93}
94
95fn parse_string(chars: &[char], pos: &mut usize) -> Result<String, SqlError> {
96 if *pos >= chars.len() || chars[*pos] != '\'' {
98 return Err(SqlError::Parse {
99 detail: format!(
100 "expected single quote at position {}, found {:?}",
101 pos,
102 chars.get(*pos)
103 ),
104 });
105 }
106 *pos += 1; let mut s = String::new();
108 loop {
109 if *pos >= chars.len() {
110 return Err(SqlError::Parse {
111 detail: "unterminated string literal".to_string(),
112 });
113 }
114 if chars[*pos] == '\'' {
115 *pos += 1; if *pos < chars.len() && chars[*pos] == '\'' {
118 s.push('\'');
119 *pos += 1;
120 } else {
121 break; }
123 } else {
124 s.push(chars[*pos]);
125 *pos += 1;
126 }
127 }
128 Ok(s)
129}
130
131fn parse_double_quoted_string(chars: &[char], pos: &mut usize) -> Result<String, SqlError> {
132 if *pos >= chars.len() || chars[*pos] != '"' {
133 return Err(SqlError::Parse {
134 detail: format!(
135 "expected double quote at position {}, found {:?}",
136 pos,
137 chars.get(*pos)
138 ),
139 });
140 }
141 *pos += 1;
142 let mut s = String::new();
143 loop {
144 if *pos >= chars.len() {
145 return Err(SqlError::Parse {
146 detail: "unterminated double-quoted string literal".to_string(),
147 });
148 }
149 match chars[*pos] {
150 '"' => {
151 *pos += 1;
152 if *pos < chars.len() && chars[*pos] == '"' {
153 s.push('"');
154 *pos += 1;
155 } else {
156 break;
157 }
158 }
159 c => {
160 s.push(c);
161 *pos += 1;
162 }
163 }
164 }
165 Ok(s)
166}
167
168fn parse_number(chars: &[char], pos: &mut usize) -> Result<Value, SqlError> {
169 let start = *pos;
170 if *pos < chars.len() && chars[*pos] == '-' {
171 *pos += 1;
172 }
173 while *pos < chars.len() && chars[*pos].is_ascii_digit() {
174 *pos += 1;
175 }
176 let is_float = *pos < chars.len() && chars[*pos] == '.';
177 if is_float {
178 *pos += 1; while *pos < chars.len() && chars[*pos].is_ascii_digit() {
180 *pos += 1;
181 }
182 }
183 let raw: String = chars[start..*pos].iter().collect();
184 if is_float {
185 raw.parse::<f64>()
186 .map(Value::Float)
187 .map_err(|_| SqlError::Parse {
188 detail: format!("invalid float: {raw}"),
189 })
190 } else {
191 raw.parse::<i64>()
192 .map(Value::Integer)
193 .map_err(|_| SqlError::Parse {
194 detail: format!("invalid integer: {raw}"),
195 })
196 }
197}
198
199fn parse_array(chars: &[char], pos: &mut usize) -> Result<Vec<Value>, SqlError> {
200 if *pos >= chars.len() || chars[*pos] != '[' {
202 return Err(SqlError::Parse {
203 detail: format!(
204 "expected '[' at position {pos}, found {:?}",
205 chars.get(*pos)
206 ),
207 });
208 }
209 *pos += 1; let mut items = Vec::new();
211 loop {
212 skip_ws(chars, pos);
213 if *pos >= chars.len() {
214 return Err(SqlError::Parse {
215 detail: "unterminated array literal".to_string(),
216 });
217 }
218 if chars[*pos] == ']' {
219 *pos += 1; break;
221 }
222 if chars[*pos] == ',' {
224 *pos += 1;
225 continue;
226 }
227 let val = parse_value(chars, pos)?;
228 items.push(val);
229 skip_ws(chars, pos);
230 if *pos < chars.len() && chars[*pos] == ',' {
231 *pos += 1; }
233 }
234 Ok(items)
235}
236
237fn parse_object(chars: &[char], pos: &mut usize) -> Result<HashMap<String, Value>, SqlError> {
238 if *pos >= chars.len() || chars[*pos] != '{' {
240 return Err(SqlError::Parse {
241 detail: format!(
242 "expected '{{' at position {pos}, found {:?}",
243 chars.get(*pos)
244 ),
245 });
246 }
247 *pos += 1; let mut map = HashMap::new();
249 loop {
250 skip_ws(chars, pos);
251 if *pos >= chars.len() {
252 return Err(SqlError::Parse {
253 detail: "unterminated object literal".to_string(),
254 });
255 }
256 if chars[*pos] == '}' {
257 *pos += 1; break;
259 }
260 if chars[*pos] == ',' {
262 *pos += 1;
263 continue;
264 }
265
266 skip_ws(chars, pos);
268 if *pos >= chars.len() {
269 return Err(SqlError::Parse {
270 detail: "expected key, reached end of input".to_string(),
271 });
272 }
273 let key = if chars[*pos] == '"' {
274 parse_double_quoted_string(chars, pos)?
275 } else {
276 let first = chars[*pos];
277 if !(first.is_ascii_alphabetic() || first == '_') {
278 return Err(SqlError::Parse {
279 detail: format!("expected identifier key at position {pos}, found '{first}'"),
280 });
281 }
282 parse_ident(chars, pos)
283 };
284 if key.is_empty() {
285 return Err(SqlError::Parse {
286 detail: format!("expected non-empty key at position {pos}"),
287 });
288 }
289
290 skip_ws(chars, pos);
292 if *pos >= chars.len() || chars[*pos] != ':' {
293 return Err(SqlError::Parse {
294 detail: format!(
295 "expected ':' after key '{key}' at position {pos}, found {:?}",
296 chars.get(*pos)
297 ),
298 });
299 }
300 *pos += 1; skip_ws(chars, pos);
304 if *pos >= chars.len() {
305 return Err(SqlError::Parse {
306 detail: format!("expected value for key '{key}', reached end of input"),
307 });
308 }
309 if chars[*pos] == '}' || chars[*pos] == ',' {
310 return Err(SqlError::Parse {
311 detail: format!("expected value for key '{key}', found '{}'", chars[*pos]),
312 });
313 }
314 let val = parse_value(chars, pos)?;
315 map.insert(key, val);
316
317 skip_ws(chars, pos);
319 if *pos < chars.len() && chars[*pos] == ',' {
320 *pos += 1;
321 }
322 }
323 Ok(map)
324}
325
326fn parse_value(chars: &[char], pos: &mut usize) -> Result<Value, SqlError> {
327 skip_ws(chars, pos);
328 if *pos >= chars.len() {
329 return Err(SqlError::Parse {
330 detail: "unexpected end of input while parsing value".to_string(),
331 });
332 }
333 match chars[*pos] {
334 '\'' => parse_string(chars, pos).map(Value::String),
335 '{' => parse_object(chars, pos).map(Value::Object),
336 '[' => parse_array(chars, pos).map(Value::Array),
337 '-' | '0'..='9' => parse_number(chars, pos),
338 _ => {
339 let word = parse_ident(chars, pos);
341 match word.to_lowercase().as_str() {
342 "true" => Ok(Value::Bool(true)),
343 "false" => Ok(Value::Bool(false)),
344 "null" => Ok(Value::Null),
345 _ if word.is_empty() => Err(SqlError::Parse {
346 detail: format!("unexpected character '{}' at position {pos}", chars[*pos]),
347 }),
348 _ => Err(SqlError::Parse {
349 detail: format!("unknown bare word: '{word}'"),
350 }),
351 }
352 }
353 }
354}
355
356#[cfg(test)]
357mod tests {
358 use super::*;
359
360 fn parse(s: &str) -> HashMap<String, Value> {
361 parse_object_literal(s).unwrap().unwrap()
362 }
363
364 #[test]
365 fn simple_string_and_int() {
366 let m = parse("{ name: 'Alice', age: 30 }");
367 assert_eq!(m["name"], Value::String("Alice".to_string()));
368 assert_eq!(m["age"], Value::Integer(30));
369 }
370
371 #[test]
372 fn nested_object() {
373 let m = parse("{ addr: { city: 'NYC' } }");
374 let inner = match &m["addr"] {
375 Value::Object(o) => o,
376 _ => panic!("expected Object"),
377 };
378 assert_eq!(inner["city"], Value::String("NYC".to_string()));
379 }
380
381 #[test]
382 fn array_value() {
383 let m = parse("{ tags: ['a', 'b'] }");
384 assert_eq!(
385 m["tags"],
386 Value::Array(vec![
387 Value::String("a".to_string()),
388 Value::String("b".to_string()),
389 ])
390 );
391 }
392
393 #[test]
394 fn mixed_types() {
395 let m = parse("{ a: 'str', b: 42, c: 2.78, d: true, e: false, f: null }");
396 assert_eq!(m["a"], Value::String("str".to_string()));
397 assert_eq!(m["b"], Value::Integer(42));
398 assert_eq!(m["c"], Value::Float(2.78));
399 assert_eq!(m["d"], Value::Bool(true));
400 assert_eq!(m["e"], Value::Bool(false));
401 assert_eq!(m["f"], Value::Null);
402 }
403
404 #[test]
405 fn escaped_quotes() {
406 let m = parse("{ name: 'O''Brien' }");
407 assert_eq!(m["name"], Value::String("O'Brien".to_string()));
408 }
409
410 #[test]
411 fn empty_object() {
412 let m = parse("{ }");
413 assert!(m.is_empty());
414 }
415
416 #[test]
417 fn trailing_comma() {
418 let m = parse("{ name: 'Alice', }");
419 assert_eq!(m["name"], Value::String("Alice".to_string()));
420 }
421
422 #[test]
423 fn not_an_object_returns_none() {
424 assert!(parse_object_literal("not an object").is_none());
425 }
426
427 #[test]
428 fn missing_value_returns_err() {
429 let result = parse_object_literal("{ name: }");
430 assert!(matches!(result, Some(Err(_))));
431 }
432
433 #[test]
434 fn missing_key_returns_err() {
435 let result = parse_object_literal("{ : 'val' }");
436 assert!(matches!(result, Some(Err(_))));
437 }
438
439 #[test]
440 fn negative_numbers() {
441 let m = parse("{ x: -42, y: -2.78 }");
442 assert_eq!(m["x"], Value::Integer(-42));
443 assert_eq!(m["y"], Value::Float(-2.78));
444 }
445
446 #[test]
447 fn nested_array_in_object() {
448 let m = parse("{ data: { items: [1, 2, 3] } }");
449 let inner = match &m["data"] {
450 Value::Object(o) => o,
451 _ => panic!("expected Object"),
452 };
453 assert_eq!(
454 inner["items"],
455 Value::Array(vec![
456 Value::Integer(1),
457 Value::Integer(2),
458 Value::Integer(3),
459 ])
460 );
461 }
462
463 #[test]
464 fn dotted_key() {
465 let m = parse("{ metadata.source: 'web' }");
466 assert_eq!(m["metadata.source"], Value::String("web".to_string()));
467 }
468
469 #[test]
470 fn parse_array_of_objects() {
471 let result = parse_object_literal_array("[{ name: 'Alice' }, { name: 'Bob' }]")
472 .unwrap()
473 .unwrap();
474 assert_eq!(result.len(), 2);
475 assert_eq!(result[0]["name"], Value::String("Alice".to_string()));
476 assert_eq!(result[1]["name"], Value::String("Bob".to_string()));
477 }
478
479 #[test]
480 fn parse_array_empty() {
481 let result = parse_object_literal_array("[]").unwrap().unwrap();
482 assert!(result.is_empty());
483 }
484
485 #[test]
486 fn parse_array_not_array_returns_none() {
487 assert!(parse_object_literal_array("{ name: 'Alice' }").is_none());
488 }
489
490 #[test]
491 fn parse_array_non_object_element_returns_err() {
492 let result = parse_object_literal_array("[42]");
493 assert!(matches!(result, Some(Err(_))));
494 }
495
496 #[test]
497 fn parse_array_trailing_comma() {
498 let result = parse_object_literal_array("[{ a: 1 }, { b: 2 },]")
499 .unwrap()
500 .unwrap();
501 assert_eq!(result.len(), 2);
502 }
503}