///|
/// The GraphQL executable-document AST, transliterated from the node types
/// `graphql-core` (strawberry's parser) produces: a `Document` is a list of
/// executable `Definition`s (operations and named fragments). These nodes are
/// pure data — the parser fills them and `to_query` prints them back to a
/// canonical GraphQL string, so a parse→print round-trip is testable.
///|
/// A parsed GraphQL document: an ordered list of executable definitions.
pub(all) struct Document {
definitions : Array[Definition]
}
///|
/// A top-level executable definition: an operation or a named fragment.
pub(all) enum Definition {
OperationDef(OperationDefinition)
FragmentDef(FragmentDefinition)
}
///|
/// Which kind of operation a definition describes.
pub(all) enum OperationType {
Query
Mutation
Subscription
} derive(Eq)
///|
/// 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`.
pub(all) struct OperationDefinition {
operation : OperationType
name : String?
variable_definitions : Array[VariableDefinition]
directives : Array[Directive]
selection_set : Array[Selection]
}
///|
/// A variable declaration in an operation's `(...)` list: `$name: Type = default`
/// with optional default value and directives.
pub(all) struct VariableDefinition {
variable : String
typ : TypeRef
default_value : Value?
directives : Array[Directive]
}
///|
/// 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).
pub(all) enum TypeRef {
NamedType(String)
ListType(TypeRef)
NonNullType(TypeRef)
} derive(Eq)
///|
/// A named fragment definition: `fragment Name on Type @dir { selection }`.
pub(all) struct FragmentDefinition {
name : String
type_condition : String
directives : Array[Directive]
selection_set : Array[Selection]
}
///|
/// One entry in a selection set: a field, a `...Name` fragment spread, or an
/// inline `... on Type { ... }` fragment.
pub(all) enum Selection {
FieldSel(QueryField)
FragmentSpreadSel(String, Array[Directive])
InlineFragmentSel(String?, Array[Directive], 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 { ... }`.
pub(all) struct QueryField {
alias_ : String?
name : String
arguments : Array[Argument]
directives : Array[Directive]
selection_set : Array[Selection]
}
///|
/// A `name: value` argument on a field or directive.
pub(all) struct Argument {
name : String
value : Value
}
///|
/// A directive application: `@name(args)`.
pub(all) struct Directive {
name : String
arguments : Array[Argument]
}
///|
/// 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.
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 string of `2 * n` spaces, used as one indentation level by the printer.
fn indent_pad(n : Int) -> String {
let sb = StringBuilder::new()
for _ in 0..<(n * 2) {
sb.write_char(' ')
}
sb.to_string()
}
///|
/// Escape a string's contents for printing inside GraphQL double quotes.
fn escape_string(s : String) -> String {
let sb = StringBuilder::new()
for c in s {
match c {
'"' => sb.write_string("\\\"")
'\\' => sb.write_string("\\\\")
'\n' => sb.write_string("\\n")
'\r' => sb.write_string("\\r")
'\t' => sb.write_string("\\t")
_ => sb.write_char(c)
}
}
sb.to_string()
}
///|
/// Print a value back to GraphQL literal syntax.
pub fn Value::to_query(self : Value) -> String {
match self {
Variable(n) => "$" + n
IntValue(s) => s
FloatValue(s) => s
StringValue(s, block) =>
if block {
"\"\"\"" + s + "\"\"\""
} else {
"\"" + escape_string(s) + "\""
}
BooleanValue(b) => if b { "true" } else { "false" }
NullValue => "null"
EnumValue(n) => n
ListValue(items) => {
let sb = StringBuilder::new()
sb.write_char('[')
for i, v in items {
if i > 0 {
sb.write_string(", ")
}
sb.write_string(v.to_query())
}
sb.write_char(']')
sb.to_string()
}
ObjectValue(fields) => {
let sb = StringBuilder::new()
sb.write_char('{')
for i, kv in fields {
if i > 0 {
sb.write_string(", ")
}
let (k, v) = kv
sb.write_string(k)
sb.write_string(": ")
sb.write_string(v.to_query())
}
sb.write_char('}')
sb.to_string()
}
}
}
///|
/// Print a type reference back to GraphQL notation (`[Int!]!`).
pub fn TypeRef::to_query(self : TypeRef) -> String {
match self {
NamedType(n) => n
ListType(inner) => "[" + inner.to_query() + "]"
NonNullType(inner) => inner.to_query() + "!"
}
}
///|
/// Print a directive: `@name(a: 1, b: 2)`.
fn directive_sdl(d : Directive) -> String {
let sb = StringBuilder::new()
sb.write_char('@')
sb.write_string(d.name)
sb.write_string(arguments_sdl(d.arguments))
sb.to_string()
}
///|
/// Print a directive list, each prefixed by a space, or the empty string.
fn directives_sdl(ds : Array[Directive]) -> String {
let sb = StringBuilder::new()
for d in ds {
sb.write_char(' ')
sb.write_string(directive_sdl(d))
}
sb.to_string()
}
///|
/// Print an argument list `(a: 1, b: 2)`, or the empty string when there are none.
fn arguments_sdl(args : Array[Argument]) -> String {
if args.length() == 0 {
return ""
}
let sb = StringBuilder::new()
sb.write_char('(')
for i, a in args {
if i > 0 {
sb.write_string(", ")
}
sb.write_string(a.name)
sb.write_string(": ")
sb.write_string(a.value.to_query())
}
sb.write_char(')')
sb.to_string()
}
///|
/// Print a selection set as an indented `{ ... }` block at the given depth.
fn selection_set_sdl(sels : Array[Selection], depth : Int) -> String {
let sb = StringBuilder::new()
sb.write_string("{\n")
let pad = indent_pad(depth + 1)
for s in sels {
sb.write_string(pad)
sb.write_string(selection_sdl(s, depth + 1))
sb.write_char('\n')
}
sb.write_string(indent_pad(depth))
sb.write_char('}')
sb.to_string()
}
///|
/// Print one selection (field / fragment spread / inline fragment) at `depth`.
fn selection_sdl(sel : Selection, depth : Int) -> String {
match sel {
FieldSel(f) => {
let sb = StringBuilder::new()
match f.alias_ {
Some(a) => {
sb.write_string(a)
sb.write_string(": ")
}
None => ()
}
sb.write_string(f.name)
sb.write_string(arguments_sdl(f.arguments))
sb.write_string(directives_sdl(f.directives))
if f.selection_set.length() > 0 {
sb.write_char(' ')
sb.write_string(selection_set_sdl(f.selection_set, depth))
}
sb.to_string()
}
FragmentSpreadSel(name, ds) => "..." + name + directives_sdl(ds)
InlineFragmentSel(cond, ds, sels) => {
let sb = StringBuilder::new()
sb.write_string("...")
match cond {
Some(c) => {
sb.write_string(" on ")
sb.write_string(c)
}
None => ()
}
sb.write_string(directives_sdl(ds))
sb.write_char(' ')
sb.write_string(selection_set_sdl(sels, depth))
sb.to_string()
}
}
}
///|
/// The keyword for an operation type.
fn operation_keyword(op : OperationType) -> String {
match op {
Query => "query"
Mutation => "mutation"
Subscription => "subscription"
}
}
///|
/// Print an operation definition, using the `{ ... }` shorthand only for an
/// anonymous query with no variables or directives (matching `graphql-core`).
fn operation_sdl(op : OperationDefinition) -> String {
let shorthand = op.operation is Query &&
op.name is None &&
op.variable_definitions.length() == 0 &&
op.directives.length() == 0
if shorthand {
return selection_set_sdl(op.selection_set, 0)
}
let sb = StringBuilder::new()
sb.write_string(operation_keyword(op.operation))
match op.name {
Some(n) => {
sb.write_char(' ')
sb.write_string(n)
}
None => ()
}
if op.variable_definitions.length() > 0 {
sb.write_char('(')
for i, vd in op.variable_definitions {
if i > 0 {
sb.write_string(", ")
}
sb.write_char('$')
sb.write_string(vd.variable)
sb.write_string(": ")
sb.write_string(vd.typ.to_query())
match vd.default_value {
Some(v) => {
sb.write_string(" = ")
sb.write_string(v.to_query())
}
None => ()
}
sb.write_string(directives_sdl(vd.directives))
}
sb.write_char(')')
}
sb.write_string(directives_sdl(op.directives))
sb.write_char(' ')
sb.write_string(selection_set_sdl(op.selection_set, 0))
sb.to_string()
}
///|
/// Print a fragment definition: `fragment Name on Type @dir { ... }`.
fn fragment_sdl(fr : FragmentDefinition) -> String {
let sb = StringBuilder::new()
sb.write_string("fragment ")
sb.write_string(fr.name)
sb.write_string(" on ")
sb.write_string(fr.type_condition)
sb.write_string(directives_sdl(fr.directives))
sb.write_char(' ')
sb.write_string(selection_set_sdl(fr.selection_set, 0))
sb.to_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.
pub fn Document::to_query(self : Document) -> String {
let sb = StringBuilder::new()
for i, def in self.definitions {
if i > 0 {
sb.write_string("\n\n")
}
match def {
OperationDef(op) => sb.write_string(operation_sdl(op))
FragmentDef(fr) => sb.write_string(fragment_sdl(fr))
}
}
sb.to_string()
}