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)]
16pub enum GraphQLParseError {
17    /// Failed to parse GraphQL syntax.
18    #[error("Failed to parse GraphQL query: {0}")]
19    Syntax(String),
20
21    /// No query or mutation operation found in the document.
22    #[error("No query or mutation operation found")]
23    MissingOperation,
24
25    /// Selection set has no fields.
26    #[error("No fields in selection set")]
27    EmptySelection,
28}
29
30/// Parse GraphQL query string into Rust AST.
31///
32/// # Errors
33///
34/// Returns an error if:
35/// - GraphQL syntax is invalid or malformed
36/// - Query structure is invalid (missing operation, invalid selections)
37///
38/// # Example
39///
40/// ```
41/// use fraiseql_core::graphql::parse_query;
42///
43/// let query = "query { users { id name } }";
44/// let parsed = parse_query(query).unwrap();
45/// assert_eq!(parsed.operation_type, "query");
46/// assert_eq!(parsed.root_field, "users");
47/// ```
48pub fn parse_query(source: &str) -> Result<ParsedQuery, GraphQLParseError> {
49    // Use graphql-parser to parse query string
50    let doc: Document<String> =
51        query::parse_query(source).map_err(|e| GraphQLParseError::Syntax(e.to_string()))?;
52
53    // Extract first operation (ignore multiple operations for now)
54    let operation = doc
55        .definitions
56        .iter()
57        .find_map(|def| match def {
58            query::Definition::Operation(op) => Some(op),
59            query::Definition::Fragment(_) => None,
60        })
61        .ok_or(GraphQLParseError::MissingOperation)?;
62
63    // Extract operation details
64    let (operation_type, operation_name, root_field, selections, variables) =
65        extract_operation(operation)?;
66
67    // Extract fragment definitions
68    let fragments = extract_fragments(&doc)?;
69
70    Ok(ParsedQuery {
71        operation_type,
72        operation_name,
73        root_field,
74        selections,
75        variables,
76        fragments,
77        source: source.to_string(),
78    })
79}
80
81/// Extract fragment definitions from GraphQL document.
82fn extract_fragments(
83    doc: &Document<String>,
84) -> Result<Vec<crate::graphql::types::FragmentDefinition>, GraphQLParseError> {
85    let mut fragments = Vec::new();
86
87    for def in &doc.definitions {
88        if let Definition::Fragment(fragment) = def {
89            let selections = parse_selection_set(&fragment.selection_set)?;
90
91            // Extract fragment spreads from selections
92            let fragment_spreads = extract_fragment_spreads(&fragment.selection_set);
93
94            // Convert type condition to string
95            let type_condition = match &fragment.type_condition {
96                query::TypeCondition::On(type_name) => type_name.clone(),
97            };
98
99            fragments.push(crate::graphql::types::FragmentDefinition {
100                name: fragment.name.clone(),
101                type_condition,
102                selections,
103                fragment_spreads,
104            });
105        }
106    }
107
108    Ok(fragments)
109}
110
111/// Extract fragment spreads from a selection set.
112fn extract_fragment_spreads(selection_set: &query::SelectionSet<String>) -> Vec<String> {
113    let mut spreads = Vec::new();
114
115    for selection in &selection_set.items {
116        match selection {
117            Selection::FragmentSpread(spread) => {
118                spreads.push(spread.fragment_name.clone());
119            },
120            Selection::InlineFragment(inline) => {
121                // Inline fragments can also contain spreads
122                spreads.extend(extract_fragment_spreads(&inline.selection_set));
123            },
124            Selection::Field(field) => {
125                // Fields can have nested selections with spreads
126                spreads.extend(extract_fragment_spreads(&field.selection_set));
127            },
128        }
129    }
130
131    spreads
132}
133
134/// Extract operation details from GraphQL operation definition.
135fn extract_operation(
136    operation: &OperationDefinition<String>,
137) -> Result<
138    (String, Option<String>, String, Vec<FieldSelection>, Vec<VariableDefinition>),
139    GraphQLParseError,
140> {
141    let operation_type = match operation {
142        OperationDefinition::Query(_) | OperationDefinition::SelectionSet(_) => "query",
143        OperationDefinition::Mutation(_) => "mutation",
144        OperationDefinition::Subscription(_) => "subscription",
145    }
146    .to_string();
147
148    let (name, selection_set, var_defs) = match operation {
149        OperationDefinition::Query(q) => (&q.name, &q.selection_set, &q.variable_definitions),
150        OperationDefinition::Mutation(m) => (&m.name, &m.selection_set, &m.variable_definitions),
151        OperationDefinition::Subscription(s) => {
152            (&s.name, &s.selection_set, &s.variable_definitions)
153        },
154        OperationDefinition::SelectionSet(sel_set) => (&None, sel_set, &Vec::new()),
155    };
156
157    // Parse selection set (recursive)
158    let selections = parse_selection_set(selection_set)?;
159
160    // Get root field name (first field in selection set)
161    let root_field = selections
162        .first()
163        .map(|s| s.name.clone())
164        .ok_or(GraphQLParseError::EmptySelection)?;
165
166    // Parse variable definitions
167    let variables = var_defs
168        .iter()
169        .map(|var_def| VariableDefinition {
170            name:          var_def.name.clone(),
171            var_type:      parse_graphql_type(&var_def.var_type),
172            default_value: var_def.default_value.as_ref().map(|v| serialize_value(v)),
173        })
174        .collect();
175
176    Ok((operation_type, name.clone(), root_field, selections, variables))
177}
178
179/// Parse GraphQL selection set recursively.
180///
181/// Handles fields, fragment spreads, and inline fragments.
182fn parse_selection_set(
183    selection_set: &query::SelectionSet<String>,
184) -> Result<Vec<FieldSelection>, GraphQLParseError> {
185    let mut fields = Vec::new();
186
187    for selection in &selection_set.items {
188        match selection {
189            Selection::Field(field) => {
190                // Parse field arguments
191                let arguments = field
192                    .arguments
193                    .iter()
194                    .map(|(name, value)| GraphQLArgument {
195                        name:       name.clone(),
196                        value_type: value_type_string(value),
197                        value_json: serialize_value(value),
198                    })
199                    .collect();
200
201                // Parse nested selection set (recursive)
202                let nested_fields = parse_selection_set(&field.selection_set)?;
203
204                let directives = field.directives.iter().map(parse_directive).collect();
205
206                fields.push(FieldSelection {
207                    name: field.name.clone(),
208                    alias: field.alias.clone(),
209                    arguments,
210                    nested_fields,
211                    directives,
212                });
213            },
214            Selection::FragmentSpread(spread) => {
215                // Represent fragment spread as a special field with "..." prefix
216                // This will be resolved by FragmentResolver
217                let directives = spread.directives.iter().map(parse_directive).collect();
218
219                fields.push(FieldSelection {
220                    name: format!("...{}", spread.fragment_name),
221                    alias: None,
222                    arguments: vec![],
223                    nested_fields: vec![],
224                    directives,
225                });
226            },
227            Selection::InlineFragment(inline) => {
228                // Represent inline fragment as special field
229                // Type condition is stored in the name
230                let type_condition =
231                    inline.type_condition.as_ref().map_or_else(String::new, |tc| match tc {
232                        query::TypeCondition::On(name) => name.clone(),
233                    });
234
235                let nested_fields = parse_selection_set(&inline.selection_set)?;
236                let directives = inline.directives.iter().map(parse_directive).collect();
237
238                fields.push(FieldSelection {
239                    name: format!("...on {type_condition}"),
240                    alias: None,
241                    arguments: vec![],
242                    nested_fields,
243                    directives,
244                });
245            },
246        }
247    }
248
249    Ok(fields)
250}
251
252/// Get type of GraphQL value for classification.
253fn value_type_string(value: &query::Value<String>) -> String {
254    match value {
255        query::Value::String(_) => "string".to_string(),
256        query::Value::Int(_) => "int".to_string(),
257        query::Value::Float(_) => "float".to_string(),
258        query::Value::Boolean(_) => "boolean".to_string(),
259        query::Value::Null => "null".to_string(),
260        query::Value::Enum(_) => "enum".to_string(),
261        query::Value::List(_) => "list".to_string(),
262        query::Value::Object(_) => "object".to_string(),
263        query::Value::Variable(_) => "variable".to_string(),
264    }
265}
266
267/// Serialize GraphQL value to JSON string.
268fn serialize_value(value: &query::Value<String>) -> String {
269    match value {
270        query::Value::String(s) => format!("\"{}\"", s.replace('"', "\\\"")),
271        query::Value::Int(i) => {
272            // Use the safe as_i64() method from graphql-parser
273            i.as_i64().map_or_else(|| "0".to_string(), |n| n.to_string())
274        },
275        query::Value::Float(f) => format!("{f}"),
276        query::Value::Boolean(b) => b.to_string(),
277        query::Value::Null => "null".to_string(),
278        query::Value::Enum(e) => format!("\"{e}\""),
279        query::Value::List(items) => {
280            let serialized: Vec<_> = items.iter().map(serialize_value).collect();
281            format!("[{}]", serialized.join(","))
282        },
283        query::Value::Object(obj) => {
284            let pairs: Vec<_> =
285                obj.iter().map(|(k, v)| format!("\"{}\":{}", k, serialize_value(v))).collect();
286            format!("{{{}}}", pairs.join(","))
287        },
288        query::Value::Variable(v) => format!("\"${v}\""),
289    }
290}
291
292/// Parse GraphQL directive from graphql-parser Directive.
293fn parse_directive(directive: &GraphQLDirective<String>) -> Directive {
294    let arguments = directive
295        .arguments
296        .iter()
297        .map(|(name, value)| GraphQLArgument {
298            name:       name.clone(),
299            value_type: value_type_string(value),
300            value_json: serialize_value(value),
301        })
302        .collect();
303
304    Directive {
305        name: directive.name.clone(),
306        arguments,
307    }
308}
309
310/// Parse GraphQL type from graphql-parser Type to our `GraphQLType`.
311fn parse_graphql_type(graphql_type: &query::Type<String>) -> GraphQLType {
312    match graphql_type {
313        query::Type::NamedType(name) => GraphQLType {
314            name:          name.clone(),
315            nullable:      true, // Named types are nullable by default
316            list:          false,
317            list_nullable: false,
318        },
319        query::Type::ListType(inner) => GraphQLType {
320            name:          format!("[{}]", parse_graphql_type(inner).name),
321            nullable:      true,
322            list:          true,
323            list_nullable: true, // List items are nullable by default
324        },
325        query::Type::NonNullType(inner) => {
326            let mut parsed = parse_graphql_type(inner);
327            parsed.nullable = false;
328            if parsed.list {
329                parsed.list_nullable = false;
330            }
331            parsed
332        },
333    }
334}
335
336#[cfg(test)]
337mod tests {
338    use super::*;
339
340    #[test]
341    fn test_parse_simple_query() {
342        let query = "query { users { id name } }";
343        let parsed = parse_query(query).unwrap();
344
345        assert_eq!(parsed.operation_type, "query");
346        assert_eq!(parsed.root_field, "users");
347        assert_eq!(parsed.selections.len(), 1);
348        assert_eq!(parsed.selections[0].nested_fields.len(), 2);
349    }
350
351    #[test]
352    fn test_parse_query_with_arguments() {
353        let query = r#"
354            query {
355                users(where: {status: "active"}, limit: 10) {
356                    id
357                    name
358                }
359            }
360        "#;
361        let parsed = parse_query(query).unwrap();
362
363        let first_field = &parsed.selections[0];
364        assert_eq!(first_field.arguments.len(), 2);
365        assert_eq!(first_field.arguments[0].name, "where");
366        assert_eq!(first_field.arguments[1].name, "limit");
367    }
368
369    #[test]
370    fn test_parse_mutation() {
371        let query = "mutation { createUser(input: {}) { id } }";
372        let parsed = parse_query(query).unwrap();
373
374        assert_eq!(parsed.operation_type, "mutation");
375        assert_eq!(parsed.root_field, "createUser");
376    }
377
378    #[test]
379    fn test_parse_query_with_variables() {
380        let query = r"
381            query GetUsers($where: UserWhere!) {
382                users(where: $where) {
383                    id
384                }
385            }
386        ";
387        let parsed = parse_query(query).unwrap();
388
389        assert_eq!(parsed.variables.len(), 1);
390        assert_eq!(parsed.variables[0].name, "where");
391    }
392
393    #[test]
394    fn test_parse_query_with_integer_argument() {
395        let query = r"
396            query {
397                users(limit: 42, offset: 100) {
398                    id
399                }
400            }
401        ";
402        let parsed = parse_query(query).unwrap();
403
404        let first_field = &parsed.selections[0];
405        assert_eq!(first_field.arguments.len(), 2);
406
407        assert_eq!(first_field.arguments[0].name, "limit");
408        assert_eq!(first_field.arguments[0].value_type, "int");
409        assert_eq!(first_field.arguments[0].value_json, "42");
410
411        assert_eq!(first_field.arguments[1].name, "offset");
412        assert_eq!(first_field.arguments[1].value_type, "int");
413        assert_eq!(first_field.arguments[1].value_json, "100");
414    }
415
416    #[test]
417    fn test_parse_query_with_fragment() {
418        let query = r"
419            fragment UserFields on User {
420                id
421                name
422                email
423            }
424
425            query {
426                users {
427                    ...UserFields
428                }
429            }
430        ";
431        let parsed = parse_query(query).unwrap();
432
433        // Should have fragment definition
434        assert_eq!(parsed.fragments.len(), 1);
435        assert_eq!(parsed.fragments[0].name, "UserFields");
436        assert_eq!(parsed.fragments[0].type_condition, "User");
437        assert_eq!(parsed.fragments[0].selections.len(), 3);
438
439        // Selection should have fragment spread
440        assert_eq!(parsed.selections[0].nested_fields.len(), 1);
441        assert_eq!(parsed.selections[0].nested_fields[0].name, "...UserFields");
442    }
443
444    #[test]
445    fn test_parse_query_with_directives() {
446        let query = r"
447            query($skipEmail: Boolean!) {
448                users {
449                    id
450                    email @skip(if: $skipEmail)
451                    name @include(if: true)
452                }
453            }
454        ";
455        let parsed = parse_query(query).unwrap();
456
457        let user_fields = &parsed.selections[0].nested_fields;
458        assert_eq!(user_fields.len(), 3);
459
460        // id has no directives
461        assert!(user_fields[0].directives.is_empty());
462
463        // email has @skip
464        assert_eq!(user_fields[1].directives.len(), 1);
465        assert_eq!(user_fields[1].directives[0].name, "skip");
466
467        // name has @include
468        assert_eq!(user_fields[2].directives.len(), 1);
469        assert_eq!(user_fields[2].directives[0].name, "include");
470    }
471
472    #[test]
473    fn test_parse_query_with_alias() {
474        let query = r"
475            query {
476                users {
477                    id
478                    writer: author {
479                        name
480                    }
481                }
482            }
483        ";
484        let parsed = parse_query(query).unwrap();
485
486        let user_fields = &parsed.selections[0].nested_fields;
487        assert_eq!(user_fields.len(), 2);
488
489        // Check aliased field
490        let aliased_field = &user_fields[1];
491        assert_eq!(aliased_field.name, "author");
492        assert_eq!(aliased_field.alias, Some("writer".to_string()));
493    }
494
495    #[test]
496    fn test_parse_inline_fragment() {
497        let query = r"
498            query {
499                users {
500                    id
501                    ... on Admin {
502                        permissions
503                    }
504                }
505            }
506        ";
507        let parsed = parse_query(query).unwrap();
508
509        let user_fields = &parsed.selections[0].nested_fields;
510        assert_eq!(user_fields.len(), 2);
511
512        // Check inline fragment
513        assert_eq!(user_fields[1].name, "...on Admin");
514        assert_eq!(user_fields[1].nested_fields.len(), 1);
515        assert_eq!(user_fields[1].nested_fields[0].name, "permissions");
516    }
517}