Skip to main content

ecat_graphql/parser/
mod.rs

1// Copyright (c) 2026 erik <erik@erik.xyz> — https://erik.xyz
2//! 手写 GraphQL 选择集解析器(两阶段):
3//! - Phase A 扫描:定位顶层字段名、参数原始区间与嵌套 selection 树
4//! - Phase B 参数区间二次解析:字面量/枚举/列表/输入对象/变量解引用
5
6mod 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    // 变量定义与指令
70    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
109/// 解析单个字段:名、别名检测、参数区间、嵌套 selection。
110/// operation 仅顶层字段有意义(嵌套字段转 FieldNode 时丢弃)。
111fn 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    // 字段级指令:Phase A 忽略(与操作级指令一致)
145    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 停在 '{'
173    *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; // '@'
206    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
216/// Phase B:解析 "(name: value, ...)" 参数区间(含首尾括号)。
217fn 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
261/// 解析单个值。variables 为 Null 时视为空对象($var 缺失即报错)。
262pub 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            // 三连引号(""" 与 ''')都是 block string 语法,显式拒绝
278            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            // 字面量 token:数字 / true / false / null / 枚举标识符
312            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                        // 裸标识符:无类型系统,枚举值映射为字符串
327                        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
340/// 解析 "[item, ...]" 列表(含首尾括号):顶层逗号切分后递归解析各项。
341fn 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
353/// 解析 "{name: value, ...}" 输入对象(含首尾大括号)。
354fn 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
387/// 按顶层逗号切分(括号/大括号组与字符串内的逗号不算)。
388fn 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
421/// 跳过平衡的 (/) / [ ] / { } 组,起始字符为 open。
422fn 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
448/// 跳过空白与 `#` 注释。
449fn 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
461/// 跳过平衡的括号组;字符串内的括号/大括号不算结构字符。
462fn 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;