mirror of
https://github.com/zap-proto/zip.git
synced 2026-10-19 15:38:57 +00:00
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.
823 lines
20 KiB
Go
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)
|
|
}
|