moongql

A code-first GraphQL library for MoonBit — define object types and fields in code and emit the schema SDL, the way strawberry does for Python. Pure logic, no runtime dependencies.

CItestsGitHublicense
$moon add Lfan-ke/moongql

The contract at a glance

let s = Schema::new()
let q = s.object("Query")
q.field("hello", NonNull(Scalar("String")))

let sdl = s.to_sdl()   // "schema { query: Query }  type Query { hello: String! }" 

§Schema & SDL

Code-first GraphQL: build object types and fields, then to_sdl emits the schema SDL. GqlType covers scalars, named refs, NonNull and List wrappers.

enum
enum GqlType

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

struct
struct Field

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.

enum
enum TypeKind

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

struct
struct ObjectType

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.

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

Add a field with no arguments to this type.

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

fn
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 { ... }.

struct
struct EnumType

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

struct
struct UnionType

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.

struct
struct ScalarType

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.

struct
struct Schema

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

fn
fn Schema::new(query? : String = "Query") -> 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.

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

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

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

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

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

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

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

Look up a declared enum type by name.

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

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

Look up a declared union type by name.

fn
fn Schema::scalar( self : Schema, name : String, serialize? : (Json) -> Json = fn(x)

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

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

Look up a registered custom scalar by name.

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

Look up a field on this type by its name.

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

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

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

fn
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 { ... }.

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

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

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

§Query Lexer

Hand-written GraphQL lexer: turns a query string into names, punctuators, int/float/string tokens (block strings dedented per spec), ignoring whitespace, commas and comments. Pure logic, no core/lexbuf dependency.

item
suberror GqlSyntaxError

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

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

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

item
impl Show for GqlSyntaxError with fn output(self, logger)
enum
enum TokenKind

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

struct
struct Token

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

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

§Document AST

The executable-document AST (operations, selection sets, fields, arguments, variables, fragments, directives, input values), with to_query printers that render each node back to canonical GraphQL.

struct
struct Document

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

enum
enum Definition

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

enum
enum OperationType

Which kind of operation a definition describes.

struct
struct OperationDefinition

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.

struct
struct VariableDefinition

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

enum
enum TypeRef

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

struct
struct FragmentDefinition

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

enum
enum Selection

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

struct
struct QueryField

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

struct
struct Argument

A name: value argument on a field or directive.

struct
struct Directive

A directive application: @name(args).

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

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

Print a value back to GraphQL literal syntax.

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

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

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

§Query Parser

Recursive-descent parser: parse(source) builds a Document from operations (query/mutation/subscription and the { ... } shorthand), fields with aliases/arguments/directives, variable definitions, and named/inline fragments — the front half of the executor.

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

§Validator

Validate a Document against the Schema before execution: fields and arguments exist, leaf vs composite selection sets, fragment spreads and type conditions resolve, and referenced variables are defined. Errors are collected, not raised.

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

§Validation Rules (spec §5)

The document-level slice of the GraphQL validation table: operation-name uniqueness and lone anonymous operation, fragment-name uniqueness, no-unused-fragments, fragment-spread cycle detection, and the variable-use collector behind the all-variables-used rule.

§Directives

The directive extension points: AppliedDirective records a directive use on a schema element (federation's @key / @shareable / @inaccessible / @override live here); DirectiveDef declares a custom directive with valid locations, args, repeatability and an optional on_field execution hook that transforms a resolved field value.

struct
struct AppliedDirective

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

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

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

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

struct
struct DirectiveDef

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

fn
fn Schema::directive( self : Schema, name : String, locations~ : Array[String], args? : Array[(String, GqlType)] = [], is_repeatable? : Bool = false, on_field? : FieldDirectiveHook? = None) -> 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.

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

The registered directive definition named name, if any.

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

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

Record an applied directive on the type type_name.

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

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

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

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

The directives applied to the type type_name.

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

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

Whether the type type_name carries the directive name.

§Executor

execute() runs a query end to end: parse, validate, then walk the document with a resolver map ((ResolveInfo) -> Json keyed by "Type.field"), supporting variables, aliases, fragments, @skip/@include, non-null error propagation and list/object results — returning { data, errors } as JSON.

struct
struct ResolveInfo

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.

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

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

item
suberror ResolverError

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

struct
struct Resolvers

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

fn
fn Resolvers::new() -> Resolvers

Create an empty resolver registry.

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

struct
struct GqlError

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

fn
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

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.

§Introspection

Answer __schema / __type / __typename from the schema: the introspection type system materialised as JSON that the executor walks like any object, with ofType wrapper chains, fields, interfaces, possibleTypes and enum values.

§Subscriptions

execute_subscription runs a single-root-field subscription against a source stream (a pull source returning Array[Json]) and returns the ordered { data, errors } payloads a client receives — the sync core the async WebSocket variant wraps.

struct
struct Subscribers

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.

fn
fn Subscribers::new() -> Subscribers

Create an empty subscription source registry.

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

fn
fn execute_subscription( schema : Schema, resolvers : Resolvers, subscribers : Subscribers, query : String, variables? : Map[String, Json] = Map([]), operation_name? : String? = None, root_value? : Json = Json::null(), context? : Json = Json::null()) -> 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.

§DataLoader

Batch-and-cache loader for the N+1 problem: keys requested in a resolution pass are queued and deduped, then one dispatch runs the batch function over the distinct keys and fills a per-loader cache. Includes prime, clear and load_many/load_now.

struct
struct DataLoader[K, V]

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.

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

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

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

Queue several keys for the next dispatch.

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

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

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

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

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

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

Drop the whole cache.

§HTTP Endpoint

graphql_handler / graphql_app wire a schema onto the moonasgi seam: GET serves the GraphiQL IDE, POST reads a { query, variables, operationName } body and returns { data, errors }. Plugs into mooncat and runs on every backend through moonasgi's TestClient.

fn
fn graphiql_html(endpoint? : String = "/graphql") -> 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.

fn
fn graphql_handler( schema : Schema, resolvers : Resolvers, path? : String = "/graphql", graphiql? : Bool = true) -> @moonasgi.Handler

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.

fn
fn graphql_app( schema : Schema, resolvers : Resolvers, path? : String = "/graphql", graphiql? : Bool = true) -> @moonasgi.AsgiApp

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.

§Apollo Federation

Federation v1 and v2 subgraph support: register @key entities and their reference resolvers, then apply installs _service { sdl }, the _Any scalar, the _Entity union and the _entities(representations) resolver. v2 opts in with @link and adds @shareable / @inaccessible / @override / @requires / @provides on fields, rendered into the subgraph SDL.

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

struct
struct EntityDef

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.

struct
struct Federation

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.

fn
fn Federation::new(v2? : Bool = false) -> 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).

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

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

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

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

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

fn
fn Federation::entity( self : Federation, name~ : String, key~ : String, resolve~ : EntityResolver, extends? : Bool = false, 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.

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

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