variables.go (433 lines of code) (raw):
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package ucfg
import (
"bytes"
"errors"
"fmt"
"strings"
"github.com/elastic/go-ucfg/parse"
)
type reference struct {
Path cfgPath
}
type expansion struct {
left, right varEvaler
pathSep string
}
type expansionSingle struct {
evaler varEvaler
pathSep string
}
type expansionDefault struct{ expansion }
type expansionAlt struct{ expansion }
type expansionErr struct{ expansion }
type splice struct {
pieces []varEvaler
}
type varEvaler interface {
eval(cfg *Config, opts *options) (string, error)
}
type constExp string
type token struct {
typ tokenType
val string
}
type parseState struct {
st int
isvar bool
op string
pieces [2][]varEvaler
}
var (
errUnterminatedBrace = errors.New("unterminated brace")
errInvalidType = errors.New("invalid type")
errEmptyPath = errors.New("empty path after expansion")
)
type tokenType uint16
const (
tokOpen tokenType = iota
tokClose
tokSep
tokString
// parser state
stLeft = 0
stRight = 1
opDefault = ":"
opAlternative = ":+"
opError = ":?"
)
var (
openToken = token{tokOpen, "${"}
closeToken = token{tokClose, "}"}
sepDefToken = token{tokSep, opDefault}
sepAltToken = token{tokSep, opAlternative}
sepErrToken = token{tokSep, opError}
)
func newReference(p cfgPath) *reference {
return &reference{p}
}
func (r *reference) String() string {
return fmt.Sprintf("${%v}", r.Path)
}
func (r *reference) resolveRef(cfg *Config, opts *options) (value, error) {
env := opts.env
if ok := opts.activeFields.AddNew(r.Path.String()); !ok {
return nil, raiseCyclicErr(r.Path.String())
}
var err Error
for {
var v value
cfg = cfgRoot(cfg)
if cfg == nil {
return nil, ErrMissing
}
v, err = r.Path.GetValue(cfg, opts)
if err == nil {
if v == nil {
break
}
return v, nil
}
if len(env) == 0 {
break
}
cfg = env[len(env)-1]
env = env[:len(env)-1]
}
return nil, err
}
func (r *reference) resolveEnv(cfg *Config, opts *options) (string, parse.Config, error) {
var err error
if len(opts.resolvers) > 0 {
key := r.Path.String()
for i := len(opts.resolvers) - 1; i >= 0; i-- {
var v string
var cfg parse.Config
resolver := opts.resolvers[i]
v, cfg, err = resolver(key)
if err == nil {
return v, cfg, nil
}
}
}
return "", parse.DefaultConfig, err
}
func (r *reference) resolve(cfg *Config, opts *options) (value, error) {
v, err := r.resolveRef(cfg, opts)
if v != nil || criticalResolveError(err) {
return v, err
}
previousErr := err
s, _, err := r.resolveEnv(cfg, opts)
if err != nil {
// TODO(ph): Not everything is an Error, will do some cleanup in another PR.
if v, ok := previousErr.(Error); ok {
if v.Reason() == ErrCyclicReference {
return nil, previousErr
}
}
return nil, err
}
if s == "" {
return nil, nil
}
return newString(context{field: r.Path.String()}, nil, s), nil
}
func (r *reference) eval(cfg *Config, opts *options) (string, error) {
v, err := r.resolve(cfg, opts)
if err != nil {
return "", err
}
if v == nil {
return "", fmt.Errorf("can not resolve reference: %v", r.Path)
}
return v.toString(opts)
}
func (s constExp) eval(*Config, *options) (string, error) {
return string(s), nil
}
func (s *splice) String() string {
return fmt.Sprintf("%v", s.pieces)
}
func (s *splice) eval(cfg *Config, opts *options) (string, error) {
buf := bytes.NewBuffer(nil)
for _, p := range s.pieces {
s, err := p.eval(cfg, opts)
if err != nil {
return "", err
}
buf.WriteString(s)
}
return buf.String(), nil
}
func (e *expansion) String() string {
return fmt.Sprintf("${%v:%v}", e.left, e.right)
}
func (e *expansionSingle) String() string {
return fmt.Sprintf("${%v}", e.evaler)
}
func (e *expansionSingle) eval(cfg *Config, opts *options) (string, error) {
path, err := e.evaler.eval(cfg, opts)
if err != nil {
return "", err
}
ref := newReference(parsePathWithOpts(path, opts))
return ref.eval(cfg, opts)
}
func (e *expansionDefault) eval(cfg *Config, opts *options) (string, error) {
path, err := e.left.eval(cfg, opts)
if err != nil || path == "" {
return e.right.eval(cfg, opts)
}
ref := newReference(parsePath(path, e.pathSep, opts.maxIdx, opts.enableNumKeys, opts.escapePath))
v, err := ref.eval(cfg, opts)
if err != nil || v == "" {
return e.right.eval(cfg, opts)
}
return v, err
}
func (e *expansionAlt) eval(cfg *Config, opts *options) (string, error) {
path, err := e.left.eval(cfg, opts)
if err != nil || path == "" {
return "", nil
}
ref := newReference(parsePath(path, e.pathSep, opts.maxIdx, opts.enableNumKeys, opts.escapePath))
tmp, err := ref.resolve(cfg, opts)
if err != nil || tmp == nil {
return "", nil
}
return e.right.eval(cfg, opts)
}
func (e *expansionErr) eval(cfg *Config, opts *options) (string, error) {
path, err := e.left.eval(cfg, opts)
if err == nil && path != "" {
ref := newReference(parsePath(path, e.pathSep, opts.maxIdx, opts.enableNumKeys, opts.escapePath))
str, err := ref.eval(cfg, opts)
if err == nil && str != "" {
return str, nil
}
}
errStr, err := e.right.eval(cfg, opts)
if err != nil {
return "", err
}
return "", errors.New(errStr)
}
func (st parseState) finalize(pathSep string, maxIdx int64, enableNumKeys, allowEscapePath bool) (varEvaler, error) {
if !st.isvar {
return nil, errors.New("fatal: processing non-variable state")
}
if len(st.pieces[stLeft]) == 0 {
return nil, errors.New("empty expansion")
}
if st.st == stLeft {
pieces := st.pieces[stLeft]
if len(pieces) == 0 {
return constExp(""), nil
}
if len(pieces) == 1 {
if str, ok := pieces[0].(constExp); ok {
return newReference(parsePath(string(str), pathSep, maxIdx, enableNumKeys, allowEscapePath)), nil
}
}
return &expansionSingle{&splice{pieces}, pathSep}, nil
}
extract := func(pieces []varEvaler) varEvaler {
switch len(pieces) {
case 0:
return constExp("")
case 1:
return pieces[0]
default:
return &splice{pieces}
}
}
left := extract(st.pieces[stLeft])
right := extract(st.pieces[stRight])
return makeOpExpansion(left, right, st.op, pathSep), nil
}
func makeOpExpansion(l, r varEvaler, op, pathSep string) varEvaler {
exp := expansion{l, r, pathSep}
switch op {
case opDefault:
return &expansionDefault{exp}
case opAlternative:
return &expansionAlt{exp}
case opError:
return &expansionErr{exp}
}
panic(fmt.Sprintf("Unknown operator: %v", op))
}
func parseSplice(in, pathSep string, maxIdx int64, enableNumKeys, allowEscapePath bool) (varEvaler, error) {
lex, errs := lexer(in)
drainLex := func() {
for range lex {
}
}
// drain lexer on return so go-routine won't leak
defer drainLex()
pieces, perr := parseVarExp(lex, pathSep, maxIdx, enableNumKeys, allowEscapePath)
if perr != nil {
return nil, perr
}
// check for lexer errors
select {
case err := <-errs:
if err != nil {
return nil, err
}
default:
}
// return parser result
return pieces, perr
}
func lexer(in string) (<-chan token, <-chan error) {
lex := make(chan token, 1)
errors := make(chan error, 1)
go func() {
off := 0
content := in
defer func() {
if len(content) > 0 {
lex <- token{tokString, content}
}
close(lex)
close(errors)
}()
strToken := func(s string) {
if s != "" {
lex <- token{tokString, s}
}
}
varcount := 0
for len(content) > 0 {
idx := -1
if varcount == 0 {
idx = strings.IndexAny(content[off:], "$")
} else {
idx = strings.IndexAny(content[off:], "$:}")
}
if idx < 0 {
return
}
idx += off
off = idx + 1
switch content[idx] {
case ':':
if len(content) <= off { // found ':' at end of string
return
}
strToken(content[:idx])
switch content[off] {
case '+':
off++
lex <- sepAltToken
case '?':
off++
lex <- sepErrToken
default:
lex <- sepDefToken
}
case '}':
strToken(content[:idx])
lex <- closeToken
varcount--
case '$':
if len(content) <= off { // found '$' at end of string
return
}
switch content[off] {
case '{': // start variable
strToken(content[:idx])
lex <- openToken
off++
varcount++
case '$', '}': // escape $} and $$
content = content[:idx] + content[off:]
continue
default:
continue
}
}
content = content[off:]
off = 0
}
}()
return lex, errors
}
func parseVarExp(lex <-chan token, pathSep string, maxIdx int64, enableNumKeys, allowEscapePath bool) (varEvaler, error) {
stack := []parseState{{st: stLeft}}
// parser loop
for tok := range lex {
switch tok.typ {
case tokOpen:
stack = append(stack, parseState{st: stLeft, isvar: true})
case tokClose:
// finalize and pop state
piece, err := stack[len(stack)-1].finalize(pathSep, maxIdx, enableNumKeys, allowEscapePath)
stack = stack[:len(stack)-1]
if err != nil {
return nil, err
}
// append result top stacked state
st := &stack[len(stack)-1]
st.pieces[st.st] = append(st.pieces[st.st], piece)
case tokSep: // switch from left to right
st := &stack[len(stack)-1]
if !st.isvar {
return nil, errors.New("default separator not within expansion")
}
if st.st == stRight {
st.pieces[st.st] = addString(st.pieces[st.st], tok.val)
} else {
// switch to 'right'
st.st = stRight
st.op = tok.val
}
case tokString:
// append raw string
st := &stack[len(stack)-1]
st.pieces[st.st] = addString(st.pieces[st.st], tok.val)
}
}
// validate and return final state
if len(stack) > 1 {
return nil, errors.New("missing '}'")
}
if len(stack) == 0 {
return nil, errors.New("fatal: expansion parse state empty")
}
result := stack[0].pieces[stLeft]
if len(result) == 1 {
return result[0], nil
}
return &splice{result}, nil
}
func cfgRoot(cfg *Config) *Config {
if cfg == nil {
return nil
}
for {
p := cfg.Parent()
if p == nil {
return cfg
}
cfg = p
}
}
func addString(ps []varEvaler, s string) []varEvaler {
if len(ps) == 0 {
return []varEvaler{constExp(s)}
}
last := ps[len(ps)-1]
c, ok := last.(constExp)
if !ok {
return append(ps, constExp(s))
}
ps[len(ps)-1] = constExp(string(c) + s)
return ps
}
func (t tokenType) String() string {
switch t {
case tokOpen:
return "<open>"
case tokClose:
return "<close>"
case tokSep:
return "<sep>"
case tokString:
return "<str>"
}
return "<unknown>"
}
func (t token) String() string {
return fmt.Sprintf("(%v, %v)", t.typ, t.val)
}