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