Move how the template language is written into internal/grammar
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
package grammar
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// CalcNode is a parsed calc expression node.
|
||||
type CalcNode interface{ calcNode() }
|
||||
|
||||
type CalcNum float64 // a number literal
|
||||
|
||||
// CalcVar is an operand, a sibling field or a name. At is its position among the
|
||||
// distinct operands in the order the expression first names each, which CalcVars lists.
|
||||
type CalcVar struct {
|
||||
Name string
|
||||
At int
|
||||
}
|
||||
|
||||
type CalcNeg struct{ X CalcNode }
|
||||
|
||||
type CalcBin struct { // L + - * / R
|
||||
Operator byte
|
||||
L, R CalcNode
|
||||
}
|
||||
|
||||
func (CalcNum) calcNode() {}
|
||||
func (CalcVar) calcNode() {}
|
||||
func (CalcNeg) calcNode() {}
|
||||
func (CalcBin) calcNode() {}
|
||||
|
||||
// CalcVars lists the distinct operands an expression reads, each at its At.
|
||||
func CalcVars(n CalcNode) []string {
|
||||
var out []string
|
||||
var walk func(CalcNode)
|
||||
walk = func(n CalcNode) {
|
||||
switch n := n.(type) {
|
||||
case CalcVar:
|
||||
if n.At == len(out) {
|
||||
out = append(out, n.Name)
|
||||
}
|
||||
case CalcNeg:
|
||||
walk(n.X)
|
||||
case CalcBin:
|
||||
walk(n.L)
|
||||
walk(n.R)
|
||||
}
|
||||
}
|
||||
walk(n)
|
||||
return out
|
||||
}
|
||||
|
||||
// CalcText spells an expression node the way an author would read it.
|
||||
func CalcText(n CalcNode) string {
|
||||
switch n := n.(type) {
|
||||
case CalcNum:
|
||||
return strconv.FormatFloat(float64(n), 'f', -1, 64)
|
||||
case CalcVar:
|
||||
return n.Name
|
||||
case CalcNeg:
|
||||
return "-" + CalcText(n.X)
|
||||
case CalcBin:
|
||||
return "(" + CalcText(n.L) + " " + string(n.Operator) + " " + CalcText(n.R) + ")"
|
||||
}
|
||||
return "?"
|
||||
}
|
||||
|
||||
// calcParser is a recursive-descent parser over the expression runes, threading
|
||||
// expr -> term -> factor for the standard * / before + - precedence.
|
||||
type calcParser struct {
|
||||
rs []rune
|
||||
pos int
|
||||
vars map[string]int
|
||||
}
|
||||
|
||||
// ParseCalc parses a whole expression, requiring it to consume all input.
|
||||
func ParseCalc(expr string) (CalcNode, error) {
|
||||
p := &calcParser{rs: []rune(expr), vars: map[string]int{}}
|
||||
if p.space(); p.pos >= len(p.rs) {
|
||||
return nil, fmt.Errorf("empty expression")
|
||||
}
|
||||
n, err := p.expr()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if p.space(); p.pos != len(p.rs) {
|
||||
return nil, fmt.Errorf("unexpected %q", string(p.rs[p.pos:]))
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (p *calcParser) space() {
|
||||
for p.pos < len(p.rs) && unicode.IsSpace(p.rs[p.pos]) {
|
||||
p.pos++
|
||||
}
|
||||
}
|
||||
|
||||
func (p *calcParser) expr() (CalcNode, error) { return p.binary(p.term, '+', '-') }
|
||||
func (p *calcParser) term() (CalcNode, error) { return p.binary(p.factor, '*', '/') }
|
||||
|
||||
// binary parses a left-associative run of next() operands joined by the given
|
||||
// operators, the one shape expr and term share.
|
||||
func (p *calcParser) binary(next func() (CalcNode, error), ops ...byte) (CalcNode, error) {
|
||||
n, err := next()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for {
|
||||
p.space()
|
||||
if p.pos >= len(p.rs) || !slices.Contains(ops, byte(p.rs[p.pos])) {
|
||||
return n, nil
|
||||
}
|
||||
op := byte(p.rs[p.pos])
|
||||
p.pos++
|
||||
r, err := next()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
n = CalcBin{op, n, r}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *calcParser) factor() (CalcNode, error) {
|
||||
p.space()
|
||||
if p.pos >= len(p.rs) {
|
||||
return nil, fmt.Errorf("unexpected end of expression")
|
||||
}
|
||||
switch c := p.rs[p.pos]; {
|
||||
case c == '-':
|
||||
p.pos++
|
||||
x, err := p.factor()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return CalcNeg{x}, nil
|
||||
case c == '(':
|
||||
p.pos++
|
||||
n, err := p.expr()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if p.space(); p.pos >= len(p.rs) || p.rs[p.pos] != ')' {
|
||||
return nil, fmt.Errorf("missing ')'")
|
||||
}
|
||||
p.pos++
|
||||
return n, nil
|
||||
case c == '.' || c >= '0' && c <= '9':
|
||||
return p.number()
|
||||
case c == '_' || unicode.IsLetter(c):
|
||||
return p.ident()
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected %q", string(c))
|
||||
}
|
||||
}
|
||||
|
||||
func (p *calcParser) number() (CalcNode, error) {
|
||||
start, dot := p.pos, false
|
||||
for p.pos < len(p.rs) {
|
||||
if c := p.rs[p.pos]; c >= '0' && c <= '9' {
|
||||
p.pos++
|
||||
} else if c == '.' && !dot {
|
||||
dot, p.pos = true, p.pos+1
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
v, err := strconv.ParseFloat(string(p.rs[start:p.pos]), 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("bad number %q", string(p.rs[start:p.pos]))
|
||||
}
|
||||
return CalcNum(v), nil
|
||||
}
|
||||
|
||||
// ident reads a field or name: a letter or '_', then letters, digits or '_'. A '-'
|
||||
// is always the minus operator, so a hyphenated one can't be an operand.
|
||||
func (p *calcParser) ident() (CalcNode, error) {
|
||||
start := p.pos
|
||||
for p.pos < len(p.rs) {
|
||||
if c := p.rs[p.pos]; c == '_' || unicode.IsLetter(c) || unicode.IsDigit(c) {
|
||||
p.pos++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
name := string(p.rs[start:p.pos])
|
||||
at, seen := p.vars[name]
|
||||
if !seen {
|
||||
at = len(p.vars)
|
||||
p.vars[name] = at
|
||||
}
|
||||
return CalcVar{name, at}, nil
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
// Package grammar is how the template language is written: format tokens, paths and
|
||||
// their selectors, reference sigils, names, and calc expressions. It reads strings only.
|
||||
package grammar
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// scanUnit is one unit of a scanned format string: a literal rune or the body of a
|
||||
// {…} token.
|
||||
type scanUnit struct {
|
||||
isToken bool // a {…} body, else a literal rune
|
||||
char rune
|
||||
body string
|
||||
}
|
||||
|
||||
// eachScanUnit scans a format string once and calls fn for each unit, the single
|
||||
// source of truth for how braces are read: "{{" and "}}" are literal braces, a "{"
|
||||
// opens a token that must reach its "}", and a lone "}" is an error.
|
||||
func eachScanUnit(format string, fn func(scanUnit) error) error {
|
||||
rs := []rune(format)
|
||||
for i := 0; i < len(rs); i++ {
|
||||
var u scanUnit
|
||||
switch c := rs[i]; c {
|
||||
case '{':
|
||||
if i+1 < len(rs) && rs[i+1] == '{' {
|
||||
u.char = '{'
|
||||
i++
|
||||
break
|
||||
}
|
||||
end := i + 1
|
||||
for end < len(rs) && rs[end] != '}' {
|
||||
if rs[end] == '{' {
|
||||
return fmt.Errorf("'{' inside a token in %q; a literal brace is written {{", format)
|
||||
}
|
||||
end++
|
||||
}
|
||||
if end >= len(rs) {
|
||||
return fmt.Errorf("unterminated '{' in %q", format)
|
||||
}
|
||||
u.isToken, u.body = true, string(rs[i+1:end])
|
||||
i = end
|
||||
case '}':
|
||||
if i+1 < len(rs) && rs[i+1] == '}' {
|
||||
u.char = '}'
|
||||
i++
|
||||
break
|
||||
}
|
||||
return fmt.Errorf("lone '}' in %q; a literal brace is written }}", format)
|
||||
default:
|
||||
u.char = c
|
||||
}
|
||||
if err := fn(u); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Token is one parsed unit of a format.
|
||||
type Token struct {
|
||||
Kind TokenKind
|
||||
Lit string // LiteralRun
|
||||
Body string // the braces' content, as written
|
||||
Fn string // BuiltinCall
|
||||
Args []string // BuiltinCall
|
||||
Names []string // NameRead: the '|' arms; BuiltinCall: the operands its builtin reads
|
||||
// NameBind: the reference it binds, and the name.
|
||||
BoundRef, Bound string
|
||||
}
|
||||
|
||||
type TokenKind uint8
|
||||
|
||||
const (
|
||||
BuiltinCall TokenKind = iota + 1
|
||||
NameRead
|
||||
LiteralRun
|
||||
NameBind
|
||||
)
|
||||
|
||||
const AsWord = " as "
|
||||
|
||||
// ParseFormat is the one reading of a format's tokens: a '(' outside a selector makes
|
||||
// a token a call. operands lists the operands a call reads; nil reads none.
|
||||
func ParseFormat(format string, operands func(fn string, args []string) []string) ([]Token, error) {
|
||||
var toks []Token
|
||||
var lit strings.Builder
|
||||
flush := func() {
|
||||
if lit.Len() > 0 {
|
||||
toks = append(toks, Token{Kind: LiteralRun, Lit: lit.String()})
|
||||
lit.Reset()
|
||||
}
|
||||
}
|
||||
err := eachScanUnit(format, func(u scanUnit) error {
|
||||
if !u.isToken {
|
||||
lit.WriteRune(u.char)
|
||||
return nil
|
||||
}
|
||||
flush()
|
||||
if ref, name, binds := CutOutside(u.body, AsWord); binds && IndexOutside(u.body, '(') < 0 {
|
||||
toks = append(toks, Token{Kind: NameBind, Body: u.body, BoundRef: ref, Bound: name})
|
||||
return nil
|
||||
}
|
||||
if IndexOutside(u.body, '(') < 0 {
|
||||
toks = append(toks, Token{Kind: NameRead, Body: u.body, Names: SplitOutside(u.body, '|')})
|
||||
return nil
|
||||
}
|
||||
name, args, ok := FuncCall(u.body)
|
||||
if !ok {
|
||||
return fmt.Errorf("malformed function token {%s}", u.body)
|
||||
}
|
||||
tok := Token{Kind: BuiltinCall, Body: u.body, Fn: name, Args: args}
|
||||
if operands != nil {
|
||||
tok.Names = operands(name, args)
|
||||
}
|
||||
toks = append(toks, tok)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
flush()
|
||||
return toks, nil
|
||||
}
|
||||
|
||||
// FuncCall splits a "{token}" body shaped name(args) into its parts; ok is false
|
||||
// for a name-read body. A '(' without a trailing ')' yields ok=false.
|
||||
func FuncCall(body string) (name string, args []string, ok bool) {
|
||||
lp := IndexOutside(body, '(')
|
||||
if lp < 0 || !strings.HasSuffix(body, ")") {
|
||||
return "", nil, false
|
||||
}
|
||||
return body[:lp], SplitArgs(body[lp+1 : len(body)-1]), true
|
||||
}
|
||||
|
||||
// SplitArgs parses a function arg list: comma-separated outside a selector or a
|
||||
// quoted layout, trimmed; empty -> none.
|
||||
func SplitArgs(s string) []string {
|
||||
if strings.TrimSpace(s) == "" {
|
||||
return nil
|
||||
}
|
||||
var args []string
|
||||
depth, quoted, start := 0, false, 0
|
||||
for i := 0; i < len(s); i++ {
|
||||
switch c := s[i]; {
|
||||
case c == '\'' && depth == 0:
|
||||
quoted = !quoted
|
||||
case quoted:
|
||||
case c == '[':
|
||||
depth++
|
||||
case c == ']' && depth > 0:
|
||||
depth--
|
||||
case c == ',' && depth == 0:
|
||||
args, start = append(args, strings.TrimSpace(s[start:i])), i+1
|
||||
}
|
||||
}
|
||||
return append(args, strings.TrimSpace(s[start:]))
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package grammar
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// reservedInName is what a category, folder or field name may not contain: a dot
|
||||
// separates the segments of a path, '|' the arms of a token, '(' opens a function
|
||||
// call, braces delimit the token, '/' starts a reference, and brackets and a quote
|
||||
// open a JSON value. A name carrying one is rejected where it is authored rather
|
||||
// than where it would be unreachable.
|
||||
const reservedInName = ".|({}/[]\""
|
||||
|
||||
var reservedList = strings.Join(strings.Split(reservedInName, ""), " ")
|
||||
|
||||
// CheckName rejects a name the dot path, {token} and JSON grammars cannot spell, or a struct
|
||||
// tag cannot read.
|
||||
// Both a category or folder and a field go through it, so there is one answer to
|
||||
// what a name may contain.
|
||||
func CheckName(name string) error {
|
||||
if name == "" {
|
||||
return fmt.Errorf("%q is empty, which is not a path segment, so List never offers it", name)
|
||||
}
|
||||
if name == "-" {
|
||||
return fmt.Errorf(`%q is reserved: the struct tag fake:"-" leaves a field unfilled, so no tag could read it; rename it`, name)
|
||||
}
|
||||
if strings.Contains(name, AsWord) {
|
||||
return fmt.Errorf("%q contains %q, which a token reads as binding a name; rename it", name, AsWord)
|
||||
}
|
||||
if i := strings.IndexAny(name, reservedInName); i >= 0 {
|
||||
return fmt.Errorf("%q contains %q; a name may not use %s, which the dot path, {token} and JSON grammars reserve",
|
||||
name, name[i:i+1], reservedList)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckPathNames rejects a dotted path with a segment no name may be.
|
||||
func CheckPathNames(path string) error {
|
||||
segs, err := SplitPath(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, seg := range NameSegments(segs) {
|
||||
if err := CheckName(seg); err != nil {
|
||||
return fmt.Errorf("path %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package grammar
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SplitPath splits a dotted path into segments, a selector — [key or name] after a
|
||||
// table's name — becoming a segment of its own, brackets kept, and so does each "..". A
|
||||
// dot inside a selector is part of the key or name.
|
||||
func SplitPath(path string) ([]string, error) {
|
||||
segs := make([]string, 0, strings.Count(path, ".")+2*strings.Count(path, "[")+1)
|
||||
start, open := 0, -1
|
||||
for i := 0; i < len(path); i++ {
|
||||
switch path[i] {
|
||||
case '[':
|
||||
if err := checkOpen(path, i, start, open); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
segs = append(segs, path[start:i])
|
||||
start, open = i, i
|
||||
case ']':
|
||||
if err := checkClose(path, i, open); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
segs = append(segs, path[start:i+1])
|
||||
open = -1
|
||||
segs, i = stepUpAt(segs, path, i+1)
|
||||
start = i + 1
|
||||
case '.':
|
||||
if open < 0 {
|
||||
segs = append(segs, path[start:i])
|
||||
segs, i = stepUpAt(segs, path, i)
|
||||
start = i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
if open >= 0 {
|
||||
return nil, fmt.Errorf(`%q opens a selector with "[" and never closes it with "]"`, path)
|
||||
}
|
||||
if start < len(path) || start == len(path) && (len(segs) == 0 || segs[len(segs)-1] != "..") {
|
||||
segs = append(segs, path[start:])
|
||||
}
|
||||
return segs, nil
|
||||
}
|
||||
|
||||
// stepUpAt appends a ".." segment where the dot at i is the first of two, returning the
|
||||
// index of the last dot it consumed.
|
||||
func stepUpAt(segs []string, path string, i int) ([]string, int) {
|
||||
if i+1 < len(path) && path[i+1] == '.' {
|
||||
return append(segs, ".."), i + 1
|
||||
}
|
||||
return segs, i
|
||||
}
|
||||
|
||||
// checkOpen refuses a "[" at i that opens no selector: one inside a selector, or one
|
||||
// following no name.
|
||||
func checkOpen(path string, i, start, open int) error {
|
||||
switch {
|
||||
case open >= 0:
|
||||
return fmt.Errorf(`%q holds a "[" inside a selector, which no key or name may`, path)
|
||||
case i == 0:
|
||||
return fmt.Errorf(`%q starts with "["; a path starts with a name, and a selector follows a table's name`, path)
|
||||
case i == start:
|
||||
return fmt.Errorf(`%q: a selector follows its table's name; write %s`, path, path[:i-1]+path[i:])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkClose refuses a "]" at i that closes no selector, closes an empty one, or is
|
||||
// followed by anything but a dot or the end.
|
||||
func checkClose(path string, i, open int) error {
|
||||
switch {
|
||||
case open < 0:
|
||||
return fmt.Errorf(`%q holds a "]" that closes no "["`, path)
|
||||
case i == open+1:
|
||||
return fmt.Errorf("%q holds an empty selector; a selector names a row by key or name", path)
|
||||
case i+1 < len(path) && path[i+1] != '.':
|
||||
return fmt.Errorf(`%q: a "]" ends its selector, so a dot or the end must follow it`, path)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IndexOutside is the first c in s outside a [selector], or -1.
|
||||
func IndexOutside(s string, c byte) int {
|
||||
depth := 0
|
||||
for i := 0; i < len(s); i++ {
|
||||
switch {
|
||||
case s[i] == '[':
|
||||
depth++
|
||||
case s[i] == ']' && depth > 0:
|
||||
depth--
|
||||
case s[i] == c && depth == 0:
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// CutOutside cuts s around the first sep outside a [selector].
|
||||
func CutOutside(s, sep string) (before, after string, found bool) {
|
||||
depth := 0
|
||||
for i := 0; i < len(s); i++ {
|
||||
switch {
|
||||
case s[i] == '[':
|
||||
depth++
|
||||
case s[i] == ']' && depth > 0:
|
||||
depth--
|
||||
case depth == 0 && strings.HasPrefix(s[i:], sep):
|
||||
return s[:i], s[i+len(sep):], true
|
||||
}
|
||||
}
|
||||
return s, "", false
|
||||
}
|
||||
|
||||
func SplitOutside(s string, c byte) []string {
|
||||
var parts []string
|
||||
for {
|
||||
i := IndexOutside(s, c)
|
||||
if i < 0 {
|
||||
return append(parts, s)
|
||||
}
|
||||
parts, s = append(parts, s[:i]), s[i+1:]
|
||||
}
|
||||
}
|
||||
|
||||
func IsSelector(seg string) bool { return strings.HasPrefix(seg, "[") }
|
||||
|
||||
func HasSelector(segs []string) bool {
|
||||
for _, s := range segs {
|
||||
if IsSelector(s) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func SelectorOf(seg string) string { return seg[1 : len(seg)-1] }
|
||||
|
||||
// NameSegments is the segments of a path that are names, its selectors and ".." left out.
|
||||
func NameSegments(segs []string) []string {
|
||||
out := segs[:0:0]
|
||||
for _, s := range segs {
|
||||
if !IsSelector(s) && s != ".." {
|
||||
out = append(out, s)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// JoinSegments spells segments as a path: a selector attaches to the name before it, and
|
||||
// so do a ".." and the name after it.
|
||||
func JoinSegments(segs []string) string {
|
||||
var b strings.Builder
|
||||
for i, s := range segs {
|
||||
if b.Len() > 0 && !IsSelector(s) && s != ".." && segs[i-1] != ".." {
|
||||
b.WriteByte('.')
|
||||
}
|
||||
b.WriteString(s)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package grammar
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// A reference names a node by path rather than as a sibling field: {/a.b} from the
|
||||
// data root, {.a} from the folder this file sits in, {..a} from the folder above.
|
||||
func IsRef(name string) bool { return strings.HasPrefix(name, ".") || strings.HasPrefix(name, "/") }
|
||||
|
||||
// RefShape splits a reference into its sigil and the dotted path after it.
|
||||
func RefShape(name string) (sigil, rest string, err error) {
|
||||
switch {
|
||||
case strings.HasPrefix(name, "/"):
|
||||
sigil, rest = "/", name[1:]
|
||||
case strings.HasPrefix(name, ".."):
|
||||
sigil, rest = "..", name[2:]
|
||||
default:
|
||||
sigil, rest = ".", name[1:]
|
||||
}
|
||||
if rest == "" {
|
||||
return "", "", fmt.Errorf("reference has no path")
|
||||
}
|
||||
if strings.HasPrefix(rest, "/") {
|
||||
return "", "", fmt.Errorf("the path after %s starts at a name, not a /; write {%s%s}", sigil, sigil, rest[1:])
|
||||
}
|
||||
if strings.HasPrefix(rest, ".") {
|
||||
return "", "", fmt.Errorf("a reference starts with / (the root), . (this folder) or .. (the folder above)")
|
||||
}
|
||||
segs, err := SplitPath(rest)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
for _, seg := range segs {
|
||||
if seg == "" {
|
||||
return "", "", fmt.Errorf("path has an empty segment")
|
||||
}
|
||||
}
|
||||
return sigil, rest, nil
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package grammar
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// IsTemplate reports whether arg is an inline template rather than a path, by its shape;
|
||||
// the root's IsTemplate documents the rule.
|
||||
func IsTemplate(arg string) (bool, error) {
|
||||
if strings.ContainsRune(arg, '{') || (isJSONStart(strings.TrimSpace(arg)) && json.Valid([]byte(arg))) {
|
||||
if path, lone := loneReference(arg); lone {
|
||||
return false, pathAdvice(path, fmt.Sprintf("%s is the path %s written as a template", arg, path))
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
if i := strings.IndexAny(arg, `}"`); i >= 0 {
|
||||
return false, fmt.Errorf("%q holds a %q, which no path may, and it is not valid JSON, so it names no template either", arg, arg[i:i+1])
|
||||
}
|
||||
if strings.HasPrefix(strings.TrimSpace(arg), "[") {
|
||||
return false, fmt.Errorf(`%q starts with "[" and is not valid JSON, so it names no template; a path starts with a name`, arg)
|
||||
}
|
||||
if _, err := SplitPath(arg); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if path := strings.TrimLeft(arg, "/"); path != arg && path != "" {
|
||||
return false, pathAdvice(path, fmt.Sprintf("path %s starts with /, and every path starts at the root already", arg))
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func isJSONStart(arg string) bool {
|
||||
return strings.HasPrefix(arg, "[") || strings.HasPrefix(arg, `"`)
|
||||
}
|
||||
|
||||
// pathAdvice refuses a spelling of path, naming path to write, or why no name can spell it.
|
||||
func pathAdvice(path, refusal string) error {
|
||||
if err := CheckPathNames(path); err != nil {
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("%s; write %s", refusal, path)
|
||||
}
|
||||
|
||||
// loneReference is the path a template spells when the value it holds — a format string, a
|
||||
// JSON string, or an object holding only a format — is one reference token and nothing else.
|
||||
func loneReference(arg string) (string, bool) {
|
||||
var raw any
|
||||
if json.Unmarshal([]byte(arg), &raw) != nil {
|
||||
raw = arg
|
||||
}
|
||||
if m, isObject := raw.(map[string]any); isObject && len(m) == 1 {
|
||||
raw = m["format"]
|
||||
}
|
||||
format, isString := raw.(string)
|
||||
if !isString {
|
||||
return "", false
|
||||
}
|
||||
toks, err := ParseFormat(format, nil)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
body, lone := loneRef(toks)
|
||||
if !lone {
|
||||
return "", false
|
||||
}
|
||||
if strings.HasPrefix(body, "/") {
|
||||
body = "/" + strings.TrimLeft(body, "/")
|
||||
}
|
||||
_, path, err := RefShape(body)
|
||||
return path, err == nil
|
||||
}
|
||||
|
||||
// loneRef is the reference a format of one reference token and nothing else reads.
|
||||
func loneRef(toks []Token) (string, bool) {
|
||||
if len(toks) != 1 || toks[0].Kind != NameRead || len(toks[0].Names) != 1 || !IsRef(toks[0].Names[0]) {
|
||||
return "", false
|
||||
}
|
||||
return toks[0].Names[0], true
|
||||
}
|
||||
Reference in New Issue
Block a user