Skip to content

Commit

Permalink
feat(parser, compiler): add parsing ast::Type from a string (#718)
Browse files Browse the repository at this point in the history
* feat(parser): add parse_type method to allow type parsing only

* setup field type parsing in apollo-compiler

* add field type to schema in compiler

* parse_field_type to return an option

* parse_field_type returns Result<(FieldType, Diagnostics), Diagnostics>

* clippy

* FieldType struct does not need sources, as we create diagnostics right away

* Parse directly into `ast::Type`

* naming

* Return Err() on parse errors

* tweak unreachable! messages

* Do not pass the same SourceMap twice

Co-authored-by: Simon Sapin <[email protected]>

* Improve doc comments for `Parser::parse_{type,selection_set}`

Co-authored-by: Simon Sapin <[email protected]>

* doc tweak

* changelogs

---------

Co-authored-by: Renée Kooi <[email protected]>
Co-authored-by: Renée <[email protected]>
Co-authored-by: Simon Sapin <[email protected]>
  • Loading branch information
4 people authored Nov 15, 2023
1 parent ed0d481 commit d0fca39
Show file tree
Hide file tree
Showing 10 changed files with 189 additions and 11 deletions.
12 changes: 12 additions & 0 deletions crates/apollo-compiler/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion crates/apollo-compiler/src/ast/from_cst.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
}
Expand Down
15 changes: 14 additions & 1 deletion crates/apollo-compiler/src/ast/impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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!();
}

Expand Down
34 changes: 33 additions & 1 deletion crates/apollo-compiler/src/parser.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::ast;
use crate::ast::from_cst::Convert;
use crate::ast::Document;
use crate::executable;
use crate::schema::SchemaBuilder;
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
37 changes: 37 additions & 0 deletions crates/apollo-compiler/tests/field_type.rs
Original file line number Diff line number Diff line change
@@ -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}");
}
}
}
1 change: 1 addition & 0 deletions crates/apollo-compiler/tests/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
mod executable;
mod extensions;
mod field_set;
mod field_type;
mod merge_schemas;
/// Formerly in src/lib.rs
mod misc;
Expand Down
22 changes: 22 additions & 0 deletions crates/apollo-parser/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion crates/apollo-parser/src/parser/grammar/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub(crate) mod document;
pub(crate) mod selection;
pub(crate) mod ty;

mod argument;
mod description;
Expand All @@ -15,7 +16,6 @@ mod object;
mod operation;
mod scalar;
mod schema;
mod ty;
mod union_;
mod value;
mod variable;
38 changes: 31 additions & 7 deletions crates/apollo-parser/src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -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);

Expand All @@ -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")
}
}
}
Expand Down Expand Up @@ -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;
Expand Down
37 changes: 37 additions & 0 deletions crates/apollo-parser/src/parser/syntax_tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)]
Expand Down

0 comments on commit d0fca39

Please sign in to comment.