Files
zip/document.go
zeekay 5331b2e456 One GraphQL engine: a resolver is handed the whole selection
A graph served for something else (MountGraphFor) now gets what a
second engine had been written for:

- The request is gathered before anything resolves: fragments spread
  once per selection set, cycles and unknown fragments refused at
  parse, @skip and @include decided (on inline fragments too), fields
  under one key merged, variables put in place. A Resolver receives
  the field as a Selection with its whole subtree, so it can fetch
  only what is selected or refuse a request too deep to answer.
- An Object answer is asked for each selected field with that field's
  own arguments, so nesting can be traversal. Selection.Bind decodes
  arguments into a struct, refusing unknown ones.
- The schema is rendered per request with the caller's context, and a
  schema that fails is a 500 that says so.
- Bounds on the gathered tree refuse whole: 64 root fields, 10000
  fields in all, depth 64.
- A query's root fields resolve at once; a mutation's in order. A
  panic fails its field.
- Keys go out in the order selected. A given variable the operation
  does not declare is ignored, and a declared one given nothing leaves
  its argument out, both as the specification says.
- MountGraphFor's routes are declared, so a host routes to them and a
  document describes them; Headers(ctx) relays a request whole.
2026-10-18 20:08:59 -07:00

823 lines
20 KiB
Go

package zip
// Reading a GraphQL document — the lexer and parser behind [App.GraphQL].
//
// Hand-written and about four hundred lines, because a dependency here would be
// a dependency in every service that links zip, to read a grammar whose whole
// executable half is six token kinds and five productions. The schema half of
// GraphQL is large; the query half a client actually sends is not.
//
// # What it reads
//
// Operations (named, anonymous, and the bare `{ ... }` shorthand), variables
// with defaults, fields with aliases and arguments, nested selections, named
// fragments, inline fragments, and @skip/@include. Every value form: strings
// including block strings, ints, floats, booleans, null, enums, lists, objects,
// and variable references.
//
// # What it refuses, and why refusing is the honest answer
//
// A subscription parses to an operation no registry can answer, since no op is a
// stream. An unknown directive changes what a field means, so ignoring one would
// answer a question that was not asked. Both are named in the error rather than
// quietly dropped.
import (
"fmt"
"strconv"
"strings"
)
// depthLimit bounds nesting. The parser recurses once per level, so a document
// nested deeply enough would exhaust the stack before any handler saw it. This
// is a bound on recursion, not a policy: no real query approaches it.
const depthLimit = 64
// gqlDoc is one parsed document: what it can run, and the fragments those runs
// may spread.
type gqlDoc struct {
ops []*gqlOp
frags map[string][]*gqlSel
}
type gqlOp struct {
kind string // "query" or "mutation"
name string
vars []gqlVar
sel []*gqlSel
}
type gqlVar struct {
name string
typ string
req bool
def any
}
// gqlSel is one selected thing: a field, a spread of a named fragment, or an
// inline fragment.
type gqlSel struct {
alias string
name string
args map[string]any
sel []*gqlSel
spread string // non-empty: spread this fragment here instead
inline bool // an inline fragment: sel is its body
skipIf any
includeIf any
}
// key is the name this selection answers under — its alias where it has one,
// which is the whole point of an alias.
func (s *gqlSel) key() string {
if s.alias != "" {
return s.alias
}
return s.name
}
// pick chooses the operation to run.
//
// An unnamed request against a multi-operation document is refused rather than
// resolved by position: which one ran would then depend on the order they were
// written in, and a client that adds an operation would silently start running
// a different one.
func (d *gqlDoc) pick(name string) (*gqlOp, error) {
if len(d.ops) == 0 {
return nil, fmt.Errorf("the document defines no operation to run")
}
if name == "" {
if len(d.ops) > 1 {
return nil, fmt.Errorf("the document defines %d operations; name the one to run", len(d.ops))
}
return d.ops[0], nil
}
for _, o := range d.ops {
if o.name == name {
return o, nil
}
}
return nil, fmt.Errorf("no operation named %q in the document", name)
}
// gqlVarRef is a variable where a value would be. It stays a reference through
// parsing so one parsed document could serve any set of variables, and so an
// undefined one is caught by name at execution rather than becoming a null.
type gqlVarRef string
// ---------------------------------------------------------------- the lexer
// gqlToken kinds: 'n' name, 'i' int, 'f' float, 's' string, 'p' punctuator,
// and 0 for the end of the document.
type gqlToken struct {
kind byte
text string
}
type gqlLexer struct {
src string
pos int
}
const punctuators = "!$&()[]{}:=@|"
func (l *gqlLexer) next() (gqlToken, error) {
l.skip()
if l.pos >= len(l.src) {
return gqlToken{}, nil
}
c := l.src[l.pos]
switch {
case c == '_' || isAlpha(c):
start := l.pos
for l.pos < len(l.src) && (isAlpha(l.src[l.pos]) || isDigit(l.src[l.pos]) || l.src[l.pos] == '_') {
l.pos++
}
return gqlToken{'n', l.src[start:l.pos]}, nil
case c == '-' || isDigit(c):
return l.number(), nil
case c == '"':
return l.text()
case strings.IndexByte(punctuators, c) >= 0:
l.pos++
return gqlToken{'p', string(c)}, nil
case strings.HasPrefix(l.src[l.pos:], "..."):
l.pos += 3
return gqlToken{'p', "..."}, nil
}
return gqlToken{}, fmt.Errorf("unexpected character %q", string(rune(c)))
}
// skip passes over everything GraphQL calls ignored: whitespace, line breaks,
// commas — a comma IS whitespace in this grammar — and comments.
func (l *gqlLexer) skip() {
for l.pos < len(l.src) {
switch l.src[l.pos] {
case ' ', '\t', '\n', '\r', ',':
l.pos++
case '#':
for l.pos < len(l.src) && l.src[l.pos] != '\n' {
l.pos++
}
default:
return
}
}
}
func (l *gqlLexer) number() gqlToken {
start := l.pos
kind := byte('i')
if l.src[l.pos] == '-' {
l.pos++
}
for l.pos < len(l.src) && isDigit(l.src[l.pos]) {
l.pos++
}
if l.pos < len(l.src) && l.src[l.pos] == '.' {
kind = 'f'
l.pos++
for l.pos < len(l.src) && isDigit(l.src[l.pos]) {
l.pos++
}
}
if l.pos < len(l.src) && (l.src[l.pos] == 'e' || l.src[l.pos] == 'E') {
kind = 'f'
l.pos++
if l.pos < len(l.src) && (l.src[l.pos] == '+' || l.src[l.pos] == '-') {
l.pos++
}
for l.pos < len(l.src) && isDigit(l.src[l.pos]) {
l.pos++
}
}
return gqlToken{kind, l.src[start:l.pos]}
}
func (l *gqlLexer) text() (gqlToken, error) {
if strings.HasPrefix(l.src[l.pos:], `"""`) {
l.pos += 3
end := strings.Index(l.src[l.pos:], `"""`)
if end < 0 {
return gqlToken{}, fmt.Errorf("unterminated block string")
}
s := l.src[l.pos : l.pos+end]
l.pos += end + 3
return gqlToken{'s', undent(s)}, nil
}
l.pos++ // the opening quote
var b strings.Builder
for l.pos < len(l.src) {
switch c := l.src[l.pos]; c {
case '"':
l.pos++
return gqlToken{'s', b.String()}, nil
case '\n', '\r':
return gqlToken{}, fmt.Errorf("unterminated string")
case '\\':
l.pos++
if l.pos >= len(l.src) {
return gqlToken{}, fmt.Errorf("unterminated string")
}
switch l.src[l.pos] {
case 'n':
b.WriteByte('\n')
case 't':
b.WriteByte('\t')
case 'r':
b.WriteByte('\r')
case 'b':
b.WriteByte('\b')
case 'f':
b.WriteByte('\f')
case '/', '\\', '"':
b.WriteByte(l.src[l.pos])
case 'u':
if l.pos+5 > len(l.src) {
return gqlToken{}, fmt.Errorf("truncated \\u escape")
}
n, err := strconv.ParseUint(l.src[l.pos+1:l.pos+5], 16, 32)
if err != nil {
return gqlToken{}, fmt.Errorf("bad \\u escape %q", l.src[l.pos+1:l.pos+5])
}
b.WriteRune(rune(n))
l.pos += 4
default:
return gqlToken{}, fmt.Errorf("unknown escape \\%s", string(rune(l.src[l.pos])))
}
l.pos++
default:
b.WriteByte(c)
l.pos++
}
}
return gqlToken{}, fmt.Errorf("unterminated string")
}
// undent removes a block string's common indentation, so a value written inside
// an indented query means what it looks like it means.
func undent(s string) string {
lines := strings.Split(strings.ReplaceAll(s, "\r\n", "\n"), "\n")
common := -1
for _, ln := range lines[1:] {
trimmed := strings.TrimLeft(ln, " \t")
if trimmed == "" {
continue // a blank line indents nothing
}
if n := len(ln) - len(trimmed); common < 0 || n < common {
common = n
}
}
if common > 0 {
for i, ln := range lines[1:] {
if len(ln) >= common {
lines[i+1] = ln[common:]
}
}
}
for len(lines) > 0 && strings.TrimSpace(lines[0]) == "" {
lines = lines[1:]
}
for len(lines) > 0 && strings.TrimSpace(lines[len(lines)-1]) == "" {
lines = lines[:len(lines)-1]
}
return strings.Join(lines, "\n")
}
func isAlpha(c byte) bool { return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' }
func isDigit(c byte) bool { return c >= '0' && c <= '9' }
// ---------------------------------------------------------------- the parser
type gqlParser struct {
lex gqlLexer
tok gqlToken
err error
depth int
}
// advance reads one token. A lexer error ends the document as well as recording
// itself, so every loop below terminates on bad input instead of spinning on a
// token that will not change.
func (p *gqlParser) advance() {
tok, err := p.lex.next()
if err != nil {
p.err, p.tok = err, gqlToken{}
return
}
p.tok = tok
}
func (p *gqlParser) is(text string) bool {
return p.tok.kind == 'p' && p.tok.text == text
}
func (p *gqlParser) name() (string, bool) {
if p.tok.kind != 'n' {
return "", false
}
s := p.tok.text
p.advance()
return s, true
}
func parseGraph(src string) (*gqlDoc, error) {
if strings.TrimSpace(src) == "" {
return nil, fmt.Errorf("the request carries no query")
}
p := &gqlParser{lex: gqlLexer{src: strings.TrimPrefix(src, "\ufeff")}}
p.advance()
if p.err != nil {
return nil, p.err
}
d := &gqlDoc{frags: map[string][]*gqlSel{}}
for p.tok.kind != 0 {
switch {
case p.is("{"):
sel, err := p.selection()
if err != nil {
return nil, err
}
d.ops = append(d.ops, &gqlOp{kind: "query", sel: sel})
case p.tok.kind == 'n' && (p.tok.text == "query" || p.tok.text == "mutation"):
op, err := p.operation()
if err != nil {
return nil, err
}
d.ops = append(d.ops, op)
case p.tok.kind == 'n' && p.tok.text == "subscription":
return nil, fmt.Errorf("subscriptions are not served: no op is a stream")
case p.tok.kind == 'n' && p.tok.text == "fragment":
name, sel, err := p.fragment()
if err != nil {
return nil, err
}
if _, dup := d.frags[name]; dup {
return nil, fmt.Errorf("fragment %q is defined twice", name)
}
d.frags[name] = sel
default:
return nil, fmt.Errorf("unexpected %q where an operation or fragment was expected", p.tok.text)
}
if p.err != nil {
return nil, p.err
}
}
if p.err != nil {
return nil, p.err
}
if err := d.check(); err != nil {
return nil, err
}
return d, nil
}
// check refuses a document whose fragments cannot be expanded: a spread of a
// fragment nobody defined, and a fragment that reaches itself through any chain
// of spreads. Both are found here, before anything runs, because a cycle found
// during execution has already spent the work up to it.
func (d *gqlDoc) check() error {
const (
open = 1 // on the path being walked
done = 2 // walked, and reaches no cycle
)
state := map[string]int{}
var walk func(sel []*gqlSel) error
walk = func(sel []*gqlSel) error {
for _, s := range sel {
if s.spread == "" {
if err := walk(s.sel); err != nil {
return err
}
continue
}
body, ok := d.frags[s.spread]
if !ok {
return fmt.Errorf("unknown fragment %q", s.spread)
}
switch state[s.spread] {
case open:
return fmt.Errorf("fragment %q spreads itself", s.spread)
case done:
continue
}
state[s.spread] = open
if err := walk(body); err != nil {
return err
}
state[s.spread] = done
}
return nil
}
for _, o := range d.ops {
if err := walk(o.sel); err != nil {
return err
}
}
for _, body := range d.frags {
if err := walk(body); err != nil {
return err
}
}
return nil
}
func (p *gqlParser) operation() (*gqlOp, error) {
op := &gqlOp{kind: p.tok.text}
p.advance()
if n, ok := p.name(); ok {
op.name = n
}
if p.is("(") {
vars, err := p.varDefs()
if err != nil {
return nil, err
}
op.vars = vars
}
if _, _, err := p.directives(); err != nil {
return nil, err
}
sel, err := p.selection()
if err != nil {
return nil, err
}
op.sel = sel
return op, nil
}
func (p *gqlParser) fragment() (string, []*gqlSel, error) {
p.advance() // "fragment"
name, ok := p.name()
if !ok {
return "", nil, fmt.Errorf("expected a name after `fragment`")
}
if on, ok := p.name(); !ok || on != "on" {
return "", nil, fmt.Errorf("expected `on` after fragment %q", name)
}
if _, ok := p.name(); !ok {
return "", nil, fmt.Errorf("expected a type after `on` in fragment %q", name)
}
if _, _, err := p.directives(); err != nil {
return "", nil, err
}
sel, err := p.selection()
if err != nil {
return "", nil, err
}
return name, sel, nil
}
func (p *gqlParser) varDefs() ([]gqlVar, error) {
p.advance() // "("
var out []gqlVar
for !p.is(")") {
if p.tok.kind == 0 {
return nil, fmt.Errorf("unterminated variable definitions")
}
if !p.is("$") {
return nil, fmt.Errorf("expected $variable, got %q", p.tok.text)
}
p.advance()
name, ok := p.name()
if !ok {
return nil, fmt.Errorf("expected a variable name after $")
}
if !p.is(":") {
return nil, fmt.Errorf("expected : after $%s", name)
}
p.advance()
typ, req, err := p.typeRef()
if err != nil {
return nil, err
}
v := gqlVar{name: name, typ: typ, req: req}
if p.is("=") {
p.advance()
def, err := p.value()
if err != nil {
return nil, err
}
v.def = def
}
out = append(out, v)
}
p.advance() // ")"
return out, nil
}
// typeRef reads a variable's declared type. The name is kept for error messages
// and the ! for whether a value must be given; the type itself is not checked
// here, because the op's own validator checks the value that actually arrives —
// checking twice would let the two disagree.
func (p *gqlParser) typeRef() (string, bool, error) {
var name string
switch {
case p.is("["):
p.advance()
inner, innerReq, err := p.typeRef()
if err != nil {
return "", false, err
}
if !p.is("]") {
return "", false, fmt.Errorf("expected ] closing a list type")
}
p.advance()
if innerReq {
inner += "!"
}
name = "[" + inner + "]"
default:
n, ok := p.name()
if !ok {
return "", false, fmt.Errorf("expected a type name, got %q", p.tok.text)
}
name = n
}
if p.is("!") {
p.advance()
return name, true, nil
}
return name, false, nil
}
func (p *gqlParser) selection() ([]*gqlSel, error) {
if !p.is("{") {
if p.err != nil {
return nil, p.err
}
return nil, fmt.Errorf("expected { opening a selection, got %q", p.tok.text)
}
p.depth++
if p.depth > depthLimit {
return nil, fmt.Errorf("selections nest deeper than %d levels", depthLimit)
}
defer func() { p.depth-- }()
p.advance()
var out []*gqlSel
for !p.is("}") {
if p.tok.kind == 0 {
if p.err != nil {
return nil, p.err
}
return nil, fmt.Errorf("unterminated selection")
}
if p.is("...") {
inner, err := p.spread()
if err != nil {
return nil, err
}
out = append(out, inner)
continue
}
f, err := p.field()
if err != nil {
return nil, err
}
out = append(out, f)
}
p.advance() // "}"
if len(out) == 0 {
return nil, fmt.Errorf("a selection cannot be empty")
}
return out, nil
}
// spread reads `...Name` or `... on Type { ... }`.
//
// An inline fragment keeps its own directives, so `... @include(if: $v) { a b }`
// decides both fields at once. Its type condition carries no meaning here: every
// type a graph publishes is a concrete object, no field answers with an
// interface or a union, so a condition can only ever name the type the field
// already has.
func (p *gqlParser) spread() (*gqlSel, error) {
p.advance() // "..."
if p.tok.kind == 'n' && p.tok.text == "on" {
p.advance()
if _, ok := p.name(); !ok {
return nil, fmt.Errorf("expected a type after `on`")
}
return p.inline()
}
if p.is("{") || p.is("@") {
return p.inline()
}
name, ok := p.name()
if !ok {
return nil, fmt.Errorf("expected a fragment name after ...")
}
s := &gqlSel{spread: name}
skip, incl, err := p.directives()
if err != nil {
return nil, err
}
s.skipIf, s.includeIf = skip, incl
return s, nil
}
// inline reads an inline fragment's directives and body.
func (p *gqlParser) inline() (*gqlSel, error) {
skip, incl, err := p.directives()
if err != nil {
return nil, err
}
sel, err := p.selection()
if err != nil {
return nil, err
}
return &gqlSel{inline: true, sel: sel, skipIf: skip, includeIf: incl}, nil
}
func (p *gqlParser) field() (*gqlSel, error) {
name, ok := p.name()
if !ok {
return nil, fmt.Errorf("expected a field name, got %q", p.tok.text)
}
f := &gqlSel{name: name}
if p.is(":") {
p.advance()
real, ok := p.name()
if !ok {
return nil, fmt.Errorf("expected a field name after the alias %q", name)
}
f.alias, f.name = name, real
}
if p.is("(") {
args, err := p.args()
if err != nil {
return nil, err
}
f.args = args
}
skip, incl, err := p.directives()
if err != nil {
return nil, err
}
f.skipIf, f.includeIf = skip, incl
if p.is("{") {
sel, err := p.selection()
if err != nil {
return nil, err
}
f.sel = sel
}
return f, nil
}
// directives reads @skip and @include and refuses anything else.
//
// A directive changes what a field means. Ignoring an unknown one would answer a
// question the client did not ask — which is worse than saying it is unknown.
func (p *gqlParser) directives() (skip, include any, err error) {
for p.is("@") {
p.advance()
name, ok := p.name()
if !ok {
return nil, nil, fmt.Errorf("expected a directive name after @")
}
var args map[string]any
if p.is("(") {
args, err = p.args()
if err != nil {
return nil, nil, err
}
}
switch name {
case "skip", "include":
cond, ok := args["if"]
if !ok {
return nil, nil, fmt.Errorf("@%s needs an `if` argument", name)
}
if name == "skip" {
skip = cond
} else {
include = cond
}
default:
return nil, nil, fmt.Errorf("unknown directive @%s", name)
}
}
return skip, include, nil
}
func (p *gqlParser) args() (map[string]any, error) {
p.advance() // "("
out := map[string]any{}
for !p.is(")") {
if p.tok.kind == 0 {
if p.err != nil {
return nil, p.err
}
return nil, fmt.Errorf("unterminated arguments")
}
name, ok := p.name()
if !ok {
return nil, fmt.Errorf("expected an argument name, got %q", p.tok.text)
}
if !p.is(":") {
return nil, fmt.Errorf("expected : after the argument %q", name)
}
p.advance()
v, err := p.value()
if err != nil {
return nil, err
}
out[name] = v
}
p.advance() // ")"
return out, nil
}
func (p *gqlParser) value() (any, error) {
p.depth++
if p.depth > depthLimit {
return nil, fmt.Errorf("values nest deeper than %d levels", depthLimit)
}
defer func() { p.depth-- }()
switch {
case p.is("$"):
p.advance()
name, ok := p.name()
if !ok {
return nil, fmt.Errorf("expected a variable name after $")
}
return gqlVarRef(name), nil
case p.tok.kind == 's':
s := p.tok.text
p.advance()
return s, nil
case p.tok.kind == 'i':
n, err := strconv.ParseInt(p.tok.text, 10, 64)
if err != nil {
return nil, fmt.Errorf("integer %q is out of range", p.tok.text)
}
p.advance()
return n, nil
case p.tok.kind == 'f':
n, err := strconv.ParseFloat(p.tok.text, 64)
if err != nil {
return nil, fmt.Errorf("number %q is out of range", p.tok.text)
}
p.advance()
return n, nil
case p.tok.kind == 'n':
switch s := p.tok.text; s {
case "true", "false":
p.advance()
return s == "true", nil
case "null":
p.advance()
return nil, nil
default:
// An enum is carried as its own name: the Go side receives a string,
// which is what an enum field is declared as.
p.advance()
return s, nil
}
case p.is("["):
p.advance()
out := []any{}
for !p.is("]") {
if p.tok.kind == 0 {
return nil, fmt.Errorf("unterminated list value")
}
v, err := p.value()
if err != nil {
return nil, err
}
out = append(out, v)
}
p.advance()
return out, nil
case p.is("{"):
p.advance()
out := map[string]any{}
for !p.is("}") {
if p.tok.kind == 0 {
return nil, fmt.Errorf("unterminated object value")
}
name, ok := p.name()
if !ok {
return nil, fmt.Errorf("expected a field name in an object value, got %q", p.tok.text)
}
if !p.is(":") {
return nil, fmt.Errorf("expected : after the object field %q", name)
}
p.advance()
v, err := p.value()
if err != nil {
return nil, err
}
out[name] = v
}
p.advance()
return out, nil
}
if p.err != nil {
return nil, p.err
}
return nil, fmt.Errorf("unexpected %q where a value was expected", p.tok.text)
}