diff --git a/crates/apollo-compiler/CHANGELOG.md b/crates/apollo-compiler/CHANGELOG.md index bc433cc89..60b399def 100644 --- a/crates/apollo-compiler/CHANGELOG.md +++ b/crates/apollo-compiler/CHANGELOG.md @@ -32,6 +32,18 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm [SimonSapin]: https://github.com/SimonSapin [pull/739]: https://github.com/apollographql/apollo-rs/pull/739 +- **Add parsing an `ast::Type` from a string - [lrlna] and [goto-bus-stop], [pull/718] fixing [issue/715]** + + Parses GraphQL type syntax: + ```rust + use apollo_compiler::ast::Type; + let ty = Type::parse("[ListItem!]!")?; + ``` + +[lrlna]: https://github.com/lrlna +[goto-bus-stop]: https://github.com/goto-bus-stop +[pull/718]: https://github.com/apollographql/apollo-rs/pull/718 +[issue/715]: https://github.com/apollographql/apollo-rs/issues/715 # [1.0.0-beta.6](https://crates.io/crates/apollo-compiler/1.0.0-beta.6) - 2023-11-10 diff --git a/crates/apollo-compiler/src/ast/from_cst.rs b/crates/apollo-compiler/src/ast/from_cst.rs index 1c6e6cb88..fc6a294e5 100644 --- a/crates/apollo-compiler/src/ast/from_cst.rs +++ b/crates/apollo-compiler/src/ast/from_cst.rs @@ -27,7 +27,7 @@ impl Document { } /// Similar to `TryFrom`, but with an `Option` return type because AST uses Option a lot. -trait Convert { +pub(crate) trait Convert { type Target; fn convert(&self, file_id: FileId) -> Option; } diff --git a/crates/apollo-compiler/src/ast/impls.rs b/crates/apollo-compiler/src/ast/impls.rs index 1fbf84a69..10a26083a 100644 --- a/crates/apollo-compiler/src/ast/impls.rs +++ b/crates/apollo-compiler/src/ast/impls.rs @@ -34,7 +34,7 @@ impl Document { Self::parser().parse_ast(source_text, path) } - /// Returns [`Diagnostics`] for cases where parsed input does not match + /// Returns [`DiagnosticList`] for cases where parsed input does not match /// the GraphQL grammar or where the parser reached a token limit or recursion limit. /// /// Does not perform any validation beyond this syntactic level. @@ -634,6 +634,19 @@ impl Type { } } + /// Parse the given source text as a reference to a type. + /// + /// `path` is the filesystem path (or arbitrary string) used in diagnostics + /// to identify this source file to users. + /// + /// Create a [`Parser`] to use different parser configuration. + pub fn parse( + source_text: impl Into, + path: impl AsRef, + ) -> Result { + Parser::new().parse_type(source_text, path) + } + serialize_method!(); } diff --git a/crates/apollo-compiler/src/parser.rs b/crates/apollo-compiler/src/parser.rs index 7644a7ac7..75e1b286b 100644 --- a/crates/apollo-compiler/src/parser.rs +++ b/crates/apollo-compiler/src/parser.rs @@ -1,4 +1,5 @@ use crate::ast; +use crate::ast::from_cst::Convert; use crate::ast::Document; use crate::executable; use crate::schema::SchemaBuilder; @@ -195,7 +196,7 @@ impl Parser { self.parse_ast(source_text, path).to_mixed() } - /// Parse the given source a selection set with optional outer brackets. + /// Parse the given source text as a selection set with optional outer brackets. /// /// `path` is the filesystem path (or arbitrary string) used in diagnostics /// to identify this source file to users. @@ -234,6 +235,37 @@ impl Parser { } } + /// Parse the given source text as a reference to a type. + /// + /// `path` is the filesystem path (or arbitrary string) used in diagnostics + /// to identify this source file to users. + pub fn parse_type( + &mut self, + source_text: impl Into, + path: impl AsRef, + ) -> Result { + let (tree, source_file) = + self.parse_common(source_text.into(), path.as_ref().to_owned(), |parser| { + parser.parse_type() + }); + let file_id = FileId::new(); + + let sources: crate::SourceMap = Arc::new([(file_id, source_file)].into()); + let mut errors = DiagnosticList::new(None, sources.clone()); + for (file_id, source) in sources.iter() { + source.validate_parse_errors(&mut errors, *file_id) + } + + if errors.is_empty() { + if let Some(ty) = tree.ty().convert(file_id) { + return Ok(ty); + } + unreachable!("conversion is infallible if there were no syntax errors"); + } else { + Err(errors) + } + } + /// What level of recursion was reached during the last call to a `parse_*` method. /// /// Collecting this on a corpus of documents can help decide diff --git a/crates/apollo-compiler/tests/field_type.rs b/crates/apollo-compiler/tests/field_type.rs new file mode 100644 index 000000000..db403ff53 --- /dev/null +++ b/crates/apollo-compiler/tests/field_type.rs @@ -0,0 +1,37 @@ +use apollo_compiler::schema::Type; + +#[test] +fn test_valid_field_type() { + let input = "String!"; + let field_type = Type::parse(input, "field_type.graphql").expect("expected a field type"); + assert_eq!(field_type.to_string(), input); + + let input = "[[[[[Int!]!]!]!]!]!"; + let field_type = Type::parse(input, "field_type.graphql").expect("expected a field type"); + assert_eq!(field_type.to_string(), input); +} + +#[test] +fn test_invalid_field_type() { + let input = "[[String]"; + match Type::parse(input, "field_type.graphql") { + Ok(parsed) => panic!("Field type should fail to parse, instead got `{parsed}`"), + Err(errors) => { + let errors = errors.to_string_no_color(); + assert!( + errors.contains("Error: syntax error: expected R_BRACK, got EOF"), + "{errors}" + ); + } + } + + let input = "[]"; + match Type::parse(input, "field_type.graphql") { + Ok(parsed) => panic!("Field type should fail to parse, instead got `{parsed}`"), + Err(diag) => { + let errors = diag.to_string_no_color(); + assert!(errors.contains("expected item type"), "{errors}"); + assert!(errors.contains("expected R_BRACK, got EOF"), "{errors}"); + } + } +} diff --git a/crates/apollo-compiler/tests/main.rs b/crates/apollo-compiler/tests/main.rs index d067acef7..b5060e10b 100644 --- a/crates/apollo-compiler/tests/main.rs +++ b/crates/apollo-compiler/tests/main.rs @@ -1,6 +1,7 @@ mod executable; mod extensions; mod field_set; +mod field_type; mod merge_schemas; /// Formerly in src/lib.rs mod misc; diff --git a/crates/apollo-parser/CHANGELOG.md b/crates/apollo-parser/CHANGELOG.md index f3b6c8e34..0e51820bb 100644 --- a/crates/apollo-parser/CHANGELOG.md +++ b/crates/apollo-parser/CHANGELOG.md @@ -16,6 +16,28 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## Maintenance ## Documentation --> +# [unreleased](https://crates.io/crates/apollo-parser/x.x.x) - 2023-xx-xx + +## Features +- **`parse_type` parses a selection set with optional outer brackets - [lrlna], [pull/718] fixing [issue/715]** + This returns a `SyntaxTree` which instead of `.document() -> cst::Document` + has `.type() -> cst::Type`. + This is intended to parse the string value of a [`@field(type:)` argument][fieldtype] + used in some Apollo Federation directives. + ```rust + let source = r#"[[NestedList!]]!"#; + + let parser = Parser::new(source); + let cst: SyntaxTree = parser.parse_type(); + let errors = cst.errors().collect::>(); + assert_eq!(errors.len(), 0); + ``` + +[lrlna]: https://github.com/lrlna +[pull/718]: https://github.com/apollographql/apollo-rs/pull/718 +[issue/715]: https://github.com/apollographql/apollo-rs/issues/715 +[fieldtype]: https://specs.apollo.dev/join/v0.3/#@field + # [0.7.3]([unreleased](https://crates.io/crates/apollo-parser/0.7.3)) - 2023-11-07 ## Fixes diff --git a/crates/apollo-parser/src/parser/grammar/mod.rs b/crates/apollo-parser/src/parser/grammar/mod.rs index 115872437..e12c0944f 100644 --- a/crates/apollo-parser/src/parser/grammar/mod.rs +++ b/crates/apollo-parser/src/parser/grammar/mod.rs @@ -1,5 +1,6 @@ pub(crate) mod document; pub(crate) mod selection; +pub(crate) mod ty; mod argument; mod description; @@ -15,7 +16,6 @@ mod object; mod operation; mod scalar; mod schema; -mod ty; mod union_; mod value; mod variable; diff --git a/crates/apollo-parser/src/parser/mod.rs b/crates/apollo-parser/src/parser/mod.rs index 30e376c91..1de85defa 100644 --- a/crates/apollo-parser/src/parser/mod.rs +++ b/crates/apollo-parser/src/parser/mod.rs @@ -8,7 +8,7 @@ pub(crate) mod grammar; use std::{cell::RefCell, rc::Rc}; use crate::{ - cst::{Document, SelectionSet}, + cst::{Document, SelectionSet, Type}, lexer::Lexer, Error, LimitTracker, Token, TokenKind, }; @@ -146,12 +146,16 @@ impl<'a> Parser<'a> { match builder { syntax_tree::SyntaxTreeWrapper::Document(tree) => tree, - syntax_tree::SyntaxTreeWrapper::FieldSet(_) => { + syntax_tree::SyntaxTreeWrapper::Type(_) + | syntax_tree::SyntaxTreeWrapper::FieldSet(_) => { unreachable!("parse constructor can only construct a document") } } } + /// Parse a selection set with optional outer braces. + /// This is the expected format of the string value of the `fields` argument of some directives + /// like [`@requires`](https://www.apollographql.com/docs/federation/federated-types/federated-directives/#requires). pub fn parse_selection_set(mut self) -> SyntaxTree { grammar::selection::field_set(&mut self); @@ -166,8 +170,30 @@ impl<'a> Parser<'a> { match builder { syntax_tree::SyntaxTreeWrapper::FieldSet(tree) => tree, - syntax_tree::SyntaxTreeWrapper::Document(_) => { - unreachable!("parse constructor can only construct a selection set") + syntax_tree::SyntaxTreeWrapper::Document(_) + | syntax_tree::SyntaxTreeWrapper::Type(_) => { + unreachable!("parse_selection_set constructor can only construct a selection set") + } + } + } + + /// Parse a GraphQL type. + /// This is the expected format of the string value of the `type` argument + /// of some directives like [`@field`](https://specs.apollo.dev/join/v0.3/#@field). + pub fn parse_type(mut self) -> SyntaxTree { + grammar::ty::ty(&mut self); + + let builder = Rc::try_unwrap(self.builder) + .expect("More than one reference to builder left") + .into_inner(); + let builder = + builder.finish_type(self.errors, self.recursion_limit, self.lexer.limit_tracker); + + match builder { + syntax_tree::SyntaxTreeWrapper::Type(tree) => tree, + syntax_tree::SyntaxTreeWrapper::FieldSet(_) + | syntax_tree::SyntaxTreeWrapper::Document(_) => { + unreachable!("parse_type constructor can only construct a type") } } } @@ -299,9 +325,7 @@ impl<'a> Parser<'a> { /// Consume the next token if it is `kind` or emit an error /// otherwise. pub(crate) fn expect(&mut self, token: TokenKind, kind: SyntaxKind) { - let current = if let Some(current) = self.current() { - current - } else { + let Some(current) = self.current() else { return; }; let is_eof = current.kind == TokenKind::Eof; diff --git a/crates/apollo-parser/src/parser/syntax_tree.rs b/crates/apollo-parser/src/parser/syntax_tree.rs index f4ca6c85e..7fd338ee2 100644 --- a/crates/apollo-parser/src/parser/syntax_tree.rs +++ b/crates/apollo-parser/src/parser/syntax_tree.rs @@ -49,6 +49,7 @@ use super::LimitTracker; pub(crate) enum SyntaxTreeWrapper { Document(SyntaxTree), FieldSet(SyntaxTree), + Type(SyntaxTree), } #[derive(PartialEq, Eq, Clone)] @@ -111,6 +112,25 @@ impl SyntaxTree { } } +impl SyntaxTree { + /// Return the root typed `SelectionSet` node. This is used for parsing + /// selection sets defined by @requires directive. + pub fn ty(&self) -> cst::Type { + match self.syntax_node().kind() { + SyntaxKind::NAMED_TYPE => cst::Type::NamedType(cst::NamedType { + syntax: self.syntax_node(), + }), + SyntaxKind::LIST_TYPE => cst::Type::ListType(cst::ListType { + syntax: self.syntax_node(), + }), + SyntaxKind::NON_NULL_TYPE => cst::Type::NonNullType(cst::NonNullType { + syntax: self.syntax_node(), + }), + _ => unreachable!("this should only return Type node"), + } + } +} + impl fmt::Debug for SyntaxTree { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn print(f: &mut fmt::Formatter<'_>, indent: usize, element: SyntaxElement) -> fmt::Result { @@ -228,6 +248,23 @@ impl SyntaxTreeBuilder { _phantom: PhantomData, }) } + + pub(crate) fn finish_type( + self, + errors: Vec, + recursion_limit: LimitTracker, + token_limit: LimitTracker, + ) -> SyntaxTreeWrapper { + SyntaxTreeWrapper::Type(SyntaxTree { + green: self.builder.finish(), + // TODO: keep the errors in the builder rather than pass it in here? + errors, + // TODO: keep the recursion and token limits in the builder rather than pass it in here? + recursion_limit, + token_limit, + _phantom: PhantomData, + }) + } } #[cfg(test)]