mirror of
https://github.com/zap-proto/zip.git
synced 2026-10-19 15:38:57 +00:00
GraphQLSDL published what the ops describe; nothing ran it. This adds the
executor and mounts both halves on one path.
The resolver is op.direct, so a field runs the contract the REST route runs:
validate, authorize, handler. Two properties are enforced rather than merely
published, and each has a test that fails when the enforcement is removed:
- A query operation resolves names against GET ops only. A mutating op is
absent from the index a query searches, so it cannot be invoked by one.
- A `header:` field is ambient. The schema does not publish it as an
argument, an argument naming it is refused, and its value is bound from
the request after the arguments — what the gateway sets, a client cannot
supply.
The projection carries the Go type alongside the decoded value, which is what
separates a field absent because it was empty (null) from a field absent from
the type (an error). Without it every misspelling silently answers null.
The reader is hand-written: a dependency here is a dependency in every service
that links zip, to read a grammar whose executable half is six token kinds and
five productions. It takes operations, variables, aliases, fragments, inline
fragments and @skip/@include. Subscriptions and introspection are refused by
name — no op is a stream, and the schema is published whole at GET.
Also corrects the schema walk to use wireFields, so an embedded struct's
promoted fields appear, and renders time.Time as String rather than by fields
that never cross the wire.
Co-authored-by: Hanzo Dev <[email protected]>
754 lines
18 KiB
Go
754 lines
18 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, or a spread of a named fragment.
|
|
type gqlSel struct {
|
|
alias string
|
|
name string
|
|
args map[string]any
|
|
sel []*gqlSel
|
|
spread string // non-empty: spread this fragment here instead
|
|
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
|
|
}
|
|
}
|
|
return d, p.err
|
|
}
|
|
|
|
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 is flattened into the selection that holds it, and its type
|
|
// condition carries no meaning here: every type this schema 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`")
|
|
}
|
|
if _, _, err := p.directives(); err != nil {
|
|
return nil, err
|
|
}
|
|
return p.selection()
|
|
}
|
|
if p.is("{") {
|
|
return p.selection()
|
|
}
|
|
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 []*gqlSel{s}, 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)
|
|
}
|