Skip to content

Commit

Permalink
feat(apollo-parser, apollo-compiler): support parsing and validating …
Browse files Browse the repository at this point in the history
…FieldSet (#685)

* feat(apollo-parser): parser allows parsing selection sets only

In order to support `FieldSet` work in `federation-next`, we'd like to allow specific `field_set` parsing. It can be accessed after constructing the parser by calling `.parse_fieldset`.

```rust
let source = r#"{ a }"#;

let parser = Parser::new(source);
let cst: SyntaxTree<cst::SelectionSet> = parser.parse_fieldset();
let errors = cst.errors().collect::<Vec<_>>();
assert_eq!(errors.len(), 0);

let sel_set: cst::SelectionSet = cst.selection_set();
let _ = sel_set.selections().map(|sel| {
    if let cst::Selection::Field(f) = sel {
        assert_eq!(f.name().unwrap().text().as_ref(), "a")
    }
});
```

* rename parse_fieldset to parse_selection_set

This follows specification naming more closely, rather than aligning the parser to federation implementation

* feat(parser): allow field sets to not have opening braces to support `@requires`

Because field sets may or may not have opening braces, we are no longer strictly following the graphql specification. As part of this change, the API points referencing parsing `selection_set` are now renamed to `field_set`

* add parse_field_set to compiler API

This commit creates several wrapper types to handle FieldSet.

- apollo-compiler's parser endpoint now has `parse_field_set` public API and an internal `parse_field_set_ast`,
- executable document now has a `FieldSet` struct that allows us to store build errors along with the actual selection set
- internal compiler's ast now has an implementation for `FieldSet` with source information for diagnostics and the actual selection set
       - it additionally implements a `FieldSet::from_cst` and `FieldSet::to_field_set` (which converts this to executable HIR `FieldSet` type)

* Compiler API for FieldSet parsing and validation

* Add a default to SyntaxTree’s new type parameter, for semver-compat

https://rust-lang.github.io/rfcs/1105-api-evolution.html#minor-change-adding-a-defaulted-type-parameter

* Changelog

* Add basic FieldSet validation tests

* slight edits to changelog

---------

Co-authored-by: Simon Sapin <[email protected]>
  • Loading branch information
lrlna and SimonSapin authored Oct 10, 2023
1 parent 5065bc7 commit da57a9d
Show file tree
Hide file tree
Showing 17 changed files with 432 additions and 38 deletions.
17 changes: 13 additions & 4 deletions crates/apollo-compiler/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,13 @@ Assorted `Schema` API changes by [SimonSapin] in [pull/678]:

## Features

- Add opt-in configuration for “orphan” extensions to be “adopted”, by [SimonSapin] in [pull/678]
- **Add `executable::FieldSet` for a selection set with optional outer brackets - [lrlna], [pull/685] fixing [issue/681]**
This is intended to parse string value of a [`FieldSet` custom scalar][fieldset]
used in some Apollo Federation directives in the context of a specific schema and type.
Its `validate` method calls a subset of validation rules relevant to selection sets.
which is not part of a document.

- **Add opt-in configuration for “orphan” extensions to be “adopted” - [SimonSapin], [pull/678]**

Type extensions and schema extensions without a corresponding definition
are normally ignored except for recording a validation error.
Expand All @@ -50,17 +56,20 @@ Assorted `Schema` API changes by [SimonSapin] in [pull/678]:
schema.validate()?;
```


## Fixes

- Allow built-in directives to be redefined, by [SimonSapin] in [pull/684], [issue/656]
- Allow schema extensions to extend a schema definition implied by object types named after default root operations, by [SimonSapin] in [pull/678], [issues/682]
- **Allow built-in directives to be redefined - [SimonSapin], [pull/684] fixing [issue/656]**
- **Allow schema extensions to extend a schema definition implied by object types named after default root operations - [SimonSapin], [pull/678] fixing [issues/682]**

[lrlna]: https://github.com/lrlna
[SimonSapin]: https://github.com/SimonSapin
[issue/656]: https://github.com/apollographql/apollo-rs/issues/656
[issue/682]: https://github.com/apollographql/apollo-rs/issues/682
[issue/681]: https://github.com/apollographql/apollo-rs/issues/681
[pull/678]: https://github.com/apollographql/apollo-rs/pull/678
[pull/684]: https://github.com/apollographql/apollo-rs/pull/684
[pull/685]: https://github.com/apollographql/apollo-rs/pull/685
[fieldset]: https://www.apollographql.com/docs/federation/subgraph-spec/#scalar-fieldset

# [1.0.0-beta.1](https://crates.io/crates/apollo-compiler/1.0.0-beta.1) - 2023-10-05

Expand Down
16 changes: 11 additions & 5 deletions crates/apollo-compiler/src/ast/from_cst.rs
Original file line number Diff line number Diff line change
Expand Up @@ -641,14 +641,20 @@ impl Convert for cst::SelectionSet {
type Target = Vec<ast::Selection>;

fn convert(&self, file_id: FileId) -> Option<Self::Target> {
Some(
self.selections()
.filter_map(|selection| selection.convert(file_id))
.collect(),
)
Some(convert_selection_set(self, file_id))
}
}

pub(crate) fn convert_selection_set(
selection_set: &cst::SelectionSet,
file_id: FileId,
) -> Vec<ast::Selection> {
selection_set
.selections()
.filter_map(|selection| selection.convert(file_id))
.collect()
}

impl Convert for cst::Selection {
type Target = ast::Selection;

Expand Down
2 changes: 1 addition & 1 deletion crates/apollo-compiler/src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ use crate::SourceFile;
use std::collections::HashMap;
use std::sync::Arc;

mod from_cst;
pub(crate) mod from_cst;
pub(crate) mod impls;
pub(crate) mod serialize;

Expand Down
8 changes: 4 additions & 4 deletions crates/apollo-compiler/src/executable/from_ast.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use super::*;

struct BuildErrors {
errors: Vec<BuildError>,
path: SelectionPath,
pub(crate) struct BuildErrors {
pub(crate) errors: Vec<BuildError>,
pub(crate) path: SelectionPath,
}

pub(crate) fn document_from_ast(
Expand Down Expand Up @@ -147,7 +147,7 @@ impl Fragment {
}

impl SelectionSet {
fn extend_from_ast(
pub(crate) fn extend_from_ast(
&mut self,
schema: Option<&Schema>,
errors: &mut BuildErrors,
Expand Down
46 changes: 44 additions & 2 deletions crates/apollo-compiler/src/executable/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,23 @@ pub struct ExecutableDocument {
pub fragments: IndexMap<Name, Node<Fragment>>,
}

/// FieldSet information created for FieldSet parsing in `@requires` directive.
/// Annotated with type information.
#[derive(Debug, Clone)]
pub struct FieldSet {
/// If this document was originally parsed from a source file,
/// that file and its ID.
///
/// The document may have been modified since.
pub source: Option<(FileId, Arc<SourceFile>)>,

/// Errors that occurred when building this FieldSet,
/// either parsing a source file or converting from AST.
pub(crate) build_errors: Vec<BuildError>,

pub selection_set: SelectionSet,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Operation {
pub operation_type: OperationType,
Expand Down Expand Up @@ -176,8 +193,8 @@ pub(crate) enum BuildError {

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct SelectionPath {
root: ExecutableDefinitionName,
nested_fields: Vec<Name>,
pub(crate) root: ExecutableDefinitionName,
pub(crate) nested_fields: Vec<Name>,
}

/// Designates by name a top-level definition in an executable document
Expand Down Expand Up @@ -673,6 +690,31 @@ impl FragmentSpread {
}
}

impl FieldSet {
/// Parse the given source 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.
///
/// Create a [`Parser`] to use different parser configuration.
pub fn parse(
schema: &Schema,
type_name: impl Into<NamedType>,
source_text: impl Into<String>,
path: impl AsRef<Path>,
) -> Self {
Parser::new().parse_field_set(schema, type_name, source_text, path)
}

pub fn validate(&self, schema: &Schema) -> Result<(), Diagnostics> {
let mut sources = schema.sources.clone();
sources.extend(self.source.clone());
let mut errors = Diagnostics::new(sources);
validation::validate_field_set(&mut errors, schema, self);
errors.into_result()
}
}

impl fmt::Display for SelectionPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.root {
Expand Down
2 changes: 1 addition & 1 deletion crates/apollo-compiler/src/executable/serialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ impl Node<Fragment> {
}

impl SelectionSet {
fn to_ast(&self) -> Vec<ast::Selection> {
pub(crate) fn to_ast(&self) -> Vec<ast::Selection> {
self.selections
.iter()
.map(|selection| match selection {
Expand Down
70 changes: 70 additions & 0 deletions crates/apollo-compiler/src/executable/validation.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use super::BuildError;
use super::FieldSet;
use crate::ast;
use crate::validation::Details;
use crate::validation::Diagnostics;
use crate::ExecutableDocument;
Expand Down Expand Up @@ -128,3 +130,71 @@ fn compiler_validation(
)
}
}

pub(crate) fn validate_field_set(errors: &mut Diagnostics, schema: &Schema, field_set: &FieldSet) {
if let Some((file_id, source)) = &field_set.source {
source.validate_parse_errors(errors, *file_id)
}
for build_error in &field_set.build_errors {
validate_build_error(errors, build_error)
}
let mut compiler = crate::ApolloCompiler::new();
let mut ids = Vec::new();
for (id, source) in &schema.sources {
ids.push(*id);
compiler.db.set_input(*id, source.into());
}
if let Some((id, source)) = &field_set.source {
ids.push(*id);
compiler.db.set_input(*id, source.into());
}

let schema_ast_id = FileId::HACK_TMP;
ids.push(schema_ast_id);
let mut ast = crate::ast::Document::new();
ast.definitions.extend(schema.to_ast());
compiler.db.set_input(
schema_ast_id,
crate::Source {
ty: crate::database::SourceType::Schema,
filename: Default::default(),
text: Default::default(),
ast: Some(Arc::new(ast)),
},
);

let ast_id = FileId::HACK_TMP_2;
ids.push(ast_id);
let ast = ast::Document::new();
compiler.db.set_input(
ast_id,
crate::Source {
ty: crate::database::SourceType::Executable,
filename: Default::default(),
text: Default::default(),
ast: Some(Arc::new(ast)),
},
);
compiler.db.set_source_files(ids);
let diagnostics = crate::validation::selection::validate_selection_set2(
&compiler.db,
ast_id,
Some(&field_set.selection_set.ty),
&field_set.selection_set.to_ast(),
crate::validation::operation::OperationValidationConfig {
has_schema: true,
variables: &[],
},
);
// if schema.is_some() {
// compiler.db.validate_executable(ast_id)
// } else {
// compiler.db.validate_standalone_executable(ast_id)
// };
for diagnostic in diagnostics {
errors.push(
Some(diagnostic.location),
Details::CompilerDiagnostic(diagnostic),
)
}
}
55 changes: 53 additions & 2 deletions crates/apollo-compiler/src/parser.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use crate::ast;
use crate::ast::Document;
use crate::executable;
use crate::schema::SchemaBuilder;
use crate::validation::Details;
use crate::validation::Diagnostics;
Expand Down Expand Up @@ -84,22 +86,32 @@ impl Parser {
path: PathBuf,
file_id: FileId,
) -> Document {
let (tree, source_file) = self.parse_common(source_text, path, |parser| parser.parse());
Document::from_cst(tree.document(), file_id, source_file)
}

pub(crate) fn parse_common<T: apollo_parser::cst::CstNode>(
&mut self,
source_text: String,
path: PathBuf,
parse: impl FnOnce(apollo_parser::Parser) -> apollo_parser::SyntaxTree<T>,
) -> (apollo_parser::SyntaxTree<T>, Arc<SourceFile>) {
let mut parser = apollo_parser::Parser::new(&source_text);
if let Some(value) = self.recursion_limit {
parser = parser.recursion_limit(value)
}
if let Some(value) = self.token_limit {
parser = parser.token_limit(value)
}
let tree = parser.parse();
let tree = parse(parser);
self.recursion_reached = tree.recursion_limit().high;
self.tokens_reached = tree.token_limit().high;
let source_file = Arc::new(SourceFile {
path,
source_text,
parse_errors: tree.errors().cloned().collect(),
});
Document::from_cst(tree.document(), file_id, source_file)
(tree, source_file)
}

/// Parse the given source text as the sole input file of a schema.
Expand Down Expand Up @@ -171,6 +183,45 @@ impl Parser {
self.parse_ast(source_text, path).to_mixed()
}

/// Parse the given source 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.
///
/// Parsing is fault-tolerant, so a selection set node is always returned.
/// TODO: document how to validate
pub fn parse_field_set(
&mut self,
schema: &Schema,
type_name: impl Into<ast::NamedType>,
source_text: impl Into<String>,
path: impl AsRef<Path>,
) -> executable::FieldSet {
let (tree, source_file) =
self.parse_common(source_text.into(), path.as_ref().to_owned(), |parser| {
parser.parse_selection_set()
});
let file_id = FileId::new();
let ast = ast::from_cst::convert_selection_set(&tree.field_set(), file_id);
let mut selection_set = executable::SelectionSet::new(type_name);
let mut build_errors = executable::from_ast::BuildErrors {
errors: Vec::new(),
path: executable::SelectionPath {
nested_fields: Vec::new(),
// 🤷
root: executable::ExecutableDefinitionName::AnonymousOperation(
ast::OperationType::Query,
),
},
};
selection_set.extend_from_ast(Some(schema), &mut build_errors, &ast);
executable::FieldSet {
source: Some((file_id, source_file)),
build_errors: build_errors.errors,
selection_set,
}
}

/// 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
4 changes: 2 additions & 2 deletions crates/apollo-compiler/src/validation/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ mod fragment;
mod input_object;
mod interface;
mod object;
mod operation;
pub(crate) mod operation;
mod scalar;
mod schema;
mod selection;
pub(crate) mod selection;
mod union_;
mod value;
mod variable;
Expand Down
2 changes: 1 addition & 1 deletion crates/apollo-compiler/src/validation/operation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::{
};

#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct OperationValidationConfig<'vars> {
pub(crate) struct OperationValidationConfig<'vars> {
/// When false, rules that require a schema to validate are disabled.
pub has_schema: bool,
/// The variables defined for this operation.
Expand Down
Loading

0 comments on commit da57a9d

Please sign in to comment.