Repository: apollographql/apollo-rs (rust)

Issue:
add parsing support for `Type`
Similar to #681, `@field` directive can be provided with `Type` information in a string form, which will then need to be parsed before being used in the schema. It seems like it does not need to be validated specifically (but we should do it anyways)

To make this change, we need to:

1. add `parse_type` method to `apollo-parser`
2. add a `cst::Type` specific method for `SyntaxTree` implementation to return the CST node, similar to doc.document(), and field_set.field_set()
3. create a `parse_type` method in `apollo-compiler`
4. add `Type` struct to `apollo-compiler`'s schema that would also be able to include `BuildErrors`
5. add a `parse` and `validate` methods to the `Type` struct impl

Pull request:
feat(parser, compiler): add parsing a type reference from a string
To parse standalone source text like `[Type]!`, for example for the federation directive [`@fields(type:)`](https://specs.apollo.dev/join/v0.3/#@field). 

closes #715 

Patch:
diff --git a/crates/apollo-compiler/CHANGELOG.md b/crates/apollo-compiler/CHANGELOG.md
index bc433cc..60b399d 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 1c6e6cb..fc6a294 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<Self::Target>;
 }
diff --git a/crates/apollo-compiler/src/ast/impls.rs b/crates/apollo-compiler/src/ast/impls.rs
index 1fbf84a..10a2608 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<String>,
+        path: impl AsRef<Path>,
+    ) -> Result<Self, DiagnosticList> {
+        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 7644a7a..75e1b28 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<String>,
+        path: impl AsRef<Path>,
+    ) -> Result<ast::Type, DiagnosticList> {
+        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-parser/CHANGELOG.md b/crates/apollo-parser/CHANGELOG.md
index f3b6c8e..0e51820 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<Type>` 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<cst::Type> = parser.parse_type();
+  let errors = cst.errors().collect::<Vec<_>>();
+  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 1158724..e12c094 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 30e376c..1de85de 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<SelectionSet> {
         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<Type> {
+        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 f4ca6c8..7fd338e 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<cst::Document>),
     FieldSet(SyntaxTree<cst::SelectionSet>),
+    Type(SyntaxTree<cst::Type>),
 }
 
 #[derive(PartialEq, Eq, Clone)]
@@ -111,6 +112,25 @@ impl SyntaxTree<cst::SelectionSet> {
     }
 }
 
+impl SyntaxTree<cst::Type> {
+    /// 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<T: CstNode> fmt::Debug for SyntaxTree<T> {
     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<Error>,
+        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)]


Test patch:
diff --git a/crates/apollo-compiler/tests/field_type.rs b/crates/apollo-compiler/tests/field_type.rs
new file mode 100644
index 0000000..db403ff
--- /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 d067ace..b5060e1 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;