///|
/// The GraphQL validator (spec §5, the subset that matters before execution):
/// every selected field exists on its parent type, leaf fields carry no
/// sub-selection while composite fields require one, field arguments are declared
/// (and required arguments supplied), fragment spreads name existing fragments,
/// fragment type conditions name existing types, and every referenced variable
/// is defined with a type that exists. Errors are collected, not raised, so
/// `execute` can report them all in one `{ errors }` response.
///|
/// Validator state: the schema, the document's fragments, and the error list.
priv struct Validator {
schema : Schema
fragments : Map[String, FragmentDefinition]
errors : Array[GqlError]
}
///|
/// Look up an output object/interface/input or synthetic introspection type.
fn Validator::lookup_type(self : Validator, name : String) -> ObjectType? {
match self.schema.type_by_name(name) {
Some(t) => Some(t)
None => introspection_type_def(name)
}
}
///|
/// Whether `name` is a leaf type (scalar or enum) with no selectable subfields.
fn Validator::is_leaf(self : Validator, name : String) -> Bool {
is_builtin_scalar(name) ||
self.schema.enum_by_name(name) is Some(_) ||
self.schema.scalar_by_name(name) is Some(_) ||
name == "__TypeKind" ||
name == "__DirectiveLocation"
}
///|
/// The base named type of a query-grammar `TypeRef` (unwrapping `!` and `[]`).
fn typeref_base(t : TypeRef) -> String {
match t {
NamedType(n) => n
ListType(inner) => typeref_base(inner)
NonNullType(inner) => typeref_base(inner)
}
}
///|
/// Whether `needle` occurs in `hay`.
fn str_in(hay : Array[String], needle : String) -> Bool {
for s in hay {
if s == needle {
return true
}
}
false
}
///|
/// Record a message-only validation error.
fn Validator::err(self : Validator, message : String) -> Unit {
self.errors.push(GqlError::msg(message))
}
///|
/// Check every `$variable` referenced by a value is defined (when `defined` is
/// `Some`; fragment bodies pass `None` since their variables come from the
/// operation that spreads them).
fn Validator::check_value_vars(
self : Validator,
v : Value,
defined : Array[String]?,
) -> Unit {
match defined {
None => return
Some(names) =>
match v {
Variable(n) =>
if not(str_in(names, n)) {
self.err("Variable '$" + n + "' is not defined")
}
ListValue(items) =>
for it in items {
self.check_value_vars(it, defined)
}
ObjectValue(fields) =>
for kv in fields {
self.check_value_vars(kv.1, defined)
}
_ => ()
}
}
}
///|
/// The valid locations of the directive `name`: a built-in executable directive
/// (`@skip`/`@include`) or a user-registered one, else `None` for an undefined
/// directive.
fn Validator::directive_locations(
self : Validator,
name : String,
) -> Array[String]? {
match builtin_exec_directive(name) {
Some(locs) => Some(locs)
None =>
match self.schema.directive_def_by_name(name) {
Some(d) => Some(d.locations)
None => None
}
}
}
///|
/// Whether the directive `name` is repeatable (may appear more than once at one
/// location). Built-ins are not repeatable.
fn Validator::directive_repeatable(self : Validator, name : String) -> Bool {
match self.schema.directive_def_by_name(name) {
Some(d) => d.is_repeatable
None => false
}
}
///|
/// Report duplicate argument names in an argument list (spec §5.4.2, Argument
/// Uniqueness), for a field or a directive named `owner`.
fn Validator::check_arg_uniqueness(
self : Validator,
args : Array[Argument],
owner : String,
) -> Unit {
let seen : Array[String] = []
for a in args {
if str_in(seen, a.name) {
self.err("Duplicate argument '" + a.name + "' on '" + owner + "'")
}
seen.push(a.name)
}
}
///|
/// Validate a directive list at type-system/executable `location`: every directive
/// is defined (§5.7.1), used at an allowed location (§5.7.2), and — unless declared
/// repeatable — appears at most once (§5.7.3); its arguments are unique (§5.4.2)
/// and their variable references are defined.
fn Validator::check_directives(
self : Validator,
directives : Array[Directive],
defined : Array[String]?,
location : String,
) -> Unit {
let seen : Array[String] = []
for d in directives {
self.check_arg_uniqueness(d.arguments, "@" + d.name)
for a in d.arguments {
self.check_value_vars(a.value, defined)
}
match self.directive_locations(d.name) {
None => self.err("Unknown directive '@" + d.name + "'")
Some(locs) =>
if not(str_in(locs, location)) {
self.err("Directive '@" + d.name + "' may not be used on " + location)
}
}
if str_in(seen, d.name) && not(self.directive_repeatable(d.name)) {
self.err(
"Directive '@" +
d.name +
"' can only be used once at this location (it is not repeatable)",
)
}
seen.push(d.name)
}
}
///|
/// Validate the arguments of a field against its declared argument list: every
/// supplied argument must be declared, every non-null argument must be supplied,
/// and every argument value's variables must be defined.
fn Validator::check_field_args(
self : Validator,
field : QueryField,
fdef : Field,
defined : Array[String]?,
) -> Unit {
self.check_arg_uniqueness(field.arguments, field.name)
let declared : Array[String] = []
for a in fdef.args {
declared.push(a.0)
}
for a in field.arguments {
if not(str_in(declared, a.name)) {
self.err(
"Unknown argument '" + a.name + "' on field '" + field.name + "'",
)
}
self.check_value_vars(a.value, defined)
}
for da in fdef.args {
if da.1 is NonNull(_) {
let mut supplied = false
for a in field.arguments {
if a.name == da.0 {
supplied = true
}
}
if not(supplied) {
self.err(
"Field '" +
field.name +
"' is missing required argument '" +
da.0 +
"'",
)
}
}
}
}
///|
/// Validate a selection set against the object type named `type_name`.
fn Validator::check_selection_set(
self : Validator,
type_name : String,
selections : Array[Selection],
defined : Array[String]?,
) -> Unit {
match self.schema.union_by_name(type_name) {
Some(u) => {
self.check_union_selection_set(type_name, u, selections, defined)
return
}
None => ()
}
let obj = match self.lookup_type(type_name) {
Some(o) => o
None => {
self.err("Cannot select on unknown type '" + type_name + "'")
return
}
}
for sel in selections {
match sel {
FieldSel(f) => {
self.check_directives(f.directives, defined, "FIELD")
if f.name == "__typename" {
if f.selection_set.length() > 0 {
self.err("Field '__typename' must not have a selection set")
}
continue
}
if type_name == self.schema.query &&
(f.name == "__schema" || f.name == "__type") {
let meta = if f.name == "__schema" { "__Schema" } else { "__Type" }
for a in f.arguments {
self.check_value_vars(a.value, defined)
}
if f.selection_set.length() == 0 {
self.err(
"Field '" + f.name + "' must have a selection of subfields",
)
} else {
self.check_selection_set(meta, f.selection_set, defined)
}
continue
}
match obj.field_by_name(f.name) {
None =>
self.err(
"Cannot query field '" + f.name + "' on type '" + type_name + "'",
)
Some(fdef) => {
self.check_field_args(f, fdef, defined)
let base = fdef.typ.named_base()
if self.is_leaf(base) {
if f.selection_set.length() > 0 {
self.err(
"Field '" +
f.name +
"' of leaf type '" +
base +
"' must not have a selection set",
)
}
} else if self.schema.union_by_name(base) is Some(u) {
if f.selection_set.length() == 0 {
self.err(
"Field '" +
f.name +
"' of union type '" +
base +
"' must have a selection of subfields",
)
} else {
self.check_union_selection_set(
base,
u,
f.selection_set,
defined,
)
}
} else if self.lookup_type(base) is Some(_) {
if f.selection_set.length() == 0 {
self.err(
"Field '" +
f.name +
"' of type '" +
base +
"' must have a selection of subfields",
)
} else {
self.check_selection_set(base, f.selection_set, defined)
}
} else {
self.err("Field '" + f.name + "' has unknown type '" + base + "'")
}
}
}
}
FragmentSpreadSel(name, dirs) => {
self.check_directives(dirs, defined, "FRAGMENT_SPREAD")
if not(self.fragments.contains(name)) {
self.err("Unknown fragment '" + name + "'")
}
}
InlineFragmentSel(cond, dirs, sels) => {
self.check_directives(dirs, defined, "INLINE_FRAGMENT")
let cond_type = match cond {
Some(c) => c
None => type_name
}
if self.type_exists(cond_type) {
self.check_selection_set(cond_type, sels, defined)
} else {
self.err("Inline fragment on unknown type '" + cond_type + "'")
}
}
}
}
}
///|
/// Validate a selection set at a union position: only `__typename` and fragments
/// on member types (or the union itself) may select into a union. A direct field
/// selection other than `__typename` is an error.
fn Validator::check_union_selection_set(
self : Validator,
union_name : String,
u : UnionType,
selections : Array[Selection],
defined : Array[String]?,
) -> Unit {
for sel in selections {
match sel {
FieldSel(f) => {
self.check_directives(f.directives, defined, "FIELD")
if f.name == "__typename" {
if f.selection_set.length() > 0 {
self.err("Field '__typename' must not have a selection set")
}
} else {
self.err(
"Cannot query field '" +
f.name +
"' directly on union type '" +
union_name +
"'; select it inside an inline fragment on a member type",
)
}
}
FragmentSpreadSel(name, dirs) => {
self.check_directives(dirs, defined, "FRAGMENT_SPREAD")
if not(self.fragments.contains(name)) {
self.err("Unknown fragment '" + name + "'")
}
}
InlineFragmentSel(cond, dirs, sels) => {
self.check_directives(dirs, defined, "INLINE_FRAGMENT")
let cond_type = match cond {
Some(c) => c
None => union_name
}
if cond_type == union_name {
self.check_union_selection_set(union_name, u, sels, defined)
} else if str_in(u.members, cond_type) {
self.check_selection_set(cond_type, sels, defined)
} else {
self.err(
"Inline fragment type '" +
cond_type +
"' is not a member of union '" +
union_name +
"'",
)
}
}
}
}
}
///|
/// Whether a name denotes a selectable type: a composite/introspection type or a
/// union. Used to accept union type conditions on fragments.
fn Validator::type_exists(self : Validator, name : String) -> Bool {
self.lookup_type(name) is Some(_) ||
self.schema.union_by_name(name) is Some(_)
}
///|
/// Validate a single operation: its root type must be configured, its variable
/// definitions must name existing input types, and its selection set must be
/// valid.
fn Validator::check_operation(
self : Validator,
op : OperationDefinition,
) -> Unit {
let root = match op.operation {
Query => Some(self.schema.query)
Mutation => self.schema.mutation
Subscription => self.schema.subscription
}
let root_type = match root {
Some(r) => r
None => {
self.err("Schema has no root type for this operation")
return
}
}
let op_location = match op.operation {
Query => "QUERY"
Mutation => "MUTATION"
Subscription => "SUBSCRIPTION"
}
self.check_directives(op.directives, None, op_location)
let defined : Array[String] = []
for vd in op.variable_definitions {
// §5.8.1 Variable Uniqueness: a variable name is defined at most once.
if str_in(defined, vd.variable) {
self.err("Duplicate variable '$" + vd.variable + "'")
}
defined.push(vd.variable)
self.check_directives(vd.directives, Some(defined), "VARIABLE_DEFINITION")
let base = typeref_base(vd.typ)
let ok = is_builtin_scalar(base) ||
self.schema.enum_by_name(base) is Some(_) ||
self.schema.scalar_by_name(base) is Some(_) ||
(match self.schema.type_by_name(base) {
Some(t) => t.kind is (Input | Object | Interface)
None => false
})
if not(ok) {
self.err(
"Variable '$" + vd.variable + "' has unknown type '" + base + "'",
)
}
}
if op.operation is Subscription {
self.check_single_root_field(op)
}
self.check_selection_set(root_type, op.selection_set, Some(defined))
// §5.8.4 All Variables Used: every defined variable is referenced somewhere in
// the operation (directly or through a spread fragment).
let used : Array[String] = []
self.collect_used_variables(op.selection_set, Map([]), used)
for vd in op.variable_definitions {
if not(str_in(used, vd.variable)) {
self.err("Variable '$" + vd.variable + "' is never used")
}
}
}
///|
/// SingleRootField (spec §5.2.3.1): a subscription must select exactly one root
/// field after fragment expansion, and it must not be an introspection field.
fn Validator::check_single_root_field(
self : Validator,
op : OperationDefinition,
) -> Unit {
let fields : Array[QueryField] = []
self.collect_root_fields(op.selection_set, Map([]), fields)
let keys : Array[String] = []
for f in fields {
let key = match f.alias_ {
Some(a) => a
None => f.name
}
if not(str_in(keys, key)) {
keys.push(key)
}
}
if keys.length() != 1 {
self.err("Subscription operation must select exactly one root field")
} else if is_meta_field(fields[0].name) {
self.err(
"Subscription root field '" +
fields[0].name +
"' must not be an introspection field",
)
}
}
///|
/// Gather the root-level fields of a selection set, expanding fragment spreads
/// and inline fragments (used to count a subscription's root fields).
fn Validator::collect_root_fields(
self : Validator,
selections : Array[Selection],
visited : Map[String, Bool],
out : Array[QueryField],
) -> Unit {
for sel in selections {
match sel {
FieldSel(f) => out.push(f)
FragmentSpreadSel(name, _) =>
if not(visited.get(name) is Some(true)) {
visited[name] = true
match self.fragments.get(name) {
Some(frag) =>
self.collect_root_fields(frag.selection_set, visited, out)
None => ()
}
}
InlineFragmentSel(_, _, sels) =>
self.collect_root_fields(sels, visited, out)
}
}
}
///|
/// Validate a fragment definition: its type condition must exist, and its body
/// must be valid against that type. Variable references inside are checked at the
/// operations that spread it.
fn Validator::check_fragment(
self : Validator,
frag : FragmentDefinition,
) -> Unit {
self.check_directives(frag.directives, None, "FRAGMENT_DEFINITION")
if self.type_exists(frag.type_condition) {
self.check_selection_set(frag.type_condition, frag.selection_set, None)
} else {
self.err(
"Fragment '" +
frag.name +
"' has unknown type condition '" +
frag.type_condition +
"'",
)
}
}
///|
/// Validate a whole document against `schema`, returning all errors found (an
/// empty array means the document is valid and ready to execute).
pub fn validate(schema : Schema, doc : Document) -> Array[GqlError] {
let v = { schema, fragments: collect_fragments(doc), errors: [] }
v.check_operation_names(doc)
v.check_fragment_uniqueness(doc)
v.check_fragment_cycles(doc)
let mut has_op = false
for def in doc.definitions {
match def {
OperationDef(op) => {
has_op = true
v.check_operation(op)
}
FragmentDef(frag) => v.check_fragment(frag)
}
}
if not(has_op) {
v.err("Document contains no operations")
}
v.check_fragments_used(doc)
v.errors
}