///|
/// The GraphQL execution engine: it walks a validated `Document` against a
/// `Schema` using a resolver map, then serialises `{ data, errors }` to JSON.
/// This is the "can actually run" half of moongql — `execute` turns a query
/// string, variable values, a root value and a context into a real response,
/// mirroring `graphql-core`'s `execute` (which strawberry drives).
///
/// Faithful equivalences to strawberry's Python (see README design notes):
/// resolvers are explicit `(ResolveInfo) -> Json` functions keyed by
/// `"Type.field"` rather than methods discovered by reflection, and the field
/// return type drives leaf/composite completion the same way graphql-core's
/// `complete_value` does.
///|
/// The information a field resolver receives: the resolved parent object (as
/// JSON), the coerced arguments, the shared context value, and the field's name.
/// A resolver returns the field's value as JSON (an object for composite types,
/// whose sub-fields are then resolved against it) and may `raise ResolverError`
/// to surface a field error.
pub(all) struct ResolveInfo {
parent : Json
args : Map[String, Json]
ctx : Json
field_name : String
}
///|
/// Read a named argument as JSON, or `Json::null()` when it was not supplied.
pub fn ResolveInfo::arg(self : ResolveInfo, name : String) -> Json {
match self.args.get(name) {
Some(v) => v
None => Json::null()
}
}
///|
/// An error raised by a field resolver; its message is reported in the response
/// `errors` list with the field's response path.
pub(all) suberror ResolverError {
ResolverError(String)
}
///|
/// A registry of field resolvers, keyed by `"TypeName.fieldName"`. Fields with
/// no registered resolver fall back to the default resolver, which reads the
/// field's name off the parent JSON object (matching graphql-core's
/// `default_field_resolver`).
pub struct Resolvers {
map : Map[String, (ResolveInfo) -> Json raise ResolverError]
}
///|
/// Create an empty resolver registry.
pub fn Resolvers::new() -> Resolvers {
{ map: Map([]) }
}
///|
/// Register `resolver` for field `field_name` on type `type_name`.
pub fn Resolvers::field(
self : Resolvers,
type_name : String,
field_name : String,
resolver : (ResolveInfo) -> Json raise ResolverError,
) -> Unit {
self.map[type_name + "." + field_name] = resolver
}
///|
/// A GraphQL error entry: a message, an optional response path (string field
/// keys and integer list indices), and optional source locations.
pub(all) struct GqlError {
message : String
path : Array[Json]
locations : Array[(Int, Int)]
}
///|
/// Build an error carrying only a message (used for request-level errors).
fn GqlError::msg(message : String) -> GqlError {
{ message, path: [], locations: [] }
}
///|
/// Serialise an error to the GraphQL JSON error shape, omitting an empty path
/// or locations list.
fn GqlError::to_json(self : GqlError) -> Json {
let entries : Array[(String, Json)] = [("message", self.message.to_json())]
if self.locations.length() > 0 {
let locs : Array[Json] = []
for lc in self.locations {
locs.push(jobj([("line", lc.0.to_json()), ("column", lc.1.to_json())]))
}
entries.push(("locations", locs.to_json()))
}
if self.path.length() > 0 {
entries.push(("path", self.path.to_json()))
}
jobj(entries)
}
///|
/// Build a JSON object from ordered `(key, value)` entries, preserving order.
fn jobj(entries : Array[(String, Json)]) -> Json {
let m : Map[String, Json] = Map([])
for e in entries {
m[e.0] = e.1
}
m.to_json()
}
///|
/// Interpret a JSON value as a boolean (`true`/`false`), or `None` otherwise.
fn json_bool(j : Json) -> Bool? {
match j {
True => Some(true)
False => Some(false)
_ => None
}
}
///|
/// Turn a numeric literal's source text into a JSON number. GraphQL int/float
/// literal syntax is a subset of JSON number syntax, so the lexer-validated text
/// parses directly; malformed text (unreachable for well-formed input) is null.
fn parse_number_json(s : String) -> Json {
@json.parse(s) catch {
_ => Json::null()
}
}
///|
/// Internal signal used to bubble a null from a non-null position up to the
/// nearest nullable field, per the GraphQL "errors and non-nullability" rules.
priv suberror NullBubble
///|
/// The per-request executor state: the schema, resolvers, the document's named
/// fragments, the coerced variable values, the context value, and the running
/// error list.
priv struct Exec {
schema : Schema
resolvers : Resolvers
fragments : Map[String, FragmentDefinition]
variables : Map[String, Json]
ctx : Json
errors : Array[GqlError]
}
///|
/// Record a field error at `path`.
fn Exec::add_error(self : Exec, message : String, path : Array[Json]) -> Unit {
self.errors.push({ message, path, locations: [] })
}
///|
/// The five built-in scalar type names, which are leaves with no declaration.
fn is_builtin_scalar(name : String) -> Bool {
name == "String" ||
name == "Int" ||
name == "Float" ||
name == "Boolean" ||
name == "ID"
}
///|
/// Whether `name` denotes a leaf type (a scalar or an enum) rather than a
/// composite type whose fields are selected into.
fn Exec::is_leaf_type(self : Exec, 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"
}
///|
/// Coerce a GraphQL AST value to JSON, substituting variables from the request.
fn Exec::value_to_json(self : Exec, v : Value) -> Json {
match v {
Variable(n) =>
match self.variables.get(n) {
Some(x) => x
None => Json::null()
}
IntValue(s) => parse_number_json(s)
FloatValue(s) => parse_number_json(s)
StringValue(s, _) => s.to_json()
BooleanValue(b) => b.to_json()
NullValue => Json::null()
EnumValue(n) => n.to_json()
ListValue(items) => {
let out : Array[Json] = []
for it in items {
out.push(self.value_to_json(it))
}
out.to_json()
}
ObjectValue(fields) => {
let m : Map[String, Json] = Map([])
for kv in fields {
m[kv.0] = self.value_to_json(kv.1)
}
m.to_json()
}
}
}
///|
/// Coerce a field's argument list to a `name -> JSON` map.
fn Exec::coerce_args(self : Exec, field : QueryField) -> Map[String, Json] {
let m : Map[String, Json] = Map([])
for a in field.arguments {
m[a.name] = self.value_to_json(a.value)
}
m
}
///|
/// Run a custom scalar's `parse_value` hook over an input value, descending
/// through non-null and list wrappers so `[DateTime!]` parses element-wise. A
/// value at a built-in scalar or non-scalar position is returned unchanged.
fn Exec::coerce_input(self : Exec, value : Json, typ : GqlType) -> Json {
match typ {
NonNull(inner) => self.coerce_input(value, inner)
ListOf(inner) =>
match value {
Array(items) => {
let out : Array[Json] = []
for it in items {
out.push(self.coerce_input(it, inner))
}
out.to_json()
}
_ => value
}
Scalar(n) | Named(n) =>
match self.schema.scalar_by_name(n) {
Some(sc) => if value is Null { value } else { (sc.parse_value)(value) }
None => value
}
}
}
///|
/// Apply custom-scalar input coercion to each declared argument of `fdef` that
/// carries a value, so a resolver reading `info.arg(..)` sees parsed input.
fn Exec::apply_input_scalars(
self : Exec,
args : Map[String, Json],
fdef : Field,
) -> Unit {
for da in fdef.args {
match args.get(da.0) {
Some(v) => args[da.0] = self.coerce_input(v, da.1)
None => ()
}
}
}
///|
/// Evaluate the `if:` argument of a `@skip`/`@include` directive (default false).
fn Exec::directive_if(self : Exec, d : Directive) -> Bool {
for a in d.arguments {
if a.name == "if" {
return match json_bool(self.value_to_json(a.value)) {
Some(b) => b
None => false
}
}
}
false
}
///|
/// Apply `@skip(if:)` / `@include(if:)` to decide whether a selection is kept.
fn Exec::should_include(self : Exec, directives : Array[Directive]) -> Bool {
let mut keep = true
for d in directives {
if d.name == "skip" && self.directive_if(d) {
keep = false
} else if d.name == "include" && not(self.directive_if(d)) {
keep = false
}
}
keep
}
///|
/// Whether a fragment's type condition `cond` applies to an object of type
/// `obj`: the condition names the object itself or an interface it implements.
fn Exec::fragment_applies(self : Exec, cond : String, obj : ObjectType) -> Bool {
if cond == obj.name {
return true
}
for iface in obj.interfaces {
if iface == cond {
return true
}
}
if self.schema.union_by_name(cond) is Some(u) {
for m in u.members {
if m == obj.name {
return true
}
}
}
false
}
///|
/// CollectFields (GraphQL spec §6.3.2): flatten a selection set into ordered
/// response keys, expanding fragment spreads and inline fragments and honouring
/// `@skip`/`@include`. Fields sharing a response key are grouped so their
/// sub-selections merge.
fn Exec::collect_fields(
self : Exec,
obj : ObjectType,
selections : Array[Selection],
visited : Map[String, Bool],
keys : Array[String],
groups : Map[String, Array[QueryField]],
) -> Unit {
for sel in selections {
match sel {
FieldSel(f) => {
if not(self.should_include(f.directives)) {
continue
}
let key = match f.alias_ {
Some(a) => a
None => f.name
}
match groups.get(key) {
Some(g) => g.push(f)
None => {
keys.push(key)
groups[key] = [f]
}
}
}
FragmentSpreadSel(name, dirs) => {
if not(self.should_include(dirs)) {
continue
}
if visited.get(name) is Some(true) {
continue
}
visited[name] = true
match self.fragments.get(name) {
Some(frag) =>
if self.fragment_applies(frag.type_condition, obj) {
self.collect_fields(
obj,
frag.selection_set,
visited,
keys,
groups,
)
}
None => ()
}
}
InlineFragmentSel(cond, dirs, sels) => {
if not(self.should_include(dirs)) {
continue
}
let applies = match cond {
None => true
Some(c) => self.fragment_applies(c, obj)
}
if applies {
self.collect_fields(obj, sels, visited, keys, groups)
}
}
}
}
}
///|
/// The declared type of `field_name` on `obj`, resolving the introspection
/// meta-fields (`__typename`, and `__schema`/`__type` on the query root).
fn Exec::field_type_of(
self : Exec,
obj : ObjectType,
field_name : String,
) -> GqlType? {
if field_name == "__typename" {
return Some(NonNull(Scalar("String")))
}
if obj.name == self.schema.query {
if field_name == "__schema" {
return Some(NonNull(Named("__Schema")))
}
if field_name == "__type" {
return Some(Named("__Type"))
}
}
match obj.field_by_name(field_name) {
Some(f) => Some(f.typ)
None => None
}
}
///|
/// Resolve a field's raw JSON value: dispatch to its registered resolver, the
/// introspection roots, or the default resolver (read the field off the parent).
/// A `ResolverError` becomes a recorded field error and a null value.
fn Exec::resolve_raw(
self : Exec,
obj : ObjectType,
field : QueryField,
parent : Json,
path : Array[Json],
) -> Json {
let name = field.name
if name == "__typename" {
return obj.name.to_json()
}
let args = self.coerce_args(field)
match obj.field_by_name(name) {
Some(fdef) => self.apply_input_scalars(args, fdef)
None => ()
}
match self.resolvers.map.get(obj.name + "." + name) {
Some(resolver) => {
let info = { parent, args, ctx: self.ctx, field_name: name }
resolver(info) catch {
ResolverError(m) => {
self.add_error(m, path)
Json::null()
}
}
}
None => {
if obj.name == self.schema.query {
if name == "__schema" {
return self.introspection_schema()
}
if name == "__type" {
let tn = match args.get("name") {
Some(String(s)) => s
_ => ""
}
return self.introspection_type_by_name(tn)
}
}
match parent {
Object(m) =>
match m.get(name) {
Some(v) => v
None => Json::null()
}
_ => Json::null()
}
}
}
}
///|
/// Choose the concrete object type for a value at an interface position: if the
/// value carries a `__typename`, resolve to that object type; otherwise keep the
/// declared type.
fn Exec::concrete_type(
self : Exec,
declared : ObjectType,
value : Json,
) -> ObjectType {
match value {
Object(m) =>
match m.get("__typename") {
Some(String(tn)) =>
match self.schema.type_by_name(tn) {
Some(t) => t
None => declared
}
_ => declared
}
_ => declared
}
}
///|
/// Serialise a leaf value through a custom scalar's `serialize` hook, or return
/// it unchanged for a built-in scalar or enum.
fn Exec::serialize_leaf(self : Exec, name : String, value : Json) -> Json {
match self.schema.scalar_by_name(name) {
Some(sc) => (sc.serialize)(value)
None => value
}
}
///|
/// Resolve the concrete member type for a value at a union position: read its
/// `__typename`, check that name is one of the union's members, and return that
/// object type. A missing or non-member `__typename` is a field error.
fn Exec::union_member(
self : Exec,
u : UnionType,
value : Json,
path : Array[Json],
) -> ObjectType? {
let tn = match value {
Object(m) =>
match m.get("__typename") {
Some(String(s)) => s
_ => ""
}
_ => ""
}
if tn == "" {
self.add_error(
"Cannot resolve concrete type for union '" +
u.name +
"': value has no __typename",
path,
)
return None
}
let mut is_member = false
for m in u.members {
if m == tn {
is_member = true
}
}
if not(is_member) {
self.add_error(
"Type '" + tn + "' is not a member of union '" + u.name + "'",
path,
)
return None
}
match self.schema.type_by_name(tn) {
Some(t) => Some(t)
None => {
self.add_error("Union member type '" + tn + "' is not defined", path)
None
}
}
}
///|
/// The merged sub-selection sets of all fields grouped under one response key.
fn merge_subselections(fields : Array[QueryField]) -> Array[Selection] {
let out : Array[Selection] = []
for f in fields {
for s in f.selection_set {
out.push(s)
}
}
out
}
///|
/// CompleteValue (GraphQL spec §6.4.3): coerce a resolved value to the shape its
/// field type demands. Non-null violations raise `NullBubble`, list items are
/// completed element-wise, leaf types serialise as-is, and composite types
/// recurse into their sub-selection. Introspection types (`__Schema`, `__Type`,
/// ...) complete against pre-materialised JSON via the default resolver.
fn Exec::complete(
self : Exec,
ftype : GqlType,
value : Json,
fields : Array[QueryField],
path : Array[Json],
) -> Json raise NullBubble {
match ftype {
NonNull(inner) => {
let r = self.complete(inner, value, fields, path)
if r is Null {
self.add_error("Cannot return null for non-nullable field", path)
raise NullBubble
}
r
}
ListOf(inner) =>
match value {
Null => Json::null()
Array(items) => {
let out : Array[Json] = []
for i, item in items {
let ipath = path.copy()
ipath.push(i.to_json())
out.push(self.complete(inner, item, fields, ipath))
}
out.to_json()
}
_ => {
self.add_error("Expected a list value", path)
Json::null()
}
}
Scalar(name) => self.serialize_leaf(name, value)
Named(name) =>
if self.is_leaf_type(name) {
self.serialize_leaf(name, value)
} else {
match value {
Null => Json::null()
_ =>
if self.schema.union_by_name(name) is Some(u) {
match self.union_member(u, value, path) {
Some(obj) =>
self.execute_selection_set(
obj,
merge_subselections(fields),
value,
path,
)
None => Json::null()
}
} else {
match self.output_type(name) {
Some(decl) => {
let obj = self.concrete_type(decl, value)
self.execute_selection_set(
obj,
merge_subselections(fields),
value,
path,
)
}
None => {
self.add_error("Unknown output type '" + name + "'", path)
Json::null()
}
}
}
}
}
}
}
///|
/// Look up an output object type by name, spanning the user schema and the
/// synthetic introspection types (`__Schema`, `__Type`, ...).
fn Exec::output_type(self : Exec, name : String) -> ObjectType? {
match self.schema.type_by_name(name) {
Some(t) => Some(t)
None => introspection_type_def(name)
}
}
///|
/// Execute one field group: resolve it, then complete it against its type,
/// absorbing a `NullBubble` into a null field value when the field is nullable
/// or re-raising it to null the parent when the field is non-null.
fn Exec::execute_field(
self : Exec,
obj : ObjectType,
fields : Array[QueryField],
parent : Json,
path : Array[Json],
) -> Json raise NullBubble {
let first = fields[0]
match self.field_type_of(obj, first.name) {
None => {
self.add_error(
"Cannot query field '" + first.name + "' on type '" + obj.name + "'",
path,
)
Json::null()
}
Some(ftype) => {
let raw = self.resolve_raw(obj, first, parent, path)
let value = self.complete(ftype, raw, fields, path) catch {
NullBubble =>
if ftype is NonNull(_) {
raise NullBubble
} else {
Json::null()
}
}
self.apply_exec_directives(first.directives, value)
}
}
}
///|
/// Apply any user-registered executable directives on `directives` to a field's
/// completed `value`, in order: each directive whose definition carries an
/// `on_field` hook transforms the value, threading the coerced directive
/// arguments. `@skip` / `@include` are not seen here — they were already resolved
/// during field collection — so only value-altering custom directives take effect.
fn Exec::apply_exec_directives(
self : Exec,
directives : Array[Directive],
value : Json,
) -> Json {
let mut out = value
for d in directives {
match self.schema.directive_def_by_name(d.name) {
Some(def) =>
match def.on_field {
Some(hook) => {
let args : Map[String, Json] = Map([])
for a in d.arguments {
args[a.name] = self.value_to_json(a.value)
}
out = hook(out, args)
}
None => ()
}
None => ()
}
}
out
}
///|
/// ExecuteSelectionSet (GraphQL spec §6.3): collect fields on `obj`, execute
/// each group in order, and assemble the result object. A non-null field that
/// nulls out raises `NullBubble`, propagating to null this whole object.
fn Exec::execute_selection_set(
self : Exec,
obj : ObjectType,
selections : Array[Selection],
parent : Json,
path : Array[Json],
) -> Json raise NullBubble {
let keys : Array[String] = []
let groups : Map[String, Array[QueryField]] = Map([])
self.collect_fields(obj, selections, Map([]), keys, groups)
let result : Map[String, Json] = Map([])
for key in keys {
let group = match groups.get(key) {
Some(g) => g
None => continue
}
let fpath = path.copy()
fpath.push(key.to_json())
result[key] = self.execute_field(obj, group, parent, fpath)
}
result.to_json()
}
///|
/// The parsed and validated inputs of one execution: the chosen operation and
/// the coerced variable values, or a request-level error list.
priv struct Prepared {
operation : OperationDefinition
root_type : String
}
///|
/// Select the operation to run and its root type. With no name, a single
/// operation is chosen; a name selects a matching operation. Returns the
/// operation plus its root type name, or a request error.
fn select_operation(
schema : Schema,
doc : Document,
operation_name : String?,
) -> Result[Prepared, GqlError] {
let ops : Array[OperationDefinition] = []
for def in doc.definitions {
match def {
OperationDef(op) => ops.push(op)
FragmentDef(_) => ()
}
}
if ops.length() == 0 {
return Err(GqlError::msg("Document contains no operations"))
}
let chosen = match operation_name {
None =>
if ops.length() == 1 {
ops[0]
} else {
return Err(
GqlError::msg(
"Must provide operation name if query contains multiple operations",
),
)
}
Some(name) => {
let mut found : OperationDefinition? = None
for op in ops {
if op.name is Some(n) && n == name {
found = Some(op)
}
}
match found {
Some(op) => op
None =>
return Err(GqlError::msg("Unknown operation named '" + name + "'"))
}
}
}
let root_type = match chosen.operation {
Query => schema.query
Mutation =>
match schema.mutation {
Some(m) => m
None =>
return Err(GqlError::msg("Schema is not configured for mutations"))
}
Subscription =>
match schema.subscription {
Some(s) => s
None =>
return Err(
GqlError::msg("Schema is not configured for subscriptions"),
)
}
}
Ok({ operation: chosen, root_type })
}
///|
/// Build the document's fragment table by name.
fn collect_fragments(doc : Document) -> Map[String, FragmentDefinition] {
let m : Map[String, FragmentDefinition] = Map([])
for def in doc.definitions {
match def {
FragmentDef(frag) => m[frag.name] = frag
OperationDef(_) => ()
}
}
m
}
///|
/// Coerce an operation's variable values: use the request-supplied value when
/// present, else the variable's default, else null.
fn coerce_variables(
op : OperationDefinition,
supplied : Map[String, Json],
fragments : Map[String, FragmentDefinition],
) -> Map[String, Json] {
let coerced : Map[String, Json] = Map([])
// A throwaway executor is used only for default-value literal coercion, which
// never dereferences a variable (defaults are constant).
let tmp = {
schema: Schema::new(),
resolvers: Resolvers::new(),
fragments,
variables: Map([]),
ctx: Json::null(),
errors: [],
}
for vd in op.variable_definitions {
match supplied.get(vd.variable) {
Some(v) => coerced[vd.variable] = v
None =>
match vd.default_value {
Some(dv) => coerced[vd.variable] = tmp.value_to_json(dv)
None => ()
}
}
}
coerced
}
///|
/// Assemble the final `{ data?, errors? }` response object. `data` is omitted
/// for request-level failures (no execution began) and included (possibly null)
/// once execution starts; `errors` is omitted when empty.
fn build_response(data : Json?, errors : Array[GqlError]) -> Json {
let entries : Array[(String, Json)] = []
match data {
Some(d) => entries.push(("data", d))
None => ()
}
if errors.length() > 0 {
let arr : Array[Json] = []
for e in errors {
arr.push(e.to_json())
}
entries.push(("errors", arr.to_json()))
}
jobj(entries)
}
///|
/// Build a fresh per-request executor state with an empty error list.
fn Exec::state(
schema : Schema,
resolvers : Resolvers,
fragments : Map[String, FragmentDefinition],
variables : Map[String, Json],
ctx : Json,
) -> Exec {
{ schema, resolvers, fragments, variables, ctx, errors: [] }
}
///|
/// Execute a top-level selection set against `root_type`, absorbing a `NullBubble`
/// that reaches the root into a null `data`.
fn Exec::run_root(
self : Exec,
root_type : ObjectType,
selections : Array[Selection],
root_value : Json,
) -> Json {
self.execute_selection_set(root_type, selections, root_value, []) catch {
NullBubble => Json::null()
}
}
///|
/// Execute a GraphQL request end to end: parse `query`, validate it against
/// `schema`, select the operation, coerce variables, then walk the selection
/// set with `resolvers` — returning the `{ data, errors }` response as JSON.
///
/// `variables` supplies operation variable values, `operation_name` picks an
/// operation when the document has several, `root_value` seeds the root object,
/// and `context` is threaded to every resolver. The function never raises: parse
/// and validation failures yield an `errors`-only response, and field errors are
/// collected alongside partial `data`.
pub fn execute(
schema : Schema,
resolvers : Resolvers,
query : String,
variables? : Map[String, Json] = Map([]),
operation_name? : String? = None,
root_value? : Json = Json::null(),
context? : Json = Json::null(),
) -> Json {
let doc = parse(query) catch {
GqlSyntaxError(m, line, col) =>
return build_response(None, [
{ message: m, path: [], locations: [(line, col)] },
])
}
let verrors = validate(schema, doc)
if verrors.length() > 0 {
return build_response(None, verrors)
}
let prepared = match select_operation(schema, doc, operation_name) {
Ok(p) => p
Err(e) => return build_response(None, [e])
}
let fragments = collect_fragments(doc)
let vars = coerce_variables(prepared.operation, variables, fragments)
let exec = Exec::state(schema, resolvers, fragments, vars, context)
let root_type = match schema.type_by_name(prepared.root_type) {
Some(t) => t
None =>
return build_response(None, [
GqlError::msg("Root type '" + prepared.root_type + "' is not defined"),
])
}
let data = exec.run_root(
root_type,
prepared.operation.selection_set,
root_value,
)
build_response(Some(data), exec.errors)
}