Repository: apollographql/apollo-rs (rust)

Issue:
add scalar definitions to compiler's context
- add scalar definitions to database
- add validations for scalars
     - custom scalars must have a `@specifiedBy` directive
     - cannot have built-in directives defined

Pull request description:
feat(apollo-compiler): add scalars to compiler's context
- adds scalars to source database
- creates two new validation errors:
    1. "BuiltInScalarDefinition" - when a built in scalar is redefined
    2. "ScalarSpecificationURL" - when a custom scalar does not have a specifiedBy directive

closes #233 

Patch:
diff --git a/crates/apollo-compiler/src/diagnostics.rs b/crates/apollo-compiler/src/diagnostics.rs
index 8a82bec..b7a8a06 100644
--- a/crates/apollo-compiler/src/diagnostics.rs
+++ b/crates/apollo-compiler/src/diagnostics.rs
@@ -34,6 +34,14 @@ pub enum ErrorDiagnostic {
         message: String,
         operation: Option<String>,
     },
+    BuiltInScalarDefinition {
+        message: String,
+        scalar: String,
+    },
+    ScalarSpecificationURL {
+        message: String,
+        scalar: String,
+    },
     UndefinedVariable {
         message: String,
         variable: String,
diff --git a/crates/apollo-compiler/src/lib.rs b/crates/apollo-compiler/src/lib.rs
index f80c6be..9794ca4 100644
--- a/crates/apollo-compiler/src/lib.rs
+++ b/crates/apollo-compiler/src/lib.rs
@@ -60,6 +60,10 @@ impl ApolloCompiler {
     pub fn object_types(&self) -> Arc<Vec<values::ObjectTypeDefinition>> {
         self.db.object_types()
     }
+
+    pub fn scalars(&self) -> Arc<Vec<values::ScalarDefinition>> {
+        self.db.scalars()
+    }
 }
 
 #[cfg(test)]
@@ -174,8 +178,34 @@ type Result {
         let errors = ctx.validate();
 
         assert!(errors.is_empty());
+    }
+
+    #[test]
+    fn it_accesses_scalar_definitions() {
+        let input = r#"
+type Query {
+  website: URL,
+  amount: Int
+}
+
+scalar URL @specifiedBy(url: "https://tools.ietf.org/html/rfc3986")
+"#;
+
+        let ctx = ApolloCompiler::new(input);
+        let errors = ctx.validate();
+
+        assert!(errors.is_empty());
 
-        let schema = ctx.schema();
-        dbg!(schema);
+        let scalars = ctx.scalars();
+
+        let directives: Vec<&str> = scalars
+            .iter()
+            .find(|scalar| scalar.name() == "URL")
+            .unwrap()
+            .directives()
+            .iter()
+            .map(|directive| directive.name())
+            .collect();
+        assert_eq!(directives, ["specifiedBy"]);
     }
 }
diff --git a/crates/apollo-compiler/src/queries/database.rs b/crates/apollo-compiler/src/queries/database.rs
index 5560d9d..75a3e97 100644
--- a/crates/apollo-compiler/src/queries/database.rs
+++ b/crates/apollo-compiler/src/queries/database.rs
@@ -44,6 +44,8 @@ pub trait SourceDatabase {
 
     fn object_types(&self) -> Arc<Vec<ObjectTypeDefinition>>;
 
+    fn scalars(&self) -> Arc<Vec<ScalarDefinition>>;
+
     fn query_operations(&self) -> Operations;
 
     fn mutation_operations(&self) -> Operations;
@@ -338,6 +340,20 @@ fn object_types(db: &dyn SourceDatabase) -> Arc<Vec<ObjectTypeDefinition>> {
     Arc::new(objects)
 }
 
+fn scalars(db: &dyn SourceDatabase) -> Arc<Vec<ScalarDefinition>> {
+    let scalars = db
+        .definitions()
+        .iter()
+        .filter_map(|definition| match definition {
+            ast::Definition::ScalarTypeDefinition(scalar_def) => {
+                Some(scalar_definition(scalar_def.clone()))
+            }
+            _ => None,
+        })
+        .collect();
+    Arc::new(scalars)
+}
+
 fn find_object_type(db: &dyn SourceDatabase, id: Uuid) -> Option<Arc<ObjectTypeDefinition>> {
     db.object_types().iter().find_map(|object_type| {
         if &id == object_type.id() {
@@ -447,6 +463,18 @@ fn object_type_definition(obj_def: ast::ObjectTypeDefinition) -> ObjectTypeDefin
     }
 }
 
+fn scalar_definition(scalar_def: ast::ScalarTypeDefinition) -> ScalarDefinition {
+    let description = description(scalar_def.description());
+    let name = name(scalar_def.name());
+    let directives = directives(scalar_def.directives());
+
+    ScalarDefinition {
+        description,
+        name,
+        directives,
+    }
+}
+
 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
diff --git a/crates/apollo-compiler/src/queries/values.rs b/crates/apollo-compiler/src/queries/values.rs
index 1a3fb7d..e6f8ae3 100644
--- a/crates/apollo-compiler/src/queries/values.rs
+++ b/crates/apollo-compiler/src/queries/values.rs
@@ -665,12 +665,12 @@ pub struct ObjectTypeDefinition {
 }
 
 impl ObjectTypeDefinition {
-    /// Get the object type definition's id.
+    /// get the object type definition's id.
     pub fn id(&self) -> &Uuid {
         &self.id
     }
 
-    /// Get a reference to the object type definition's name.
+    /// get a reference to the object type definition's name.
     pub fn name(&self) -> &str {
         self.name.as_ref()
     }
@@ -703,3 +703,22 @@ pub struct InputValueDefinition {
     pub(crate) default_value: Option<DefaultValue>,
     pub(crate) directives: Arc<Vec<Directive>>,
 }
+
+#[derive(Clone, Debug, Hash, PartialEq, Eq)]
+pub struct ScalarDefinition {
+    pub(crate) description: Option<String>,
+    pub(crate) name: String,
+    pub(crate) directives: Arc<Vec<Directive>>,
+}
+
+impl ScalarDefinition {
+    /// get a reference to the scalar definition's name.
+    pub fn name(&self) -> &str {
+        self.name.as_ref()
+    }
+
+    /// Get a reference to scalar definition's directives.
+    pub fn directives(&self) -> &[Directive] {
+        self.directives.as_ref()
+    }
+}
diff --git a/crates/apollo-compiler/src/validation/mod.rs b/crates/apollo-compiler/src/validation/mod.rs
index 3e23a8a..58bef8a 100644
--- a/crates/apollo-compiler/src/validation/mod.rs
+++ b/crates/apollo-compiler/src/validation/mod.rs
@@ -1,6 +1,7 @@
 use crate::{ApolloDiagnostic, SourceDatabase};
 
 pub mod operations;
+pub mod scalars;
 pub mod schema;
 pub mod unused_implements_interfaces;
 pub mod unused_variables;
@@ -21,6 +22,7 @@ impl<'a> Validator<'a> {
     pub fn validate(&mut self) -> &mut [ApolloDiagnostic] {
         self.errors.extend(self.db.syntax_errors());
         self.errors.extend(schema::check(self.db));
+        self.errors.extend(scalars::check(self.db));
         self.errors.extend(operations::check(self.db));
         self.errors.extend(unused_variables::check(self.db));
         self.errors.as_mut()
diff --git a/crates/apollo-compiler/src/validation/scalars.rs b/crates/apollo-compiler/src/validation/scalars.rs
new file mode 100644
index 0000000..d9bba51
--- /dev/null
+++ b/crates/apollo-compiler/src/validation/scalars.rs
@@ -0,0 +1,37 @@
+use crate::{diagnostics::ErrorDiagnostic, ApolloDiagnostic, SourceDatabase};
+
+const BUILT_IN_SCALARS: [&str; 5] = ["Int", "Float", "Boolean", "String", "ID"];
+
+pub fn check(db: &dyn SourceDatabase) -> Vec<ApolloDiagnostic> {
+    let mut errors = Vec::new();
+
+    // All built-in scalars must be omitted for brevity.
+    for scalar in db.scalars().iter() {
+        let name = scalar.name();
+        if BUILT_IN_SCALARS.contains(&name) {
+            errors.push(ApolloDiagnostic::Error(
+                ErrorDiagnostic::BuiltInScalarDefinition {
+                    message: "Built-in scalars must be omitted for brevity".into(),
+                    scalar: name.into(),
+                },
+            ));
+        } else {
+            // Custom scalars must provide a scalar specification URL via the
+            // @specifiedBy directive
+            if !scalar
+                .directives()
+                .iter()
+                .any(|directive| directive.name() == "specifiedBy")
+            {
+                errors.push(ApolloDiagnostic::Error(
+                ErrorDiagnostic::ScalarSpecificationURL {
+                    message: "Custom scalars must provide a scalar specification URL via the @specifiedBy directive".into(),
+                    scalar: name.into(),
+                },
+            ));
+            }
+        }
+    }
+
+    errors
+}


Test patch:
diff --git a/crates/apollo-compiler/test_data/err/0016_schema_with_built_in_scalar_definition.graphql b/crates/apollo-compiler/test_data/err/0016_schema_with_built_in_scalar_definition.graphql
new file mode 100644
index 0000000..ad0e6b5
--- /dev/null
+++ b/crates/apollo-compiler/test_data/err/0016_schema_with_built_in_scalar_definition.graphql
@@ -0,0 +1,5 @@
+type Query {
+  amount: Int
+}
+
+scalar Int
\ No newline at end of file
diff --git a/crates/apollo-compiler/test_data/err/0016_schema_with_built_in_scalar_definition.txt b/crates/apollo-compiler/test_data/err/0016_schema_with_built_in_scalar_definition.txt
new file mode 100644
index 0000000..e4dd497
--- /dev/null
+++ b/crates/apollo-compiler/test_data/err/0016_schema_with_built_in_scalar_definition.txt
@@ -0,0 +1,1 @@
+[Error(BuiltInScalarDefinition { message: "Built-in scalars must be omitted for brevity", scalar: "Int" })]
diff --git a/crates/apollo-compiler/test_data/err/0017_schema_with_unspecified_scalar.graphql b/crates/apollo-compiler/test_data/err/0017_schema_with_unspecified_scalar.graphql
new file mode 100644
index 0000000..1ab89b4
--- /dev/null
+++ b/crates/apollo-compiler/test_data/err/0017_schema_with_unspecified_scalar.graphql
@@ -0,0 +1,5 @@
+type Query {
+  website: URL
+}
+
+scalar URL
\ No newline at end of file
diff --git a/crates/apollo-compiler/test_data/err/0017_schema_with_unspecified_scalar.txt b/crates/apollo-compiler/test_data/err/0017_schema_with_unspecified_scalar.txt
new file mode 100644
index 0000000..d977b16
--- /dev/null
+++ b/crates/apollo-compiler/test_data/err/0017_schema_with_unspecified_scalar.txt
@@ -0,0 +1,1 @@
+[Error(ScalarSpecificationURL { message: "Custom scalars must provide a scalar specification URL via the @specifiedBy directive", scalar: "URL" })]
diff --git a/crates/apollo-compiler/test_data/err/0018_schema_with_specified_scalar_missing_values.graphql b/crates/apollo-compiler/test_data/err/0018_schema_with_specified_scalar_missing_values.graphql
new file mode 100644
index 0000000..d4fc25e
--- /dev/null
+++ b/crates/apollo-compiler/test_data/err/0018_schema_with_specified_scalar_missing_values.graphql
@@ -0,0 +1,6 @@
+type Query {
+  website: URL,
+  amount: Int
+}
+
+scalar URL @specifiedBy()
\ No newline at end of file
diff --git a/crates/apollo-compiler/test_data/err/0018_schema_with_specified_scalar_missing_values.txt b/crates/apollo-compiler/test_data/err/0018_schema_with_specified_scalar_missing_values.txt
new file mode 100644
index 0000000..020ba71
--- /dev/null
+++ b/crates/apollo-compiler/test_data/err/0018_schema_with_specified_scalar_missing_values.txt
@@ -0,0 +1,1 @@
+[Error(SyntaxError { message: "expected an Argument", data: ")", index: 70 })]
diff --git a/crates/apollo-compiler/test_data/ok/0004_schema_with_custom_scalars.graphql b/crates/apollo-compiler/test_data/ok/0004_schema_with_custom_scalars.graphql
new file mode 100644
index 0000000..762e95a
--- /dev/null
+++ b/crates/apollo-compiler/test_data/ok/0004_schema_with_custom_scalars.graphql
@@ -0,0 +1,6 @@
+type Query {
+  website: URL,
+  amount: Int
+}
+
+scalar URL @specifiedBy(url: "https://tools.ietf.org/html/rfc3986")
\ No newline at end of file
diff --git a/crates/apollo-compiler/test_data/ok/0004_schema_with_custom_scalars.txt b/crates/apollo-compiler/test_data/ok/0004_schema_with_custom_scalars.txt
new file mode 100644
index 0000000..4b137a9
--- /dev/null
+++ b/crates/apollo-compiler/test_data/ok/0004_schema_with_custom_scalars.txt
@@ -0,0 +1,53 @@
+- DOCUMENT@0..113
+    - OBJECT_TYPE_DEFINITION@0..46
+        - type_KW@0..4 "type"
+        - WHITESPACE@4..5 " "
+        - NAME@5..11
+            - IDENT@5..10 "Query"
+            - WHITESPACE@10..11 " "
+        - FIELDS_DEFINITION@11..46
+            - L_CURLY@11..12 "{"
+            - WHITESPACE@12..15 "\n  "
+            - FIELD_DEFINITION@15..31
+                - NAME@15..22
+                    - IDENT@15..22 "website"
+                - COLON@22..23 ":"
+                - WHITESPACE@23..24 " "
+                - NAMED_TYPE@24..27
+                    - NAME@24..27
+                        - IDENT@24..27 "URL"
+                - COMMA@27..28 ","
+                - WHITESPACE@28..31 "\n  "
+            - FIELD_DEFINITION@31..43
+                - NAME@31..37
+                    - IDENT@31..37 "amount"
+                - COLON@37..38 ":"
+                - WHITESPACE@38..39 " "
+                - NAMED_TYPE@39..42
+                    - NAME@39..42
+                        - IDENT@39..42 "Int"
+                - WHITESPACE@42..43 "\n"
+            - R_CURLY@43..44 "}"
+            - WHITESPACE@44..46 "\n\n"
+    - SCALAR_TYPE_DEFINITION@46..113
+        - scalar_KW@46..52 "scalar"
+        - WHITESPACE@52..53 " "
+        - NAME@53..57
+            - IDENT@53..56 "URL"
+            - WHITESPACE@56..57 " "
+        - DIRECTIVES@57..113
+            - DIRECTIVE@57..113
+                - AT@57..58 "@"
+                - NAME@58..69
+                    - IDENT@58..69 "specifiedBy"
+                - ARGUMENTS@69..113
+                    - L_PAREN@69..70 "("
+                    - ARGUMENT@70..112
+                        - NAME@70..73
+                            - IDENT@70..73 "url"
+                        - COLON@73..74 ":"
+                        - WHITESPACE@74..75 " "
+                        - STRING_VALUE@75..112
+                            - STRING@75..112 "\"https://tools.ietf.org/html/rfc3986\""
+                    - R_PAREN@112..113 ")"
+