Repository: apollographql/apollo-rs
Pull request #757 (2023-11-22 13:44:25)

feat(compiler): add schema coordinates
Implements parsing and printing for the [Schema Coordinates RFC](https://github.com/graphql/graphql-wg/blob/main/rfcs/SchemaCoordinates.md).

Here, `SchemaCoordinate` is an enum of all the different kinds of coordinates. Each kind of coordinate is also its own type.

```rust
use apollo_compiler::name;
use apollo_compiler::coordinate::{SchemaCoordinate, FieldArgumentCoordinate};

let coord: SchemaCoordinate = "Type.field(argument:)".parse().unwrap();
assert_eq!(coord, SchemaCoordinate::FieldArgument(
    FieldArgumentCoordinate {
        ty: name!("Type"),
        field: name!("field"),
        argument: name!("argument"),
    },
));
```

```rust
use apollo_compiler::coord;

assert_eq!(coord!(@directive).to_string(), "@directive");
assert_eq!(coord!(@directive(arg:)).to_string(), "@directive(arg:)");
assert_eq!(coord!(Type).to_string(), "Type");
assert_eq!(coord!(Type.field).to_string(), "Type.field");
assert_eq!(coord!(Type.field(arg:)).to_string(), "Type.field(arg:)");
```

The goal of this API is:
 - easily tracking the current coordinate while walking a tree (eg with the `.argument()` method). Each kind of coordinate having its own `impl` is handy for this
 - look up a typed coordinate in the schema
   - not yet implemented but imagine an `fn lookup(&Schema, FieldArgumentCoordinate) -> Option<Node<InputValueDefinition>>`, so the return value can be typed. I'm not 100% convinced of how important this is as you may often have just a `SchemaCoordinate`, which can return multiple types.
   - Backed out for now, we can do it later, typed lookup is certainly possible.
 - Printing for use in error messages (https://github.com/apollographql/apollo-rs/issues/691)

Some other options:
 - `TypeCoordinate` could just be an alias of `NamedType` 🤪 
 - Just have a `SchemaCoordinate` enum without all the specific types--I like this less for how it's used in validation (different types with simple methods for pushing/popping are ideal)
 - A more flat text representation, so not having 3 separate refcounted strings like here. Just representing a schema coordinate as its source text is actually fairly efficient (like literally storing the string `Type.field(arg:)`). A schema coordinate could then be pointer-sized, here it's 3 pointers + a discriminant. A downside is that you need to allocate when you append a field or argument or something.

goto-bus-stop: Oh hmm, a `FieldCoordinate` can also refer to an enum value. The RFC actually calls it a type attribute rather than a field: https://github.com/graphql/graphql-wg/blob/main/rfcs/SchemaCoordinates.md#typeattribute

So you can have `Query.products` referring to a field and `ProductType.book` referring to an enum value. that makes the typed lookup by coordinate idea a bit less interesting.
goto-bus-stop: I think for pointing out a specific `implements`, or pointing out a directive application, we would use both a coordinate + a string (or another coordinate) for the more specific thing in the error message. It does mean that the coordinates for error locations would often be a parent node of where the error actually occurred, but it's not gonna be super far away...

For example, if you do something like
```graphql
type Example {
  name @deprecated(reason: false)
}
```
you could get an error with its location at coordinate `Example.name`, and the message formatting using two coordinates:
```
Expected type String, but Boolean was provided to `@deprecated(reason:)` on `Example.name`
```
(would need some copy workshopping to make it read well)
goto-bus-stop: Something else you can't point to is a schema definition, so we can't *require* a coordinate for diagnostics on those
goto-bus-stop: As discussed in checkin, we keep lookup out for now. Replaced `coordinate: String` uses in diagnostics with either a specific coordinate type or `SchemaCoordinate`.

Patch:
diff --git a/crates/apollo-compiler/CHANGELOG.md b/crates/apollo-compiler/CHANGELOG.md
index f7ed2a1..43174a7 100644
--- a/crates/apollo-compiler/CHANGELOG.md
+++ b/crates/apollo-compiler/CHANGELOG.md
@@ -41,6 +41,11 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
 - **Add `NodeStr::from(Name)` - [goto-bus-stop], [pull/773]**
 - **Convenience accessors for `ast::Selection` enum - [SimonSapin], [pull/777]**
   `as_field`, `as_inline_fragment`, and `as_fragment_spread`; all returning `Option<&_>`.
+- **Add schema coordinates - [goto-bus-stop], [pull/757]:**
+  Schema coordinates are a compact, human-readable way to uniquely point to an item defined in a schema.
+  - `string.parse::<SchemaCoordinate>()` parses a coordinate from a string.
+  - Coordinates have a `Display` impl that writes them out with schema coordinate syntax.
+  - The `coord!()` macro creates a static coordinate at compile time from spec syntax.
 
 ## Fixes
 - **Fix serializing single-line strings with leading whitespace - [goto-bus-stop], [pull/774]**
diff --git a/crates/apollo-compiler/src/coordinate.rs b/crates/apollo-compiler/src/coordinate.rs
new file mode 100644
index 0000000..a9d4db6
--- /dev/null
+++ b/crates/apollo-compiler/src/coordinate.rs
@@ -0,0 +1,438 @@
+//! Parsing and printing for schema coordinates as described in [the RFC].
+//!
+//! Schema coordinates uniquely point to an item defined in a schema.
+//!
+//! [the RFC]: https://github.com/graphql/graphql-wg/blob/main/rfcs/SchemaCoordinates.md
+
+use crate::ast::InvalidNameError;
+use crate::ast::Name;
+use crate::schema::NamedType;
+use std::fmt;
+use std::str::FromStr;
+
+/// Create a static schema coordinate at compile time.
+///
+/// ```rust
+/// use apollo_compiler::coord;
+///
+/// assert_eq!(coord!(@directive).to_string(), "@directive");
+/// assert_eq!(coord!(@directive(arg:)).to_string(), "@directive(arg:)");
+/// assert_eq!(coord!(Type).to_string(), "Type");
+/// assert_eq!(coord!(Type.field).to_string(), "Type.field");
+/// assert_eq!(coord!(Type.field(arg:)).to_string(), "Type.field(arg:)");
+/// assert_eq!(coord!(EnumType.ENUM_VALUE).to_string(), "EnumType.ENUM_VALUE");
+/// ```
+#[macro_export]
+macro_rules! coord {
+    ( @ $name:ident ) => {
+        $crate::coordinate::DirectiveCoordinate {
+            directive: $crate::name!($name),
+        }
+    };
+    ( @ $name:ident ( $arg:ident : ) ) => {
+        $crate::coordinate::DirectiveArgumentCoordinate {
+            directive: $crate::name!($name),
+            argument: $crate::name!($arg),
+        }
+    };
+    ( $name:ident ) => {
+        $crate::coordinate::TypeCoordinate {
+            ty: $crate::name!($name),
+        }
+    };
+    ( $name:ident . $attribute:ident ) => {
+        $crate::coordinate::TypeAttributeCoordinate {
+            ty: $crate::name!($name),
+            attribute: $crate::name!($attribute),
+        }
+    };
+    ( $name:ident . $field:ident ( $arg:ident : ) ) => {
+        $crate::coordinate::FieldArgumentCoordinate {
+            ty: $crate::name!($name),
+            field: $crate::name!($field),
+            argument: $crate::name!($arg),
+        }
+    };
+}
+
+/// A schema coordinate targeting a type definition: `Type`.
+///
+/// # Example
+/// ```
+/// use apollo_compiler::name;
+/// use apollo_compiler::coordinate::TypeCoordinate;
+///
+/// assert_eq!(TypeCoordinate { ty: name!("Type") }.to_string(), "Type");
+/// ```
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct TypeCoordinate {
+    pub ty: NamedType,
+}
+
+/// A schema coordinate targeting a field definition or an enum value: `Type.field`, `Enum.VALUE`.
+///
+/// # Example
+/// ```
+/// use apollo_compiler::name;
+/// use apollo_compiler::coordinate::TypeAttributeCoordinate;
+///
+/// assert_eq!(TypeAttributeCoordinate {
+///     ty: name!("Type"),
+///     attribute: name!("field"),
+/// }.to_string(), "Type.field");
+/// ```
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct TypeAttributeCoordinate {
+    pub ty: NamedType,
+    pub attribute: Name,
+}
+
+/// A schema coordinate targeting a field argument definition: `Type.field(argument:)`.
+///
+/// # Example
+/// ```
+/// use apollo_compiler::name;
+/// use apollo_compiler::coordinate::FieldArgumentCoordinate;
+///
+/// assert_eq!(FieldArgumentCoordinate {
+///     ty: name!("Type"),
+///     field: name!("field"),
+///     argument: name!("argument"),
+/// }.to_string(), "Type.field(argument:)");
+/// ```
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct FieldArgumentCoordinate {
+    pub ty: NamedType,
+    pub field: Name,
+    pub argument: Name,
+}
+
+/// A schema coordinate targeting a directive definition: `@directive`.
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct DirectiveCoordinate {
+    pub directive: Name,
+}
+
+/// A schema coordinate targeting a directive argument definition: `@directive(argument:)`.
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct DirectiveArgumentCoordinate {
+    pub directive: Name,
+    pub argument: Name,
+}
+
+/// Any schema coordinate.
+///
+/// # Example
+/// ```
+/// use apollo_compiler::name;
+/// use apollo_compiler::coordinate::{SchemaCoordinate, FieldArgumentCoordinate};
+///
+/// let coord: SchemaCoordinate = "Type.field(argument:)".parse().unwrap();
+/// assert_eq!(coord, SchemaCoordinate::FieldArgument(
+///     FieldArgumentCoordinate {
+///         ty: name!("Type"),
+///         field: name!("field"),
+///         argument: name!("argument"),
+///     },
+/// ));
+/// ```
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub enum SchemaCoordinate {
+    Type(TypeCoordinate),
+    TypeAttribute(TypeAttributeCoordinate),
+    FieldArgument(FieldArgumentCoordinate),
+    Directive(DirectiveCoordinate),
+    DirectiveArgument(DirectiveArgumentCoordinate),
+}
+
+/// Errors that can occur while parsing a schema coordinate.
+#[derive(Debug, Clone, thiserror::Error)]
+#[non_exhaustive]
+pub enum SchemaCoordinateParseError {
+    /// Invalid format, eg. unexpected characters
+    #[error("invalid schema coordinate")]
+    InvalidFormat,
+    /// A name part contains invalid characters
+    #[error(transparent)]
+    InvalidName(#[from] InvalidNameError),
+}
+
+impl TypeCoordinate {
+    /// Create a schema coordinate that points to an attribute on this type.
+    ///
+    /// For object types and interfaces, the resulting coordinate points to a field. For enums, the
+    /// resulting coordinate points to a value.
+    pub fn with_attribute(&self, attribute: Name) -> TypeAttributeCoordinate {
+        TypeAttributeCoordinate {
+            ty: self.ty.clone(),
+            attribute,
+        }
+    }
+}
+
+impl From<NamedType> for TypeCoordinate {
+    fn from(ty: NamedType) -> Self {
+        Self { ty }
+    }
+}
+
+impl FromStr for TypeCoordinate {
+    type Err = SchemaCoordinateParseError;
+    fn from_str(input: &str) -> Result<Self, Self::Err> {
+        Ok(Self {
+            ty: NamedType::try_from(input)?,
+        })
+    }
+}
+
+impl TypeAttributeCoordinate {
+    /// Create a schema coordinate that points to the type this attribute is part of.
+    pub fn type_coordinate(&self) -> TypeCoordinate {
+        TypeCoordinate {
+            ty: self.ty.clone(),
+        }
+    }
+
+    /// Assume this attribute is a field, and create a schema coordinate that points to an argument on this field.
+    pub fn with_argument(&self, argument: Name) -> FieldArgumentCoordinate {
+        FieldArgumentCoordinate {
+            ty: self.ty.clone(),
+            field: self.attribute.clone(),
+            argument,
+        }
+    }
+}
+
+impl FromStr for TypeAttributeCoordinate {
+    type Err = SchemaCoordinateParseError;
+    fn from_str(input: &str) -> Result<Self, Self::Err> {
+        let Some((type_name, field)) = input.split_once('.') else {
+            return Err(SchemaCoordinateParseError::InvalidFormat);
+        };
+        Ok(Self {
+            ty: NamedType::try_from(type_name)?,
+            attribute: Name::try_from(field)?,
+        })
+    }
+}
+
+impl FieldArgumentCoordinate {
+    /// Create a schema coordinate that points to the type this argument is defined in.
+    pub fn type_coordinate(&self) -> TypeCoordinate {
+        TypeCoordinate {
+            ty: self.ty.clone(),
+        }
+    }
+
+    /// Create a schema coordinate that points to the field this argument is defined in.
+    pub fn field_coordinate(&self) -> TypeAttributeCoordinate {
+        TypeAttributeCoordinate {
+            ty: self.ty.clone(),
+            attribute: self.field.clone(),
+        }
+    }
+}
+
+impl FromStr for FieldArgumentCoordinate {
+    type Err = SchemaCoordinateParseError;
+    fn from_str(input: &str) -> Result<Self, Self::Err> {
+        let Some((field, rest)) = input.split_once('(') else {
+            return Err(SchemaCoordinateParseError::InvalidFormat);
+        };
+        let field = TypeAttributeCoordinate::from_str(field)?;
+
+        let Some((argument, ")")) = rest.split_once(':') else {
+            return Err(SchemaCoordinateParseError::InvalidFormat);
+        };
+        Ok(Self {
+            ty: field.ty,
+            field: field.attribute,
+            argument: Name::try_from(argument)?,
+        })
+    }
+}
+
+impl DirectiveCoordinate {
+    /// Create a schema coordinate that points to an argument of this directive.
+    pub fn with_argument(&self, argument: Name) -> DirectiveArgumentCoordinate {
+        DirectiveArgumentCoordinate {
+            directive: self.directive.clone(),
+            argument,
+        }
+    }
+}
+
+impl From<Name> for DirectiveCoordinate {
+    fn from(directive: Name) -> Self {
+        Self { directive }
+    }
+}
+
+impl FromStr for DirectiveCoordinate {
+    type Err = SchemaCoordinateParseError;
+    fn from_str(input: &str) -> Result<Self, Self::Err> {
+        if let Some(directive) = input.strip_prefix('@') {
+            Ok(Self {
+                directive: Name::try_from(directive)?,
+            })
+        } else {
+            Err(SchemaCoordinateParseError::InvalidFormat)
+        }
+    }
+}
+
+impl DirectiveArgumentCoordinate {
+    /// Create a schema coordinate that points to the directive this argument is defined in.
+    pub fn directive_coordinate(&self) -> DirectiveCoordinate {
+        DirectiveCoordinate {
+            directive: self.directive.clone(),
+        }
+    }
+}
+
+impl FromStr for DirectiveArgumentCoordinate {
+    type Err = SchemaCoordinateParseError;
+    fn from_str(input: &str) -> Result<Self, Self::Err> {
+        let Some((directive, rest)) = input.split_once('(') else {
+            return Err(SchemaCoordinateParseError::InvalidFormat);
+        };
+        let directive = DirectiveCoordinate::from_str(directive)?;
+
+        let Some((argument, ")")) = rest.split_once(':') else {
+            return Err(SchemaCoordinateParseError::InvalidFormat);
+        };
+        Ok(Self {
+            directive: directive.directive,
+            argument: Name::try_from(argument)?,
+        })
+    }
+}
+
+impl FromStr for SchemaCoordinate {
+    type Err = SchemaCoordinateParseError;
+    fn from_str(input: &str) -> Result<Self, Self::Err> {
+        if input.starts_with('@') {
+            DirectiveArgumentCoordinate::from_str(input)
+                .map(Self::DirectiveArgument)
+                .or_else(|_| DirectiveCoordinate::from_str(input).map(Self::Directive))
+        } else {
+            FieldArgumentCoordinate::from_str(input)
+                .map(Self::FieldArgument)
+                .or_else(|_| TypeAttributeCoordinate::from_str(input).map(Self::TypeAttribute))
+                .or_else(|_| TypeCoordinate::from_str(input).map(Self::Type))
+        }
+    }
+}
+
+impl From<TypeCoordinate> for SchemaCoordinate {
+    fn from(inner: TypeCoordinate) -> Self {
+        Self::Type(inner)
+    }
+}
+
+impl From<TypeAttributeCoordinate> for SchemaCoordinate {
+    fn from(inner: TypeAttributeCoordinate) -> Self {
+        Self::TypeAttribute(inner)
+    }
+}
+
+impl From<FieldArgumentCoordinate> for SchemaCoordinate {
+    fn from(inner: FieldArgumentCoordinate) -> Self {
+        Self::FieldArgument(inner)
+    }
+}
+
+impl From<DirectiveCoordinate> for SchemaCoordinate {
+    fn from(inner: DirectiveCoordinate) -> Self {
+        Self::Directive(inner)
+    }
+}
+
+impl From<DirectiveArgumentCoordinate> for SchemaCoordinate {
+    fn from(inner: DirectiveArgumentCoordinate) -> Self {
+        Self::DirectiveArgument(inner)
+    }
+}
+
+impl fmt::Display for TypeCoordinate {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        let Self { ty } = self;
+        write!(f, "{ty}")
+    }
+}
+
+impl fmt::Display for TypeAttributeCoordinate {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        let Self {
+            ty,
+            attribute: field,
+        } = self;
+        write!(f, "{ty}.{field}")
+    }
+}
+
+impl fmt::Display for FieldArgumentCoordinate {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        let Self {
+            ty,
+            field,
+            argument,
+        } = self;
+        write!(f, "{ty}.{field}({argument}:)")
+    }
+}
+
+impl fmt::Display for DirectiveCoordinate {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        let Self { directive } = self;
+        write!(f, "@{directive}")
+    }
+}
+
+impl fmt::Display for DirectiveArgumentCoordinate {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        let Self {
+            directive,
+            argument,
+        } = self;
+        write!(f, "@{directive}({argument}:)")
+    }
+}
+
+impl fmt::Display for SchemaCoordinate {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::Type(inner) => inner.fmt(f),
+            Self::TypeAttribute(inner) => inner.fmt(f),
+            Self::FieldArgument(inner) => inner.fmt(f),
+            Self::Directive(inner) => inner.fmt(f),
+            Self::DirectiveArgument(inner) => inner.fmt(f),
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn invalid_coordinates() {
+        SchemaCoordinate::from_str("Type\\.field(arg:)").expect_err("invalid character");
+        SchemaCoordinate::from_str("@directi^^ve").expect_err("invalid character");
+        SchemaCoordinate::from_str("@directi@ve").expect_err("invalid character");
+        SchemaCoordinate::from_str("@  spaces  ").expect_err("invalid character");
+
+        SchemaCoordinate::from_str("@(:)").expect_err("directive argument syntax without names");
+        SchemaCoordinate::from_str("@dir(:)")
+            .expect_err("directive argument syntax without argument name");
+        SchemaCoordinate::from_str("@(arg:)")
+            .expect_err("directive argument syntax without directive name");
+
+        SchemaCoordinate::from_str("Type.")
+            .expect_err("type attribute syntax without attribute name");
+        SchemaCoordinate::from_str(".field").expect_err("type attribute syntax without type name");
+        SchemaCoordinate::from_str("Type.field(:)")
+            .expect_err("field argument syntax without field name");
+        SchemaCoordinate::from_str("Type.field(arg)").expect_err("field argument syntax without :");
+    }
+}
diff --git a/crates/apollo-compiler/src/lib.rs b/crates/apollo-compiler/src/lib.rs
index c389bde..7e53680 100644
--- a/crates/apollo-compiler/src/lib.rs
+++ b/crates/apollo-compiler/src/lib.rs
@@ -3,6 +3,7 @@
 #[macro_use]
 mod macros;
 pub mod ast;
+pub mod coordinate;
 mod database;
 pub mod diagnostic;
 pub mod executable;
diff --git a/crates/apollo-compiler/src/validation/diagnostics.rs b/crates/apollo-compiler/src/validation/diagnostics.rs
index 241e074..528e4d9 100644
--- a/crates/apollo-compiler/src/validation/diagnostics.rs
+++ b/crates/apollo-compiler/src/validation/diagnostics.rs
@@ -3,6 +3,8 @@ use crate::ast::DirectiveLocation;
 use crate::ast::Name;
 use crate::ast::Type;
 use crate::ast::Value;
+use crate::coordinate::SchemaCoordinate;
+use crate::coordinate::TypeAttributeCoordinate;
 use crate::diagnostic::CliReport;
 use crate::diagnostic::NodeLocation;
 use crate::Node;
@@ -60,7 +62,7 @@ pub(crate) enum DiagnosticData {
     #[error("the argument `{name}` is not supported by `{coordinate}`")]
     UndefinedArgument {
         name: Name,
-        coordinate: String,
+        coordinate: SchemaCoordinate,
         definition_location: Option<NodeLocation>,
     },
     #[error("cannot find type `{name}` in this document")]
@@ -112,7 +114,13 @@ pub(crate) enum DiagnosticData {
     #[error("the required argument `{coordinate}` is not provided")]
     RequiredArgument {
         name: Name,
-        coordinate: String,
+        coordinate: SchemaCoordinate,
+        definition_location: Option<NodeLocation>,
+    },
+    #[error("the required field `{coordinate}` is not provided")]
+    RequiredField {
+        name: Name,
+        coordinate: TypeAttributeCoordinate,
         definition_location: Option<NodeLocation>,
     },
     #[error(
@@ -217,7 +225,7 @@ pub(crate) enum DiagnosticData {
     },
     #[error("interface, union and object types must have a subselection set")]
     MissingSubselection {
-        coordinate: String,
+        coordinate: TypeAttributeCoordinate,
         describe_type: &'static str,
     },
     #[error("operation must not select different types using the same field name `{field}`")]
@@ -405,6 +413,17 @@ impl ValidationError {
                 );
                 report.with_label_opt(*definition_location, "argument defined here");
             }
+            DiagnosticData::RequiredField {
+                name,
+                coordinate: _,
+                definition_location,
+            } => {
+                report.with_label_opt(
+                    self.location,
+                    format_args!("missing value for field `{name}`"),
+                );
+                report.with_label_opt(*definition_location, "field defined here");
+            }
             DiagnosticData::UndefinedDefinition { .. } => {
                 report.with_label_opt(self.location, "not found in this scope");
             }
diff --git a/crates/apollo-compiler/src/validation/directive.rs b/crates/apollo-compiler/src/validation/directive.rs
index 75deed3..a7e98d1 100644
--- a/crates/apollo-compiler/src/validation/directive.rs
+++ b/crates/apollo-compiler/src/validation/directive.rs
@@ -1,3 +1,5 @@
+use crate::coordinate::DirectiveArgumentCoordinate;
+use crate::coordinate::DirectiveCoordinate;
 use crate::validation::diagnostics::{DiagnosticData, ValidationError};
 use crate::validation::{NodeLocation, RecursionGuard, RecursionStack};
 use crate::{ast, schema, Node, ValidationDatabase};
@@ -270,7 +272,10 @@ pub(crate) fn validate_directives<'dir>(
                         argument.location(),
                         DiagnosticData::UndefinedArgument {
                             name: argument.name.clone(),
-                            coordinate: format!("@{}", dir.name),
+                            coordinate: DirectiveCoordinate {
+                                directive: dir.name.clone(),
+                            }
+                            .into(),
                             definition_location: loc,
                         },
                     ));
@@ -294,10 +299,11 @@ pub(crate) fn validate_directives<'dir>(
                         dir.location(),
                         DiagnosticData::RequiredArgument {
                             name: arg_def.name.clone(),
-                            coordinate: format!(
-                                "@{}({}:)",
-                                directive_definition.name, arg_def.name
-                            ),
+                            coordinate: DirectiveArgumentCoordinate {
+                                directive: directive_definition.name.clone(),
+                                argument: arg_def.name.clone(),
+                            }
+                            .into(),
                             definition_location: arg_def.location(),
                         },
                     ));
diff --git a/crates/apollo-compiler/src/validation/field.rs b/crates/apollo-compiler/src/validation/field.rs
index 20f1380..c0111c0 100644
--- a/crates/apollo-compiler/src/validation/field.rs
+++ b/crates/apollo-compiler/src/validation/field.rs
@@ -1,3 +1,4 @@
+use crate::coordinate::{FieldArgumentCoordinate, TypeAttributeCoordinate};
 use crate::validation::diagnostics::{DiagnosticData, ValidationError};
 use crate::validation::{FileId, ValidationDatabase};
 use crate::{ast, schema, Node};
@@ -63,7 +64,11 @@ pub(crate) fn validate_field(
                     argument.location(),
                     DiagnosticData::UndefinedArgument {
                         name: argument.name.clone(),
-                        coordinate: format!("{}.{}", against_type, field.name),
+                        coordinate: TypeAttributeCoordinate {
+                            ty: against_type.clone(),
+                            attribute: field.name.clone(),
+                        }
+                        .into(),
                         definition_location: loc,
                     },
                 ));
@@ -87,10 +92,12 @@ pub(crate) fn validate_field(
                     field.location(),
                     DiagnosticData::RequiredArgument {
                         name: arg_definition.name.clone(),
-                        coordinate: format!(
-                            "{}.{}({}:)",
-                            against_type, field.name, arg_definition.name
-                        ),
+                        coordinate: FieldArgumentCoordinate {
+                            ty: against_type.clone(),
+                            field: field.name.clone(),
+                            argument: arg_definition.name.clone(),
+                        }
+                        .into(),
                         definition_location: arg_definition.location(),
                     },
                 ));
@@ -200,7 +207,10 @@ pub(crate) fn validate_leaf_field_selection(
         Err(ValidationError::new(
             field.location(),
             DiagnosticData::MissingSubselection {
-                coordinate: format!("{tname}.{fname}"),
+                coordinate: TypeAttributeCoordinate {
+                    ty: tname.clone(),
+                    attribute: fname.clone(),
+                },
                 describe_type: type_def.describe(),
             },
         ))
diff --git a/crates/apollo-compiler/src/validation/value.rs b/crates/apollo-compiler/src/validation/value.rs
index 153cb33..04720fe 100644
--- a/crates/apollo-compiler/src/validation/value.rs
+++ b/crates/apollo-compiler/src/validation/value.rs
@@ -1,9 +1,9 @@
-use crate::{
-    ast, schema,
-    validation::diagnostics::{DiagnosticData, ValidationError},
-    validation::ValidationDatabase,
-    Node,
-};
+use crate::ast;
+use crate::coordinate::TypeAttributeCoordinate;
+use crate::schema;
+use crate::validation::diagnostics::{DiagnosticData, ValidationError};
+use crate::validation::ValidationDatabase;
+use crate::Node;
 
 fn unsupported_type(value: &Node<ast::Value>, declared_type: &Node<ast::Type>) -> ValidationError {
     ValidationError::new(
@@ -232,9 +232,12 @@ pub(crate) fn value_of_correct_type(
                     if (ty.is_non_null() && f.default_value.is_none()) && (is_missing || is_null) {
                         diagnostics.push(ValidationError::new(
                             arg_value.location(),
-                            DiagnosticData::RequiredArgument {
+                            DiagnosticData::RequiredField {
                                 name: input_name.clone(),
-                                coordinate: format!("{}.{}", input_obj.name, input_name),
+                                coordinate: TypeAttributeCoordinate {
+                                    ty: input_obj.name.clone(),
+                                    attribute: input_name.clone(),
+                                },
                                 definition_location: f.location(),
                             },
                         ));
diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml
index 8a83f44..eb1fb4a 100644
--- a/fuzz/Cargo.toml
+++ b/fuzz/Cargo.toml
@@ -46,3 +46,9 @@ name = "strings"
 path = "fuzz_targets/strings.rs"
 test = false
 doc = false
+
+[[bin]]
+name = "coordinate"
+path = "fuzz_targets/coordinate.rs"
+test = false
+doc = false
diff --git a/fuzz/fuzz_targets/coordinate.rs b/fuzz/fuzz_targets/coordinate.rs
new file mode 100644
index 0000000..480a4ca
--- /dev/null
+++ b/fuzz/fuzz_targets/coordinate.rs
@@ -0,0 +1,16 @@
+#![no_main]
+use apollo_compiler::coordinate::SchemaCoordinate;
+use libfuzzer_sys::fuzz_target;
+use log::debug;
+
+fuzz_target!(|data: &str| {
+    let coord = data.parse::<SchemaCoordinate>();
+    if let Ok(coord) = &coord {
+        assert_eq!(
+            &coord.to_string().parse::<SchemaCoordinate>().unwrap(),
+            coord
+        );
+    }
+
+    debug!("{:?}", coord);
+});
diff --git a/fuzz/fuzz_targets/strings.rs b/fuzz/fuzz_targets/strings.rs
index 332141a..121928c 100644
--- a/fuzz/fuzz_targets/strings.rs
+++ b/fuzz/fuzz_targets/strings.rs
@@ -1,19 +1,12 @@
 #![no_main]
 use apollo_compiler::{name, Schema};
-use libfuzzer_sys::{
-    arbitrary::{Arbitrary, Unstructured},
-    fuzz_target,
-};
+use libfuzzer_sys::fuzz_target;
 use log::debug;
 
-fuzz_target!(|data: &[u8]| {
-    let _ = env_logger::try_init();
-
-    let mut u = Unstructured::new(data);
-    let string = String::arbitrary(&mut u).unwrap();
+fuzz_target!(|data: &str| {
     let mut input = Schema::new();
     let def = input.schema_definition.make_mut();
-    def.description = Some(string.into());
+    def.description = Some(data.into());
     // We can refer to a type that doesn't exist as we won't run validation
     def.query = Some(name!("Dangling").into());
     let doc_generated = input.to_string();


Test patch:
diff --git a/crates/apollo-compiler/test_data/diagnostics/0102_invalid_string_values.txt b/crates/apollo-compiler/test_data/diagnostics/0102_invalid_string_values.txt
index 1a65e05..aee14a3 100644
--- a/crates/apollo-compiler/test_data/diagnostics/0102_invalid_string_values.txt
+++ b/crates/apollo-compiler/test_data/diagnostics/0102_invalid_string_values.txt
@@ -372,16 +372,16 @@ Error: expected value of type Int!, found null
      │                        ──┬─  
      │                          ╰─── provided value is null
 ─────╯
-Error: the required argument `ComplexInput.requiredField` is not provided
+Error: the required field `ComplexInput.requiredField` is not provided
      ╭─[0102_invalid_string_values.graphql:254:33]
      │
   32 │   requiredField: Boolean!
      │   ───────────┬───────────  
-     │              ╰───────────── argument defined here
+     │              ╰───────────── field defined here
      │ 
  254 │     complexArgField(complexArg: { intField: 4 })
      │                                 ───────┬───────  
-     │                                        ╰───────── missing value for argument `requiredField`
+     │                                        ╰───────── missing value for field `requiredField`
 ─────╯
 Error: expected value of type String, found an integer
      ╭─[0102_invalid_string_values.graphql:261:32]
@@ -462,16 +462,16 @@ Error: expected value of type String!, found null
      │                   │   
      │                   ╰─── provided value is null
 ─────╯
-Error: the required argument `ComplexInput.requiredField` is not provided
+Error: the required field `ComplexInput.requiredField` is not provided
      ╭─[0102_invalid_string_values.graphql:296:22]
      │
   32 │   requiredField: Boolean!
      │   ───────────┬───────────  
-     │              ╰───────────── argument defined here
+     │              ╰───────────── field defined here
      │ 
  296 │   $c: ComplexInput = { requiredField: null, intField: null }
      │                      ───────────────────┬───────────────────  
-     │                                         ╰───────────────────── missing value for argument `requiredField`
+     │                                         ╰───────────────────── missing value for field `requiredField`
 ─────╯
 Error: expected value of type Boolean!, found null
      ╭─[0102_invalid_string_values.graphql:296:24]
@@ -533,16 +533,16 @@ Error: expected value of type Int, found a string
      │                                            ───────┬───────  
      │                                                   ╰───────── provided value is a string
 ─────╯
-Error: the required argument `ComplexInput.requiredField` is not provided
+Error: the required field `ComplexInput.requiredField` is not provided
      ╭─[0102_invalid_string_values.graphql:326:22]
      │
   32 │   requiredField: Boolean!
      │   ───────────┬───────────  
-     │              ╰───────────── argument defined here
+     │              ╰───────────── field defined here
      │ 
  326 │   $a: ComplexInput = {intField: 3}
      │                      ──────┬──────  
-     │                            ╰──────── missing value for argument `requiredField`
+     │                            ╰──────── missing value for field `requiredField`
 ─────╯
 Error: expected value of type String, found an integer
      ╭─[0102_invalid_string_values.graphql:335:26]
diff --git a/crates/apollo-compiler/tests/validation/types.rs b/crates/apollo-compiler/tests/validation/types.rs
index 7bd53ee..0ec56e6 100644
--- a/crates/apollo-compiler/tests/validation/types.rs
+++ b/crates/apollo-compiler/tests/validation/types.rs
@@ -1556,18 +1556,18 @@ mod invalid_input_object_values {
         }
       ",
             expect![[r#"
-                Error: the required argument `ComplexInput.requiredField` is not provided
+                Error: the required field `ComplexInput.requiredField` is not provided
                     ╭─[query.graphql:3:33]
                     │
                   3 │     complexArgField(complexArg: { intField: 4 })
                     │                                 ───────┬───────  
-                    │                                        ╰───────── missing value for argument `requiredField`
+                    │                                        ╰───────── missing value for field `requiredField`
                     │
                     ├─[schema.graphql:60:3]
                     │
                  60 │   requiredField: Boolean!
                     │   ───────────┬───────────  
-                    │              ╰───────────── argument defined here
+                    │              ╰───────────── field defined here
                 ────╯
             "#]],
         );
@@ -1846,18 +1846,18 @@ mod variable_default_values {
                    │                   │   
                    │                   ╰─── provided value is null
                 ───╯
-                Error: the required argument `ComplexInput.requiredField` is not provided
+                Error: the required field `ComplexInput.requiredField` is not provided
                     ╭─[query.graphql:4:22]
                     │
                   4 │   $c: ComplexInput = { requiredField: null, intField: null }
                     │                      ───────────────────┬───────────────────  
-                    │                                         ╰───────────────────── missing value for argument `requiredField`
+                    │                                         ╰───────────────────── missing value for field `requiredField`
                     │
                     ├─[schema.graphql:60:3]
                     │
                  60 │   requiredField: Boolean!
                     │   ───────────┬───────────  
-                    │              ╰───────────── argument defined here
+                    │              ╰───────────── field defined here
                 ────╯
                 Error: expected value of type Boolean!, found null
                     ╭─[query.graphql:4:24]
@@ -1977,18 +1977,18 @@ mod variable_default_values {
         }
       ",
             expect![[r#"
-                Error: the required argument `ComplexInput.requiredField` is not provided
+                Error: the required field `ComplexInput.requiredField` is not provided
                     ╭─[query.graphql:1:47]
                     │
                   1 │ query MissingRequiredField($a: ComplexInput = {intField: 3}) {
                     │                                               ──────┬──────  
-                    │                                                     ╰──────── missing value for argument `requiredField`
+                    │                                                     ╰──────── missing value for field `requiredField`
                     │
                     ├─[schema.graphql:60:3]
                     │
                  60 │   requiredField: Boolean!
                     │   ───────────┬───────────  
-                    │              ╰───────────── argument defined here
+                    │              ╰───────────── field defined here
                 ────╯
             "#]],
         );