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.
40const 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        source: source.to_string(),
90    })
91}
92
93/// Extract fragment definitions from GraphQL document.
94fn extract_fragments(
95    doc: &Document<String>,
96) -> Result<Vec<crate::graphql::types::FragmentDefinition>, GraphQLParseError> {
97    let mut fragments = Vec::new();
98
99    for def in &doc.definitions {
100        if let Definition::Fragment(fragment) = def {
101            let selections = parse_selection_set(&fragment.selection_set)?;
102
103            // Extract fragment spreads from selections
104            let fragment_spreads = extract_fragment_spreads(&fragment.selection_set);
105
106            // Convert type condition to string
107            let type_condition = match &fragment.type_condition {
108                query::TypeCondition::On(type_name) => type_name.clone(),
109            };
110
111            fragments.push(crate::graphql::types::FragmentDefinition {
112                name: fragment.name.clone(),
113                type_condition,
114                selections,
115                fragment_spreads,
116            });
117        }
118    }
119
120    Ok(fragments)
121}
122
123/// Extract fragment spreads from a selection set.
124fn extract_fragment_spreads(selection_set: &query::SelectionSet<String>) -> Vec<String> {
125    let mut spreads = Vec::new();
126
127    for selection in &selection_set.items {
128        match selection {
129            Selection::FragmentSpread(spread) => {
130                spreads.push(spread.fragment_name.clone());
131            },
132            Selection::InlineFragment(inline) => {
133                // Inline fragments can also contain spreads
134                spreads.extend(extract_fragment_spreads(&inline.selection_set));
135            },
136            Selection::Field(field) => {
137                // Fields can have nested selections with spreads
138                spreads.extend(extract_fragment_spreads(&field.selection_set));
139            },
140        }
141    }
142
143    spreads
144}
145
146/// Extract operation details from GraphQL operation definition.
147fn extract_operation(
148    operation: &OperationDefinition<String>,
149) -> Result<
150    (String, Option<String>, String, Vec<FieldSelection>, Vec<VariableDefinition>),
151    GraphQLParseError,
152> {
153    let operation_type = match operation {
154        OperationDefinition::Query(_) | OperationDefinition::SelectionSet(_) => "query",
155        OperationDefinition::Mutation(_) => "mutation",
156        OperationDefinition::Subscription(_) => "subscription",
157    }
158    .to_string();
159
160    let (name, selection_set, var_defs) = match operation {
161        OperationDefinition::Query(q) => (&q.name, &q.selection_set, &q.variable_definitions),
162        OperationDefinition::Mutation(m) => (&m.name, &m.selection_set, &m.variable_definitions),
163        OperationDefinition::Subscription(s) => {
164            (&s.name, &s.selection_set, &s.variable_definitions)
165        },
166        OperationDefinition::SelectionSet(sel_set) => (&None, sel_set, &Vec::new()),
167    };
168
169    // Parse selection set (recursive)
170    let selections = parse_selection_set(selection_set)?;
171
172    // Get root field name (first field in selection set)
173    let root_field = selections
174        .first()
175        .map(|s| s.name.clone())
176        .ok_or(GraphQLParseError::EmptySelection)?;
177
178    // Parse variable definitions
179    let variables = var_defs
180        .iter()
181        .map(|var_def| VariableDefinition {
182            name:          var_def.name.clone(),
183            var_type:      parse_graphql_type(&var_def.var_type),
184            default_value: var_def.default_value.as_ref().map(|v| serialize_value(v)),
185        })
186        .collect();
187
188    Ok((operation_type, name.clone(), root_field, selections, variables))
189}
190
191/// Parse GraphQL selection set recursively.
192///
193/// Handles fields, fragment spreads, and inline fragments.
194fn parse_selection_set(
195    selection_set: &query::SelectionSet<String>,
196) -> Result<Vec<FieldSelection>, GraphQLParseError> {
197    let mut fields = Vec::new();
198
199    for selection in &selection_set.items {
200        match selection {
201            Selection::Field(field) => {
202                // Parse field arguments
203                let arguments = field
204                    .arguments
205                    .iter()
206                    .map(|(name, value)| GraphQLArgument {
207                        name:       name.clone(),
208                        value_type: value_type_string(value),
209                        value_json: serialize_value(value),
210                    })
211                    .collect();
212
213                // Parse nested selection set (recursive)
214                let nested_fields = parse_selection_set(&field.selection_set)?;
215
216                let directives = field.directives.iter().map(parse_directive).collect();
217
218                fields.push(FieldSelection {
219                    name: field.name.clone(),
220                    alias: field.alias.clone(),
221                    arguments,
222                    nested_fields,
223                    directives,
224                });
225            },
226            Selection::FragmentSpread(spread) => {
227                // Represent fragment spread as a special field with "..." prefix
228                // This will be resolved by FragmentResolver
229                let directives = spread.directives.iter().map(parse_directive).collect();
230
231                fields.push(FieldSelection {
232                    name: format!("...{}", spread.fragment_name),
233                    alias: None,
234                    arguments: vec![],
235                    nested_fields: vec![],
236                    directives,
237                });
238            },
239            Selection::InlineFragment(inline) => {
240                // Represent inline fragment as special field
241                // Type condition is stored in the name
242                let type_condition =
243                    inline.type_condition.as_ref().map_or_else(String::new, |tc| match tc {
244                        query::TypeCondition::On(name) => name.clone(),
245                    });
246
247                let nested_fields = parse_selection_set(&inline.selection_set)?;
248                let directives = inline.directives.iter().map(parse_directive).collect();
249
250                fields.push(FieldSelection {
251                    name: format!("...on {type_condition}"),
252                    alias: None,
253                    arguments: vec![],
254                    nested_fields,
255                    directives,
256                });
257            },
258        }
259    }
260
261    Ok(fields)
262}
263
264/// Get type of GraphQL value for classification.
265fn value_type_string(value: &query::Value<String>) -> String {
266    match value {
267        query::Value::String(_) => "string".to_string(),
268        query::Value::Int(_) => "int".to_string(),
269        query::Value::Float(_) => "float".to_string(),
270        query::Value::Boolean(_) => "boolean".to_string(),
271        query::Value::Null => "null".to_string(),
272        query::Value::Enum(_) => "enum".to_string(),
273        query::Value::List(_) => "list".to_string(),
274        query::Value::Object(_) => "object".to_string(),
275        query::Value::Variable(_) => "variable".to_string(),
276    }
277}
278
279/// Serialize GraphQL value to JSON string.
280///
281/// Returns `None` when the recursion depth exceeds `MAX_SERIALIZE_DEPTH`.
282/// The public wrapper `serialize_value` returns a fallback `"null"` in that case;
283/// callers that need to surface the error can call `try_serialize_value` directly.
284fn serialize_value_inner(value: &query::Value<String>, depth: usize) -> Option<String> {
285    if depth > MAX_SERIALIZE_DEPTH {
286        return None;
287    }
288
289    let s = match value {
290        query::Value::String(s) => format!("\"{}\"", s.replace('"', "\\\"")),
291        query::Value::Int(i) => {
292            // Use the safe as_i64() method from graphql-parser
293            i.as_i64().map_or_else(|| "0".to_string(), |n| n.to_string())
294        },
295        query::Value::Float(f) => format!("{f}"),
296        query::Value::Boolean(b) => b.to_string(),
297        query::Value::Null => "null".to_string(),
298        query::Value::Enum(e) => format!("\"{e}\""),
299        query::Value::List(items) => {
300            let mut parts = Vec::with_capacity(items.len());
301            for item in items {
302                parts.push(serialize_value_inner(item, depth + 1)?);
303            }
304            format!("[{}]", parts.join(","))
305        },
306        query::Value::Object(obj) => {
307            let mut pairs = Vec::with_capacity(obj.len());
308            for (k, v) in obj {
309                let serialized = serialize_value_inner(v, depth + 1)?;
310                pairs.push(format!("\"{}\":{serialized}", k));
311            }
312            format!("{{{}}}", pairs.join(","))
313        },
314        query::Value::Variable(v) => format!("\"${v}\""),
315    };
316
317    Some(s)
318}
319
320/// Serialize a GraphQL value to a JSON string.
321///
322/// Returns `"null"` if the value is nested more than `MAX_SERIALIZE_DEPTH` levels deep,
323/// preventing stack exhaustion from adversarially crafted variable payloads.
324fn serialize_value(value: &query::Value<String>) -> String {
325    serialize_value_inner(value, 0).unwrap_or_else(|| "null".to_string())
326}
327
328/// Parse GraphQL directive from graphql-parser Directive.
329fn parse_directive(directive: &GraphQLDirective<String>) -> Directive {
330    let arguments = directive
331        .arguments
332        .iter()
333        .map(|(name, value)| GraphQLArgument {
334            name:       name.clone(),
335            value_type: value_type_string(value),
336            value_json: serialize_value(value),
337        })
338        .collect();
339
340    Directive {
341        name: directive.name.clone(),
342        arguments,
343    }
344}
345
346/// Parse GraphQL type from graphql-parser Type to our `GraphQLType`.
347fn parse_graphql_type(graphql_type: &query::Type<String>) -> GraphQLType {
348    match graphql_type {
349        query::Type::NamedType(name) => GraphQLType {
350            name:          name.clone(),
351            nullable:      true, // Named types are nullable by default
352            list:          false,
353            list_nullable: false,
354        },
355        query::Type::ListType(inner) => GraphQLType {
356            name:          format!("[{}]", parse_graphql_type(inner).name),
357            nullable:      true,
358            list:          true,
359            list_nullable: true, // List items are nullable by default
360        },
361        query::Type::NonNullType(inner) => {
362            let mut parsed = parse_graphql_type(inner);
363            parsed.nullable = false;
364            if parsed.list {
365                parsed.list_nullable = false;
366            }
367            parsed
368        },
369    }
370}
371
372#[cfg(test)]
373mod tests {
374    #![allow(clippy::unwrap_used)] // Reason: test code, panics are acceptable
375
376    use super::*;
377
378    #[test]
379    fn test_parse_simple_query() {
380        let query = "query { users { id name } }";
381        let parsed = parse_query(query).unwrap();
382
383        assert_eq!(parsed.operation_type, "query");
384        assert_eq!(parsed.root_field, "users");
385        assert_eq!(parsed.selections.len(), 1);
386        assert_eq!(parsed.selections[0].nested_fields.len(), 2);
387    }
388
389    #[test]
390    fn test_parse_query_with_arguments() {
391        let query = r#"
392            query {
393                users(where: {status: "active"}, limit: 10) {
394                    id
395                    name
396                }
397            }
398        "#;
399        let parsed = parse_query(query).unwrap();
400
401        let first_field = &parsed.selections[0];
402        assert_eq!(first_field.arguments.len(), 2);
403        assert_eq!(first_field.arguments[0].name, "where");
404        assert_eq!(first_field.arguments[1].name, "limit");
405    }
406
407    #[test]
408    fn test_parse_mutation() {
409        let query = "mutation { createUser(input: {}) { id } }";
410        let parsed = parse_query(query).unwrap();
411
412        assert_eq!(parsed.operation_type, "mutation");
413        assert_eq!(parsed.root_field, "createUser");
414    }
415
416    #[test]
417    fn test_parse_query_with_variables() {
418        let query = r"
419            query GetUsers($where: UserWhere!) {
420                users(where: $where) {
421                    id
422                }
423            }
424        ";
425        let parsed = parse_query(query).unwrap();
426
427        assert_eq!(parsed.variables.len(), 1);
428        assert_eq!(parsed.variables[0].name, "where");
429    }
430
431    #[test]
432    fn test_parse_query_with_integer_argument() {
433        let query = r"
434            query {
435                users(limit: 42, offset: 100) {
436                    id
437                }
438            }
439        ";
440        let parsed = parse_query(query).unwrap();
441
442        let first_field = &parsed.selections[0];
443        assert_eq!(first_field.arguments.len(), 2);
444
445        assert_eq!(first_field.arguments[0].name, "limit");
446        assert_eq!(first_field.arguments[0].value_type, "int");
447        assert_eq!(first_field.arguments[0].value_json, "42");
448
449        assert_eq!(first_field.arguments[1].name, "offset");
450        assert_eq!(first_field.arguments[1].value_type, "int");
451        assert_eq!(first_field.arguments[1].value_json, "100");
452    }
453
454    #[test]
455    fn test_parse_query_with_fragment() {
456        let query = r"
457            fragment UserFields on User {
458                id
459                name
460                email
461            }
462
463            query {
464                users {
465                    ...UserFields
466                }
467            }
468        ";
469        let parsed = parse_query(query).unwrap();
470
471        // Should have fragment definition
472        assert_eq!(parsed.fragments.len(), 1);
473        assert_eq!(parsed.fragments[0].name, "UserFields");
474        assert_eq!(parsed.fragments[0].type_condition, "User");
475        assert_eq!(parsed.fragments[0].selections.len(), 3);
476
477        // Selection should have fragment spread
478        assert_eq!(parsed.selections[0].nested_fields.len(), 1);
479        assert_eq!(parsed.selections[0].nested_fields[0].name, "...UserFields");
480    }
481
482    #[test]
483    fn test_parse_query_with_directives() {
484        let query = r"
485            query($skipEmail: Boolean!) {
486                users {
487                    id
488                    email @skip(if: $skipEmail)
489                    name @include(if: true)
490                }
491            }
492        ";
493        let parsed = parse_query(query).unwrap();
494
495        let user_fields = &parsed.selections[0].nested_fields;
496        assert_eq!(user_fields.len(), 3);
497
498        // id has no directives
499        assert!(user_fields[0].directives.is_empty());
500
501        // email has @skip
502        assert_eq!(user_fields[1].directives.len(), 1);
503        assert_eq!(user_fields[1].directives[0].name, "skip");
504
505        // name has @include
506        assert_eq!(user_fields[2].directives.len(), 1);
507        assert_eq!(user_fields[2].directives[0].name, "include");
508    }
509
510    #[test]
511    fn test_parse_query_with_alias() {
512        let query = r"
513            query {
514                users {
515                    id
516                    writer: author {
517                        name
518                    }
519                }
520            }
521        ";
522        let parsed = parse_query(query).unwrap();
523
524        let user_fields = &parsed.selections[0].nested_fields;
525        assert_eq!(user_fields.len(), 2);
526
527        // Check aliased field
528        let aliased_field = &user_fields[1];
529        assert_eq!(aliased_field.name, "author");
530        assert_eq!(aliased_field.alias, Some("writer".to_string()));
531    }
532
533    #[test]
534    fn test_parse_inline_fragment() {
535        let query = r"
536            query {
537                users {
538                    id
539                    ... on Admin {
540                        permissions
541                    }
542                }
543            }
544        ";
545        let parsed = parse_query(query).unwrap();
546
547        let user_fields = &parsed.selections[0].nested_fields;
548        assert_eq!(user_fields.len(), 2);
549
550        // Check inline fragment
551        assert_eq!(user_fields[1].name, "...on Admin");
552        assert_eq!(user_fields[1].nested_fields.len(), 1);
553        assert_eq!(user_fields[1].nested_fields[0].name, "permissions");
554    }
555
556    // ── serialize_value depth guard ────────────────────────────────────────────
557
558    #[test]
559    fn test_serialize_value_flat_list_accepted() {
560        // A flat list of scalars is well within the depth limit.
561        let value = query::Value::List(vec![
562            query::Value::Int(graphql_parser::query::Number::from(1_i32)),
563            query::Value::String("hello".to_string()),
564            query::Value::Boolean(true),
565        ]);
566        let result = serialize_value(&value);
567        assert_eq!(result, r#"[1,"hello",true]"#);
568    }
569
570    #[test]
571    fn test_serialize_value_nested_at_limit_accepted() {
572        // Build a list nested exactly MAX_SERIALIZE_DEPTH levels — must serialize.
573        let mut v: query::Value<String> = query::Value::Boolean(true);
574        for _ in 0..MAX_SERIALIZE_DEPTH {
575            v = query::Value::List(vec![v]);
576        }
577        let result = serialize_value(&v);
578        // Verify it didn't fall back to "null" — it should contain "true".
579        assert!(result.contains("true"), "value at limit should serialize correctly: {result}");
580    }
581
582    #[test]
583    fn test_serialize_value_exceeds_depth_returns_null() {
584        // Build a list nested MAX_SERIALIZE_DEPTH + 1 levels — must return "null".
585        let mut v: query::Value<String> = query::Value::Boolean(true);
586        for _ in 0..=MAX_SERIALIZE_DEPTH {
587            v = query::Value::List(vec![v]);
588        }
589        let result = serialize_value(&v);
590        assert_eq!(result, "null", "over-limit value must fall back to null: {result}");
591    }
592
593    #[test]
594    fn test_serialize_value_deeply_nested_object_returns_null() {
595        // Deeply nested object should also hit the depth cap.
596        let mut v: query::Value<String> = query::Value::Boolean(false);
597        for i in 0..=MAX_SERIALIZE_DEPTH {
598            let mut map = std::collections::BTreeMap::new();
599            map.insert(format!("k{i}"), v);
600            v = query::Value::Object(map);
601        }
602        let result = serialize_value(&v);
603        assert_eq!(result, "null", "over-limit object must fall back to null: {result}");
604    }
605}