// The type system's own checks: §3 of the specification, asked of a `Schema`
// rather than of a document. `Schema::check` answers every problem it finds, so
// a schema assembled in code — or read back from SDL — can be held to the same
// rules a document is.
///|
/// The state of one pass over a schema: the schema itself and the errors found so
/// far.
priv struct Checker {
schema : Schema
errors : Array[@lang.GqlError]
}
///|
/// Record one problem.
fn Checker::fail(self : Checker, message : String) -> Unit {
self.errors.push(@lang.GqlError::msg(message))
}
///|
/// Whether a value of `ty` may be sent in: a scalar, an enum, or an input object,
/// wrapped in any number of list and non-null markers (§3.6.1.4).
fn Checker::is_input_type(self : Checker, ty : GqlType) -> Bool {
match ty {
NonNull(inner) | ListOf(inner) => self.is_input_type(inner)
Scalar(name) =>
is_builtin_scalar(name) || self.schema.scalar_by_name(name) is Some(_)
Named(name) =>
match self.schema.type_by_name(name) {
Some(t) => t.kind is Input
None =>
self.schema.enum_by_name(name) is Some(_) ||
self.schema.scalar_by_name(name) is Some(_) ||
is_builtin_scalar(name)
}
}
}
///|
/// Whether a value of `ty` may be handed out: a scalar, an enum, an object, an
/// interface, or a union, wrapped in any number of list and non-null markers.
fn Checker::is_output_type(self : Checker, ty : GqlType) -> Bool {
match ty {
NonNull(inner) | ListOf(inner) => self.is_output_type(inner)
Scalar(name) =>
is_builtin_scalar(name) || self.schema.scalar_by_name(name) is Some(_)
Named(name) =>
match self.schema.type_by_name(name) {
Some(t) => !(t.kind is Input)
None =>
self.schema.enum_by_name(name) is Some(_) ||
self.schema.union_by_name(name) is Some(_) ||
self.schema.scalar_by_name(name) is Some(_) ||
is_builtin_scalar(name)
}
}
}
///|
/// Whether `sub` may stand where `sup` is declared (§3.6.3.1): equal types, or a
/// non-null / list narrowing of one, or an object that is a member of the union
/// or an implementation of the interface.
fn Checker::is_sub_type(self : Checker, sub : GqlType, sup : GqlType) -> Bool {
if sub == sup {
return true
}
match (sub, sup) {
(NonNull(a), NonNull(b)) => self.is_sub_type(a, b)
(_, NonNull(_)) => false
// A non-null type may stand where a nullable one is declared, the other way
// round is a widening.
(NonNull(a), _) => self.is_sub_type(a, sup)
(ListOf(a), ListOf(b)) => self.is_sub_type(a, b)
(_, ListOf(_)) | (ListOf(_), _) => false
(Named(a), Named(b)) =>
if a == b {
true
} else {
match self.schema.union_by_name(b) {
Some(u) => u.members.contains(a)
None =>
match self.schema.type_by_name(b) {
Some(t) if t.kind is Interface =>
match self.schema.type_by_name(a) {
Some(concrete) => concrete.interfaces.contains(b)
None => false
}
_ => false
}
}
}
_ => false
}
}
///|
/// A type reference as an error reports it.
fn rule_text(ty : GqlType) -> String {
type_sdl(ty)
}
///|
/// The names of every type the schema declares, in declaration order, with the
/// built-in scalars and the introspection types left out — those are the ones a
/// name may collide with.
fn declared_names(schema : Schema) -> Array[String] {
let out : Array[String] = []
for t in schema.types {
out.push(t.name)
}
for e in schema.enums {
out.push(e.name)
}
for u in schema.unions {
out.push(u.name)
}
for s in schema.scalars {
out.push(s.name)
}
out
}
///|
/// §3.9: the root operation types are named, exist, and are object types.
fn Checker::check_roots(self : Checker) -> Unit {
let root = self.schema.type_by_name(self.schema.query)
match root {
None => self.fail("Query root type must be provided.")
Some(t) =>
if !(t.kind is Object) {
self.fail(
"Query root type must be Object type, it cannot be \{t.name}.",
)
}
}
for r in [self.schema.mutation, self.schema.subscription] {
match r {
None => ()
Some(name) =>
match self.schema.type_by_name(name) {
None => self.fail("Root type '\{name}' is not defined in the schema.")
Some(t) =>
if !(t.kind is Object) {
self.fail(
"Root type '\{name}' must be Object type, it cannot be \{kind_keyword(t.kind)} '\{name}'.",
)
}
}
}
}
}
///|
/// §3.6.1.1, §3.6.1.2 and §3.4: every type name is unique and none begins with
/// `__`, which introspection owns.
fn Checker::check_type_names(self : Checker) -> Unit {
let seen : Map[String, Unit] = Map([])
for name in declared_names(self.schema) {
if seen.get(name) is Some(_) {
self.fail("There can be only one type named '\{name}'.")
} else {
seen[name] = ()
}
if name.has_prefix("__") {
self.fail(
"Name '\{name}' must not begin with '__', which is reserved by GraphQL introspection.",
)
}
}
}
///|
/// §3.6.1.4 and §3.6.2: each field's type is of the right side of the schema, its
/// arguments are input types, and its own name and its arguments' names are each
/// written once.
fn Checker::check_fields(self : Checker, t : ObjectType) -> Unit {
if t.fields.is_empty() {
self.fail(
if t.kind is Input {
"Input Object type \{t.name} must define one or more fields."
} else if t.kind is Interface {
"Interface type \{t.name} must define one or more fields."
} else {
"Type \{t.name} must define one or more fields."
},
)
}
let seen : Map[String, Unit] = Map([])
for f in t.fields {
if seen.get(f.name) is Some(_) {
self.fail("Field '\{t.name}.\{f.name}' can only be defined once.")
} else {
seen[f.name] = ()
}
if t.kind is Input {
if !f.args.is_empty() {
self.fail("Input field '\{t.name}.\{f.name}' cannot have arguments.")
}
if !self.is_input_type(f.typ) {
self.fail(
"The type of \{t.name}.\{f.name} must be Input Type but got: \{rule_text(f.typ)}.",
)
}
} else if !self.is_output_type(f.typ) {
self.fail(
"The type of \{t.name}.\{f.name} must be Output Type but got: \{rule_text(f.typ)}.",
)
}
let args : Map[String, Unit] = Map([])
for a in f.args {
let (name, ty) = a
if args.get(name) is Some(_) {
self.fail(
"Argument '\{t.name}.\{f.name}(\{name}:)' can only be defined once.",
)
} else {
args[name] = ()
}
if !self.is_input_type(ty) {
self.fail(
"The type of \{t.name}.\{f.name}(\{name}:) must be Input Type but got: \{rule_text(ty)}.",
)
}
}
}
}
///|
/// §3.6.3.1: an object or interface that implements an interface carries every
/// field of it, with a type that is a sub-type and arguments that match exactly,
/// and it declares the interfaces its interfaces implement.
fn Checker::check_implementation(
self : Checker,
t : ObjectType,
iface_name : String,
) -> Unit {
let iface = match self.schema.type_by_name(iface_name) {
Some(i) =>
if i.kind is Interface {
i
} else {
self.fail(
"Type \{t.name} must only implement Interface types, it cannot implement \{iface_name}.",
)
return
}
None => {
self.fail(
"Type \{t.name} must only implement Interface types, it cannot implement \{iface_name}.",
)
return
}
}
if t.name == iface_name {
self.fail(
"Type \{t.name} cannot implement itself because it would create a circular reference.",
)
return
}
for i in iface.interfaces {
if !t.interfaces.contains(i) {
self.fail(
"Type \{t.name} must implement \{i} because it is implemented by \{iface_name}.",
)
}
}
for field in iface.fields {
let own = match t.field_by_name(field.name) {
Some(f) => f
None => {
self.fail(
"Interface field \{iface_name}.\{field.name} expected but \{t.name} does not provide it.",
)
continue
}
}
if !self.is_sub_type(own.typ, field.typ) {
self.fail(
"Interface field \{iface_name}.\{field.name} expects type \{rule_text(field.typ)} but \{t.name}.\{own.name} is type \{rule_text(own.typ)}.",
)
}
for arg in field.args {
let (name, ty) = arg
match own.args.iter().find_first(a => a.0 == name) {
None =>
self.fail(
"Interface field argument \{iface_name}.\{field.name}(\{name}:) expected but \{t.name}.\{own.name} does not provide it.",
)
Some((_, own_ty)) =>
if !(own_ty == ty) {
self.fail(
"Interface field argument \{iface_name}.\{field.name}(\{name}:) expects type \{rule_text(ty)} but \{t.name}.\{own.name}(\{name}:) is type \{rule_text(own_ty)}.",
)
}
}
}
for arg in own.args {
let (name, ty) = arg
if ty is NonNull(_) && !field.args.iter().any(a => a.0 == name) {
self.fail(
"Object field \{t.name}.\{own.name} includes required argument \{name} that is missing from the Interface field \{iface_name}.\{field.name}.",
)
}
}
}
}
///|
/// §3.6.3: every declared interface is checked against the object or interface
/// that implements it.
fn Checker::check_interfaces(self : Checker) -> Unit {
for t in self.schema.types {
if t.kind is Input {
continue
}
for iface_name in t.interfaces {
self.check_implementation(t, iface_name)
}
}
}
///|
/// §3.6.4.2: no input object reaches itself through a chain of non-null fields —
/// there would be no value that satisfies it. `path` is the chain walked so far,
/// and `seen` the objects already on it.
fn Checker::check_input_cycle(
self : Checker,
t : ObjectType,
path : Array[String],
seen : Array[String],
) -> Unit {
if seen.contains(t.name) {
self.fail(
"Cannot reference Input Object '\{t.name}' within itself through a series of non-null fields: '\{path.join(".")}'.",
)
return
}
let seen = seen.copy()
seen.push(t.name)
for f in t.fields {
if f.typ is NonNull(inner) {
let base = inner.named_base()
match self.schema.type_by_name(base) {
Some(next) if next.kind is Input => {
let here = path.copy()
here.push(f.name)
self.check_input_cycle(next, here, seen)
}
_ => ()
}
}
}
}
///|
/// §3.6.4: every input object is walked for a required-field cycle.
fn Checker::check_input_cycles(self : Checker) -> Unit {
for t in self.schema.types {
if t.kind is Input {
self.check_input_cycle(t, [], [])
}
}
}
///|
/// §3.6.5: a union's members are object types, each named once.
fn Checker::check_unions(self : Checker) -> Unit {
for u in self.schema.unions {
if u.members.is_empty() {
self.fail("Union type \{u.name} must include one or more types.")
}
let seen : Map[String, Unit] = Map([])
for m in u.members {
match self.schema.type_by_name(m) {
Some(t) =>
if !(t.kind is Object) {
self.fail(
"Union type \{u.name} can only include Object types, it cannot include \{m}.",
)
}
None =>
self.fail(
"Union type \{u.name} can only include Object types, it cannot include \{m}.",
)
}
if seen.get(m) is Some(_) {
self.fail("Union type \{u.name} can only include type \{m} once.")
} else {
seen[m] = ()
}
}
}
}
///|
/// §3.6.6: an enum's values are named once each, and none is `true`, `false` or
/// `null`, which are not names a document could write.
fn Checker::check_enums(self : Checker) -> Unit {
for e in self.schema.enums {
if e.values.is_empty() {
self.fail("Enum type \{e.name} must define one or more values.")
}
let seen : Map[String, Unit] = Map([])
for v in e.values {
if v == "true" || v == "false" || v == "null" {
self.fail("Enum type \{e.name} cannot include value: \{v}.")
}
if seen.get(v) is Some(_) {
self.fail("Enum type \{e.name} can only include value '\{v}' once.")
} else {
seen[v] = ()
}
}
}
}
///|
/// The places a directive may be used (§3.13).
fn is_directive_location(name : String) -> Bool {
match name {
"QUERY"
| "MUTATION"
| "SUBSCRIPTION"
| "FIELD"
| "FRAGMENT_DEFINITION"
| "FRAGMENT_SPREAD"
| "INLINE_FRAGMENT"
| "VARIABLE_DEFINITION"
| "SCHEMA"
| "SCALAR"
| "OBJECT"
| "FIELD_DEFINITION"
| "ARGUMENT_DEFINITION"
| "INTERFACE"
| "UNION"
| "ENUM"
| "ENUM_VALUE"
| "INPUT_OBJECT"
| "INPUT_FIELD_DEFINITION" => true
_ => false
}
}
///|
/// §3.6.8: a directive is defined once, its arguments are input types, and every
/// location it names is one of the specification's.
fn Checker::check_directives(self : Checker) -> Unit {
let seen : Map[String, Unit] = Map([])
for d in self.schema.directive_defs {
if seen.get(d.name) is Some(_) {
self.fail("Directive @\{d.name} can only be defined once.")
} else {
seen[d.name] = ()
}
if d.name.has_prefix("__") {
self.fail(
"Name '\{d.name}' must not begin with '__', which is reserved by GraphQL introspection.",
)
}
if d.locations.is_empty() {
self.fail("Directive @\{d.name} must include 1 or more locations.")
}
for loc in d.locations {
if !is_directive_location(loc) {
self.fail("Directive @\{d.name} may not be used on \{loc}.")
}
}
for a in d.args {
if !self.is_input_type(a.1) {
self.fail(
"The type of @\{d.name}(\{a.0}:) must be Input Type but got: \{rule_text(a.1)}.",
)
}
}
}
}
///|
/// Every problem the type system has with this schema, in the order the
/// specification states the rules: names, then each shape of type, then the
/// interfaces over them, then the roots that tie them together. An empty array
/// means the schema is one the specification allows.
///
/// The rules are §3's, not §5's: this asks whether the schema itself holds
/// together, where `validate` asks whether a document is right for it.
pub fn Schema::check(self : Schema) -> Array[@lang.GqlError] {
let c : Checker = { schema: self, errors: [], }
c.check_type_names()
for t in self.types {
c.check_fields(t)
}
c.check_enums()
c.check_unions()
c.check_directives()
c.check_interfaces()
c.check_input_cycles()
c.check_roots()
c.errors
}