moongql

moongql — a code-first GraphQL library for MoonBit (← strawberry-graphql): define object types and fields in code, emit the schema SDL.

graphql
strawberry
schema
sdl
code-first
moonbit
moon add Lfan-ke/moongql@0.6.2
Download zip
Author
Version
0.6.2
License
Apache-2.0
Last updated
18 days ago
Downloads
24

Dependencies

README

#moongql

A code-first GraphQL library for MoonBit — ← strawberry-graphql.

Check and Test License mooncakes

moongql defines a GraphQL schema in code — object types and fields — the way strawberry-graphql does for Python, and emits the schema SDL. v0 ships the code-first schema and SDL printer; it's pure logic with no runtime dependencies, so it runs on every backend.

#Quickstart

let s = @moongql.Schema::new()

let q = s.object("Query")
q.field("hello", @moongql.NonNull(@moongql.Scalar("String")))
q.field("user", @moongql.Named("User"))

let u = s.object("User")
u.field("id", @moongql.NonNull(@moongql.Scalar("ID")))
u.field("name", @moongql.NonNull(@moongql.Scalar("String")))
u.field("tags", @moongql.ListOf(@moongql.NonNull(@moongql.Scalar("String"))))

let sdl = s.to_sdl()

produces:

schema { query: Query } type Query { hello: String! user: User } type User { id: ID! name: String! tags: [String!] }

Verified across all backends (wasm, wasm-gc, js, native) in CI, 0 warnings under --deny-warn.

#Roadmap (transliterating strawberry)

The code-first schema and SDL are here, with field arguments (field_args), input object types (Schema::input), enum types (Schema::enum_), and interfaces (Schema::interface + object implements). The query parser now lands too — a hand-written lexer and a recursive-descent parser that turn a query string into a Document AST:

let doc = @moongql.parse(
"query ($id: ID!) { user(id: $id) { name ...fields @skip(if: false) } }",
)
// doc.definitions[0] is an OperationDefinition; doc.to_query() prints it back.

It covers operations (query/mutation/subscription and the { ... } shorthand), selection sets, fields with aliases/arguments/directives, variable definitions with default values, named and inline fragments, @skip/@include directives, and every input value (variables, ints, floats, strings, block strings, booleans, null, enums, lists, objects) — enough to parse the full GraphQL introspection query.

#Executing a query

moongql now runs queries. execute parses, validates against the schema, then walks the document with a resolver map(ResolveInfo) -> Json functions keyed by "Type.field" — returning the { data, errors } response as JSON. It supports variables (with defaults), aliases, named and inline fragments, @skip/@include, nested selection, list and object results, non-null error propagation, and full introspection (__schema / __type / __typename).

let s = @moongql.Schema::new()
let q = s.object("Query")
q.field_args("user", [("id", @moongql.NonNull(@moongql.Scalar("ID")))],
@moongql.Named("User"))
let u = s.object("User")
u.field("id", @moongql.NonNull(@moongql.Scalar("ID")))
u.field("name", @moongql.NonNull(@moongql.Scalar("String")))

let r = @moongql.Resolvers::new()
r.field("Query", "user", fn(info) {
match info.arg("id") {
String("1") => { "id": "1", "name": "Alice" }
_ => Json::null()
}
})
// User.id / User.name fall back to the default resolver (read off the parent).

let vars : Map[String, Json] = { "uid": "1" }
let res = @moongql.execute(s, r,
"query ($uid: ID!) { hero: user(id: $uid) { id name } }", variables=vars)
// res.stringify() == {"data":{"hero":{"id":"1","name":"Alice"}}}

#Design: faithful equivalences to strawberry

MoonBit has no runtime reflection, so where strawberry discovers resolvers and schema from Python classes, moongql uses explicit values: resolvers are functions registered by "Type.field" (like Rust/Go GraphQL servers), and a field's declared return type drives leaf-vs-composite completion exactly as graphql-core's complete_value does. Fields with no registered resolver read their value off the parent JSON object — the equivalent of graphql-core's default_field_resolver.

Union types, custom scalars, subscriptions, a DataLoader, a GraphiQL HTTP endpoint, and Apollo Federation all land too (below).

let s = @moongql.Schema::new()

let node = s.interface("Node")
node.field("id", @moongql.NonNull(@moongql.Scalar("ID")))

let q = s.object("Query")
q.field_args(
"user",
[("id", @moongql.NonNull(@moongql.Scalar("ID")))],
@moongql.Named("User"),
)

let u = s.object("User")
u.implements("Node")
u.field("id", @moongql.NonNull(@moongql.Scalar("ID")))

let inp = s.input("UserFilter")
inp.field("name", @moongql.Scalar("String"))

s.enum_("Role", ["ADMIN", "USER"])

emits user(id: ID!): User, type User implements Node { .. }, input UserFilter { .. }, and enum Role { ADMIN USER }.

#Unions and custom scalars

Declare a union with Schema::union and select into it through inline fragments; each value is discriminated by its __typename:

s.union("SearchResult", ["Book", "Author"])
// { search { __typename ... on Book { title } ... on Author { name } } }

A custom scalar carries its own serialize (output) and parse_value (input) hooks, the same two coercions strawberry's Scalar defines. Input arguments and variables run through parse_value before a resolver sees them; resolver results run through serialize:

s.scalar("DateTime",
serialize=fn(j) { j }, // value -> output JSON
parse_value=fn(j) { j }) // input JSON -> value

Both show up in SDL (union SearchResult = Book | Author, scalar DateTime) and in introspection (kind: UNION with possibleTypes, kind: SCALAR).

#Subscriptions

A subscription has one root field backed by a source stream. execute_subscription returns the ordered payloads a client would receive, one { data, errors } per event. The stream is a pull source — a resolver returning Array[Json] — so the whole pipeline runs on every backend; the async native variant over a WebSocket wraps the same per-event core.

let subs = @moongql.Subscribers::new()
subs.field("Subscription", "messageAdded", fn(info) {
// one Json payload per event, in delivery order
[msg1, msg2, msg3]
})

let payloads = @moongql.execute_subscription(schema, resolvers, subs,
"subscription { messageAdded(channel: \"general\") { id body } }")
// payloads[0].stringify() == {"data":{"messageAdded":{"id":"1","body":"hello"}}}

The single-root-field rule (spec §5.2.3.1) is enforced, and an introspection field can't be a subscription root.

#DataLoader

DataLoader is the batch-and-cache tool for the N+1 problem. Keys requested during a resolution pass are queued and deduped; one dispatch runs the batch function once over the distinct keys:

let loader : @moongql.DataLoader[Int, String] = @moongql.DataLoader::new(
fn(ids) { load_authors(ids) }, // called once per pass
fn(id) { id.to_string() }, // cache-key function
)
loader.load(1); loader.load(2); loader.load(1) // three requests, two keys
loader.dispatch() // one batch call for {1, 2}
loader.get(1) // Some("author#1")

prime, clear, clear_all, load_many and load_now round out the API. Where Facebook's DataLoader coalesces within an event-loop tick, this drains the queue explicitly so it stays testable without a runtime.

#HTTP endpoint

graphql_handler wires a schema and its resolvers onto the moonasgi seam, so moongql serves over mooncat (or any server that binds the ASGI callable). A GET returns the GraphiQL IDE; a POST reads a { query, variables, operationName } body, runs it, and replies with the { data, errors } JSON:

let handler = @moongql.graphql_handler(schema, resolvers) // path defaults to /graphql
let app = @moongql.graphql_app(schema, resolvers) // the same, lifted onto an AsgiApp

graphql_app is what a server mounts. The request/response logic is shared with graphql_handler, which moonasgi's TestClient drives without a socket — so the endpoint is tested on every backend:

let client = @moonasgi.TestClient::new(@moongql.graphql_handler(schema, resolvers))
let body = @utf8.encode(
"{\"query\":\"{ user(id: \\\"1\\\") { name } }\"}",
)
let resp = client.post("/graphql", body~)
// resp.status == 200, resp.text() == {"data":{"user":{"name":"Alice"}}}

#Apollo Federation

A subgraph exposes _service { sdl } (its SDL, annotated with the federation directives), the _entities(representations:) resolver that turns a { __typename, <key> } reference back into a full object, and @key / @extends / @external markers. Register the entity types and their reference resolvers, then apply installs the machinery — the _Service type, the _Any scalar, the _Entity union, and the two root fields — so the ordinary executor answers a federated query:

let fed = @moongql.Federation::new()
fed.entity(name="User", key="id", resolve=(rep, _ctx) => {
let id = match rep {
Object(m) => match m.get("id") { Some(String(s)) => s; _ => "" }
_ => ""
}
{ "id": id, "name": "User#" + id }.to_json() // materialise from the key
})
fed.apply(schema, resolvers)

// { _service { sdl } } -> the subgraph SDL with `type User @key(fields: "id")`
// _entities(representations: [{ __typename: "User", id: "7" }]) -> the User object

An extended type declares extends=true and lists its external fields, which render as extend type ... @key(...) with @external on the borrowed fields — the shape a gateway composes.

#Federation v2

Pass v2=true for a Federation v2 subgraph. Its SDL opens with extend schema @link(...) onto the federation spec, and the v2 field directives — @shareable, @inaccessible, @override(from:), @requires(fields:), @provides(fields:) — are declared on fields and rendered into the subgraph SDL:

let fed = @moongql.Federation::new(v2=true)
fed.entity(name="Product", key="upc", extends=true, external=["weight"],
resolve=(rep, _ctx) => {
// @requires(fields: "weight"): the gateway ships the external weight in the
// representation, so the resolver can read it to compute the estimate.
let weight = read_int(rep, "weight")
{ "upc": read_str(rep, "upc"), "shippingEstimate": weight * 2 }.to_json()
})
fed.shareable("Product", "name")
fed.override_("Product", "price", from="legacy")
fed.requires("Product", "shippingEstimate", fields="weight")
fed.inaccessible("User", "ssn") // present in the subgraph, hidden from the composed schema
fed.apply(schema, resolvers)

@inaccessible is the one v2 directive with a runtime effect beyond SDL: the field stays resolvable inside the subgraph but is hidden from introspection, so a gateway composing the public schema never sees it.

#Custom directives

Beyond @skip / @include, a schema can register its own directives. A directive definition carries its valid locations, arguments, repeatability, and an optional on_field hook that transforms a resolved field value during execution:

s.directive("prefix",
locations=["FIELD"],
args=[("text", @moongql.NonNull(@moongql.Scalar("String")))],
on_field=Some((value, args) => match (value, args.get("text")) {
(String(x), Some(String(p))) => (p + x).to_json()
_ => value
}))
// { greeting @prefix(text: "Hello, ") } -> "Hello, world"

Registered directives show up in __schema { directives } beside the built-ins, and the validator accepts them only where they are declared.

#Spec validation

The validator implements a broad slice of the GraphQL spec's §5 validation table. Beyond the per-selection rules (fields and arguments exist, leaf-vs-composite selections, fragment type conditions, defined variables), it enforces the document-level rules:

RuleSpec
Operation name uniqueness§5.2.1.1
Lone anonymous operation§5.2.2.1
Argument uniqueness§5.4.2
Fragment name uniqueness§5.5.1.1
Fragments must be used§5.5.1.4
Fragment spreads must not form cycles§5.5.2.2
Directives are defined§5.7.1
Directives are in valid locations§5.7.2
Directive repeatability§5.7.3
Variable uniqueness§5.8.1
All variables used§5.8.4

#License

Apache-2.0.

#
EntityResolver

type EntityResolver = (Json, Json) -> Json raise ResolverError

The reference resolver for an entity type: given a representation (a JSON object carrying __typename and the entity's key fields) and the request context, return the fully resolved entity as JSON. This is the subgraph's answer to "you have a key, give me the object" — Apollo's __resolveReference.

#
FieldDirectiveHook

type FieldDirectiveHook = (Json, Map[String, Json]) -> Json

The run-time hook a user directive may carry: given the resolved field value and the directive's coerced arguments, return the transformed value. This is how a custom executable directive (@upper, @default(value:), ...) alters a resolved field, the moongql equivalent of a strawberry SchemaDirective with a resolver-side effect.

#
GqlSyntaxError

pub suberror GqlSyntaxError {
GqlSyntaxError(String, Int, Int)
}

A syntax error raised by the lexer or parser, carrying a message and the 1-based line/column where the offending token starts.

#
GqlSyntaxError::to_string

fn GqlSyntaxError::to_string(self : GqlSyntaxError) -> String

Render a syntax error as Syntax error at L:C: message.

#
ResolverError

pub(all) suberror ResolverError {
ResolverError(String)
}

An error raised by a field resolver; its message is reported in the response errors list with the field's response path.

#
AppliedDirective

pub(all) struct AppliedDirective {
name : String
args : Array[(String, Json)]
}

A directive applied to a schema element: its name and its constant arguments as ordered (name, value) pairs. Build one with AppliedDirective::new.

#
AppliedDirective::new

fn AppliedDirective::new(name : String, args? : Array[(String, Json)]) -> AppliedDirective

An applied directive with no arguments, or with the given constant arguments.

#
Argument

pub(all) struct Argument {
name : String
value : Value
}

A name: value argument on a field or directive.

#
DataLoader

pub struct DataLoader[K, V] {
batch_load : (Array[K]) -> Array[V]
key : (K) -> String
cache : Map[String, V]
queue : Array[K]
queued : Map[String, Bool]
}

A batching, caching loader over key type K and value type V. batch_load receives the distinct, un-cached keys and must return their values positionally (result i is the value for key i). key derives a stable cache/identity string for a key.

#
DataLoader::clear

fn[K, V] DataLoader::clear(self : DataLoader[K, V], k : K) -> Unit

Drop the cached value for k, so the next load+dispatch reloads it.

#
DataLoader::clear_all

fn[K, V] DataLoader::clear_all(self : DataLoader[K, V]) -> Unit

Drop the whole cache.

#
DataLoader::dispatch

fn[K, V] DataLoader::dispatch(self : DataLoader[K, V]) -> Unit

Run the pending batch: call batch_load once with the queued distinct keys, store each returned value in the cache under its key, and clear the queue. A no-op when nothing is queued, so it is safe to call after every pass.

#
DataLoader::get

fn[K, V] DataLoader::get(self : DataLoader[K, V], k : K) -> V?

The cached value for k, or None if it has not been loaded and dispatched.

#
DataLoader::load

fn[K, V] DataLoader::load(self : DataLoader[K, V], k : K) -> Unit

Queue k for the next dispatch. A key already cached or already queued in this pass is dropped, so the batch that dispatch runs sees each distinct key exactly once — this dedupe is what collapses N+1 into one call.

#
DataLoader::load_many

fn[K, V] DataLoader::load_many(self : DataLoader[K, V], ks : Array[K]) -> Unit

Queue several keys for the next dispatch.

#
DataLoader::load_now

fn[K, V] DataLoader::load_now(self : DataLoader[K, V], k : K) -> V?

Load one key and dispatch immediately, returning its value. Convenience for a single lookup; batching still applies to anything already queued.

#
DataLoader::new

fn[K, V] DataLoader::new(batch_load : (Array[K]) -> Array[V], key : (K) -> String) -> DataLoader[K, V]

Create a loader from its batch-load function and a key-identity function.

#
DataLoader::prime

fn[K, V] DataLoader::prime(self : DataLoader[K, V], k : K, v : V) -> Unit

Seed the cache with a known value so a later load for k never hits the backend (strawberry/Facebook DataLoader's prime).

#
Definition

pub(all) enum Definition {
OperationDef(OperationDefinition)
FragmentDef(FragmentDefinition)
}

A top-level executable definition: an operation or a named fragment.

#
Directive

pub(all) struct Directive {
name : String
arguments : Array[Argument]
}

A directive application: @name(args).

#
DirectiveDef

pub(all) struct DirectiveDef {
name : String
locations : Array[String]
args : Array[(String, GqlType)]
is_repeatable : Bool
on_field : (Json, Map[String, Json]) -> Json?
}

A directive definition: its name, the locations it may appear at (the __DirectiveLocation enum values, e.g. "FIELD", "FIELD_DEFINITION"), its declared args, whether it is_repeatable, and an optional executable on_field hook. A definition with locations that include executable ones and an on_field hook is applied during execution; one with only type-system locations is a pure schema directive (SDL + introspection only).

#
Document

pub(all) struct Document {
definitions : Array[Definition]
}

A parsed GraphQL document: an ordered list of executable definitions.

#
Document::to_query

fn Document::to_query(self : Document) -> String

Print a whole document back to a canonical GraphQL string: each definition, separated by a blank line. Parsing this output yields an equivalent AST.

#
EntityDef

pub(all) struct EntityDef {
name : String
key : String
extends : Bool
external : Array[String]
resolve : (Json, Json) -> Json raise ResolverError
}

One federated entity: its type name, the key field set (@key(fields:)), whether the subgraph only extends a type it does not own, the names of its external fields (@external, owned by another subgraph), and the reference resolver that materialises it from a representation.

#
EnumType

pub(all) struct EnumType {
name : String
values : Array[String]
}

A GraphQL enum type: a name and an ordered list of value names.

#
Federation

pub struct Federation {
entities : Array[EntityDef]
v2 : Bool
field_dirs : Array[(String, String, AppliedDirective)]
}

The federation configuration for a subgraph: the set of entity types it contributes, whether it is a Federation v2 subgraph (which opts in with an @link to the federation spec and unlocks @shareable / @inaccessible / @override), and the field-level federation directives it declares. Build it with new, register entities with entity, mark fields with shareable / inaccessible / override_ / requires / provides, then apply it to a schema and resolver map.

#
Federation::apply

fn Federation::apply(self : Federation, schema : Schema, resolvers : Resolvers) -> Unit

Install the federation machinery onto schema and resolvers: declare the _Any scalar, the _Service type with its sdl field, and the _Entity union over every registered entity type; add the _service and _entities fields to the query root; and register the two resolvers. After this the schema answers a federated query — { _service { sdl } } and _entities(representations:) — through the normal executor.

#
Federation::entity

fn Federation::entity(self : Federation, name~ : String, key~ : String, resolve~ : (Json, Json) -> Json raise ResolverError, extends? : Bool, external? : Array[String]) -> Unit

Register an entity type. name is the object type (which must also be declared on the schema), key is its @key field set ("id", or space- separated "upc sku" for a compound key), resolve turns a representation back into the object. Set extends when this subgraph extends a type owned by another, and list external fields that other subgraphs own.

#
Federation::inaccessible

fn Federation::inaccessible(self : Federation, type_name : String, field_name : String) -> Unit

Mark type_name.field_name @inaccessible (v2) — present in this subgraph but omitted from the composed public schema, and hidden from introspection.

#
Federation::new

fn Federation::new(v2? : Bool) -> Federation

An empty federation config. Pass v2=true for a Federation v2 subgraph, whose SDL opens with extend schema @link(...) importing the federation spec and which may use the v2-only directives (@shareable, @inaccessible, @override).

#
Federation::override_

fn Federation::override_(self : Federation, type_name : String, field_name : String, from~ : String) -> Unit

Declare that type_name.field_name is @override-taken from the subgraph from (v2) — this subgraph now resolves the field the other used to own.

#
Federation::provides

fn Federation::provides(self : Federation, type_name : String, field_name : String, fields~ : String) -> Unit

Declare that resolving type_name.field_name @provides the named fields of the returned entity, so the gateway can skip a round trip for them.

#
Federation::requires

fn Federation::requires(self : Federation, type_name : String, field_name : String, fields~ : String) -> Unit

Declare that resolving type_name.field_name @requires the named external key fields ("weight size"), which the gateway then includes in the entity representation the _entities resolver receives.

#
Federation::sdl

fn Federation::sdl(self : Federation, schema : Schema) -> String

Render the subgraph SDL: the schema as the developer wrote it, annotated with @key on entity types (prefixed extend when the type is an extension), @external on external fields, and with the federation-internal additions (_Service, _Entity, _Any, and the _service / _entities root fields) left out. This is what _service { sdl } returns.

#
Federation::shareable

fn Federation::shareable(self : Federation, type_name : String, field_name : String) -> Unit

Mark type_name.field_name @shareable — resolvable by more than one subgraph (v2). Without it, a non-key field must be owned by exactly one subgraph.

#
Field

pub(all) struct Field {
name : String
args : Array[(String, GqlType)]
typ : GqlType
}

A field on an object type: a name, an ordered list of arguments (each a (name, type) pair), and the field's return type. A field with no arguments prints as name: Type; with arguments it prints as name(a: A, b: B): Type.

#
FragmentDefinition

pub(all) struct FragmentDefinition {
name : String
type_condition : String
directives : Array[Directive]
selection_set : Array[Selection]
}

A named fragment definition: fragment Name on Type @dir { selection }.

#
GqlError

pub(all) struct GqlError {
message : String
path : Array[Json]
locations : Array[(Int, Int)]
}

A GraphQL error entry: a message, an optional response path (string field keys and integer list indices), and optional source locations.

#
GqlType

pub(all) enum GqlType {
Scalar(String)
Named(String)
NonNull(GqlType)
ListOf(GqlType)
} derive(Eq)

A GraphQL type reference: a scalar (String, Int, Boolean, Float, ID), a named object type, or a non-null / list wrapper around another type.

#
GqlType::named_base

fn GqlType::named_base(self : GqlType) -> String

The base named type a possibly-wrapped type refers to (unwrapping !/[]).

#
ObjectType

pub(all) struct ObjectType {
name : String
fields : Array[Field]
kind : TypeKind
interfaces : Array[String]
}

A GraphQL composite type with an ordered set of fields. The same shape backs output objects, input objects, and interfaces; kind selects the SDL keyword and interfaces lists the interfaces an object implements.

#
ObjectType::field

fn ObjectType::field(self : ObjectType, name : String, typ : GqlType) -> Unit

Add a field with no arguments to this type.

#
ObjectType::field_args

fn ObjectType::field_args(self : ObjectType, name : String, args : Array[(String, GqlType)], typ : GqlType) -> Unit

Add a field carrying arguments to this type. Each argument is a (name, type) pair and renders as name(a: A, b: B): RetType.

#
ObjectType::field_by_name

fn ObjectType::field_by_name(self : ObjectType, name : String) -> Field?

Look up a field on this type by its name.

#
ObjectType::implements

fn ObjectType::implements(self : ObjectType, name : String) -> Unit

Declare that this object type implements a named interface. Implemented interfaces render as type Name implements A & B { ... }.

#
OperationDefinition

pub(all) struct OperationDefinition {
operation : OperationType
name : String?
variable_definitions : Array[VariableDefinition]
directives : Array[Directive]
selection_set : Array[Selection]
}

An operation: its type, an optional name, variable definitions, directives and a selection set. The anonymous { ... } query shorthand parses to an OperationDefinition with operation = Query and name = None.

#
OperationType

pub(all) enum OperationType {
Query
Mutation
Subscription
} derive(Eq)

Which kind of operation a definition describes.

#
QueryField

pub(all) struct QueryField {
alias_ : String?
name : String
arguments : Array[Argument]
directives : Array[Directive]
selection_set : Array[Selection]
}

A queried field: an optional alias, the field name, arguments, directives and an optional nested selection set. Prints as alias: name(args) @dir { ... }.

#
ResolveInfo

pub(all) struct ResolveInfo {
parent : Json
args : Map[String, Json]
ctx : Json
field_name : String
}

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.

#
ResolveInfo::arg

fn ResolveInfo::arg(self : ResolveInfo, name : String) -> Json

Read a named argument as JSON, or Json::null() when it was not supplied.

#
Resolvers

pub struct Resolvers {
map : Map[String, (ResolveInfo) -> Json raise ResolverError]
}

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).

#
Resolvers::field

fn Resolvers::field(self : Resolvers, type_name : String, field_name : String, resolver : (ResolveInfo) -> Json raise ResolverError) -> Unit

Register resolver for field field_name on type type_name.

#
Resolvers::new

fn Resolvers::new() -> Resolvers

Create an empty resolver registry.

#
ScalarType

pub(all) struct ScalarType {
name : String
serialize : (Json) -> Json
parse_value : (Json) -> Json
}

A custom scalar type with the two coercion hooks strawberry's Scalar carries: serialize maps a resolved value to its output JSON, and parse_value maps an input JSON value (an argument or variable) to the value a resolver sees. Both default to identity when a scalar only renames an existing representation.

#
Schema

pub struct Schema {
types : Array[ObjectType]
enums : Array[EnumType]
unions : Array[UnionType]
scalars : Array[ScalarType]
query : String
mutation : String?
subscription : String?
field_directives : Map[String, Array[AppliedDirective]]
type_directives : Map[String, Array[AppliedDirective]]
schema_directives : Array[AppliedDirective]
directive_defs : Array[DirectiveDef]
}

A code-first GraphQL schema: composite types (objects, inputs, interfaces), enum types, plus the names of the root operation types. query is required; mutation and subscription are optional (a schema without them cannot run operations of that kind).

#
Schema::apply_field_directive

fn Schema::apply_field_directive(self : Schema, type_name : String, field_name : String, directive : AppliedDirective) -> Unit

Record an applied directive on the field type_name.field_name.

#
Schema::apply_schema_directive

fn Schema::apply_schema_directive(self : Schema, directive : AppliedDirective) -> Unit

Record an applied directive on the schema itself (e.g. federation's @link).

#
Schema::apply_type_directive

fn Schema::apply_type_directive(self : Schema, type_name : String, directive : AppliedDirective) -> Unit

Record an applied directive on the type type_name.

#
Schema::directive

fn Schema::directive(self : Schema, name : String, locations~ : Array[String], args? : Array[(String, GqlType)], is_repeatable? : Bool, on_field? : (Json, Map[String, Json]) -> Json?) -> Unit

Register a directive definition on the schema. locations lists where it may be used ("FIELD", "FRAGMENT_SPREAD", "INLINE_FRAGMENT", "OBJECT", "FIELD_DEFINITION", ...); args declares its arguments; on_field — when given and the directive targets fields — transforms the resolved value of any field the directive is applied to. The directive then appears in introspection (__schema { directives }) and is accepted by the validator.

#
Schema::directive_def_by_name

fn Schema::directive_def_by_name(self : Schema, name : String) -> DirectiveDef?

The registered directive definition named name, if any.

#
Schema::enum_

fn Schema::enum_(self : Schema, name : String, values : Array[String]) -> Unit

Declare an enum type with an ordered set of value names. Renders as enum Name { A B } with one value per line.

#
Schema::enum_by_name

fn Schema::enum_by_name(self : Schema, name : String) -> EnumType?

Look up a declared enum type by name.

#
Schema::field_applied_directives

fn Schema::field_applied_directives(self : Schema, type_name : String, field_name : String) -> Array[AppliedDirective]

The directives applied to the field type_name.field_name.

#
Schema::field_has_directive

fn Schema::field_has_directive(self : Schema, type_name : String, field_name : String, name : String) -> Bool

Whether the field type_name.field_name carries the directive name.

#
Schema::input

fn Schema::input(self : Schema, name : String) -> ObjectType

Declare an input object type and return it so fields can be added. Renders as input Name { ... }.

#
Schema::interface

fn Schema::interface(self : Schema, name : String) -> ObjectType

Declare an interface type and return it so fields can be added. Renders as interface Name { ... }; objects declare conformance via implements.

#
Schema::new

fn Schema::new(query? : String) -> Schema

Create an empty schema whose root query type is query (default Query), with no mutation or subscription root until set_mutation / set_subscription name them.

#
Schema::object

fn Schema::object(self : Schema, name : String) -> ObjectType

Declare an output object type and return it so fields can be added. The type is registered by reference, so later .field(...) calls are seen.

#
Schema::scalar

fn Schema::scalar(self : Schema, name : String, serialize? : (Json) -> Json, parse_value? : (Json) -> Json) -> Unit

Register a custom scalar with its serialize / parse_value hooks. Both default to identity, which is enough for a scalar that only renames JSON it already carries (a DateTime stored as an ISO string, say).

#
Schema::scalar_by_name

fn Schema::scalar_by_name(self : Schema, name : String) -> ScalarType?

Look up a registered custom scalar by name.

#
Schema::set_mutation

fn Schema::set_mutation(self : Schema, name : String) -> Unit

Name the root mutation type; it must also be declared with object.

#
Schema::set_subscription

fn Schema::set_subscription(self : Schema, name : String) -> Unit

Name the root subscription type; it must also be declared with object.

#
Schema::to_sdl

fn Schema::to_sdl(self : Schema) -> String

Emit the schema as GraphQL SDL: a schema { query: ... } block, then one block per composite type (type / input / interface, with implements and field arguments), then one block per enum type.

#
Schema::type_applied_directives

fn Schema::type_applied_directives(self : Schema, type_name : String) -> Array[AppliedDirective]

The directives applied to the type type_name.

#
Schema::type_by_name

fn Schema::type_by_name(self : Schema, name : String) -> ObjectType?

Look up a declared composite type (object / input / interface) by name.

#
Schema::type_has_directive

fn Schema::type_has_directive(self : Schema, type_name : String, name : String) -> Bool

Whether the type type_name carries the directive name.

#
Schema::union

fn Schema::union(self : Schema, name : String, members : Array[String]) -> Unit

Declare a union type over the named member object types. Renders as union Name = A | B.

#
Schema::union_by_name

fn Schema::union_by_name(self : Schema, name : String) -> UnionType?

Look up a declared union type by name.

#
Selection

pub(all) enum Selection {
FieldSel(QueryField)
FragmentSpreadSel(String, Array[Directive])
InlineFragmentSel(String?, Array[Directive], Array[Selection])
}

One entry in a selection set: a field, a ...Name fragment spread, or an inline ... on Type { ... } fragment.

#
Subscribers

pub struct Subscribers {
map : Map[String, (ResolveInfo) -> Array[Json] raise ResolverError]
}

A registry of subscription source resolvers, keyed by "TypeName.fieldName" on the subscription root type. A source returns the ordered stream of event payloads; each payload is the resolved value of the root field for one event, and its sub-selection is resolved against it like any object value.

#
Subscribers::field

fn Subscribers::field(self : Subscribers, type_name : String, field_name : String, source : (ResolveInfo) -> Array[Json] raise ResolverError) -> Unit

Register a source stream for the root subscription field field_name on type_name. The source is called once per operation and yields the ordered events to deliver.

#
Subscribers::new

Create an empty subscription source registry.

#
Token

pub(all) struct Token {
kind : TokenKind
value : String
line : Int
col : Int
}

A lexical token: its kind, its text (for value-bearing kinds; unescaped for strings), and the 1-based line/column of its first character.

#
TokenKind

pub(all) enum TokenKind {
Name
IntVal
FloatVal
StringVal
BlockStringVal
Bang
Dollar
Amp
ParenL
ParenR
Spread
Colon
Equals
At
BracketL
BracketR
BraceL
BraceR
Pipe
Eof
} derive(Eq)

The lexical category of a token. Value-bearing kinds (Name, IntVal, FloatVal, StringVal, BlockStringVal) carry their text in Token::value.

#
TypeKind

pub(all) enum TypeKind {
Object
Input
Interface
} derive(Eq)

Which kind of composite type an ObjectType describes: an output object (type), an input object, or an interface.

#
TypeRef

pub(all) enum TypeRef {
NamedType(String)
ListType(TypeRef)
NonNullType(TypeRef)
} derive(Eq)

A type reference as it appears in the query grammar: a named type, a list wrapper [T], or a non-null wrapper T!. Distinct from the schema builder's GqlType (which carries a Scalar sugar the parser never sees).

#
TypeRef::to_query

fn TypeRef::to_query(self : TypeRef) -> String

Print a type reference back to GraphQL notation ([Int!]!).

#
UnionType

pub(all) struct UnionType {
name : String
members : Array[String]
}

A GraphQL union type: a name and the ordered names of its member object types. A value at a union position is resolved to one member via its __typename, and only inline/named fragments on member types (plus __typename) may select into it.

#
Value

pub(all) enum Value {
Variable(String)
IntValue(String)
FloatValue(String)
StringValue(String, Bool)
BooleanValue(Bool)
NullValue
EnumValue(String)
ListValue(Array[Value])
ObjectValue(Array[(String, Value)])
}

A GraphQL input value. Numeric and string literals keep their source text (matching graphql-core, whose IntValueNode.value etc. are strings), so no lossy numeric round-trip is baked into the AST. StringValue's second field is the block-string flag.

#
Value::to_query

fn Value::to_query(self : Value) -> String

Print a value back to GraphQL literal syntax.

#
VariableDefinition

pub(all) struct VariableDefinition {
variable : String
typ : TypeRef
default_value : Value?
directives : Array[Directive]
}

A variable declaration in an operation's (...) list: $name: Type = default with optional default value and directives.

#
execute

fn execute(schema : Schema, resolvers : Resolvers, query : String, variables? : Map[String, Json], operation_name? : String?, root_value? : Json, context? : Json) -> Json

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.

#
execute_subscription

fn execute_subscription(schema : Schema, resolvers : Resolvers, subscribers : Subscribers, query : String, variables? : Map[String, Json], operation_name? : String?, root_value? : Json, context? : Json) -> Array[Json]

Run a subscription operation and collect its ordered response payloads.

Parses and validates query, requires a single-root-field subscription operation, invokes the registered source for that field, then maps every event to a { data, errors } response — returning them in stream order. A parse/validation failure or a missing source yields a single error response. variables, operation_name, root_value and context mirror execute.

#
graphiql_html

fn graphiql_html(endpoint? : String) -> String

The GraphiQL IDE page, wired to fetch against endpoint. This is the same standalone GraphiQL build strawberry serves: React and GraphiQL load from a CDN via an import map, and a fetcher points at the GraphQL endpoint. Splitting the template around the endpoint keeps it a plain string with no interpolation machinery.

#
graphql_app

fn graphql_app(schema : Schema, resolvers : Resolvers, path? : String, graphiql? : Bool) -> (async (
Scope
, async () ->
Event
, async (
Event
) -> Unit) -> Unit)

Lift the GraphQL handler onto the load-bearing AsgiApp a server binds to. This is what mooncat (or any moonasgi server) mounts to serve the schema over real HTTP; the request→response logic is shared with graphql_handler, which TestClient exercises without a socket.

#
graphql_handler

fn graphql_handler(schema : Schema, resolvers : Resolvers, path? : String, graphiql? : Bool) -> ((
Request
) ->
Response
)

Build the moonasgi request handler for a schema and its resolvers. A GET on path serves the GraphiQL IDE (when graphiql is true); a POST on path executes a GraphQL request and returns { data, errors }. Any other path is a 404 and any other method a 405, both as JSON errors. Lift it onto an AsgiApp with graphql_app, or drive it directly with moonasgi's TestClient.

#
parse

fn parse(source : String) -> Document raise GqlSyntaxError

Parse a GraphQL executable document from source text. This is the front half of the executor: parse(query).definitions yields the operations and fragments to validate and execute.

#
tokenize

fn tokenize(src : String) -> Array[Token] raise GqlSyntaxError

Tokenize an entire source string into a token array ending with Eof. Primarily for tests and tooling; the parser pulls tokens on demand.

#
validate

fn validate(schema : Schema, doc : Document) -> Array[GqlError]

Validate a whole document against schema, returning all errors found (an empty array means the document is valid and ready to execute).