///|
/// GraphQL introspection (spec §4): answer `__schema`, `__type` and `__typename`
/// from the schema. The introspection type system (`__Schema`, `__Type`,
/// `__Field`, `__InputValue`, `__EnumValue`, `__Directive`) is defined here as
/// synthetic `ObjectType`s so the executor's normal field-completion machinery
/// can walk an introspection selection set, while the data is materialised as
/// JSON that the default resolver reads through.
///|
/// The introspection `__TypeKind` enum name for a type name in this schema.
fn Exec::type_kind(self : Exec, name : String) -> String {
if @schema.is_builtin_scalar(name) {
return "SCALAR"
}
match self.schema.type_by_name(name) {
Some(t) =>
match t.kind {
Object => "OBJECT"
Input => "INPUT_OBJECT"
Interface => "INTERFACE"
}
None =>
if self.schema.enum_by_name(name) is Some(_) {
"ENUM"
} else if self.schema.union_by_name(name) is Some(_) {
"UNION"
} else {
"SCALAR"
}
}
}
///|
/// A shallow `__Type` reference: `{ kind, name, ofType: null }` with the detail
/// fields null. Used wherever introspection points at a type by reference
/// (`ofType`, `queryType`, a field's `type`, interface / possibleType entries).
fn Exec::named_type_ref(self : Exec, name : String) -> Json {
jobj([
("kind", Json(self.type_kind(name))),
("name", Json(name)),
("description", Json::null()),
("fields", Json::null()),
("interfaces", Json::null()),
("possibleTypes", Json::null()),
("enumValues", Json::null()),
("inputFields", Json::null()),
("ofType", Json::null()),
])
}
///|
/// A `__Type` wrapper reference (`NON_NULL` / `LIST`) around `of_type`.
fn wrapper_ref(kind : String, of_type : Json) -> Json {
jobj([
("kind", Json(kind)),
("name", Json::null()),
("description", Json::null()),
("fields", Json::null()),
("interfaces", Json::null()),
("possibleTypes", Json::null()),
("enumValues", Json::null()),
("inputFields", Json::null()),
("ofType", of_type),
])
}
///|
/// The introspection `__Type` reference for a possibly-wrapped `GqlType`,
/// nesting `ofType` for each `!` / `[]` wrapper.
fn Exec::type_ref_json(self : Exec, t : @schema.GqlType) -> Json {
match t {
NonNull(inner) => wrapper_ref("NON_NULL", self.type_ref_json(inner))
ListOf(inner) => wrapper_ref("LIST", self.type_ref_json(inner))
Scalar(n) => self.named_type_ref(n)
Named(n) => self.named_type_ref(n)
}
}
///|
/// Materialise a `__InputValue` for an argument or input field. `default` is the
/// GraphQL literal the spec asks for — a string, not the JSON value — because that
/// is what codegen and GraphiQL render back into a query.
fn Exec::input_value_json(
self : Exec,
name : String,
typ : @schema.GqlType,
default? : Json,
description~ : String = "",
) -> Json {
jobj([
("name", Json(name)),
(
"description",
if description == "" {
Json::null()
} else {
Json(description)
},
),
("type", self.type_ref_json(typ)),
(
"defaultValue",
match default {
Some(d) => Json(@schema.json_to_gql_literal(d))
None => Json::null()
},
),
])
}
///|
/// Materialise a `__Field` for an output field.
fn Exec::field_json(self : Exec, f : @schema.Field) -> Json {
let args : Array[Json] = []
for a in f.args {
let default = f.arg_defaults.get(a.0)
args.push(self.input_value_json(a.0, a.1, default?))
}
jobj([
("name", Json(f.name)),
("description", reason_json(f.description)),
("args", Json(args)),
("type", self.type_ref_json(f.typ)),
("isDeprecated", Json(f.deprecation_reason is Some(_))),
("deprecationReason", reason_json(f.deprecation_reason)),
])
}
///|
/// A `deprecationReason` value: the reason string, or JSON null when not set.
fn reason_json(reason : String?) -> Json {
match reason {
Some(r) => Json(r)
None => Json::null()
}
}
///|
/// Filter a materialised `__Field` / `__EnumValue` list for a `fields` /
/// `enumValues` selection: unless `include` is set, drop entries carrying
/// `isDeprecated: true` (graphql-js `includeDeprecated` behaviour).
fn filter_deprecated(items : Json, included : Bool) -> Json {
if included {
return items
}
match items {
Array(arr) => {
let out : Array[Json] = []
for it in arr {
let deprecated = match it {
Object(m) => m.get("isDeprecated") is Some(True)
_ => false
}
if !deprecated {
out.push(it)
}
}
Json(out)
}
_ => items
}
}
///|
/// Materialise the full `__Type` for a declared composite type (object / input
/// object / interface), including its fields, interfaces and possibleTypes.
fn Exec::composite_type_json(self : Exec, def : @schema.ObjectType) -> Json {
let kind = match def.kind {
Object => "OBJECT"
Input => "INPUT_OBJECT"
Interface => "INTERFACE"
}
let fields = if def.kind is Input {
Json::null()
} else {
let fs : Array[Json] = []
for f in def.fields {
// @inaccessible fields are present in the subgraph but hidden from the
// composed/public schema, so introspection must not report them.
if self.schema.field_has_directive(def.name, f.name, "inaccessible") {
continue
}
fs.push(self.field_json(f))
}
Json(fs)
}
let input_fields = if def.kind is Input {
let fs : Array[Json] = []
for f in def.fields {
if self.schema.field_has_directive(def.name, f.name, "inaccessible") {
continue
}
fs.push(self.input_value_json(f.name, f.typ))
}
Json(fs)
} else {
Json::null()
}
let interfaces = if def.kind is Input {
Json::null()
} else {
let is_ : Array[Json] = []
for i in def.interfaces {
is_.push(self.named_type_ref(i))
}
Json(is_)
}
let possible_types = if def.kind is Interface {
let ps : Array[Json] = []
for t in self.schema.types {
for i in t.interfaces {
if i == def.name {
ps.push(self.named_type_ref(t.name))
break
}
}
}
Json(ps)
} else {
Json::null()
}
jobj([
("kind", Json(kind)),
("name", Json(def.name)),
("description", reason_json(def.description)),
("fields", fields),
("interfaces", interfaces),
("possibleTypes", possible_types),
("enumValues", Json::null()),
("inputFields", input_fields),
("ofType", Json::null()),
])
}
///|
/// Materialise the full `__Type` for an enum type.
fn Exec::enum_type_json(self : Exec, def : @schema.EnumType) -> Json {
ignore(self)
let vals : Array[Json] = []
for v in def.values {
let reason = def.deprecations.get(v)
vals.push(
jobj([
("name", Json(v)),
("description", reason_json(def.value_descriptions.get(v))),
("isDeprecated", Json(reason is Some(_))),
("deprecationReason", reason_json(reason)),
]),
)
}
jobj([
("kind", Json("ENUM")),
("name", Json(def.name)),
("description", reason_json(def.description)),
("fields", Json::null()),
("interfaces", Json::null()),
("possibleTypes", Json::null()),
("enumValues", Json(vals)),
("inputFields", Json::null()),
("ofType", Json::null()),
])
}
///|
/// Materialise the full `__Type` for a union type: `kind = UNION` and
/// `possibleTypes` listing its members as type references.
fn Exec::union_type_json(self : Exec, def : @schema.UnionType) -> Json {
let members : Array[Json] = []
for m in def.members {
members.push(self.named_type_ref(m))
}
jobj([
("kind", Json("UNION")),
("name", Json(def.name)),
("description", Json::null()),
("fields", Json::null()),
("interfaces", Json::null()),
("possibleTypes", Json(members)),
("enumValues", Json::null()),
("inputFields", Json::null()),
("ofType", Json::null()),
])
}
///|
/// Materialise the full `__Type` for a built-in or custom scalar.
fn scalar_type_json(name : String, spec_url? : String) -> Json {
jobj([
("kind", Json("SCALAR")),
("name", Json(name)),
("description", Json::null()),
("fields", Json::null()),
("interfaces", Json::null()),
("possibleTypes", Json::null()),
("enumValues", Json::null()),
("inputFields", Json::null()),
("ofType", Json::null()),
// The custom-scalar spec URL (← the `@specifiedBy` directive), null for the
// built-in scalars.
("specifiedByURL", reason_json(spec_url)),
])
}
///|
/// Resolve `__type(name:)` to a fully-materialised `__Type`, or JSON null.
fn Exec::introspection_type_by_name(self : Exec, name : String) -> Json {
// An @inaccessible type is hidden from the composed schema, so `__type(name:)`
// reports it as absent.
if self.schema.type_has_directive(name, "inaccessible") {
return Json::null()
}
match self.schema.type_by_name(name) {
Some(def) => return self.composite_type_json(def)
None => ()
}
match self.schema.enum_by_name(name) {
Some(def) => return self.enum_type_json(def)
None => ()
}
match self.schema.union_by_name(name) {
Some(def) => return self.union_type_json(def)
None => ()
}
// The introspection types are types the server supports, so `__type(name:
// "__Schema")` has to answer with one.
match @schema.introspection_type_def(name) {
Some(def) => return self.composite_type_json(def)
None => ()
}
if @schema.is_builtin_scalar(name) {
return scalar_type_json(name)
}
match self.schema.scalar_by_name(name) {
Some(sc) => return scalar_type_json(name, spec_url?=sc.spec_url)
None => ()
}
Json::null()
}
///|
/// The three standard built-in directives, materialised as `__Directive`s.
fn Exec::directives_json(self : Exec) -> Json {
let field_locs : Array[Json] = [
Json("FIELD"),
Json("FRAGMENT_SPREAD"),
Json("INLINE_FRAGMENT"),
]
let bool_if : Array[Json] = [
self.input_value_json("if", NonNull(@schema.Scalar("Boolean"))),
]
let deprecated_reason : Array[Json] = [
self.input_value_json(
"reason",
Scalar("String"),
default=Json("No longer supported"),
),
]
let directive = fn(
name : String,
locs : Array[Json],
args : Array[Json],
) -> Json {
jobj([
("name", Json(name)),
("description", Json::null()),
("locations", Json(locs)),
("args", Json(args)),
("isRepeatable", Json(false)),
])
}
let out : Array[Json] = [
directive("include", field_locs, bool_if),
directive("skip", field_locs, bool_if),
directive(
"deprecated",
[
Json("FIELD_DEFINITION"),
Json("ARGUMENT_DEFINITION"),
Json("INPUT_FIELD_DEFINITION"),
Json("ENUM_VALUE"),
],
deprecated_reason,
),
directive("specifiedBy", [Json("SCALAR")], [
self.input_value_json("url", NonNull(@schema.Scalar("String"))),
]),
]
// User-registered directives (@upper, @auth, ...) show up alongside the built-ins.
for d in self.schema.directive_defs {
let locs : Array[Json] = []
for l in d.locations {
locs.push(Json(l))
}
let args : Array[Json] = []
for a in d.args {
args.push(self.input_value_json(a.0, a.1))
}
out.push(
jobj([
("name", Json(d.name)),
("description", Json::null()),
("locations", Json(locs)),
("args", Json(args)),
("isRepeatable", Json(d.is_repeatable)),
]),
)
}
Json(out)
}
///|
/// Materialise the `__schema` root: query/mutation/subscription roots, every
/// declared type (user composites, enums and the built-in scalars), and the
/// built-in directives.
fn Exec::introspection_schema(self : Exec) -> Json {
let types : Array[Json] = []
for t in self.schema.types {
// @inaccessible types are hidden from the composed schema's type list.
if self.schema.type_has_directive(t.name, "inaccessible") {
continue
}
types.push(self.composite_type_json(t))
}
for e in self.schema.enums {
if self.schema.type_has_directive(e.name, "inaccessible") {
continue
}
types.push(self.enum_type_json(e))
}
for u in self.schema.unions {
if self.schema.type_has_directive(u.name, "inaccessible") {
continue
}
types.push(self.union_type_json(u))
}
for s in ["String", "Int", "Float", "Boolean", "ID"] {
types.push(scalar_type_json(s))
}
// `types` is every type the server supports, which includes the introspection
// system itself — a client generating code off the schema needs them.
for
name in [
"__Schema", "__Type", "__Field", "__InputValue", "__EnumValue", "__Directive",
] {
match @schema.introspection_type_def(name) {
Some(def) => types.push(self.composite_type_json(def))
None => ()
}
}
for sc in self.schema.scalars {
types.push(scalar_type_json(sc.name, spec_url?=sc.spec_url))
}
let mutation = match self.schema.mutation {
Some(m) => self.named_type_ref(m)
None => Json::null()
}
let subscription = match self.schema.subscription {
Some(s) => self.named_type_ref(s)
None => Json::null()
}
jobj([
("description", Json::null()),
("queryType", self.named_type_ref(self.schema.query)),
("mutationType", mutation),
("subscriptionType", subscription),
("types", Json(types)),
("directives", self.directives_json()),
])
}