Repository: apollographql/apollo-rs (rust)

Issue:
add directive definitions to compiler's context
- add directive definition to source database
- link directive definitions to all its references
- basic directive validation:
    - uniqueness
    - a directive definition must not contain the use of a directive which references itself directly


Pull request description:
feat(apollo-compiler): add directive definitions to source database
- adds directive definition to compiler's context
- adds find_directive_definition and find_directive_definition_by_name functions
- adds built-in directives to cumulative directive definitions in compiler (`@include`, `@skip`, `@deprecated`, `@specifiedBy`)
- adds directive definition validations:
    - Directive definitions must have unique names (Unique Definition error)
    - A directive definition must not contain the use of a directive which references itself directly. (Recursive Definition error)

closes #241 

Patch:
diff --git a/crates/apollo-compiler/src/lib.rs b/crates/apollo-compiler/src/lib.rs
index f3dadfe..dfb7617 100644
--- a/crates/apollo-compiler/src/lib.rs
+++ b/crates/apollo-compiler/src/lib.rs
@@ -72,6 +72,10 @@ impl ApolloCompiler {
     pub fn unions(&self) -> Arc<Vec<values::UnionDefinition>> {
         self.db.unions()
     }
+
+    pub fn directive_definitions(&self) -> Arc<Vec<values::DirectiveDefinition>> {
+        self.db.directive_definitions()
+    }
 }
 
 #[cfg(test)]
@@ -309,4 +313,44 @@ type SearchQuery {
             assert_eq!(fields, ["name", "age"])
         }
     }
+
+    #[test]
+    fn it_accesses_directive_definitions() {
+        let input = r#"
+type Query {
+    literature: Book
+}
+
+directive @delegateField(name: String!) repeatable on OBJECT | INTERFACE
+
+type Book @delegateField(name: "pageCount") @delegateField(name: "author") {
+  id: ID!
+}
+"#;
+
+        let ctx = ApolloCompiler::new(input);
+        let errors = ctx.validate();
+
+        assert!(errors.is_empty());
+
+        let directives = ctx.directive_definitions();
+        let locations: Vec<String> = directives
+            .iter()
+            .filter_map(|dir| {
+                if dir.name() == "delegateField" {
+                    let locations: Vec<String> = dir
+                        .directive_locations()
+                        .iter()
+                        .map(|loc| loc.clone().into())
+                        .collect();
+                    Some(locations)
+                } else {
+                    None
+                }
+            })
+            .flatten()
+            .collect();
+
+        assert_eq!(["OBJECT", "INTERFACE"], locations.as_ref());
+    }
 }
diff --git a/crates/apollo-compiler/src/queries/database.rs b/crates/apollo-compiler/src/queries/database.rs
index a1f3919..5c198ac 100644
--- a/crates/apollo-compiler/src/queries/database.rs
+++ b/crates/apollo-compiler/src/queries/database.rs
@@ -52,6 +52,8 @@ pub trait SourceDatabase {
 
     fn interfaces(&self) -> Arc<Vec<InterfaceDefinition>>;
 
+    fn directive_definitions(&self) -> Arc<Vec<DirectiveDefinition>>;
+
     fn query_operations(&self) -> Operations;
 
     fn mutation_operations(&self) -> Operations;
@@ -72,6 +74,10 @@ pub trait SourceDatabase {
 
     fn find_interface_by_name(&self, name: String) -> Option<Arc<InterfaceDefinition>>;
 
+    fn find_directive_definition(&self, id: Uuid) -> Option<Arc<DirectiveDefinition>>;
+
+    fn find_directive_definition_by_name(&self, name: String) -> Option<Arc<DirectiveDefinition>>;
+
     fn operation_definition_variables(&self, id: Uuid) -> Arc<HashSet<String>>;
 
     fn selection_variables(&self, id: Uuid) -> Arc<HashSet<String>>;
@@ -448,6 +454,47 @@ fn find_interface_by_name(
     })
 }
 
+fn directive_definitions(db: &dyn SourceDatabase) -> Arc<Vec<DirectiveDefinition>> {
+    let directives = db
+        .definitions()
+        .iter()
+        .filter_map(|definition| match definition {
+            ast::Definition::DirectiveDefinition(directive_def) => {
+                Some(directive_definition(directive_def.clone()))
+            }
+            _ => None,
+        })
+        .collect();
+
+    let directives = built_in_directives(directives);
+
+    Arc::new(directives)
+}
+
+fn find_directive_definition(
+    db: &dyn SourceDatabase,
+    id: Uuid,
+) -> Option<Arc<DirectiveDefinition>> {
+    db.directive_definitions().iter().find_map(|directive_def| {
+        if &id == directive_def.id() {
+            return Some(Arc::new(directive_def.clone()));
+        }
+        None
+    })
+}
+
+fn find_directive_definition_by_name(
+    db: &dyn SourceDatabase,
+    name: String,
+) -> Option<Arc<DirectiveDefinition>> {
+    db.directive_definitions().iter().find_map(|directive_def| {
+        if name == directive_def.name() {
+            return Some(Arc::new(directive_def.clone()));
+        }
+        None
+    })
+}
+
 fn operation_definition(
     db: &dyn SourceDatabase,
     op_def: ast::OperationDefinition,
@@ -651,6 +698,23 @@ fn interface_definition(interface_def: ast::InterfaceTypeDefinition) -> Interfac
     }
 }
 
+fn directive_definition(directive_def: ast::DirectiveDefinition) -> DirectiveDefinition {
+    let name = name(directive_def.name());
+    let description = description(directive_def.description());
+    let arguments = arguments_definition(directive_def.arguments_definition());
+    let repeatable = directive_def.repeatable_token().is_some();
+    let directive_locations = directive_locations(directive_def.directive_locations());
+
+    DirectiveDefinition {
+        id: Uuid::new_v4(),
+        description,
+        name,
+        arguments,
+        repeatable,
+        directive_locations,
+    }
+}
+
 fn add_object_type_id_to_schema(db: &dyn SourceDatabase) -> Arc<Vec<RootOperationTypeDefinition>> {
     // Schema Definition does not have to be present in the SDL if ObjectType name is
     // - Query
@@ -882,6 +946,22 @@ fn named_type(n: Option<ast::Name>) -> Type {
     Type::Named { name: name(n) }
 }
 
+fn directive_locations(
+    directive_locations: Option<ast::DirectiveLocations>,
+) -> Arc<Vec<DirectiveLocation>> {
+    match directive_locations {
+        Some(directive_loc) => {
+            let locations: Vec<DirectiveLocation> = directive_loc
+                .directive_locations()
+                .into_iter()
+                .map(|loc| loc.into())
+                .collect();
+            Arc::new(locations)
+        }
+        None => Arc::new(Vec::new()),
+    }
+}
+
 fn directives(directives: Option<ast::Directives>) -> Arc<Vec<Directive>> {
     match directives {
         Some(directives) => {
@@ -895,6 +975,7 @@ fn directives(directives: Option<ast::Directives>) -> Arc<Vec<Directive>> {
 fn directive(directive: ast::Directive) -> Directive {
     let name = name(directive.name());
     let arguments = arguments(directive.arguments());
+
     Directive { name, arguments }
 }
 
@@ -1060,3 +1141,156 @@ fn alias(alias: Option<ast::Alias>) -> Option<Arc<Alias>> {
         Arc::new(alias_data)
     })
 }
+
+fn built_in_directives(mut directives: Vec<DirectiveDefinition>) -> Vec<DirectiveDefinition> {
+    if !directives.iter().any(|dir| dir.name == "skip") {
+        directives.push(skip_directive());
+    }
+
+    if !directives.iter().any(|dir| dir.name == "specifiedBy") {
+        directives.push(specified_by_directive());
+    }
+
+    if !directives.iter().any(|dir| dir.name == "deprecated") {
+        directives.push(deprecated_directive());
+    }
+
+    if !directives.iter().any(|dir| dir.name == "include") {
+        directives.push(include_directive());
+    }
+
+    directives
+}
+
+fn skip_directive() -> DirectiveDefinition {
+    // "Directs the executor to skip this field or fragment when the `if` argument is true."
+    // directive @skip(
+    //   "Skipped when true."
+    //   if: Boolean!
+    // ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
+    DirectiveDefinition {
+        id: Uuid::new_v4(),
+        description: Some(
+            "Directs the executor to skip this field or fragment when the `if` argument is true."
+                .into(),
+        ),
+        name: "skip".into(),
+        arguments: ArgumentsDefinition {
+            input_values: Arc::new(vec![InputValueDefinition {
+                description: Some("Skipped when true.".into()),
+                name: "if".into(),
+                ty: Type::NonNull {
+                    ty: Box::new(Type::Named {
+                        name: "Boolean".into(),
+                    }),
+                },
+                default_value: None,
+                directives: Arc::new(Vec::new()),
+            }]),
+        },
+        repeatable: false,
+        directive_locations: Arc::new(vec![
+            DirectiveLocation::Field,
+            DirectiveLocation::FragmentSpread,
+            DirectiveLocation::InlineFragment,
+        ]),
+    }
+}
+
+fn specified_by_directive() -> DirectiveDefinition {
+    // "Exposes a URL that specifies the behaviour of this scalar."
+    // directive @specifiedBy(
+    //     "The URL that specifies the behaviour of this scalar."
+    //     url: String!
+    // ) on SCALAR
+    DirectiveDefinition {
+        id: Uuid::new_v4(),
+        description: Some("Exposes a URL that specifies the behaviour of this scalar.".into()),
+        name: "specifiedBy".into(),
+        arguments: ArgumentsDefinition {
+            input_values: Arc::new(vec![InputValueDefinition {
+                description: Some("The URL that specifies the behaviour of this scalar.".into()),
+                name: "url".into(),
+                ty: Type::NonNull {
+                    ty: Box::new(Type::Named {
+                        name: "String".into(),
+                    }),
+                },
+                default_value: None,
+                directives: Arc::new(Vec::new()),
+            }]),
+        },
+        repeatable: false,
+        directive_locations: Arc::new(vec![DirectiveLocation::Scalar]),
+    }
+}
+
+fn deprecated_directive() -> DirectiveDefinition {
+    // "Marks an element of a GraphQL schema as no longer supported."
+    // directive @deprecated(
+    //   """
+    //   Explains why this element was deprecated, usually also including a
+    //   suggestion for how to access supported similar data. Formatted using
+    //   the Markdown syntax, as specified by
+    //   [CommonMark](https://commonmark.org/).
+    //   """
+    //   reason: String = "No longer supported"
+    // ) on FIELD_DEFINITION | ENUM_VALUE
+    DirectiveDefinition {
+        id: Uuid::new_v4(),
+        description: Some("Marks an element of a GraphQL schema as no longer supported.".into()),
+        name: "deprecated".into(),
+        arguments: ArgumentsDefinition {
+            input_values: Arc::new(vec![InputValueDefinition {
+                description: Some(
+                    "Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted using the Markdown syntax, as specified by [CommonMark](https://commonmark.org/).".into(),
+                ),
+                name: "reason".into(),
+                ty: Type::Named {
+                    name: "String".into(),
+                },
+                default_value: Some(DefaultValue::String("No longer supported".into())),
+                directives: Arc::new(Vec::new()),
+            }]),
+        },
+        repeatable: false,
+        directive_locations: Arc::new(vec![
+            DirectiveLocation::FieldDefinition,
+            DirectiveLocation::EnumValue
+        ]),
+    }
+}
+
+fn include_directive() -> DirectiveDefinition {
+    // "Directs the executor to include this field or fragment only when the `if` argument is true."
+    // directive @include(
+    //   "Included when true."
+    //   if: Boolean!
+    // ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
+    DirectiveDefinition {
+        id: Uuid::new_v4(),
+        description: Some("Directs the executor to include this field or fragment only when the `if` argument is true.".into()),
+        name: "include".into(),
+        arguments: ArgumentsDefinition {
+            input_values: Arc::new(vec![InputValueDefinition {
+                description: Some(
+                    "Included when true.".into(),
+                ),
+                name: "if".into(),
+                ty: Type::NonNull {
+                    ty: Box::new(Type::Named {
+                        name: "Boolean".into(),
+                    }),
+                },
+                default_value: None,
+                directives: Arc::new(Vec::new()),
+            }]),
+        },
+        repeatable: false,
+        directive_locations: Arc::new(vec![
+            DirectiveLocation::Field,
+            DirectiveLocation::FragmentDefinition,
+            DirectiveLocation::InlineFragment,
+        ]),
+    }
+}
diff --git a/crates/apollo-compiler/src/queries/values.rs b/crates/apollo-compiler/src/queries/values.rs
index 29c8d63..9851a8f 100644
--- a/crates/apollo-compiler/src/queries/values.rs
+++ b/crates/apollo-compiler/src/queries/values.rs
@@ -1,5 +1,6 @@
 use std::{ops::Deref, sync::Arc};
 
+use apollo_parser::ast;
 use ordered_float::{self, OrderedFloat};
 use uuid::Uuid;
 
@@ -350,7 +351,138 @@ impl Directive {
     pub fn arguments(&self) -> &[Argument] {
         self.arguments.as_ref()
     }
+
+    // Get directive definition of the currently used directive
+    pub fn directive(&self, db: &dyn SourceDatabase) -> Option<Arc<DirectiveDefinition>> {
+        db.find_directive_definition_by_name(self.name().to_string())
+    }
+}
+
+#[derive(Clone, Debug, Hash, PartialEq, Eq)]
+pub struct DirectiveDefinition {
+    pub(crate) id: Uuid,
+    pub(crate) description: Option<String>,
+    pub(crate) name: String,
+    pub(crate) arguments: ArgumentsDefinition,
+    pub(crate) repeatable: bool,
+    pub(crate) directive_locations: Arc<Vec<DirectiveLocation>>,
+}
+
+impl DirectiveDefinition {
+    /// Get a reference to the directive definition's name.
+    pub fn name(&self) -> &str {
+        self.name.as_ref()
+    }
+
+    /// Get a reference to the directive definition's id.
+    pub fn id(&self) -> &Uuid {
+        &self.id
+    }
+
+    // Get a reference to argument definition's locations.
+    pub fn arguments(&self) -> &ArgumentsDefinition {
+        &self.arguments
+    }
+
+    // Get a reference to directive definition's locations.
+    pub fn directive_locations(&self) -> &[DirectiveLocation] {
+        self.directive_locations.as_ref()
+    }
 }
+
+#[derive(Clone, Debug, Hash, PartialEq, Eq)]
+pub enum DirectiveLocation {
+    Query,
+    Mutation,
+    Subscription,
+    Field,
+    FragmentDefinition,
+    FragmentSpread,
+    InlineFragment,
+    VariableDefinition,
+    Schema,
+    Scalar,
+    Object,
+    FieldDefinition,
+    ArgumentDefinition,
+    Interface,
+    Union,
+    Enum,
+    EnumValue,
+    InputObject,
+    InputFieldDefinition,
+}
+
+impl From<ast::DirectiveLocation> for DirectiveLocation {
+    fn from(directive_loc: ast::DirectiveLocation) -> Self {
+        if directive_loc.query_token().is_some() {
+            DirectiveLocation::Query
+        } else if directive_loc.mutation_token().is_some() {
+            DirectiveLocation::Mutation
+        } else if directive_loc.subscription_token().is_some() {
+            DirectiveLocation::Subscription
+        } else if directive_loc.field_token().is_some() {
+            DirectiveLocation::Field
+        } else if directive_loc.fragment_definition_token().is_some() {
+            DirectiveLocation::FragmentDefinition
+        } else if directive_loc.fragment_spread_token().is_some() {
+            DirectiveLocation::FragmentSpread
+        } else if directive_loc.inline_fragment_token().is_some() {
+            DirectiveLocation::InlineFragment
+        } else if directive_loc.variable_definition_token().is_some() {
+            DirectiveLocation::VariableDefinition
+        } else if directive_loc.schema_token().is_some() {
+            DirectiveLocation::Schema
+        } else if directive_loc.scalar_token().is_some() {
+            DirectiveLocation::Scalar
+        } else if directive_loc.object_token().is_some() {
+            DirectiveLocation::Object
+        } else if directive_loc.field_definition_token().is_some() {
+            DirectiveLocation::FieldDefinition
+        } else if directive_loc.argument_definition_token().is_some() {
+            DirectiveLocation::ArgumentDefinition
+        } else if directive_loc.interface_token().is_some() {
+            DirectiveLocation::Interface
+        } else if directive_loc.union_token().is_some() {
+            DirectiveLocation::Union
+        } else if directive_loc.enum_token().is_some() {
+            DirectiveLocation::Enum
+        } else if directive_loc.enum_value_token().is_some() {
+            DirectiveLocation::EnumValue
+        } else if directive_loc.input_object_token().is_some() {
+            DirectiveLocation::InputObject
+        } else {
+            DirectiveLocation::InputFieldDefinition
+        }
+    }
+}
+
+impl From<DirectiveLocation> for String {
+    fn from(dir_loc: DirectiveLocation) -> Self {
+        match dir_loc {
+            DirectiveLocation::Query => "QUERY".to_string(),
+            DirectiveLocation::Mutation => "MUTATION".to_string(),
+            DirectiveLocation::Subscription => "SUBSCRIPTION".to_string(),
+            DirectiveLocation::Field => "FIELD".to_string(),
+            DirectiveLocation::FragmentDefinition => "FRAGMENT_DEFINITION".to_string(),
+            DirectiveLocation::FragmentSpread => "FRAGMENT_SPREAD".to_string(),
+            DirectiveLocation::InlineFragment => "INLINE_FRAGMENT".to_string(),
+            DirectiveLocation::VariableDefinition => "VARIABLE_DEFINITION".to_string(),
+            DirectiveLocation::Schema => "SCHEMA".to_string(),
+            DirectiveLocation::Scalar => "SCALAR".to_string(),
+            DirectiveLocation::Object => "OBJECT".to_string(),
+            DirectiveLocation::FieldDefinition => "FIELD_DEFINITION".to_string(),
+            DirectiveLocation::ArgumentDefinition => "ARGUMENT_DEFINITION".to_string(),
+            DirectiveLocation::Interface => "INTERFACE".to_string(),
+            DirectiveLocation::Union => "UNION".to_string(),
+            DirectiveLocation::Enum => "ENUM".to_string(),
+            DirectiveLocation::EnumValue => "ENUM_VALUE".to_string(),
+            DirectiveLocation::InputObject => "INPUT_OBJECT".to_string(),
+            DirectiveLocation::InputFieldDefinition => "INPUT_FIELD_DEFINITION".to_string(),
+        }
+    }
+}
+
 #[derive(Clone, Debug, Hash, PartialEq, Eq)]
 pub struct Argument {
     pub(crate) name: String,
@@ -725,6 +857,13 @@ pub struct ArgumentsDefinition {
     pub(crate) input_values: Arc<Vec<InputValueDefinition>>,
 }
 
+impl ArgumentsDefinition {
+    /// Get a reference to arguments definition's input values.
+    pub fn input_values(&self) -> &[InputValueDefinition] {
+        self.input_values.as_ref()
+    }
+}
+
 #[derive(Clone, Debug, Hash, PartialEq, Eq)]
 pub struct InputValueDefinition {
     pub(crate) description: Option<String>,
@@ -734,6 +873,13 @@ pub struct InputValueDefinition {
     pub(crate) directives: Arc<Vec<Directive>>,
 }
 
+impl InputValueDefinition {
+    // Get a reference to input value definition's directives.
+    pub fn directives(&self) -> &[Directive] {
+        self.directives.as_ref()
+    }
+}
+
 #[derive(Clone, Debug, Hash, PartialEq, Eq)]
 pub struct ScalarDefinition {
     pub(crate) description: Option<String>,
diff --git a/crates/apollo-compiler/src/validation/directives.rs b/crates/apollo-compiler/src/validation/directives.rs
new file mode 100644
index 0000000..3d58caa
--- /dev/null
+++ b/crates/apollo-compiler/src/validation/directives.rs
@@ -0,0 +1,46 @@
+use std::collections::HashSet;
+
+use crate::{diagnostics::ErrorDiagnostic, ApolloDiagnostic, SourceDatabase};
+
+pub fn check(db: &dyn SourceDatabase) -> Vec<ApolloDiagnostic> {
+    let mut errors = Vec::new();
+
+    // Directive definitions must have unique names.
+    //
+    // Return a Unique Definition error in case of a duplicate name.
+    let mut seen = HashSet::new();
+    for directive_definitions in db.directive_definitions().iter() {
+        let name = directive_definitions.name();
+        if seen.contains(name) {
+            errors.push(ApolloDiagnostic::Error(ErrorDiagnostic::UniqueDefinition {
+                message: "Directive Definitions must have unique names".into(),
+                definition: name.to_string(),
+            }));
+        } else {
+            seen.insert(name);
+        }
+    }
+
+    // A directive definition must not contain the use of a directive which
+    // references itself directly.
+    //
+    // Returns Recursive Definition error.
+    for directive_def in db.directive_definitions().iter() {
+        let name = directive_def.name();
+        for input_values in directive_def.arguments().input_values() {
+            for directive in input_values.directives().iter() {
+                let directive_name = directive.name();
+                if name == directive_name {
+                    errors.push(ApolloDiagnostic::Error(
+                        ErrorDiagnostic::RecursiveDefinition {
+                            message: "Directive definition cannot reference itself".into(),
+                            definition: directive_name.to_string(),
+                        },
+                    ));
+                }
+            }
+        }
+    }
+
+    errors
+}
diff --git a/crates/apollo-compiler/src/validation/mod.rs b/crates/apollo-compiler/src/validation/mod.rs
index 992d431..2f112de 100644
--- a/crates/apollo-compiler/src/validation/mod.rs
+++ b/crates/apollo-compiler/src/validation/mod.rs
@@ -9,6 +9,7 @@ pub mod scalars;
 pub mod unions;
 
 // composite nodes
+pub mod directives;
 pub mod interfaces;
 
 // executable definitions
@@ -40,6 +41,7 @@ impl<'a> Validator<'a> {
         self.errors.extend(unions::check(self.db));
 
         self.errors.extend(interfaces::check(self.db));
+        self.errors.extend(directives::check(self.db));
 
         self.errors.extend(operations::check(self.db));
         self.errors.extend(unused_variables::check(self.db));


Test patch:
diff --git a/crates/apollo-compiler/test_data/err/0028_self_referential_directive_definition.graphql b/crates/apollo-compiler/test_data/err/0028_self_referential_directive_definition.graphql
new file mode 100644
index 0000000..ddb0745
--- /dev/null
+++ b/crates/apollo-compiler/test_data/err/0028_self_referential_directive_definition.graphql
@@ -0,0 +1,6 @@
+type Query {
+    id: ID!
+    field(arg: Int @invalidExample): String
+}
+
+directive @invalidExample(arg: String @invalidExample) on ARGUMENT_DEFINITION
\ No newline at end of file
diff --git a/crates/apollo-compiler/test_data/err/0028_self_referential_directive_definition.txt b/crates/apollo-compiler/test_data/err/0028_self_referential_directive_definition.txt
new file mode 100644
index 0000000..d6ad7d3
--- /dev/null
+++ b/crates/apollo-compiler/test_data/err/0028_self_referential_directive_definition.txt
@@ -0,0 +1,1 @@
+[Error(RecursiveDefinition { message: "Directive definition cannot reference itself", definition: "invalidExample" })]
diff --git a/crates/apollo-compiler/test_data/err/0029_directive_definitions_with_duplicate_names.graphql b/crates/apollo-compiler/test_data/err/0029_directive_definitions_with_duplicate_names.graphql
new file mode 100644
index 0000000..d42a9b7
--- /dev/null
+++ b/crates/apollo-compiler/test_data/err/0029_directive_definitions_with_duplicate_names.graphql
@@ -0,0 +1,11 @@
+schema @foo {
+    query: Query
+}
+
+type Query {
+    id: ID!
+    field: String
+}
+
+directive @foo on SCHEMA
+directive @foo on SCHEMA
\ No newline at end of file
diff --git a/crates/apollo-compiler/test_data/err/0029_directive_definitions_with_duplicate_names.txt b/crates/apollo-compiler/test_data/err/0029_directive_definitions_with_duplicate_names.txt
new file mode 100644
index 0000000..e669779
--- /dev/null
+++ b/crates/apollo-compiler/test_data/err/0029_directive_definitions_with_duplicate_names.txt
@@ -0,0 +1,1 @@
+[Error(UniqueRootOperationType { message: "Root Operation Types must be unique", named_type: "Query", operation_type: "Subscription" }), Error(UniqueDefinition { message: "Directive Definitions must have unique names", definition: "foo" })]
diff --git a/crates/apollo-compiler/test_data/ok/0008_schema_with_directive_definition.graphql b/crates/apollo-compiler/test_data/ok/0008_schema_with_directive_definition.graphql
new file mode 100644
index 0000000..285f208
--- /dev/null
+++ b/crates/apollo-compiler/test_data/ok/0008_schema_with_directive_definition.graphql
@@ -0,0 +1,9 @@
+type Query {
+    literature: Book
+}
+
+directive @delegateField(name: String!) repeatable on OBJECT | INTERFACE
+
+type Book @delegateField(name: "pageCount") @delegateField(name: "author") {
+  id: ID!
+}
\ No newline at end of file
diff --git a/crates/apollo-compiler/test_data/ok/0008_schema_with_directive_definition.txt b/crates/apollo-compiler/test_data/ok/0008_schema_with_directive_definition.txt
new file mode 100644
index 0000000..7d6cf15
--- /dev/null
+++ b/crates/apollo-compiler/test_data/ok/0008_schema_with_directive_definition.txt
@@ -0,0 +1,107 @@
+- DOCUMENT@0..199
+    - OBJECT_TYPE_DEFINITION@0..37
+        - type_KW@0..4 "type"
+        - WHITESPACE@4..5 " "
+        - NAME@5..11
+            - IDENT@5..10 "Query"
+            - WHITESPACE@10..11 " "
+        - FIELDS_DEFINITION@11..37
+            - L_CURLY@11..12 "{"
+            - WHITESPACE@12..17 "\n    "
+            - FIELD_DEFINITION@17..34
+                - NAME@17..27
+                    - IDENT@17..27 "literature"
+                - COLON@27..28 ":"
+                - WHITESPACE@28..29 " "
+                - NAMED_TYPE@29..33
+                    - NAME@29..33
+                        - IDENT@29..33 "Book"
+                - WHITESPACE@33..34 "\n"
+            - R_CURLY@34..35 "}"
+            - WHITESPACE@35..37 "\n\n"
+    - DIRECTIVE_DEFINITION@37..111
+        - directive_KW@37..46 "directive"
+        - WHITESPACE@46..47 " "
+        - AT@47..48 "@"
+        - NAME@48..61
+            - IDENT@48..61 "delegateField"
+        - ARGUMENTS_DEFINITION@61..77
+            - L_PAREN@61..62 "("
+            - INPUT_VALUE_DEFINITION@62..75
+                - NAME@62..66
+                    - IDENT@62..66 "name"
+                - COLON@66..67 ":"
+                - WHITESPACE@67..68 " "
+                - NON_NULL_TYPE@68..75
+                    - NAMED_TYPE@68..74
+                        - NAME@68..74
+                            - IDENT@68..74 "String"
+                    - BANG@74..75 "!"
+            - R_PAREN@75..76 ")"
+            - WHITESPACE@76..77 " "
+        - repeatable_KW@77..87 "repeatable"
+        - WHITESPACE@87..88 " "
+        - on_KW@88..90 "on"
+        - WHITESPACE@90..91 " "
+        - DIRECTIVE_LOCATIONS@91..111
+            - DIRECTIVE_LOCATION@91..98
+                - OBJECT_KW@91..97 "OBJECT"
+                - WHITESPACE@97..98 " "
+            - PIPE@98..99 "|"
+            - WHITESPACE@99..100 " "
+            - DIRECTIVE_LOCATION@100..111
+                - INTERFACE_KW@100..109 "INTERFACE"
+                - WHITESPACE@109..111 "\n\n"
+    - OBJECT_TYPE_DEFINITION@111..199
+        - type_KW@111..115 "type"
+        - WHITESPACE@115..116 " "
+        - NAME@116..121
+            - IDENT@116..120 "Book"
+            - WHITESPACE@120..121 " "
+        - DIRECTIVES@121..186
+            - DIRECTIVE@121..155
+                - AT@121..122 "@"
+                - NAME@122..135
+                    - IDENT@122..135 "delegateField"
+                - ARGUMENTS@135..155
+                    - L_PAREN@135..136 "("
+                    - ARGUMENT@136..153
+                        - NAME@136..140
+                            - IDENT@136..140 "name"
+                        - COLON@140..141 ":"
+                        - WHITESPACE@141..142 " "
+                        - STRING_VALUE@142..153
+                            - STRING@142..153 "\"pageCount\""
+                    - R_PAREN@153..154 ")"
+                    - WHITESPACE@154..155 " "
+            - DIRECTIVE@155..186
+                - AT@155..156 "@"
+                - NAME@156..169
+                    - IDENT@156..169 "delegateField"
+                - ARGUMENTS@169..186
+                    - L_PAREN@169..170 "("
+                    - ARGUMENT@170..184
+                        - NAME@170..174
+                            - IDENT@170..174 "name"
+                        - COLON@174..175 ":"
+                        - WHITESPACE@175..176 " "
+                        - STRING_VALUE@176..184
+                            - STRING@176..184 "\"author\""
+                    - R_PAREN@184..185 ")"
+                    - WHITESPACE@185..186 " "
+        - FIELDS_DEFINITION@186..199
+            - L_CURLY@186..187 "{"
+            - WHITESPACE@187..190 "\n  "
+            - FIELD_DEFINITION@190..198
+                - NAME@190..192
+                    - IDENT@190..192 "id"
+                - COLON@192..193 ":"
+                - WHITESPACE@193..194 " "
+                - NON_NULL_TYPE@194..198
+                    - NAMED_TYPE@194..196
+                        - NAME@194..196
+                            - IDENT@194..196 "ID"
+                    - BANG@196..197 "!"
+                    - WHITESPACE@197..198 "\n"
+            - R_CURLY@198..199 "}"
+