///|
/// A GraphQL type reference: a scalar (`String`, `Int`, `Boolean`, `Float`, `ID`),
/// a named object type, or a non-null / list wrapper around another type.
pub(all) enum GqlType {
Scalar(String)
Named(String)
NonNull(GqlType)
ListOf(GqlType)
} derive(Eq)
///|
/// Render a type reference to GraphQL SDL notation (`String!`, `[User!]`, ...).
fn type_sdl(t : GqlType) -> String {
match t {
Scalar(s) => s
Named(n) => n
NonNull(inner) => type_sdl(inner) + "!"
ListOf(inner) => "[" + type_sdl(inner) + "]"
}
}
///|
/// 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`.
pub(all) struct Field {
name : String
args : Array[(String, GqlType)]
typ : GqlType
}
///|
/// Render a single field to SDL, including its argument list when non-empty.
fn field_sdl(f : Field) -> String {
let mut out = " " + f.name
if f.args.length() > 0 {
out = out + "("
for i, arg in f.args {
let (an, at) = arg
if i > 0 {
out = out + ", "
}
out = out + an + ": " + type_sdl(at)
}
out = out + ")"
}
out + ": " + type_sdl(f.typ)
}
///|
/// Which kind of composite type an `ObjectType` describes: an output `object`
/// (`type`), an `input` object, or an `interface`.
pub(all) enum TypeKind {
Object
Input
Interface
} derive(Eq)
///|
/// 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.
pub(all) struct ObjectType {
name : String
fields : Array[Field]
kind : TypeKind
interfaces : Array[String]
}
///|
/// Add a field with no arguments to this type.
pub fn ObjectType::field(
self : ObjectType,
name : String,
typ : GqlType,
) -> Unit {
self.fields.push({ name, args: [], typ })
}
///|
/// Add a field carrying arguments to this type. Each argument is a `(name, type)`
/// pair and renders as `name(a: A, b: B): RetType`.
pub fn ObjectType::field_args(
self : ObjectType,
name : String,
args : Array[(String, GqlType)],
typ : GqlType,
) -> Unit {
self.fields.push({ name, args, typ })
}
///|
/// Declare that this object type implements a named interface. Implemented
/// interfaces render as `type Name implements A & B { ... }`.
pub fn ObjectType::implements(self : ObjectType, name : String) -> Unit {
self.interfaces.push(name)
}
///|
/// A GraphQL enum type: a name and an ordered list of value names.
pub(all) struct EnumType {
name : String
values : 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.
pub(all) struct UnionType {
name : String
members : Array[String]
}
///|
/// 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.
pub(all) struct ScalarType {
name : String
serialize : (Json) -> Json
parse_value : (Json) -> Json
}
///|
/// 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).
pub struct Schema {
types : Array[ObjectType]
enums : Array[EnumType]
unions : Array[UnionType]
scalars : Array[ScalarType]
query : String
mut mutation : String?
mut subscription : String?
// Applied directives (directive uses) recorded against a "Type.field" key, a
// type name, and the schema itself, plus the definitions of user directives.
field_directives : Map[String, Array[AppliedDirective]]
type_directives : Map[String, Array[AppliedDirective]]
schema_directives : Array[AppliedDirective]
directive_defs : Array[DirectiveDef]
}
///|
/// 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.
pub fn Schema::new(query? : String = "Query") -> Schema {
{
types: [],
enums: [],
unions: [],
scalars: [],
query,
mutation: None,
subscription: None,
field_directives: Map([]),
type_directives: Map([]),
schema_directives: [],
directive_defs: [],
}
}
///|
/// Name the root mutation type; it must also be declared with `object`.
pub fn Schema::set_mutation(self : Schema, name : String) -> Unit {
self.mutation = Some(name)
}
///|
/// Name the root subscription type; it must also be declared with `object`.
pub fn Schema::set_subscription(self : Schema, name : String) -> Unit {
self.subscription = Some(name)
}
///|
/// Look up a declared composite type (object / input / interface) by name.
pub fn Schema::type_by_name(self : Schema, name : String) -> ObjectType? {
for t in self.types {
if t.name == name {
return Some(t)
}
}
None
}
///|
/// Look up a declared enum type by name.
pub fn Schema::enum_by_name(self : Schema, name : String) -> EnumType? {
for e in self.enums {
if e.name == name {
return Some(e)
}
}
None
}
///|
/// Declare a union type over the named member object types. Renders as
/// `union Name = A | B`.
pub fn Schema::union(
self : Schema,
name : String,
members : Array[String],
) -> Unit {
self.unions.push({ name, members })
}
///|
/// Look up a declared union type by name.
pub fn Schema::union_by_name(self : Schema, name : String) -> UnionType? {
for u in self.unions {
if u.name == name {
return Some(u)
}
}
None
}
///|
/// 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).
pub fn Schema::scalar(
self : Schema,
name : String,
serialize? : (Json) -> Json = fn(x) { x },
parse_value? : (Json) -> Json = fn(x) { x },
) -> Unit {
self.scalars.push({ name, serialize, parse_value })
}
///|
/// Look up a registered custom scalar by name.
pub fn Schema::scalar_by_name(self : Schema, name : String) -> ScalarType? {
for s in self.scalars {
if s.name == name {
return Some(s)
}
}
None
}
///|
/// Look up a field on this type by its name.
pub fn ObjectType::field_by_name(self : ObjectType, name : String) -> Field? {
for f in self.fields {
if f.name == name {
return Some(f)
}
}
None
}
///|
/// The base named type a possibly-wrapped type refers to (unwrapping `!`/`[]`).
pub fn GqlType::named_base(self : GqlType) -> String {
match self {
Scalar(s) => s
Named(n) => n
NonNull(inner) => inner.named_base()
ListOf(inner) => inner.named_base()
}
}
///|
/// 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.
pub fn Schema::object(self : Schema, name : String) -> ObjectType {
let o : ObjectType = { name, fields: [], kind: Object, interfaces: [] }
self.types.push(o)
o
}
///|
/// Declare an input object type and return it so fields can be added. Renders as
/// `input Name { ... }`.
pub fn Schema::input(self : Schema, name : String) -> ObjectType {
let o : ObjectType = { name, fields: [], kind: Input, interfaces: [] }
self.types.push(o)
o
}
///|
/// Declare an interface type and return it so fields can be added. Renders as
/// `interface Name { ... }`; objects declare conformance via `implements`.
pub fn Schema::interface(self : Schema, name : String) -> ObjectType {
let o : ObjectType = { name, fields: [], kind: Interface, interfaces: [] }
self.types.push(o)
o
}
///|
/// Declare an enum type with an ordered set of value names. Renders as
/// `enum Name { A B }` with one value per line.
pub fn Schema::enum_(
self : Schema,
name : String,
values : Array[String],
) -> Unit {
self.enums.push({ name, values })
}
///|
/// The SDL keyword introducing a composite type of the given kind.
fn kind_keyword(k : TypeKind) -> String {
match k {
Object => "type"
Input => "input"
Interface => "interface"
}
}
///|
/// 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.
pub fn Schema::to_sdl(self : Schema) -> String {
let mut out = "schema {\n query: " + self.query + "\n}\n\n"
for obj in self.types {
out = out + kind_keyword(obj.kind) + " " + obj.name
if obj.interfaces.length() > 0 {
out = out + " implements "
for i, iface in obj.interfaces {
if i > 0 {
out = out + " & "
}
out = out + iface
}
}
out = out + " {\n"
for f in obj.fields {
out = out + field_sdl(f) + "\n"
}
out = out + "}\n\n"
}
for en in self.enums {
out = out + "enum " + en.name + " {\n"
for v in en.values {
out = out + " " + v + "\n"
}
out = out + "}\n\n"
}
for un in self.unions {
out = out + "union " + un.name + " = "
for i, m in un.members {
if i > 0 {
out = out + " | "
}
out = out + m
}
out = out + "\n\n"
}
for sc in self.scalars {
out = out + "scalar " + sc.name + "\n\n"
}
out
}