Repository: apollographql/apollo-rs (rust)

Issue:
Add parsing + validation support for `FieldSet` scalars
Apollo Federation has multiple directives that take an argument of type [`FieldSet`](https://www.apollographql.com/docs/federation/subgraph-spec/#scalar-fieldset) to designate one or more fields of an object (or interface?) type. `FieldSet` is a custom scalar serialized as a string. Its value is expected to be parsed as a [`SelectionSet`](https://spec.graphql.org/October2021/#SelectionSet) with optional outer brackets. (This is GraphQL syntax inside of a string in GraphQL syntax.)

Currently apollo-rs can only parse GraphQL documents, but we’d rather Rust federation not need string concatenation hacks to create a parser input that looks like a document.

## API sketch

In apollo-parser:

* Give `apollo_parser::SyntaxTree` a `Cst` type parameter
* `apollo_parser::Parser::new(string).parse()` now returns `SyntaxTree<apollo_parser::cst::Document>` instead of just `SyntaxTree`
* Add `apollo_parser::Parser::new(string).parse_fieldset()` which returns `SyntaxTree<cst::SelectionSet>`
* The `SyntaxTree::document(self) -> cst::Document` method now only exists on `SyntaxTree<cst::Document>`
* Add `SyntaxTree::selection_set(self) -> cst::SelectionSet` for `SyntaxTree<cst::SelectionSet>`

In apollo-compiler:

* Add a top-level free function:
  ```rust
  fn parse_fieldset(
      schema: &Schema,
      type_name: impl Into<ast::NamedType>,
      source_text: impl Into<String>,
      path: impl AsRef<Path>,
  ) -> executable::SelectionSet
  ```
* Add a `Parser` method with the same signature

Pull request:
feat(apollo-parser, apollo-compiler): support parsing and validating FieldSet
This starts work on supporting FieldSet.

The current items in scope:
- [x] support FieldSet parsing in apollo_parser
- [x] create `SyntaxTree::selection_set` getter method for accessing selection set created from FieldSet parsing
- [x] incorporate FieldSet parsing in apollo_compiler
- [x] create FieldSet validation in apollo_compiler that only runs specific validation rules

closes #681 

Patch:
diff --git a/crates/apollo-compiler/CHANGELOG.md b/crates/apollo-compiler/CHANGELOG.md
index d4eceea..7324fe8 100644
--- a/crates/apollo-compiler/CHANGELOG.md
+++ b/crates/apollo-compiler/CHANGELOG.md
@@ -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.
@@ -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
 
diff --git a/crates/apollo-compiler/src/ast/from_cst.rs b/crates/apollo-compiler/src/ast/from_cst.rs
index ec2a2dc..4e01a61 100644
--- a/crates/apollo-compiler/src/ast/from_cst.rs
+++ b/crates/apollo-compiler/src/ast/from_cst.rs
@@ -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;
 
diff --git a/crates/apollo-compiler/src/ast/mod.rs b/crates/apollo-compiler/src/ast/mod.rs
index e455621..c1e6d86 100644
--- a/crates/apollo-compiler/src/ast/mod.rs
+++ b/crates/apollo-compiler/src/ast/mod.rs
@@ -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;
 
diff --git a/crates/apollo-compiler/src/executable/from_ast.rs b/crates/apollo-compiler/src/executable/from_ast.rs
index 8b41076..fd0234b 100644
--- a/crates/apollo-compiler/src/executable/from_ast.rs
+++ b/crates/apollo-compiler/src/executable/from_ast.rs
@@ -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(
@@ -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,
diff --git a/crates/apollo-compiler/src/executable/mod.rs b/crates/apollo-compiler/src/executable/mod.rs
index e733dba..d6fc968 100644
--- a/crates/apollo-compiler/src/executable/mod.rs
+++ b/crates/apollo-compiler/src/executable/mod.rs
@@ -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,
@@ -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
@@ -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 {
diff --git a/crates/apollo-compiler/src/executable/serialize.rs b/crates/apollo-compiler/src/executable/serialize.rs
index 9dadb63..c933738 100644
--- a/crates/apollo-compiler/src/executable/serialize.rs
+++ b/crates/apollo-compiler/src/executable/serialize.rs
@@ -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 {
diff --git a/crates/apollo-compiler/src/executable/validation.rs b/crates/apollo-compiler/src/executable/validation.rs
index 27facc7..fe9a695 100644
--- a/crates/apollo-compiler/src/executable/validation.rs
+++ b/crates/apollo-compiler/src/executable/validation.rs
@@ -1,4 +1,6 @@
 use super::BuildError;
+use super::FieldSet;
+use crate::ast;
 use crate::validation::Details;
 use crate::validation::Diagnostics;
 use crate::ExecutableDocument;
@@ -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),
+        )
+    }
+}
diff --git a/crates/apollo-compiler/src/parser.rs b/crates/apollo-compiler/src/parser.rs
index f1db79a..cf224d6 100644
--- a/crates/apollo-compiler/src/parser.rs
+++ b/crates/apollo-compiler/src/parser.rs
@@ -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;
@@ -84,6 +86,16 @@ 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)
@@ -91,7 +103,7 @@ impl Parser {
         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 {
@@ -99,7 +111,7 @@ impl Parser {
             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.
@@ -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
diff --git a/crates/apollo-compiler/src/validation/mod.rs b/crates/apollo-compiler/src/validation/mod.rs
index 60e4c08..e1c46b2 100644
--- a/crates/apollo-compiler/src/validation/mod.rs
+++ b/crates/apollo-compiler/src/validation/mod.rs
@@ -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;
diff --git a/crates/apollo-compiler/src/validation/operation.rs b/crates/apollo-compiler/src/validation/operation.rs
index 262442b..60f316c 100644
--- a/crates/apollo-compiler/src/validation/operation.rs
+++ b/crates/apollo-compiler/src/validation/operation.rs
@@ -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.
diff --git a/crates/apollo-parser/CHANGELOG.md b/crates/apollo-parser/CHANGELOG.md
index 871676b..be3dde9 100644
--- a/crates/apollo-parser/CHANGELOG.md
+++ b/crates/apollo-parser/CHANGELOG.md
@@ -17,6 +17,21 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
 
 ## Documentation -->
 
+# [x.x.x] (unreleased) - 2022-mm-dd
+
+## Features
+- **`parse_field_set` parses a selection set with optional outer brackets - [lrlna], [pull/685] fixing [issue/681]**
+  This returns a `SyntaxTree<SelectionSet>` which instead of `.document() -> cst::Document`
+  has `.field_set() -> cst::SelectionSet`.
+  This is intended to parse string value of a [`FieldSet` custom scalar][fieldset]
+  used in some Apollo Federation directives.
+
+[lrlna]: https://github.com/lrlna
+[pull/685]: https://github.com/apollographql/apollo-rs/pull/685
+[issue/681]: https://github.com/apollographql/apollo-rs/issues/681
+[fieldset]: https://www.apollographql.com/docs/federation/subgraph-spec/#scalar-fieldset
+
+
 # [0.7.0](https://crates.io/crates/apollo-parser/0.7.0) - 2023-10-05
 
 ## BREAKING
diff --git a/crates/apollo-parser/src/parser/grammar/mod.rs b/crates/apollo-parser/src/parser/grammar/mod.rs
index aa492ef..1158724 100644
--- a/crates/apollo-parser/src/parser/grammar/mod.rs
+++ b/crates/apollo-parser/src/parser/grammar/mod.rs
@@ -1,4 +1,5 @@
 pub(crate) mod document;
+pub(crate) mod selection;
 
 mod argument;
 mod description;
@@ -14,7 +15,6 @@ mod object;
 mod operation;
 mod scalar;
 mod schema;
-mod selection;
 mod ty;
 mod union_;
 mod value;
diff --git a/crates/apollo-parser/src/parser/grammar/selection.rs b/crates/apollo-parser/src/parser/grammar/selection.rs
index 27fb383..43cb780 100644
--- a/crates/apollo-parser/src/parser/grammar/selection.rs
+++ b/crates/apollo-parser/src/parser/grammar/selection.rs
@@ -26,6 +26,23 @@ pub(crate) fn selection_set(p: &mut Parser) {
     }
 }
 
+pub(crate) fn field_set(p: &mut Parser) {
+    if let Some(T!['{']) = p.peek() {
+        selection_set(p)
+    } else {
+        let _g = p.start_node(SyntaxKind::SELECTION_SET);
+        // We need to enforce recursion limits to prevent
+        // excessive resource consumption or (more seriously)
+        // stack overflows.
+        if p.recursion_limit.check_and_increment() {
+            p.limit_err("parser recursion limit reached");
+            return;
+        }
+        selection(p);
+        p.recursion_limit.decrement();
+    }
+}
+
 /// See: https://spec.graphql.org/October2021/#Selection
 ///
 /// *Selection*:
diff --git a/crates/apollo-parser/src/parser/mod.rs b/crates/apollo-parser/src/parser/mod.rs
index 3d8b35b..30e376c 100644
--- a/crates/apollo-parser/src/parser/mod.rs
+++ b/crates/apollo-parser/src/parser/mod.rs
@@ -7,7 +7,11 @@ pub(crate) mod grammar;
 
 use std::{cell::RefCell, rc::Rc};
 
-use crate::{lexer::Lexer, Error, LimitTracker, Token, TokenKind};
+use crate::{
+    cst::{Document, SelectionSet},
+    lexer::Lexer,
+    Error, LimitTracker, Token, TokenKind,
+};
 
 pub use generated::syntax_kind::SyntaxKind;
 pub use language::{SyntaxElement, SyntaxNode, SyntaxNodeChildren, SyntaxNodePtr, SyntaxToken};
@@ -131,13 +135,41 @@ impl<'a> Parser<'a> {
     }
 
     /// Parse the current tokens.
-    pub fn parse(mut self) -> SyntaxTree {
+    pub fn parse(mut self) -> SyntaxTree<Document> {
         grammar::document::document(&mut self);
 
         let builder = Rc::try_unwrap(self.builder)
             .expect("More than one reference to builder left")
             .into_inner();
-        builder.finish(self.errors, self.recursion_limit, self.lexer.limit_tracker)
+        let builder =
+            builder.finish_document(self.errors, self.recursion_limit, self.lexer.limit_tracker);
+
+        match builder {
+            syntax_tree::SyntaxTreeWrapper::Document(tree) => tree,
+            syntax_tree::SyntaxTreeWrapper::FieldSet(_) => {
+                unreachable!("parse constructor can only construct a document")
+            }
+        }
+    }
+
+    pub fn parse_selection_set(mut self) -> SyntaxTree<SelectionSet> {
+        grammar::selection::field_set(&mut self);
+
+        let builder = Rc::try_unwrap(self.builder)
+            .expect("More than one reference to builder left")
+            .into_inner();
+        let builder = builder.finish_selection_set(
+            self.errors,
+            self.recursion_limit,
+            self.lexer.limit_tracker,
+        );
+
+        match builder {
+            syntax_tree::SyntaxTreeWrapper::FieldSet(tree) => tree,
+            syntax_tree::SyntaxTreeWrapper::Document(_) => {
+                unreachable!("parse constructor can only construct a selection set")
+            }
+        }
     }
 
     /// Check if the current token is `kind`.
@@ -467,7 +499,7 @@ impl Checkpoint {
 #[cfg(test)]
 mod tests {
     use super::DEFAULT_RECURSION_LIMIT;
-    use crate::{Error, Parser, SyntaxTree};
+    use crate::{cst, Error, Parser, SyntaxTree};
     use expect_test::expect;
 
     #[test]
@@ -786,4 +818,39 @@ mod tests {
             check(doc);
         }
     }
+
+    #[test]
+    fn parse_field_set() {
+        let source = r#"{ a }"#;
+
+        let parser = Parser::new(source);
+        let cst: SyntaxTree<cst::SelectionSet> = parser.parse_selection_set();
+        let errors = cst.errors().collect::<Vec<_>>();
+        assert_eq!(errors.len(), 0);
+
+        let sel_set: cst::SelectionSet = cst.field_set();
+        let _ = sel_set.selections().map(|sel| {
+            if let cst::Selection::Field(f) = sel {
+                assert_eq!(f.name().unwrap().text().as_ref(), "a")
+            } else {
+                panic!("no field a in field set selection")
+            }
+        });
+
+        let source = r#"a { a }"#;
+
+        let parser = Parser::new(source);
+        let cst: SyntaxTree<cst::SelectionSet> = parser.parse_selection_set();
+        let errors = cst.errors().collect::<Vec<_>>();
+        assert_eq!(errors.len(), 0);
+
+        let sel_set: cst::SelectionSet = cst.field_set();
+        let _ = sel_set.selections().map(|sel| {
+            if let cst::Selection::Field(f) = sel {
+                assert_eq!(f.name().unwrap().text().as_ref(), "a")
+            } else {
+                panic!("no field a in field set selection")
+            }
+        });
+    }
 }
diff --git a/crates/apollo-parser/src/parser/syntax_tree.rs b/crates/apollo-parser/src/parser/syntax_tree.rs
index 7aade7c..db8ee04 100644
--- a/crates/apollo-parser/src/parser/syntax_tree.rs
+++ b/crates/apollo-parser/src/parser/syntax_tree.rs
@@ -1,8 +1,11 @@
-use std::{fmt, slice::Iter};
+use std::{fmt, marker::PhantomData, slice::Iter};
 
 use rowan::{GreenNode, GreenNodeBuilder};
 
-use crate::{cst::Document, Error, SyntaxElement, SyntaxKind, SyntaxNode};
+use crate::{
+    cst::{self, CstNode},
+    Error, SyntaxElement, SyntaxKind, SyntaxNode,
+};
 
 use super::LimitTracker;
 
@@ -38,15 +41,26 @@ use super::LimitTracker;
 /// assert_eq!(nodes.len(), 1);
 /// ```
 
+// NOTE(@lrlna): This enum helps us setup a type state for document and field
+// set parsing.  Without the wrapper we'd have to add type annotations to
+// SyntaxTreeBuilder, which then annotates the vast majority of the parser's
+// function with the same type parameter. This is tedious and makes for a more
+// complex design than necessary.
+pub(crate) enum SyntaxTreeWrapper {
+    Document(SyntaxTree<cst::Document>),
+    FieldSet(SyntaxTree<cst::SelectionSet>),
+}
+
 #[derive(PartialEq, Eq, Clone)]
-pub struct SyntaxTree {
+pub struct SyntaxTree<T: CstNode = cst::Document> {
     pub(crate) green: GreenNode,
     pub(crate) errors: Vec<crate::Error>,
     pub(crate) recursion_limit: LimitTracker,
     pub(crate) token_limit: LimitTracker,
+    _phantom: PhantomData<T>,
 }
 
-impl SyntaxTree {
+impl<T: CstNode> SyntaxTree<T> {
     /// Get a reference to the syntax tree's errors.
     pub fn errors(&self) -> Iter<'_, crate::Error> {
         self.errors.iter()
@@ -69,16 +83,28 @@ impl SyntaxTree {
     pub(crate) fn syntax_node(&self) -> SyntaxNode {
         rowan::SyntaxNode::new_root(self.green.clone())
     }
+}
 
+impl SyntaxTree<cst::Document> {
     /// Return the root typed `Document` node.
-    pub fn document(&self) -> Document {
-        Document {
+    pub fn document(&self) -> cst::Document {
+        cst::Document {
             syntax: self.syntax_node(),
         }
     }
 }
 
-impl fmt::Debug for SyntaxTree {
+impl SyntaxTree<cst::SelectionSet> {
+    /// Return the root typed `SelectionSet` node. This is used for parsing
+    /// selection sets defined by @requires directive.
+    pub fn field_set(&self) -> cst::SelectionSet {
+        cst::SelectionSet {
+            syntax: self.syntax_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 {
             let kind: SyntaxKind = element.kind();
@@ -162,20 +188,38 @@ impl SyntaxTreeBuilder {
         self.builder.token(rowan::SyntaxKind(kind as u16), text);
     }
 
-    pub(crate) fn finish(
+    pub(crate) fn finish_document(
         self,
         errors: Vec<Error>,
         recursion_limit: LimitTracker,
         token_limit: LimitTracker,
-    ) -> SyntaxTree {
-        SyntaxTree {
+    ) -> SyntaxTreeWrapper {
+        SyntaxTreeWrapper::Document(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,
+        })
+    }
+
+    pub(crate) fn finish_selection_set(
+        self,
+        errors: Vec<Error>,
+        recursion_limit: LimitTracker,
+        token_limit: LimitTracker,
+    ) -> SyntaxTreeWrapper {
+        SyntaxTreeWrapper::FieldSet(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,
+        })
     }
 }
 


Test patch:
diff --git a/crates/apollo-compiler/tests/field_set.rs b/crates/apollo-compiler/tests/field_set.rs
new file mode 100644
index 0000000..d354645
--- /dev/null
+++ b/crates/apollo-compiler/tests/field_set.rs
@@ -0,0 +1,72 @@
+use apollo_compiler::executable::FieldSet;
+use apollo_compiler::Schema;
+
+fn common_schema() -> Schema {
+    let input = r#"
+        type Query {
+            id: ID
+            organization: Org
+        }
+        type Org {
+            id: ID
+        }
+    "#;
+    let schema = Schema::parse(input, "schema.graphql");
+    schema.validate().unwrap();
+    schema
+}
+
+#[test]
+fn test_valid_field_sets() {
+    let schema = common_schema();
+
+    let input = "id";
+    let field_set = FieldSet::parse(&schema, "Query", input, "field_set.graphql");
+    field_set.validate(&schema).unwrap();
+
+    let input = "id organization { id }";
+    let field_set = FieldSet::parse(&schema, "Query", input, "field_set.graphql");
+    field_set.validate(&schema).unwrap();
+}
+
+#[test]
+fn test_invalid_field_sets() {
+    let schema = common_schema();
+
+    let input = "name";
+    let field_set = FieldSet::parse(&schema, "Query", input, "field_set.graphql");
+    let errors = field_set
+        .validate(&schema)
+        .unwrap_err()
+        .to_string_no_color();
+    assert!(
+        errors.contains("type `Query` does not have a field `name`"),
+        "{errors}"
+    );
+
+    let input = "id organization";
+    let field_set = FieldSet::parse(&schema, "Query", input, "field_set.graphql");
+    let errors = field_set
+        .validate(&schema)
+        .unwrap_err()
+        .to_string_no_color();
+    assert!(
+        errors.contains("interface, union and object types must have a subselection set"),
+        "{errors}"
+    );
+    assert!(
+        errors.contains("field `organization` type `Org` is an object and must select fields"),
+        "{errors}"
+    );
+
+    let input = "id(arg: true)";
+    let field_set = FieldSet::parse(&schema, "Query", input, "field_set.graphql");
+    let errors = field_set
+        .validate(&schema)
+        .unwrap_err()
+        .to_string_no_color();
+    assert!(
+        errors.contains("the argument `arg` is not supported"),
+        "{errors}"
+    );
+}
diff --git a/crates/apollo-compiler/tests/main.rs b/crates/apollo-compiler/tests/main.rs
index b336d8e..d3a80a7 100644
--- a/crates/apollo-compiler/tests/main.rs
+++ b/crates/apollo-compiler/tests/main.rs
@@ -1,5 +1,6 @@
 mod executable;
 mod extensions;
+mod field_set;
 mod merge_schemas;
 /// Formerly in src/lib.rs
 mod misc;