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    /// The requested `operationName` matches no operation in the document
27    /// (GraphQL § 6.1 *`GetOperation`*).
28    #[error("Unknown operation named '{0}'")]
29    UnknownOperation(String),
30
31    /// The document defines more than one operation and the request named none
32    /// (GraphQL § 6.1 *`GetOperation`*).
33    #[error(
34        "Document defines {0} operations but no `operationName` was provided — the request must \
35         name the operation to execute"
36    )]
37    AmbiguousOperation(usize),
38
39    /// Selection set has no fields.
40    #[error("No fields in selection set")]
41    EmptySelection,
42
43    /// GraphQL value nesting exceeds the allowed depth limit.
44    #[error("GraphQL value nesting exceeds maximum depth ({0} levels)")]
45    ValueNestingTooDeep(usize),
46}
47
48impl GraphQLParseError {
49    /// Whether this is a *request* error about which operation to run, rather
50    /// than a syntax error in the document.
51    ///
52    /// The distinction is what the client sees: a document that does not parse
53    /// is a `PARSE_ERROR`, but `operationName: "Nope"` against a document that
54    /// parses perfectly is a `VALIDATION_ERROR`. Reporting the second as a
55    /// parse failure sends the client hunting for a syntax mistake that is not
56    /// there.
57    #[must_use]
58    pub const fn is_operation_selection(&self) -> bool {
59        matches!(self, Self::UnknownOperation(_) | Self::AmbiguousOperation(_))
60    }
61}
62
63/// Parse GraphQL query string into Rust AST.
64///
65/// # Errors
66///
67/// Returns an error if:
68/// - GraphQL syntax is invalid or malformed
69/// - Query structure is invalid (missing operation, invalid selections)
70///
71/// # Example
72///
73/// ```
74/// use fraiseql_core::graphql::parse_query;
75///
76/// let query = "query { users { id name } }";
77/// let parsed = parse_query(query).unwrap();
78/// assert_eq!(parsed.operation_type, "query");
79/// assert_eq!(parsed.root_field, "users");
80/// ```
81pub fn parse_query(source: &str) -> Result<ParsedQuery, GraphQLParseError> {
82    parse_query_with_operation_name(source, None)
83}
84
85/// Parse a GraphQL document and select the operation to execute, per GraphQL
86/// § 6.1 *`GetOperation`*.
87///
88/// - `Some(name)` selects the operation with that name, and fails if no operation carries it.
89/// - `None` selects the document's only operation, and fails if the document defines more than one.
90///
91/// [`parse_query`] is this function with `None`.
92///
93/// # Why this is not "take the first operation"
94///
95/// The previous behaviour took the first operation in the document and ignored
96/// `operationName` entirely. A client sending a two-operation document and
97/// naming the second received the **first** one's data, under HTTP 200, with a
98/// perfectly plausible body — the same silent-wrong-answer failure mode as
99/// #1154. Naming an operation that does not exist, and sending a
100/// multi-operation document with no name at all, were both accepted the same
101/// way.
102///
103/// # Errors
104///
105/// - [`GraphQLParseError::Syntax`] — the document does not parse.
106/// - [`GraphQLParseError::MissingOperation`] — the document defines no operation.
107/// - [`GraphQLParseError::UnknownOperation`] — `operation_name` matches nothing.
108/// - [`GraphQLParseError::AmbiguousOperation`] — several operations, none named.
109pub fn parse_query_with_operation_name(
110    source: &str,
111    operation_name: Option<&str>,
112) -> Result<ParsedQuery, GraphQLParseError> {
113    // Through the guarded seam, not `query::parse_query` directly: the parser can
114    // panic on a client-controlled document (#976), and this entry point is public.
115    let doc: Document<String> = crate::graphql::complexity::parse_graphql_document(source)
116        .map_err(|e| GraphQLParseError::Syntax(e.to_string()))?;
117
118    let operation = select_operation(&doc, operation_name)?;
119    parse_selected_operation(operation, &doc, source)
120}
121
122/// Choose the operation to execute from a parsed document (GraphQL § 6.1).
123fn select_operation<'d>(
124    doc: &'d Document<'d, String>,
125    operation_name: Option<&str>,
126) -> Result<&'d OperationDefinition<'d, String>, GraphQLParseError> {
127    let mut operations = doc.definitions.iter().filter_map(|def| match def {
128        query::Definition::Operation(op) => Some(op),
129        query::Definition::Fragment(_) => None,
130    });
131
132    let Some(requested) = operation_name else {
133        // Anonymous request: legal only when the document is unambiguous.
134        let first = operations.next().ok_or(GraphQLParseError::MissingOperation)?;
135        let extra = operations.count();
136        if extra > 0 {
137            return Err(GraphQLParseError::AmbiguousOperation(extra + 1));
138        }
139        return Ok(first);
140    };
141
142    let mut saw_any = false;
143    for op in operations {
144        saw_any = true;
145        if operation_definition_name(op).is_some_and(|name| name == requested) {
146            return Ok(op);
147        }
148    }
149
150    if saw_any {
151        Err(GraphQLParseError::UnknownOperation(requested.to_string()))
152    } else {
153        Err(GraphQLParseError::MissingOperation)
154    }
155}
156
157/// The declared name of an operation, if it has one.
158///
159/// A bare selection set (`{ users { id } }`) is anonymous by construction and
160/// can never be selected by name.
161fn operation_definition_name<'a>(
162    operation: &'a OperationDefinition<'a, String>,
163) -> Option<&'a str> {
164    match operation {
165        OperationDefinition::Query(q) => q.name.as_deref(),
166        OperationDefinition::Mutation(m) => m.name.as_deref(),
167        OperationDefinition::Subscription(s) => s.name.as_deref(),
168        OperationDefinition::SelectionSet(_) => None,
169    }
170}
171
172/// Build a [`ParsedQuery`] from an operation the caller has **already selected**.
173///
174/// The seam for callers that do their own operation selection and must not have
175/// it silently redone. `routes/subscriptions.rs` is the motivating case: it
176/// filters for the single *subscription* operation and rejects documents with
177/// more than one, so routing it back through [`parse_query`] would validate the
178/// document's *first* operation — which on a document mixing `query Q {…}` with
179/// `subscription S {…}` is the wrong one.
180///
181/// # Errors
182///
183/// - [`GraphQLParseError::EmptySelection`] — the operation selects no field.
184/// - [`GraphQLParseError::ValueNestingTooDeep`] — an argument value nests too deeply.
185pub fn parse_selected_operation(
186    operation: &OperationDefinition<String>,
187    doc: &Document<String>,
188    source: &str,
189) -> Result<ParsedQuery, GraphQLParseError> {
190    let (operation_type, operation_name, root_field, selections, variables) =
191        extract_operation(operation)?;
192
193    // Fragments are document-scoped, not operation-scoped: an operation may
194    // spread any fragment the document defines, so the whole set travels with
195    // every selected operation.
196    let fragments = extract_fragments(doc)?;
197
198    Ok(ParsedQuery {
199        operation_type,
200        operation_name,
201        root_field,
202        selections,
203        variables,
204        fragments,
205        // `Arc<str>` is the same one-allocation cost as `String::from(&str)` at
206        // construction time, but downstream clones of `ParsedQuery` (notably in
207        // the parse cache and during fragment resolution) become atomic
208        // ref-count bumps instead of full string copies.
209        source: std::sync::Arc::from(source),
210    })
211}
212
213/// Extract fragment definitions from GraphQL document.
214fn extract_fragments(
215    doc: &Document<String>,
216) -> Result<Vec<crate::graphql::types::FragmentDefinition>, GraphQLParseError> {
217    let mut fragments = Vec::new();
218
219    for def in &doc.definitions {
220        if let Definition::Fragment(fragment) = def {
221            let selections = parse_selection_set(&fragment.selection_set)?;
222
223            // Extract fragment spreads from selections
224            let fragment_spreads = extract_fragment_spreads(&fragment.selection_set);
225
226            // Convert type condition to string
227            let type_condition = match &fragment.type_condition {
228                query::TypeCondition::On(type_name) => type_name.clone(),
229            };
230
231            fragments.push(crate::graphql::types::FragmentDefinition {
232                name: fragment.name.clone(),
233                type_condition,
234                selections,
235                fragment_spreads,
236            });
237        }
238    }
239
240    Ok(fragments)
241}
242
243/// Extract fragment spreads from a selection set.
244fn extract_fragment_spreads(selection_set: &query::SelectionSet<String>) -> Vec<String> {
245    let mut spreads = Vec::new();
246
247    for selection in &selection_set.items {
248        match selection {
249            Selection::FragmentSpread(spread) => {
250                spreads.push(spread.fragment_name.clone());
251            },
252            Selection::InlineFragment(inline) => {
253                // Inline fragments can also contain spreads
254                spreads.extend(extract_fragment_spreads(&inline.selection_set));
255            },
256            Selection::Field(field) => {
257                // Fields can have nested selections with spreads
258                spreads.extend(extract_fragment_spreads(&field.selection_set));
259            },
260        }
261    }
262
263    spreads
264}
265
266/// Extract operation details from GraphQL operation definition.
267fn extract_operation(
268    operation: &OperationDefinition<String>,
269) -> Result<
270    (String, Option<String>, String, Vec<FieldSelection>, Vec<VariableDefinition>),
271    GraphQLParseError,
272> {
273    let operation_type = match operation {
274        OperationDefinition::Query(_) | OperationDefinition::SelectionSet(_) => "query",
275        OperationDefinition::Mutation(_) => "mutation",
276        OperationDefinition::Subscription(_) => "subscription",
277    }
278    .to_string();
279
280    let (name, selection_set, var_defs) = match operation {
281        OperationDefinition::Query(q) => (&q.name, &q.selection_set, &q.variable_definitions),
282        OperationDefinition::Mutation(m) => (&m.name, &m.selection_set, &m.variable_definitions),
283        OperationDefinition::Subscription(s) => {
284            (&s.name, &s.selection_set, &s.variable_definitions)
285        },
286        OperationDefinition::SelectionSet(sel_set) => (&None, sel_set, &Vec::new()),
287    };
288
289    // Parse selection set (recursive)
290    let selections = parse_selection_set(selection_set)?;
291
292    // Get root field name (first field in selection set)
293    let root_field = selections
294        .first()
295        .map(|s| s.name.clone())
296        .ok_or(GraphQLParseError::EmptySelection)?;
297
298    // Parse variable definitions
299    let variables = var_defs
300        .iter()
301        .map(|var_def| VariableDefinition {
302            name:          var_def.name.clone(),
303            var_type:      parse_graphql_type(&var_def.var_type),
304            default_value: var_def.default_value.as_ref().map(|v| serialize_value(v)),
305        })
306        .collect();
307
308    Ok((operation_type, name.clone(), root_field, selections, variables))
309}
310
311/// Parse GraphQL selection set recursively.
312///
313/// Handles fields, fragment spreads, and inline fragments.
314fn parse_selection_set(
315    selection_set: &query::SelectionSet<String>,
316) -> Result<Vec<FieldSelection>, GraphQLParseError> {
317    let mut fields = Vec::new();
318
319    for selection in &selection_set.items {
320        match selection {
321            Selection::Field(field) => {
322                // Parse field arguments
323                let arguments = field
324                    .arguments
325                    .iter()
326                    .map(|(name, value)| GraphQLArgument {
327                        name:       name.clone(),
328                        value_type: value_type_string(value),
329                        value_json: serialize_value(value),
330                    })
331                    .collect();
332
333                // Parse nested selection set (recursive)
334                let nested_fields = parse_selection_set(&field.selection_set)?;
335
336                let directives = field.directives.iter().map(parse_directive).collect();
337
338                fields.push(FieldSelection {
339                    name: field.name.clone(),
340                    alias: field.alias.clone(),
341                    arguments,
342                    nested_fields,
343                    directives,
344                });
345            },
346            Selection::FragmentSpread(spread) => {
347                // Represent fragment spread as a special field with "..." prefix
348                // This will be resolved by FragmentResolver
349                let directives = spread.directives.iter().map(parse_directive).collect();
350
351                fields.push(FieldSelection {
352                    name: format!("...{}", spread.fragment_name),
353                    alias: None,
354                    arguments: vec![],
355                    nested_fields: vec![],
356                    directives,
357                });
358            },
359            Selection::InlineFragment(inline) => {
360                // Represent inline fragment as special field
361                // Type condition is stored in the name
362                let type_condition =
363                    inline.type_condition.as_ref().map_or_else(String::new, |tc| match tc {
364                        query::TypeCondition::On(name) => name.clone(),
365                    });
366
367                let nested_fields = parse_selection_set(&inline.selection_set)?;
368                let directives = inline.directives.iter().map(parse_directive).collect();
369
370                fields.push(FieldSelection {
371                    name: format!("...on {type_condition}"),
372                    alias: None,
373                    arguments: vec![],
374                    nested_fields,
375                    directives,
376                });
377            },
378        }
379    }
380
381    Ok(fields)
382}
383
384/// Get type of GraphQL value for classification.
385fn value_type_string(value: &query::Value<String>) -> String {
386    match value {
387        query::Value::String(_) => "string".to_string(),
388        query::Value::Int(_) => "int".to_string(),
389        query::Value::Float(_) => "float".to_string(),
390        query::Value::Boolean(_) => "boolean".to_string(),
391        query::Value::Null => "null".to_string(),
392        query::Value::Enum(_) => "enum".to_string(),
393        query::Value::List(_) => "list".to_string(),
394        query::Value::Object(_) => "object".to_string(),
395        query::Value::Variable(_) => "variable".to_string(),
396    }
397}
398
399/// Serialize a GraphQL value to the shared `value_json` representation.
400///
401/// Delegates to [`crate::graphql::value_json::encode`], which uses `serde_json`
402/// rather than hand-rolled escaping and tags variable references out of band.
403/// A value too deeply nested to serialize yields `"null"` — the same conservative
404/// fallback the previous depth guard produced, and the depth cap is shared.
405pub(crate) fn serialize_value(value: &query::Value<String>) -> String {
406    crate::graphql::value_json::encode(value).unwrap_or_else(|_| "null".to_string())
407}
408
409/// Parse GraphQL directive from graphql-parser Directive.
410fn parse_directive(directive: &GraphQLDirective<String>) -> Directive {
411    let arguments = directive
412        .arguments
413        .iter()
414        .map(|(name, value)| GraphQLArgument {
415            name:       name.clone(),
416            value_type: value_type_string(value),
417            value_json: serialize_value(value),
418        })
419        .collect();
420
421    Directive {
422        name: directive.name.clone(),
423        arguments,
424    }
425}
426
427/// Parse GraphQL type from graphql-parser Type to our `GraphQLType`.
428fn parse_graphql_type(graphql_type: &query::Type<String>) -> GraphQLType {
429    match graphql_type {
430        query::Type::NamedType(name) => GraphQLType {
431            name:          name.clone(),
432            nullable:      true, // Named types are nullable by default
433            list:          false,
434            list_nullable: false,
435        },
436        query::Type::ListType(inner) => GraphQLType {
437            name:          format!("[{}]", parse_graphql_type(inner).name),
438            nullable:      true,
439            list:          true,
440            list_nullable: true, // List items are nullable by default
441        },
442        query::Type::NonNullType(inner) => {
443            let mut parsed = parse_graphql_type(inner);
444            parsed.nullable = false;
445            if parsed.list {
446                parsed.list_nullable = false;
447            }
448            parsed
449        },
450    }
451}
452
453/// Map a parse failure onto the error kind the client should see.
454///
455/// A document that does not parse is a parse error. A document that parses but
456/// names an operation that is not there — or names none when it must — is a
457/// *validation* error: the syntax is fine, the request is wrong.
458#[must_use]
459pub fn operation_selection_error(e: &GraphQLParseError) -> crate::error::FraiseQLError {
460    if e.is_operation_selection() {
461        crate::error::FraiseQLError::Validation {
462            message: e.to_string(),
463            path:    Some("operationName".to_string()),
464        }
465    } else {
466        crate::error::FraiseQLError::Parse {
467            message:  e.to_string(),
468            location: "query".to_string(),
469        }
470    }
471}