1mod strings;
7
8use serde_json::{Map, Value};
9use std::collections::HashMap;
10use strings::{parse_string_literal, skip_string};
11
12pub const MAX_DEPTH: usize = 32;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum Operation {
16 Query,
17 Mutation,
18 Subscription,
19}
20
21#[derive(Debug, PartialEq)]
22pub struct ParsedField {
23 pub operation: Operation,
24 pub name: String,
25 pub args: Map<String, Value>,
26 pub selection: Option<SelectionSet>,
27}
28
29pub type SelectionSet = HashMap<String, FieldNode>;
30
31#[derive(Debug, Clone, PartialEq)]
32pub struct FieldNode {
33 pub args: Map<String, Value>,
34 pub selection: Option<SelectionSet>,
35}
36
37pub fn parse_query(query: &str, variables: &Value) -> Result<ParsedField, String> {
38 let b = query.as_bytes();
39 let mut i = 0;
40 let n = b.len();
41
42 skip_ws(b, &mut i);
43
44 let mut operation = Operation::Query;
45 for (kw, op) in [
46 ("query".as_bytes(), Operation::Query),
47 ("mutation".as_bytes(), Operation::Mutation),
48 ("subscription".as_bytes(), Operation::Subscription),
49 ] {
50 if b[i..].starts_with(kw) {
51 let after = i + kw.len();
52 if after >= n || b[after].is_ascii_whitespace() || b[after] == b'(' || b[after] == b'{'
53 {
54 operation = op;
55 i = after;
56 break;
57 }
58 }
59 }
60 skip_ws(b, &mut i);
61
62 if i < n && is_ident_byte(b[i]) {
63 while i < n && is_ident_byte(b[i]) {
64 i += 1;
65 }
66 skip_ws(b, &mut i);
67 }
68
69 loop {
71 if i < n && b[i] == b'(' {
72 skip_parens(b, &mut i)?;
73 skip_ws(b, &mut i);
74 } else if i < n && b[i] == b'@' {
75 skip_directive(b, &mut i)?;
76 skip_ws(b, &mut i);
77 } else {
78 break;
79 }
80 }
81
82 if i >= n || b[i] != b'{' {
83 return Err("expected '{' before selection".into());
84 }
85 i += 1;
86 skip_ws(b, &mut i);
87
88 if i >= n || b[i] == b'}' {
89 return Err("empty selection set".into());
90 }
91 if b[i] == b'.' {
92 return Err("fragment spreads are not supported".into());
93 }
94
95 let field = parse_field(b, &mut i, 0, operation, variables)?;
96
97 skip_ws(b, &mut i);
98 if i >= n {
99 return Err("expected '}'".into());
100 }
101 match b[i] {
102 b'}' => {}
103 b'.' => return Err("fragment spreads are not supported".into()),
104 _ => return Err("multiple top-level fields not supported".into()),
105 }
106 Ok(field)
107}
108
109fn parse_field(
112 b: &[u8],
113 i: &mut usize,
114 depth: usize,
115 operation: Operation,
116 variables: &Value,
117) -> Result<ParsedField, String> {
118 if depth > MAX_DEPTH {
119 return Err("selection too deep".into());
120 }
121 skip_ws(b, i);
122 let start = *i;
123 while *i < b.len() && is_ident_byte(b[*i]) {
124 *i += 1;
125 }
126 if *i == start {
127 return Err("expected field name".into());
128 }
129 let name = String::from_utf8_lossy(&b[start..*i]).into_owned();
130 skip_ws(b, i);
131
132 if *i < b.len() && b[*i] == b':' {
133 return Err("aliases not supported".into());
134 }
135
136 let mut args = Map::new();
137 if *i < b.len() && b[*i] == b'(' {
138 let args_start = *i;
139 skip_parens(b, i)?;
140 args = parse_args(&b[args_start..*i], variables, depth)?;
141 skip_ws(b, i);
142 }
143
144 while *i < b.len() && b[*i] == b'@' {
146 skip_directive(b, i)?;
147 skip_ws(b, i);
148 }
149
150 let selection = if *i < b.len() && b[*i] == b'{' {
151 Some(parse_selection_set(b, i, depth + 1, variables)?)
152 } else {
153 None
154 };
155 Ok(ParsedField {
156 operation,
157 name,
158 args,
159 selection,
160 })
161}
162
163fn parse_selection_set(
164 b: &[u8],
165 i: &mut usize,
166 depth: usize,
167 variables: &Value,
168) -> Result<SelectionSet, String> {
169 if depth > MAX_DEPTH {
170 return Err("selection too deep".into());
171 }
172 *i += 1;
174 skip_ws(b, i);
175 if *i >= b.len() || b[*i] == b'}' {
176 return Err("empty selection set".into());
177 }
178 let mut set = SelectionSet::new();
179 loop {
180 if *i >= b.len() {
181 return Err("expected '}'".into());
182 }
183 match b[*i] {
184 b'}' => {
185 *i += 1;
186 return Ok(set);
187 }
188 b'.' => return Err("fragment spreads are not supported".into()),
189 _ => {
190 let f = parse_field(b, i, depth, Operation::Query, variables)?;
191 set.insert(
192 f.name,
193 FieldNode {
194 args: f.args,
195 selection: f.selection,
196 },
197 );
198 skip_ws(b, i);
199 }
200 }
201 }
202}
203
204fn skip_directive(b: &[u8], i: &mut usize) -> Result<(), String> {
205 *i += 1; while *i < b.len() && is_ident_byte(b[*i]) {
207 *i += 1;
208 }
209 skip_ws(b, i);
210 if *i < b.len() && b[*i] == b'(' {
211 skip_parens(b, i)?;
212 }
213 Ok(())
214}
215
216fn parse_args(raw: &[u8], variables: &Value, depth: usize) -> Result<Map<String, Value>, String> {
218 let b = raw;
219 let mut i = 0;
220 let n = b.len();
221 let mut args = Map::new();
222 skip_ws(b, &mut i);
223 if i >= n || b[i] != b'(' {
224 return Err("expected '(' in arguments".into());
225 }
226 i += 1;
227 loop {
228 skip_ws(b, &mut i);
229 if i >= n {
230 return Err("expected ')' in arguments".into());
231 }
232 if b[i] == b')' {
233 return Ok(args);
234 }
235 let start = i;
236 while i < n && is_ident_byte(b[i]) {
237 i += 1;
238 }
239 if i == start {
240 return Err("expected argument name".into());
241 }
242 let name = String::from_utf8_lossy(&b[start..i]).into_owned();
243 skip_ws(b, &mut i);
244 if i >= n || b[i] != b':' {
245 return Err(format!("expected ':' after argument '{name}'"));
246 }
247 i += 1;
248 skip_ws(b, &mut i);
249 let value = parse_value(b, &mut i, variables, depth)?;
250 if args.contains_key(&name) {
251 return Err(format!("duplicate argument '{name}'"));
252 }
253 args.insert(name, value);
254 skip_ws(b, &mut i);
255 if i < n && b[i] == b',' {
256 i += 1;
257 }
258 }
259}
260
261pub fn parse_value(
263 b: &[u8],
264 i: &mut usize,
265 variables: &Value,
266 depth: usize,
267) -> Result<Value, String> {
268 if depth > MAX_DEPTH {
269 return Err("selection too deep".into());
270 }
271 skip_ws(b, i);
272 if *i >= b.len() {
273 return Err("expected value".into());
274 }
275 match b[*i] {
276 b'"' | b'\'' => {
277 if *i + 2 < b.len() && b[*i + 1] == b[*i] && b[*i + 2] == b[*i] {
279 return Err("block string value not supported".into());
280 }
281 let start = *i;
282 skip_string(b, i)?;
283 parse_string_literal(&b[start..*i])
284 }
285 b'[' => {
286 let start = *i;
287 skip_balanced(b, i, b'[', b']')?;
288 parse_list(&b[start..*i], variables, depth)
289 }
290 b'{' => {
291 let start = *i;
292 skip_balanced(b, i, b'{', b'}')?;
293 parse_input_object(&b[start..*i], variables, depth)
294 }
295 b'$' => {
296 *i += 1;
297 let start = *i;
298 while *i < b.len() && is_ident_byte(b[*i]) {
299 *i += 1;
300 }
301 let name = String::from_utf8_lossy(&b[start..*i]).into_owned();
302 match variables {
303 Value::Object(m) => m
304 .get(&name)
305 .cloned()
306 .ok_or_else(|| format!("variable '{name}' is not provided")),
307 _ => Err(format!("variable '{name}' is not provided")),
308 }
309 }
310 _ => {
311 let start = *i;
313 while *i < b.len()
314 && !b[*i].is_ascii_whitespace()
315 && !matches!(b[*i], b',' | b')' | b']' | b'}')
316 {
317 *i += 1;
318 }
319 let token = &b[start..*i];
320 match serde_json::from_slice::<Value>(token) {
321 Ok(v) => Ok(v),
322 Err(_) => {
323 if token.iter().all(|c| is_ident_byte(*c))
324 && !token.first().is_some_and(|c| c.is_ascii_digit())
325 {
326 Ok(Value::String(String::from_utf8_lossy(token).into_owned()))
328 } else {
329 Err(format!(
330 "invalid value '{}'",
331 String::from_utf8_lossy(token)
332 ))
333 }
334 }
335 }
336 }
337 }
338}
339
340fn parse_list(raw: &[u8], variables: &Value, depth: usize) -> Result<Value, String> {
342 let mut items = Vec::new();
343 for item in split_top_level(&raw[1..raw.len() - 1]) {
344 if item.iter().all(|c| c.is_ascii_whitespace()) {
345 continue;
346 }
347 let mut i = 0;
348 items.push(parse_value(item, &mut i, variables, depth + 1)?);
349 }
350 Ok(Value::Array(items))
351}
352
353fn parse_input_object(raw: &[u8], variables: &Value, depth: usize) -> Result<Value, String> {
355 let mut map = Map::new();
356 for item in split_top_level(&raw[1..raw.len() - 1]) {
357 if item.iter().all(|c| c.is_ascii_whitespace()) {
358 continue;
359 }
360 let mut i = 0;
361 skip_ws(item, &mut i);
362 let start = i;
363 while i < item.len() && is_ident_byte(item[i]) {
364 i += 1;
365 }
366 if i == start {
367 if matches!(item.get(start), Some(b'"') | Some(b'\'')) {
368 return Err("quoted field names in input objects not supported".into());
369 }
370 return Err("expected field name in input object".into());
371 }
372 let name = String::from_utf8_lossy(&item[start..i]).into_owned();
373 skip_ws(item, &mut i);
374 if i >= item.len() || item[i] != b':' {
375 return Err(format!("expected ':' after '{name}' in input object"));
376 }
377 i += 1;
378 let value = parse_value(item, &mut i, variables, depth + 1)?;
379 if map.contains_key(&name) {
380 return Err(format!("duplicate argument '{name}'"));
381 }
382 map.insert(name, value);
383 }
384 Ok(Value::Object(map))
385}
386
387fn split_top_level(raw: &[u8]) -> Vec<&[u8]> {
389 let mut parts = Vec::new();
390 let mut start = 0;
391 let mut round = 0usize;
392 let mut square = 0usize;
393 let mut curly = 0usize;
394 let mut i = 0;
395 while i < raw.len() {
396 match raw[i] {
397 b'"' | b'\'' => {
398 let mut j = i;
399 let _ = skip_string(raw, &mut j);
400 i = j;
401 continue;
402 }
403 b'(' => round += 1,
404 b')' => round = round.saturating_sub(1),
405 b'[' => square += 1,
406 b']' => square = square.saturating_sub(1),
407 b'{' => curly += 1,
408 b'}' => curly = curly.saturating_sub(1),
409 b',' if round == 0 && square == 0 && curly == 0 => {
410 parts.push(&raw[start..i]);
411 start = i + 1;
412 }
413 _ => {}
414 }
415 i += 1;
416 }
417 parts.push(&raw[start..]);
418 parts
419}
420
421fn skip_balanced(b: &[u8], i: &mut usize, open: u8, close: u8) -> Result<(), String> {
423 let mut depth = 0usize;
424 while *i < b.len() {
425 match b[*i] {
426 c if c == open => {
427 depth += 1;
428 *i += 1;
429 }
430 c if c == close => {
431 depth = depth.saturating_sub(1);
432 *i += 1;
433 if depth == 0 {
434 return Ok(());
435 }
436 }
437 b'"' | b'\'' => skip_string(b, i)?,
438 _ => *i += 1,
439 }
440 }
441 Err("unbalanced value group".into())
442}
443
444fn is_ident_byte(b: u8) -> bool {
445 b.is_ascii_alphanumeric() || b == b'_'
446}
447
448fn skip_ws(b: &[u8], i: &mut usize) {
450 while *i < b.len() && (b[*i].is_ascii_whitespace() || b[*i] == b'#') {
451 if b[*i] == b'#' {
452 while *i < b.len() && b[*i] != b'\n' {
453 *i += 1;
454 }
455 } else {
456 *i += 1;
457 }
458 }
459}
460
461fn skip_parens(b: &[u8], i: &mut usize) -> Result<(), String> {
463 let mut depth = 0usize;
464 while *i < b.len() {
465 match b[*i] {
466 b'(' => {
467 depth += 1;
468 *i += 1;
469 }
470 b')' => {
471 depth -= 1;
472 *i += 1;
473 if depth == 0 {
474 return Ok(());
475 }
476 }
477 b'"' | b'\'' => skip_string(b, i)?,
478 _ => *i += 1,
479 }
480 }
481 Err("unbalanced parentheses".into())
482}
483
484#[cfg(test)]
485mod parser_tests;