bluejay-parser 0.4.0

A GraphQL parser
Documentation
use crate::ast::executable::{
    ExecutableDefinition, ExplicitOperationDefinition, Field, FragmentDefinition, FragmentSpread,
    ImplicitOperationDefinition, InlineFragment, OperationDefinition, Selection, SelectionSet,
    VariableDefinition, VariableDefinitions, VariableType,
};
use crate::ast::{
    Argument, Arguments, DepthLimiter, Directive, Directives, Parse, ParseDetails, ParseError,
    Tokens, TryFromTokens, Value,
};

#[derive(Debug)]
pub struct ExecutableDocument<'a> {
    operation_definitions: Vec<OperationDefinition<'a>>,
    fragment_definitions: Vec<FragmentDefinition<'a>>,
}

impl<'a> ExecutableDocument<'a> {
    pub(crate) fn new(
        operation_definitions: Vec<OperationDefinition<'a>>,
        fragment_definitions: Vec<FragmentDefinition<'a>>,
    ) -> Self {
        Self {
            operation_definitions,
            fragment_definitions,
        }
    }

    pub fn operation_definitions(&self) -> &[OperationDefinition<'a>] {
        &self.operation_definitions
    }

    pub fn fragment_definitions(&self) -> &[FragmentDefinition<'a>] {
        &self.fragment_definitions
    }

    #[inline]
    fn is_empty(&self) -> bool {
        self.operation_definitions.is_empty() && self.fragment_definitions.is_empty()
    }
}

impl<'a> Parse<'a> for ExecutableDocument<'a> {
    #[inline]
    fn parse_from_tokens(mut tokens: impl Tokens<'a>, max_depth: usize) -> ParseDetails<Self> {
        let mut instance: Self = Self::new(Vec::new(), Vec::new());
        let mut errors = Vec::new();
        let mut last_pass_had_error = false;

        loop {
            last_pass_had_error = match ExecutableDefinition::try_from_tokens(
                &mut tokens,
                DepthLimiter::new(max_depth),
            ) {
                Ok(Some(ExecutableDefinition::Operation(operation_definition))) => {
                    instance.operation_definitions.push(operation_definition);
                    false
                }
                Ok(Some(ExecutableDefinition::Fragment(fragment_definition))) => {
                    instance.fragment_definitions.push(fragment_definition);
                    false
                }
                Ok(None) => {
                    if let Some(token) = tokens.next() {
                        if !last_pass_had_error {
                            errors.push(ParseError::UnexpectedToken { span: token.into() });
                        }
                        true
                    } else {
                        break;
                    }
                }
                Err(ParseError::MaxDepthExceeded) => {
                    errors.push(ParseError::MaxDepthExceeded);
                    // no sense in continuing to parse if we've hit the depth limit
                    break;
                }
                Err(err) => {
                    if !last_pass_had_error {
                        errors.push(err);
                    }
                    true
                }
            }
        }

        let token_count = tokens.token_count();
        let lex_errors = tokens.into_errors();

        let errors = if lex_errors.is_empty() {
            if errors.is_empty() && instance.is_empty() {
                vec![ParseError::EmptyDocument.into()]
            } else {
                errors.into_iter().map(Into::into).collect()
            }
        } else {
            lex_errors.into_iter().map(Into::into).collect()
        };

        let result = if errors.is_empty() {
            Ok(instance)
        } else {
            Err(errors)
        };

        ParseDetails::new(result, token_count)
    }
}

impl<'a> bluejay_core::executable::ExecutableDocument for ExecutableDocument<'a> {
    type Value<const CONST: bool> = Value<'a, CONST>;
    type VariableType = VariableType<'a>;
    type Argument<const CONST: bool> = Argument<'a, CONST>;
    type Arguments<const CONST: bool> = Arguments<'a, CONST>;
    type Directive<const CONST: bool> = Directive<'a, CONST>;
    type Directives<const CONST: bool> = Directives<'a, CONST>;
    type FragmentSpread = FragmentSpread<'a>;
    type Field = Field<'a>;
    type Selection = Selection<'a>;
    type SelectionSet = SelectionSet<'a>;
    type InlineFragment = InlineFragment<'a>;
    type VariableDefinition = VariableDefinition<'a>;
    type VariableDefinitions = VariableDefinitions<'a>;
    type ExplicitOperationDefinition = ExplicitOperationDefinition<'a>;
    type ImplicitOperationDefinition = ImplicitOperationDefinition<'a>;
    type OperationDefinition = OperationDefinition<'a>;
    type FragmentDefinition = FragmentDefinition<'a>;
    type FragmentDefinitions<'b>
        = std::slice::Iter<'b, Self::FragmentDefinition>
    where
        Self: 'b;
    type OperationDefinitions<'b>
        = std::slice::Iter<'b, Self::OperationDefinition>
    where
        Self: 'b;

    fn operation_definitions(&self) -> Self::OperationDefinitions<'_> {
        self.operation_definitions.iter()
    }

    fn fragment_definitions(&self) -> Self::FragmentDefinitions<'_> {
        self.fragment_definitions.iter()
    }
}

#[cfg(test)]
mod tests {
    use super::{ExecutableDocument, Parse};
    use crate::ast::ParseOptions;

    /// Verifies that the ExecutableDefinitionsRule from the GraphQL spec
    /// (sec 5.1.1) is enforced at the parser level: type and schema definitions
    /// are rejected when parsing as an ExecutableDocument.
    #[test]
    fn rejects_type_definition() {
        let doc = r#"
            type Foo {
                bar: String
            }
        "#;
        let result = ExecutableDocument::parse(doc).result;
        assert!(
            result.is_err(),
            "Expected parse error for type definition in executable document"
        );
    }

    #[test]
    fn rejects_schema_definition() {
        let doc = r#"
            schema {
                query: Query
            }
        "#;
        let result = ExecutableDocument::parse(doc).result;
        assert!(
            result.is_err(),
            "Expected parse error for schema definition in executable document"
        );
    }

    #[test]
    fn rejects_mixed_executable_and_type_definitions() {
        let doc = r#"
            query { foo }
            type Bar { baz: Int }
        "#;
        let result = ExecutableDocument::parse(doc).result;
        assert!(
            result.is_err(),
            "Expected parse error when mixing executable and type definitions"
        );
    }

    #[test]
    fn test_success() {
        let document = r#"
            {
                dog {
                    ...fragmentOne
                    ...fragmentTwo
                }
            }

            fragment fragmentOne on Dog {
                name
            }

            fragment fragmentTwo on Dog {
                owner {
                    name
                }
            }
        "#;

        let defs = ExecutableDocument::parse(document).result.unwrap();

        assert_eq!(2, defs.fragment_definitions().len());
        assert_eq!(1, defs.operation_definitions().len());
    }

    #[test]
    fn test_depth_limit() {
        // Depth is bumped to 1 entering the selection set (`{`)
        // Depth is bumped to 2 entering the field (`foo`)
        // Depth is only bumped further when args/directives/sub-selections are present
        let document = r#"query { foo }"#;

        let errors = ExecutableDocument::parse_with_options(
            document,
            ParseOptions {
                graphql_ruby_compatibility: false,
                max_depth: 1,
                max_tokens: None,
            },
        )
        .result
        .unwrap_err();

        assert_eq!(1, errors.len(), "{errors:?}");

        assert_eq!("Max depth exceeded", errors[0].message());

        let executable_document = ExecutableDocument::parse_with_options(
            document,
            ParseOptions {
                graphql_ruby_compatibility: false,
                max_depth: 2,
                max_tokens: None,
            },
        )
        .result
        .unwrap();
        assert_eq!(1, executable_document.operation_definitions().len());
    }
}