Skip to main content

fraiseql_core/graphql/
parser.rs

1//! GraphQL query parser using graphql-parser crate.
2//!
3//! Parses GraphQL query strings into a Rust AST for further processing
4//! by fragment resolution and directive evaluation.
5
6use graphql_parser::query::{
7    self, Definition, Directive as GraphQLDirective, Document, OperationDefinition, Selection,
8};
9
10use crate::graphql::types::{
11    Directive, FieldSelection, GraphQLArgument, GraphQLType, ParsedQuery, VariableDefinition,
12};
13
14/// Errors that can occur when parsing a GraphQL query.
15#[derive(Debug, thiserror::Error)]
16#[non_exhaustive]
17pub enum GraphQLParseError {
18    /// Failed to parse GraphQL syntax.
19    #[error("Failed to parse GraphQL query: {0}")]
20    Syntax(String),
21
22    /// No query or mutation operation found in the document.
23    #[error("No query or mutation operation found")]
24    MissingOperation,
25
26    /// Selection set has no fields.
27    #[error("No fields in selection set")]
28    EmptySelection,
29
30    /// GraphQL value nesting exceeds the allowed depth limit.
31    #[error("GraphQL value nesting exceeds maximum depth ({0} levels)")]
32    ValueNestingTooDeep(usize),
33}
34
35/// Maximum nesting depth for `serialize_value` recursion.
36///
37/// Real-world GraphQL variables rarely exceed 5-10 levels of nesting.  A cap
38/// of 64 is generous while preventing stack-exhaustion from a crafted payload
39/// like `[[[[…]]]]` with tens-of-thousands of levels.
40pub(crate) const MAX_SERIALIZE_DEPTH: usize = 64;
41
42/// Parse GraphQL query string into Rust AST.
43///
44/// # Errors
45///
46/// Returns an error if:
47/// - GraphQL syntax is invalid or malformed
48/// - Query structure is invalid (missing operation, invalid selections)
49///
50/// # Example
51///
52/// ```
53/// use fraiseql_core::graphql::parse_query;
54///
55/// let query = "query { users { id name } }";
56/// let parsed = parse_query(query).unwrap();
57/// assert_eq!(parsed.operation_type, "query");
58/// assert_eq!(parsed.root_field, "users");
59/// ```
60pub fn parse_query(source: &str) -> Result<ParsedQuery, GraphQLParseError> {
61    // Use graphql-parser to parse query string
62    let doc: Document<String> =
63        query::parse_query(source).map_err(|e| GraphQLParseError::Syntax(e.to_string()))?;
64
65    // Extract first operation (ignore multiple operations for now)
66    let operation = doc
67        .definitions
68        .iter()
69        .find_map(|def| match def {
70            query::Definition::Operation(op) => Some(op),
71            query::Definition::Fragment(_) => None,
72        })
73        .ok_or(GraphQLParseError::MissingOperation)?;
74
75    // Extract operation details
76    let (operation_type, operation_name, root_field, selections, variables) =
77        extract_operation(operation)?;
78
79    // Extract fragment definitions
80    let fragments = extract_fragments(&doc)?;
81
82    Ok(ParsedQuery {
83        operation_type,
84        operation_name,
85        root_field,
86        selections,
87        variables,
88        fragments,
89        // `Arc<str>` is the same one-allocation cost as `String::from(&str)` at
90        // construction time, but downstream clones of `ParsedQuery` (notably in
91        // the parse cache and during fragment resolution) become atomic
92        // ref-count bumps instead of full string copies.
93        source: std::sync::Arc::from(source),
94    })
95}
96
97/// Extract fragment definitions from GraphQL document.
98fn extract_fragments(
99    doc: &Document<String>,
100) -> Result<Vec<crate::graphql::types::FragmentDefinition>, GraphQLParseError> {
101    let mut fragments = Vec::new();
102
103    for def in &doc.definitions {
104        if let Definition::Fragment(fragment) = def {
105            let selections = parse_selection_set(&fragment.selection_set)?;
106
107            // Extract fragment spreads from selections
108            let fragment_spreads = extract_fragment_spreads(&fragment.selection_set);
109
110            // Convert type condition to string
111            let type_condition = match &fragment.type_condition {
112                query::TypeCondition::On(type_name) => type_name.clone(),
113            };
114
115            fragments.push(crate::graphql::types::FragmentDefinition {
116                name: fragment.name.clone(),
117                type_condition,
118                selections,
119                fragment_spreads,
120            });
121        }
122    }
123
124    Ok(fragments)
125}
126
127/// Extract fragment spreads from a selection set.
128fn extract_fragment_spreads(selection_set: &query::SelectionSet<String>) -> Vec<String> {
129    let mut spreads = Vec::new();
130
131    for selection in &selection_set.items {
132        match selection {
133            Selection::FragmentSpread(spread) => {
134                spreads.push(spread.fragment_name.clone());
135            },
136            Selection::InlineFragment(inline) => {
137                // Inline fragments can also contain spreads
138                spreads.extend(extract_fragment_spreads(&inline.selection_set));
139            },
140            Selection::Field(field) => {
141                // Fields can have nested selections with spreads
142                spreads.extend(extract_fragment_spreads(&field.selection_set));
143            },
144        }
145    }
146
147    spreads
148}
149
150/// Extract operation details from GraphQL operation definition.
151fn extract_operation(
152    operation: &OperationDefinition<String>,
153) -> Result<
154    (String, Option<String>, String, Vec<FieldSelection>, Vec<VariableDefinition>),
155    GraphQLParseError,
156> {
157    let operation_type = match operation {
158        OperationDefinition::Query(_) | OperationDefinition::SelectionSet(_) => "query",
159        OperationDefinition::Mutation(_) => "mutation",
160        OperationDefinition::Subscription(_) => "subscription",
161    }
162    .to_string();
163
164    let (name, selection_set, var_defs) = match operation {
165        OperationDefinition::Query(q) => (&q.name, &q.selection_set, &q.variable_definitions),
166        OperationDefinition::Mutation(m) => (&m.name, &m.selection_set, &m.variable_definitions),
167        OperationDefinition::Subscription(s) => {
168            (&s.name, &s.selection_set, &s.variable_definitions)
169        },
170        OperationDefinition::SelectionSet(sel_set) => (&None, sel_set, &Vec::new()),
171    };
172
173    // Parse selection set (recursive)
174    let selections = parse_selection_set(selection_set)?;
175
176    // Get root field name (first field in selection set)
177    let root_field = selections
178        .first()
179        .map(|s| s.name.clone())
180        .ok_or(GraphQLParseError::EmptySelection)?;
181
182    // Parse variable definitions
183    let variables = var_defs
184        .iter()
185        .map(|var_def| VariableDefinition {
186            name:          var_def.name.clone(),
187            var_type:      parse_graphql_type(&var_def.var_type),
188            default_value: var_def.default_value.as_ref().map(|v| serialize_value(v)),
189        })
190        .collect();
191
192    Ok((operation_type, name.clone(), root_field, selections, variables))
193}
194
195/// Parse GraphQL selection set recursively.
196///
197/// Handles fields, fragment spreads, and inline fragments.
198fn parse_selection_set(
199    selection_set: &query::SelectionSet<String>,
200) -> Result<Vec<FieldSelection>, GraphQLParseError> {
201    let mut fields = Vec::new();
202
203    for selection in &selection_set.items {
204        match selection {
205            Selection::Field(field) => {
206                // Parse field arguments
207                let arguments = field
208                    .arguments
209                    .iter()
210                    .map(|(name, value)| GraphQLArgument {
211                        name:       name.clone(),
212                        value_type: value_type_string(value),
213                        value_json: serialize_value(value),
214                    })
215                    .collect();
216
217                // Parse nested selection set (recursive)
218                let nested_fields = parse_selection_set(&field.selection_set)?;
219
220                let directives = field.directives.iter().map(parse_directive).collect();
221
222                fields.push(FieldSelection {
223                    name: field.name.clone(),
224                    alias: field.alias.clone(),
225                    arguments,
226                    nested_fields,
227                    directives,
228                });
229            },
230            Selection::FragmentSpread(spread) => {
231                // Represent fragment spread as a special field with "..." prefix
232                // This will be resolved by FragmentResolver
233                let directives = spread.directives.iter().map(parse_directive).collect();
234
235                fields.push(FieldSelection {
236                    name: format!("...{}", spread.fragment_name),
237                    alias: None,
238                    arguments: vec![],
239                    nested_fields: vec![],
240                    directives,
241                });
242            },
243            Selection::InlineFragment(inline) => {
244                // Represent inline fragment as special field
245                // Type condition is stored in the name
246                let type_condition =
247                    inline.type_condition.as_ref().map_or_else(String::new, |tc| match tc {
248                        query::TypeCondition::On(name) => name.clone(),
249                    });
250
251                let nested_fields = parse_selection_set(&inline.selection_set)?;
252                let directives = inline.directives.iter().map(parse_directive).collect();
253
254                fields.push(FieldSelection {
255                    name: format!("...on {type_condition}"),
256                    alias: None,
257                    arguments: vec![],
258                    nested_fields,
259                    directives,
260                });
261            },
262        }
263    }
264
265    Ok(fields)
266}
267
268/// Get type of GraphQL value for classification.
269fn value_type_string(value: &query::Value<String>) -> String {
270    match value {
271        query::Value::String(_) => "string".to_string(),
272        query::Value::Int(_) => "int".to_string(),
273        query::Value::Float(_) => "float".to_string(),
274        query::Value::Boolean(_) => "boolean".to_string(),
275        query::Value::Null => "null".to_string(),
276        query::Value::Enum(_) => "enum".to_string(),
277        query::Value::List(_) => "list".to_string(),
278        query::Value::Object(_) => "object".to_string(),
279        query::Value::Variable(_) => "variable".to_string(),
280    }
281}
282
283/// Serialize GraphQL value to JSON string.
284///
285/// Returns `None` when the recursion depth exceeds `MAX_SERIALIZE_DEPTH`.
286/// The public wrapper `serialize_value` returns a fallback `"null"` in that case;
287/// callers that need to surface the error can call `try_serialize_value` directly.
288fn serialize_value_inner(value: &query::Value<String>, depth: usize) -> Option<String> {
289    if depth > MAX_SERIALIZE_DEPTH {
290        return None;
291    }
292
293    let s = match value {
294        query::Value::String(s) => format!("\"{}\"", s.replace('"', "\\\"")),
295        query::Value::Int(i) => {
296            // Use the safe as_i64() method from graphql-parser
297            i.as_i64().map_or_else(|| "0".to_string(), |n| n.to_string())
298        },
299        query::Value::Float(f) => format!("{f}"),
300        query::Value::Boolean(b) => b.to_string(),
301        query::Value::Null => "null".to_string(),
302        query::Value::Enum(e) => format!("\"{e}\""),
303        query::Value::List(items) => {
304            let mut parts = Vec::with_capacity(items.len());
305            for item in items {
306                parts.push(serialize_value_inner(item, depth + 1)?);
307            }
308            format!("[{}]", parts.join(","))
309        },
310        query::Value::Object(obj) => {
311            let mut pairs = Vec::with_capacity(obj.len());
312            for (k, v) in obj {
313                let serialized = serialize_value_inner(v, depth + 1)?;
314                pairs.push(format!("\"{}\":{serialized}", k));
315            }
316            format!("{{{}}}", pairs.join(","))
317        },
318        query::Value::Variable(v) => format!("\"${v}\""),
319    };
320
321    Some(s)
322}
323
324/// Serialize a GraphQL value to a JSON string.
325///
326/// Returns `"null"` if the value is nested more than `MAX_SERIALIZE_DEPTH` levels deep,
327/// preventing stack exhaustion from adversarially crafted variable payloads.
328pub(crate) fn serialize_value(value: &query::Value<String>) -> String {
329    serialize_value_inner(value, 0).unwrap_or_else(|| "null".to_string())
330}
331
332/// Parse GraphQL directive from graphql-parser Directive.
333fn parse_directive(directive: &GraphQLDirective<String>) -> Directive {
334    let arguments = directive
335        .arguments
336        .iter()
337        .map(|(name, value)| GraphQLArgument {
338            name:       name.clone(),
339            value_type: value_type_string(value),
340            value_json: serialize_value(value),
341        })
342        .collect();
343
344    Directive {
345        name: directive.name.clone(),
346        arguments,
347    }
348}
349
350/// Parse GraphQL type from graphql-parser Type to our `GraphQLType`.
351fn parse_graphql_type(graphql_type: &query::Type<String>) -> GraphQLType {
352    match graphql_type {
353        query::Type::NamedType(name) => GraphQLType {
354            name:          name.clone(),
355            nullable:      true, // Named types are nullable by default
356            list:          false,
357            list_nullable: false,
358        },
359        query::Type::ListType(inner) => GraphQLType {
360            name:          format!("[{}]", parse_graphql_type(inner).name),
361            nullable:      true,
362            list:          true,
363            list_nullable: true, // List items are nullable by default
364        },
365        query::Type::NonNullType(inner) => {
366            let mut parsed = parse_graphql_type(inner);
367            parsed.nullable = false;
368            if parsed.list {
369                parsed.list_nullable = false;
370            }
371            parsed
372        },
373    }
374}