Skip to main content

bluejay_parser/ast/executable/
executable_document.rs

1use crate::ast::executable::{
2    ExecutableDefinition, ExplicitOperationDefinition, Field, FragmentDefinition, FragmentSpread,
3    ImplicitOperationDefinition, InlineFragment, OperationDefinition, Selection, SelectionSet,
4    VariableDefinition, VariableDefinitions, VariableType,
5};
6use crate::ast::{
7    Argument, Arguments, DepthLimiter, Directive, Directives, Parse, ParseDetails, ParseError,
8    Tokens, TryFromTokens, Value,
9};
10
11#[derive(Debug)]
12pub struct ExecutableDocument<'a> {
13    operation_definitions: Vec<OperationDefinition<'a>>,
14    fragment_definitions: Vec<FragmentDefinition<'a>>,
15}
16
17impl<'a> ExecutableDocument<'a> {
18    pub(crate) fn new(
19        operation_definitions: Vec<OperationDefinition<'a>>,
20        fragment_definitions: Vec<FragmentDefinition<'a>>,
21    ) -> Self {
22        Self {
23            operation_definitions,
24            fragment_definitions,
25        }
26    }
27
28    pub fn operation_definitions(&self) -> &[OperationDefinition<'a>] {
29        &self.operation_definitions
30    }
31
32    pub fn fragment_definitions(&self) -> &[FragmentDefinition<'a>] {
33        &self.fragment_definitions
34    }
35
36    #[inline]
37    fn is_empty(&self) -> bool {
38        self.operation_definitions.is_empty() && self.fragment_definitions.is_empty()
39    }
40}
41
42impl<'a> Parse<'a> for ExecutableDocument<'a> {
43    #[inline]
44    fn parse_from_tokens(mut tokens: impl Tokens<'a>, max_depth: usize) -> ParseDetails<Self> {
45        let mut instance: Self = Self::new(Vec::new(), Vec::new());
46        let mut errors = Vec::new();
47        let mut last_pass_had_error = false;
48
49        loop {
50            last_pass_had_error = match ExecutableDefinition::try_from_tokens(
51                &mut tokens,
52                DepthLimiter::new(max_depth),
53            ) {
54                Ok(Some(ExecutableDefinition::Operation(operation_definition))) => {
55                    instance.operation_definitions.push(operation_definition);
56                    false
57                }
58                Ok(Some(ExecutableDefinition::Fragment(fragment_definition))) => {
59                    instance.fragment_definitions.push(fragment_definition);
60                    false
61                }
62                Ok(None) => {
63                    if let Some(token) = tokens.next() {
64                        if !last_pass_had_error {
65                            errors.push(ParseError::UnexpectedToken { span: token.into() });
66                        }
67                        true
68                    } else {
69                        break;
70                    }
71                }
72                Err(ParseError::MaxDepthExceeded) => {
73                    errors.push(ParseError::MaxDepthExceeded);
74                    // no sense in continuing to parse if we've hit the depth limit
75                    break;
76                }
77                Err(err) => {
78                    if !last_pass_had_error {
79                        errors.push(err);
80                    }
81                    true
82                }
83            }
84        }
85
86        let token_count = tokens.token_count();
87        let lex_errors = tokens.into_errors();
88
89        let errors = if lex_errors.is_empty() {
90            if errors.is_empty() && instance.is_empty() {
91                vec![ParseError::EmptyDocument.into()]
92            } else {
93                errors.into_iter().map(Into::into).collect()
94            }
95        } else {
96            lex_errors.into_iter().map(Into::into).collect()
97        };
98
99        let result = if errors.is_empty() {
100            Ok(instance)
101        } else {
102            Err(errors)
103        };
104
105        ParseDetails::new(result, token_count)
106    }
107}
108
109impl<'a> bluejay_core::executable::ExecutableDocument for ExecutableDocument<'a> {
110    type Value<const CONST: bool> = Value<'a, CONST>;
111    type VariableType = VariableType<'a>;
112    type Argument<const CONST: bool> = Argument<'a, CONST>;
113    type Arguments<const CONST: bool> = Arguments<'a, CONST>;
114    type Directive<const CONST: bool> = Directive<'a, CONST>;
115    type Directives<const CONST: bool> = Directives<'a, CONST>;
116    type FragmentSpread = FragmentSpread<'a>;
117    type Field = Field<'a>;
118    type Selection = Selection<'a>;
119    type SelectionSet = SelectionSet<'a>;
120    type InlineFragment = InlineFragment<'a>;
121    type VariableDefinition = VariableDefinition<'a>;
122    type VariableDefinitions = VariableDefinitions<'a>;
123    type ExplicitOperationDefinition = ExplicitOperationDefinition<'a>;
124    type ImplicitOperationDefinition = ImplicitOperationDefinition<'a>;
125    type OperationDefinition = OperationDefinition<'a>;
126    type FragmentDefinition = FragmentDefinition<'a>;
127    type FragmentDefinitions<'b>
128        = std::slice::Iter<'b, Self::FragmentDefinition>
129    where
130        Self: 'b;
131    type OperationDefinitions<'b>
132        = std::slice::Iter<'b, Self::OperationDefinition>
133    where
134        Self: 'b;
135
136    fn operation_definitions(&self) -> Self::OperationDefinitions<'_> {
137        self.operation_definitions.iter()
138    }
139
140    fn fragment_definitions(&self) -> Self::FragmentDefinitions<'_> {
141        self.fragment_definitions.iter()
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use super::{ExecutableDocument, Parse};
148    use crate::ast::ParseOptions;
149
150    /// Verifies that the ExecutableDefinitionsRule from the GraphQL spec
151    /// (sec 5.1.1) is enforced at the parser level: type and schema definitions
152    /// are rejected when parsing as an ExecutableDocument.
153    #[test]
154    fn rejects_type_definition() {
155        let doc = r#"
156            type Foo {
157                bar: String
158            }
159        "#;
160        let result = ExecutableDocument::parse(doc).result;
161        assert!(
162            result.is_err(),
163            "Expected parse error for type definition in executable document"
164        );
165    }
166
167    #[test]
168    fn rejects_schema_definition() {
169        let doc = r#"
170            schema {
171                query: Query
172            }
173        "#;
174        let result = ExecutableDocument::parse(doc).result;
175        assert!(
176            result.is_err(),
177            "Expected parse error for schema definition in executable document"
178        );
179    }
180
181    #[test]
182    fn rejects_mixed_executable_and_type_definitions() {
183        let doc = r#"
184            query { foo }
185            type Bar { baz: Int }
186        "#;
187        let result = ExecutableDocument::parse(doc).result;
188        assert!(
189            result.is_err(),
190            "Expected parse error when mixing executable and type definitions"
191        );
192    }
193
194    #[test]
195    fn test_success() {
196        let document = r#"
197            {
198                dog {
199                    ...fragmentOne
200                    ...fragmentTwo
201                }
202            }
203
204            fragment fragmentOne on Dog {
205                name
206            }
207
208            fragment fragmentTwo on Dog {
209                owner {
210                    name
211                }
212            }
213        "#;
214
215        let defs = ExecutableDocument::parse(document).result.unwrap();
216
217        assert_eq!(2, defs.fragment_definitions().len());
218        assert_eq!(1, defs.operation_definitions().len());
219    }
220
221    #[test]
222    fn test_depth_limit() {
223        // Depth is bumped to 1 entering the selection set (`{`)
224        // Depth is bumped to 2 entering the field (`foo`)
225        // Depth is only bumped further when args/directives/sub-selections are present
226        let document = r#"query { foo }"#;
227
228        let errors = ExecutableDocument::parse_with_options(
229            document,
230            ParseOptions {
231                graphql_ruby_compatibility: false,
232                max_depth: 1,
233                max_tokens: None,
234            },
235        )
236        .result
237        .unwrap_err();
238
239        assert_eq!(1, errors.len(), "{errors:?}");
240
241        assert_eq!("Max depth exceeded", errors[0].message());
242
243        let executable_document = ExecutableDocument::parse_with_options(
244            document,
245            ParseOptions {
246                graphql_ruby_compatibility: false,
247                max_depth: 2,
248                max_tokens: None,
249            },
250        )
251        .result
252        .unwrap();
253        assert_eq!(1, executable_document.operation_definitions().len());
254    }
255}