nodedb_sql/parser/object_literal/
api.rs1use std::collections::HashMap;
7
8use nodedb_types::Value;
9
10use crate::error::SqlError;
11
12use super::scan::{parse_object, skip_ws};
13
14pub fn parse_object_literal(s: &str) -> Option<Result<HashMap<String, Value>, SqlError>> {
31 let trimmed = s.trim();
32 if !trimmed.starts_with('{') {
33 return None;
34 }
35 let chars: Vec<char> = trimmed.chars().collect();
36 let mut pos = 0;
37 Some(parse_object(&chars, &mut pos))
38}
39
40pub fn parse_object_literal_complete(s: &str) -> Option<Result<HashMap<String, Value>, SqlError>> {
47 let trimmed = s.trim();
48 if !trimmed.starts_with('{') {
49 return None;
50 }
51 let chars: Vec<char> = trimmed.chars().collect();
52 let mut pos = 0;
53 Some(
54 parse_object(&chars, &mut pos).and_then(|fields| match leftover(&chars, pos) {
55 Some(rest) => Err(trailing_input_error("object literal", &rest)),
56 None => Ok(fields),
57 }),
58 )
59}
60
61pub fn parse_object_literal_array_complete(
66 s: &str,
67) -> Option<Result<Vec<HashMap<String, Value>>, SqlError>> {
68 let trimmed = s.trim();
69 if !trimmed.starts_with('[') {
70 return None;
71 }
72 let objects = match parse_object_literal_array(trimmed)? {
73 Ok(objects) => objects,
74 Err(error) => return Some(Err(error)),
75 };
76 let chars: Vec<char> = trimmed.chars().collect();
81 let Some(end) = matching_bracket(&chars) else {
82 return Some(Err(SqlError::Parse {
83 detail: "unterminated array of objects".to_string(),
84 }));
85 };
86 Some(match leftover(&chars, end + 1) {
87 Some(rest) => Err(trailing_input_error("array of object literals", &rest)),
88 None => Ok(objects),
89 })
90}
91
92fn leftover(chars: &[char], pos: usize) -> Option<String> {
96 let rest: String = chars.get(pos..).unwrap_or_default().iter().collect();
97 let rest = rest.trim().trim_end_matches(';').trim();
98 if rest.is_empty() {
99 None
100 } else {
101 Some(rest.to_string())
102 }
103}
104
105fn trailing_input_error(what: &str, rest: &str) -> SqlError {
110 SqlError::Parse {
111 detail: format!(
112 "unexpected input after {what}: `{rest}`. The brace form takes no trailing clause — \
113 write `INSERT INTO <collection> (cols) VALUES (...) {rest}` to use it"
114 ),
115 }
116}
117
118fn matching_bracket(chars: &[char]) -> Option<usize> {
121 let mut depth = 0usize;
122 let mut in_string = false;
123 let mut i = 0;
124 while i < chars.len() {
125 match chars[i] {
126 '\'' if in_string && chars.get(i + 1) == Some(&'\'') => i += 1,
127 '\'' => in_string = !in_string,
128 '[' if !in_string => depth += 1,
129 ']' if !in_string => {
130 depth = depth.saturating_sub(1);
131 if depth == 0 {
132 return Some(i);
133 }
134 }
135 _ => {}
136 }
137 i += 1;
138 }
139 None
140}
141
142pub fn parse_object_literal_array(
148 s: &str,
149) -> Option<Result<Vec<HashMap<String, Value>>, SqlError>> {
150 let trimmed = s.trim();
151 if !trimmed.starts_with('[') {
152 return None;
153 }
154 let chars: Vec<char> = trimmed.chars().collect();
155 let mut pos = 0;
156
157 pos += 1;
159 let mut objects = Vec::new();
160 loop {
161 skip_ws(&chars, &mut pos);
162 if pos >= chars.len() {
163 return Some(Err(SqlError::Parse {
164 detail: "unterminated array of objects".to_string(),
165 }));
166 }
167 if chars[pos] == ']' {
168 break;
169 }
170 if chars[pos] == ',' {
171 pos += 1;
172 continue;
173 }
174 if chars[pos] != '{' {
175 return Some(Err(SqlError::Parse {
176 detail: format!("expected '{{' at position {pos}, found '{}'", chars[pos]),
177 }));
178 }
179 match parse_object(&chars, &mut pos) {
180 Ok(obj) => objects.push(obj),
181 Err(e) => return Some(Err(e)),
182 }
183 skip_ws(&chars, &mut pos);
184 if pos < chars.len() && chars[pos] == ',' {
185 pos += 1;
186 }
187 }
188 Some(Ok(objects))
189}