///|
/// The directive extension points (GraphQL spec §3.13). moongql carries two kinds
/// of directive:
///
/// * **Applied directives** (`AppliedDirective`) — a directive *use* on a schema
/// element, recorded on the `Schema` against a `"Type.field"` key, a type name,
/// or the schema itself. Apollo Federation's `@key` / `@shareable` /
/// `@inaccessible` / `@override` and any user schema directive live here. They
/// surface in the subgraph SDL and can influence introspection (an
/// `@inaccessible` element is hidden).
///
/// * **Directive definitions** (`DirectiveDef`) — a directive *declaration*:
/// its name, valid locations, argument types, repeatability, and an optional
/// **executable hook** that transforms a resolved field value at run time. The
/// built-in `@skip` / `@include` are handled inline by the executor; a
/// user-defined directive registered here shows up in introspection and, if it
/// carries an `on_field` hook, alters the field's value during execution.
///|
/// A directive applied to a schema element: its `name` and its constant arguments
/// as ordered `(name, value)` pairs. Build one with `AppliedDirective::new`.
pub(all) struct AppliedDirective {
name : String
args : Array[(String, Json)]
}
///|
/// An applied directive with no arguments, or with the given constant arguments.
pub fn AppliedDirective::new(
name : String,
args? : Array[(String, Json)] = [],
) -> AppliedDirective {
{ name, args }
}
///|
/// 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.
pub type FieldDirectiveHook = (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).
pub(all) struct DirectiveDef {
name : String
locations : Array[String]
args : Array[(String, GqlType)]
is_repeatable : Bool
on_field : FieldDirectiveHook?
}
///|
/// 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.
pub fn Schema::directive(
self : Schema,
name : String,
locations~ : Array[String],
args? : Array[(String, GqlType)] = [],
is_repeatable? : Bool = false,
on_field? : FieldDirectiveHook? = None,
) -> Unit {
self.directive_defs.push({ name, locations, args, is_repeatable, on_field })
}
///|
/// The registered directive definition named `name`, if any.
pub fn Schema::directive_def_by_name(
self : Schema,
name : String,
) -> DirectiveDef? {
for d in self.directive_defs {
if d.name == name {
return Some(d)
}
}
None
}
///|
/// Record an applied directive on the field `type_name.field_name`.
pub fn Schema::apply_field_directive(
self : Schema,
type_name : String,
field_name : String,
directive : AppliedDirective,
) -> Unit {
let key = type_name + "." + field_name
match self.field_directives.get(key) {
Some(ds) => ds.push(directive)
None => self.field_directives[key] = [directive]
}
}
///|
/// Record an applied directive on the type `type_name`.
pub fn Schema::apply_type_directive(
self : Schema,
type_name : String,
directive : AppliedDirective,
) -> Unit {
match self.type_directives.get(type_name) {
Some(ds) => ds.push(directive)
None => self.type_directives[type_name] = [directive]
}
}
///|
/// Record an applied directive on the schema itself (e.g. federation's `@link`).
pub fn Schema::apply_schema_directive(
self : Schema,
directive : AppliedDirective,
) -> Unit {
self.schema_directives.push(directive)
}
///|
/// The directives applied to the field `type_name.field_name`.
pub fn Schema::field_applied_directives(
self : Schema,
type_name : String,
field_name : String,
) -> Array[AppliedDirective] {
match self.field_directives.get(type_name + "." + field_name) {
Some(ds) => ds
None => []
}
}
///|
/// The directives applied to the type `type_name`.
pub fn Schema::type_applied_directives(
self : Schema,
type_name : String,
) -> Array[AppliedDirective] {
match self.type_directives.get(type_name) {
Some(ds) => ds
None => []
}
}
///|
/// Whether the field `type_name.field_name` carries the directive `name`.
pub fn Schema::field_has_directive(
self : Schema,
type_name : String,
field_name : String,
name : String,
) -> Bool {
for d in self.field_applied_directives(type_name, field_name) {
if d.name == name {
return true
}
}
false
}
///|
/// Whether the type `type_name` carries the directive `name`.
pub fn Schema::type_has_directive(
self : Schema,
type_name : String,
name : String,
) -> Bool {
for d in self.type_applied_directives(type_name) {
if d.name == name {
return true
}
}
false
}
///|
/// Render a JSON value as a GraphQL literal, for printing an applied directive's
/// arguments into SDL (`@override(from: "accounts")`, `@key(fields: "id name")`).
fn json_to_gql_literal(j : Json) -> String {
match j {
Null => "null"
True => "true"
False => "false"
String(s) => "\"" + escape_string(s) + "\""
Number(n, repr~) =>
match repr {
Some(r) => r
None => n.to_string()
}
Array(items) => {
let mut out = "["
for i, it in items {
if i > 0 {
out = out + ", "
}
out = out + json_to_gql_literal(it)
}
out + "]"
}
Object(m) => {
let mut out = "{"
let mut first = true
for k, v in m {
if not(first) {
out = out + ", "
}
first = false
out = out + k + ": " + json_to_gql_literal(v)
}
out + "}"
}
}
}
///|
/// Render one applied directive to SDL: `@name` or `@name(a: v, b: w)`.
fn applied_directive_sdl(d : AppliedDirective) -> String {
if d.args.length() == 0 {
return "@" + d.name
}
let mut out = "@" + d.name + "("
for i, a in d.args {
if i > 0 {
out = out + ", "
}
out = out + a.0 + ": " + json_to_gql_literal(a.1)
}
out + ")"
}
///|
/// Render a whole applied-directive list, each prefixed with a space (empty for an
/// empty list), for appending after a field or type in SDL.
fn applied_directives_sdl(ds : Array[AppliedDirective]) -> String {
let mut out = ""
for d in ds {
out = out + " " + applied_directive_sdl(d)
}
out
}
///|
/// The valid locations of a built-in executable directive (`@skip` / `@include`),
/// or `None` for a name that is not a built-in. Used by the validator to check
/// that a directive appears where it is allowed.
fn builtin_exec_directive(name : String) -> Array[String]? {
match name {
"skip" | "include" => Some(["FIELD", "FRAGMENT_SPREAD", "INLINE_FRAGMENT"])
_ => None
}
}