///|
/// Result of completing one field or selection set.
///
/// `bubble_null` implements GraphQL non-null propagation: when true, the parent
/// completion step must replace its own value with null and continue bubbling if
/// its declared type is also non-null.
priv struct CompleteResult {
value : GqlValue
errors : Array[GraphQLError]
bubble_null : Bool
}
///|
priv struct RuntimeFieldCollection {
keys : Array[String]
fields : Map[String, Array[@parser.Field]]
}
///|
/// Execute a GraphQL request and return only the GraphQL response envelope.
///
/// Use `execute_with_status` when an HTTP transport needs to distinguish parse,
/// validation, and variable coercion failures from successful execution.
pub async fn[Ctx] execute(
schema : ExecutableSchema[Ctx],
request : GraphQLRequest,
context : Ctx,
) -> GraphQLResponse {
let result = execute_with_status(schema, request, context)
result.response
}
///|
/// HTTP-oriented execution result containing status code and GraphQL response.
pub(all) struct ExecutionHttpResult {
status_code : Int32
response : GraphQLResponse
} derive(Eq, Debug)
///|
/// Execute a GraphQL request with parser, validator, variable coercion, resolver
/// execution, and GraphQL value completion.
///
/// Parse, validation, variable coercion, and root operation errors return
/// status `400`. Resolver and completion errors are represented inside the
/// GraphQL response with status `200`, following common GraphQL HTTP behavior.
pub async fn[Ctx] execute_with_status(
schema : ExecutableSchema[Ctx],
request : GraphQLRequest,
context : Ctx,
) -> ExecutionHttpResult {
let document = try @parser.parse_query(request.query) catch {
err =>
return {
status_code: 400,
response: GraphQLResponse::error(err.to_string()),
}
} noraise {
document => document
}
let (operation, fragments) = try
validate_document(schema, document, request.operation_name)
catch {
err =>
return {
status_code: 400,
response: GraphQLResponse::error(err.to_string()),
}
} noraise {
result => result
}
let variables = try
coerce_variable_values(
schema.registry,
operation.variable_definitions,
request.variables,
)
catch {
err =>
return {
status_code: 400,
response: GraphQLResponse::error(err.to_string()),
}
} noraise {
values => values
}
let root_type = try operation_root_type(schema, operation) catch {
err =>
return {
status_code: 400,
response: GraphQLResponse::error(err.to_string()),
}
} noraise {
type_name => type_name
}
let result = execute_selection_set(
schema,
root_type,
operation.selection_set,
GqlNull,
variables,
fragments,
context,
[],
)
{
status_code: 200,
response: { data: Some(result.value), errors: result.errors, },
}
}
///|
/// Start a subscription operation and return its response stream.
///
/// This validates the request as a subscription, coerces variables and
/// arguments, and invokes the registered subscription resolver for the single
/// selected root field.
pub async fn[Ctx] start_subscription(
schema : ExecutableSchema[Ctx],
request : GraphQLRequest,
context : Ctx,
) -> Result[SubscriptionStream, GraphQLResponse] {
let document = try @parser.parse_query(request.query) catch {
err => return Err(GraphQLResponse::error(err.to_string()))
} noraise {
document => document
}
let (operation, _) = try
validate_document(schema, document, request.operation_name)
catch {
err => return Err(GraphQLResponse::error(err.to_string()))
} noraise {
result => result
}
if operation.kind != "subscription" {
return Err(GraphQLResponse::error("operation is not a subscription"))
}
let variables = try
coerce_variable_values(
schema.registry,
operation.variable_definitions,
request.variables,
)
catch {
err => return Err(GraphQLResponse::error(err.to_string()))
} noraise {
values => values
}
let root_type = try operation_root_type(schema, operation) catch {
err => return Err(GraphQLResponse::error(err.to_string()))
} noraise {
type_name => type_name
}
if operation.selection_set.items.length() != 1 {
return Err(
GraphQLResponse::error("subscription must select exactly one root field"),
)
}
let field = match operation.selection_set.items[0] {
FieldSelection(field) => field
_ =>
return Err(
GraphQLResponse::error("subscription root selection must be a field"),
)
}
let field_def = match schema.registry.field(root_type, field.name) {
Some(field_def) => field_def
None =>
return Err(
GraphQLResponse::error(
"unknown subscription field `\{root_type}.\{field.name}`",
),
)
}
let args = try
coerce_argument_values(
schema.registry,
field_def.arguments,
field.arguments,
variables,
)
catch {
err => return Err(GraphQLResponse::error(err.to_string()))
} noraise {
args => args
}
let resolver = match
schema.subscriptions.get(resolver_key(root_type, field.name)) {
Some(resolver) => resolver
None =>
return Err(
GraphQLResponse::error(
"missing subscription resolver for `\{root_type}.\{field.name}`",
),
)
}
let response_key = field.field_alias.unwrap_or(field.name)
let path = [PathField(response_key)]
let info = {
type_name: root_type,
field_name: field.name,
response_key,
path,
source: GqlNull,
}
try resolver(info, args, context) catch {
err => Err(GraphQLResponse::error("\{err}"))
} noraise {
stream => Ok(stream)
}
}
///|
/// Execute every selected field and fragment against a runtime object type.
///
/// Fragment spreads are evaluated only when their type condition overlaps with
/// the current runtime type, and object fields with the same response key are
/// merged into one response map.
async fn[Ctx] execute_selection_set(
schema : ExecutableSchema[Ctx],
type_name : String,
selection_set : @parser.SelectionSet,
source : GqlValue,
variables : Map[String, InputValue],
fragments : Map[String, @parser.FragmentDefinition],
context : Ctx,
path : Array[PathSegment],
) -> CompleteResult {
let object : Map[String, GqlValue] = Map([])
let errors : Array[GraphQLError] = []
let runtime_type = source.runtime_type(fallback=type_name)
let collection = collect_execution_fields(
schema, runtime_type, selection_set, variables, fragments,
)
for response_key in collection.keys {
let fields = collection.fields[response_key]
let merged_field = merge_runtime_fields(fields)
let field_result = execute_field(
schema, runtime_type, merged_field, source, variables, fragments, context,
path,
)
object[response_key] = field_result.value
errors.append(field_result.errors)
if field_result.bubble_null {
return { value: GqlNull, errors, bubble_null: true, }
}
}
{ value: GqlObject(object), errors, bubble_null: false, }
}
///|
/// Resolve and complete a single field, including built-in introspection fields.
///
/// When no explicit resolver is registered, object values fall back to field
/// lookup on the parent `GqlObject` or `GqlTypedObject`.
async fn[Ctx] execute_field(
schema : ExecutableSchema[Ctx],
type_name : String,
field : @parser.Field,
source : GqlValue,
variables : Map[String, InputValue],
fragments : Map[String, @parser.FragmentDefinition],
context : Ctx,
parent_path : Array[PathSegment],
) -> CompleteResult {
let response_key = field.field_alias.unwrap_or(field.name)
let path = parent_path.copy()
path.push(PathField(response_key))
if field.name == "__typename" {
return { value: GqlString(type_name), errors: [], bubble_null: false, }
}
if field.name == "__schema" {
return {
value: gql_value_from_json(introspect(schema, format=Full)),
errors: [],
bubble_null: false,
}
}
if field.name == "__type" {
let type_name_arg = find_argument(field.arguments, "name")
let type_json = match type_name_arg {
Some((_, value)) =>
match parser_value_to_input(value, variables) {
InputString(name) | InputEnum(name) => introspect_type(schema, name)
_ => Json::null()
}
None => Json::null()
}
return {
value: gql_value_from_json(type_json),
errors: [],
bubble_null: false,
}
}
let field_def = match schema.registry.field(type_name, field.name) {
Some(field_def) => field_def
None =>
return {
value: GqlNull,
errors: [
GraphQLError::new("unknown field `\{type_name}.\{field.name}`", path~),
],
bubble_null: false,
}
}
let args = try
coerce_argument_values(
schema.registry,
field_def.arguments,
field.arguments,
variables,
)
catch {
err =>
return {
value: GqlNull,
errors: [GraphQLError::new(err.to_string(), path~)],
bubble_null: is_non_null(field_def.field_type),
}
} noraise {
args => args
}
let raw = match find_resolver(schema, type_name, field.name) {
Some(resolver) => {
let info = {
type_name,
field_name: field.name,
response_key,
path,
source,
}
try resolver(info, args, context) catch {
err =>
return {
value: GqlNull,
errors: [GraphQLError::new("\{err}", path~)],
bubble_null: is_non_null(field_def.field_type),
}
} noraise {
value => value
}
}
None =>
match source.field(field.name) {
Some(value) => value
None =>
return {
value: GqlNull,
errors: [
GraphQLError::new(
"missing resolver for `\{type_name}.\{field.name}`",
path~,
),
],
bubble_null: is_non_null(field_def.field_type),
}
}
}
complete_value(
schema,
field_def.field_type,
field.selection_set,
raw,
variables,
fragments,
context,
path,
)
}
///|
/// Complete a raw resolver value according to a GraphQL output type.
///
/// This is where list item completion, object sub-selection, and non-null
/// bubbling are enforced.
async fn[Ctx] complete_value(
schema : ExecutableSchema[Ctx],
gql_type : @parser.GqlType,
selection_set : @parser.SelectionSet,
value : GqlValue,
variables : Map[String, InputValue],
fragments : Map[String, @parser.FragmentDefinition],
context : Ctx,
path : Array[PathSegment],
) -> CompleteResult {
match gql_type {
NonNullType(inner) => {
let result = complete_value(
schema, inner, selection_set, value, variables, fragments, context, path,
)
if result.bubble_null || result.value == GqlNull {
let errors = result.errors
if errors.is_empty() {
errors.push(
GraphQLError::new("non-null field resolved to null", path~),
)
}
{ value: GqlNull, errors, bubble_null: true, }
} else {
{ value: result.value, errors: result.errors, bubble_null: false, }
}
}
ListType(inner) =>
match value {
GqlNull => { value: GqlNull, errors: [], bubble_null: false, }
GqlList(values) => {
let completed : Array[GqlValue] = []
let errors : Array[GraphQLError] = []
for index, item in values.iter2() {
let item_path = path.copy()
item_path.push(PathIndex(index))
let result = complete_value(
schema, inner, selection_set, item, variables, fragments, context,
item_path,
)
errors.append(result.errors)
if result.bubble_null {
completed.push(GqlNull)
if is_non_null(inner) {
return { value: GqlNull, errors, bubble_null: true, }
}
} else {
completed.push(result.value)
}
}
{ value: GqlList(completed), errors, bubble_null: false, }
}
_ =>
{
value: GqlNull,
errors: [
GraphQLError::new(
"expected list for `\{type_to_string(gql_type)}`",
path~,
),
],
bubble_null: false,
}
}
NamedType(name) =>
match schema.registry.types.get(name) {
Some(ScalarDef(_)) | Some(EnumDef(_)) =>
{ value, errors: [], bubble_null: false, }
Some(ObjectDef(_)) =>
if value == GqlNull {
{ value: GqlNull, errors: [], bubble_null: false, }
} else {
let runtime_type = value.runtime_type(fallback=name)
if runtime_type != name {
return {
value: GqlNull,
errors: [
GraphQLError::new(
"runtime type `\{runtime_type}` is not valid for object `\{name}`",
path~,
),
],
bubble_null: false,
}
}
execute_selection_set(
schema, runtime_type, selection_set, value, variables, fragments, context,
path,
)
}
Some(InterfaceDef(_)) | Some(UnionDef(_)) =>
if value == GqlNull {
{ value: GqlNull, errors: [], bubble_null: false, }
} else {
match
resolve_abstract_runtime_type(schema, name, value, context, path) {
Ok(runtime_type) =>
execute_selection_set(
schema, runtime_type, selection_set, value, variables, fragments,
context, path,
)
Err(error) =>
{ value: GqlNull, errors: [error], bubble_null: false, }
}
}
Some(InputObjectDef(_)) | None =>
{
value: GqlNull,
errors: [
GraphQLError::new("cannot complete output type `\{name}`", path~),
],
bubble_null: false,
}
}
}
}
///|
fn[Ctx] collect_execution_fields(
schema : ExecutableSchema[Ctx],
runtime_type : String,
selection_set : @parser.SelectionSet,
variables : Map[String, InputValue],
fragments : Map[String, @parser.FragmentDefinition],
) -> RuntimeFieldCollection {
let collection = RuntimeFieldCollection::{ keys: [], fields: {}, }
collect_execution_fields_into(
schema, runtime_type, selection_set, variables, fragments, collection,
)
collection
}
///|
fn[Ctx] collect_execution_fields_into(
schema : ExecutableSchema[Ctx],
runtime_type : String,
selection_set : @parser.SelectionSet,
variables : Map[String, InputValue],
fragments : Map[String, @parser.FragmentDefinition],
collection : RuntimeFieldCollection,
) -> Unit {
for selection in selection_set.items {
match selection {
FieldSelection(field) =>
if should_include(field.directives, variables) {
append_execution_field(collection, field)
}
FragmentSpreadSelection(spread) =>
if should_include(spread.directives, variables) {
match fragments.get(spread.fragment_name) {
Some(fragment) => {
let On(fragment_type) = fragment.type_condition
if types_overlap(schema.registry, runtime_type, fragment_type) {
collect_execution_fields_into(
schema,
runtime_type,
fragment.selection_set,
variables,
fragments,
collection,
)
}
}
None => ()
}
}
InlineFragmentSelection(fragment) =>
if should_include(fragment.directives, variables) {
let next_type = match fragment.type_condition {
Some(On(name)) => name
None => runtime_type
}
if types_overlap(schema.registry, runtime_type, next_type) {
collect_execution_fields_into(
schema,
runtime_type,
fragment.selection_set,
variables,
fragments,
collection,
)
}
}
}
}
}
///|
fn append_execution_field(
collection : RuntimeFieldCollection,
field : @parser.Field,
) -> Unit {
let response_key = field.field_alias.unwrap_or(field.name)
match collection.fields.get(response_key) {
Some(fields) => fields.push(field)
None => {
collection.keys.push(response_key)
collection.fields[response_key] = [field]
}
}
}
///|
fn merge_runtime_fields(fields : Array[@parser.Field]) -> @parser.Field {
let first = fields[0]
let items : Array[@parser.Selection] = []
for field in fields {
items.append(field.selection_set.items)
}
{
position: first.position,
field_alias: first.field_alias,
name: first.name,
arguments: first.arguments,
directives: first.directives,
selection_set: { span: first.selection_set.span, items, },
}
}
///|
async fn[Ctx] resolve_abstract_runtime_type(
schema : ExecutableSchema[Ctx],
abstract_type : String,
value : GqlValue,
context : Ctx,
path : Array[PathSegment],
) -> Result[String, GraphQLError] {
let runtime_type = match value {
GqlTypedObject(type_name, _) => type_name
_ =>
match schema.type_resolvers.get(abstract_type) {
Some(resolver) =>
try resolver(abstract_type, value, context) catch {
err =>
return Err(
GraphQLError::new(
"type resolver for `\{abstract_type}` failed: \{err}",
path~,
),
)
} noraise {
type_name => type_name
}
None =>
return Err(
GraphQLError::new(
"missing runtime type for abstract type `\{abstract_type}`",
path~,
),
)
}
}
if abstract_type_accepts_runtime(schema.registry, abstract_type, runtime_type) {
Ok(runtime_type)
} else {
Err(
GraphQLError::new(
"runtime type `\{runtime_type}` is not valid for abstract type `\{abstract_type}`",
path~,
),
)
}
}
///|
fn abstract_type_accepts_runtime(
registry : TypeRegistry,
abstract_type : String,
runtime_type : String,
) -> Bool {
match registry.types.get(runtime_type) {
Some(ObjectDef(_)) => ()
_ => return false
}
match registry.types.get(abstract_type) {
Some(InterfaceDef(_)) =>
type_implements_interface(registry, runtime_type, abstract_type)
Some(UnionDef(members~, ..)) => members.contains(runtime_type)
_ => false
}
}
///|
fn[Ctx] find_resolver(
schema : ExecutableSchema[Ctx],
type_name : String,
field_name : String,
) -> Resolver[Ctx]? {
schema.resolvers.get(resolver_key(type_name, field_name))
}
///|
fn should_include(
directives : Array[@parser.Directive],
variables : Map[String, InputValue],
) -> Bool {
let mut is_included = true
for directive in directives {
if directive.name == "skip" || directive.name == "include" {
let if_arg = find_argument(directive.arguments, "if")
let condition = match if_arg {
Some((_, value)) =>
try parser_value_to_input(value, variables) catch {
_ => false
} noraise {
InputBoolean(value) => value
_ => false
}
None => false
}
if directive.name == "skip" && condition {
is_included = false
} else if directive.name == "include" && !condition {
is_included = false
}
}
}
is_included
}
///|
fn find_argument(
arguments : Array[(String, @parser.Value)],
name : String,
) -> (String, @parser.Value)? {
for argument in arguments {
if argument.0 == name {
return Some(argument)
}
}
None
}
///|
fn[Ctx] introspect_type(schema : ExecutableSchema[Ctx], name : String) -> Json {
match schema.registry.types.get(name) {
Some(type_def) =>
Json::object({
"name": Json::string(name),
"kind": Json::string(type_kind_name(type_def)),
"fields": Json::array(introspection_fields(type_def)),
"enumValues": Json::array(introspection_enum_values(type_def)),
"inputFields": Json::array(introspection_input_fields(type_def)),
"possibleTypes": Json::array(introspection_possible_types(type_def)),
})
None => Json::null()
}
}