path.go (242 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 (
"fmt"
"regexp"
"strconv"
"strings"
)
type cfgPath struct {
fields []field
sep string
}
type field interface {
String() string
SetValue(opt *options, elem value, v value) Error
GetValue(opt *options, elem value) (value, Error)
Remove(opt *options, elem value) (bool, Error)
}
type namedField struct {
name string
}
type idxField struct {
i int
}
func parsePathIdx(in string, idx int, opts *options) cfgPath {
if in == "" {
return cfgPath{
sep: opts.pathSep,
fields: []field{idxField{idx}},
}
}
p := parsePathWithOpts(in, opts)
if idx >= 0 {
p.fields = append(p.fields, idxField{idx})
}
return p
}
const (
escapePathRegExp = `^\[.*\]$`
)
var escapePathReg = regexp.MustCompile(escapePathRegExp)
func parsePath(in, sep string, maxIdx int64, enableNumKeys, allowEscapePath bool) cfgPath {
if sep == "" || (allowEscapePath && escapePathReg.MatchString(in)) {
return cfgPath{
sep: sep,
fields: []field{parseField(in, maxIdx, enableNumKeys)},
}
}
elems := strings.Split(in, sep)
fields := make([]field, 0, len(elems))
// If property is the name with separators, for example "inputs.0.i"
// fall back to original implementation
if len(elems) > 1 {
enableNumKeys = false
}
for _, elem := range elems {
fields = append(fields, parseField(elem, maxIdx, enableNumKeys))
}
return cfgPath{fields: fields, sep: sep}
}
func parsePathWithOpts(in string, opts *options) cfgPath {
return parsePath(in, opts.pathSep, opts.maxIdx, opts.enableNumKeys, opts.escapePath)
}
func parseField(in string, maxIdx int64, enableNumKeys bool) field {
// If numeric keys are not enabled, fallback to the original implementation
if !enableNumKeys {
idx, err := strconv.ParseInt(in, 0, 64)
// Limit index value to the configurable max.
// If the idx > opts.maxIdx treat it as a regular named field.
// This preserves the current behavour for small index fields values (<= opts.maxIdx)
// and prevents large memory allocations or OOM if the string is large numeric value
if err == nil && idx <= int64(maxIdx) {
return idxField{int(idx)}
}
}
return namedField{in}
}
func (p cfgPath) String() string {
if len(p.fields) == 0 {
return ""
}
if len(p.fields) == 1 {
return p.fields[0].String()
}
s := make([]string, 0, len(p.fields))
for _, f := range p.fields {
s = append(s, f.String())
}
sep := p.sep
if sep == "" {
sep = "."
}
return strings.Join(s, sep)
}
func (n namedField) String() string {
return n.name
}
func (i idxField) String() string {
return fmt.Sprintf("%d", i.i)
}
func (p cfgPath) Has(cfg *Config, opt *options) (bool, Error) {
fields := p.fields
cur := value(cfgSub{cfg})
for ; len(fields) > 0; fields = fields[1:] {
field := fields[0]
next, err := field.GetValue(opt, cur)
if err != nil {
// has checks if a value is missing -> ErrMissing is no error but a valid
// outcome
if err.Reason() == ErrMissing {
err = nil
}
return false, err
}
if next == nil {
return false, nil
}
cur = next
}
return true, nil
}
func (p cfgPath) GetValue(cfg *Config, opt *options) (value, Error) {
fields := p.fields
cur := value(cfgSub{cfg})
for ; len(fields) > 1; fields = fields[1:] {
field := fields[0]
next, err := field.GetValue(opt, cur)
if err != nil {
return nil, err
}
if next == nil {
return nil, raiseMissing(cfg, field.String())
}
cur = next
}
field := fields[0]
v, err := field.GetValue(opt, cur)
if err != nil {
return nil, raiseMissing(cfg, field.String())
}
return v, nil
}
func (n namedField) GetValue(opts *options, elem value) (value, Error) {
cfg, err := elem.toConfig(opts)
if err != nil {
return nil, raiseExpectedObject(opts, elem)
}
v, _ := cfg.fields.get(n.name)
return v, nil
}
func (i idxField) GetValue(opts *options, elem value) (value, Error) {
cfg, err := elem.toConfig(opts)
if err != nil {
if i.i == 0 {
return elem, nil
}
return nil, raiseExpectedObject(opts, elem)
}
arr := cfg.fields.array()
if i.i >= len(arr) {
return nil, raiseMissing(cfg, i.String())
}
return arr[i.i], nil
}
func (p cfgPath) SetValue(cfg *Config, opt *options, val value) Error {
fields := p.fields
node := value(cfgSub{cfg})
// 1. iterate until intermediate node not having some required child node
for ; len(fields) > 1; fields = fields[1:] {
field := fields[0]
v, err := field.GetValue(opt, node)
if err != nil {
if err.Reason() == ErrMissing {
break
}
return err
}
if isNil(v) {
break
}
node = v
}
// 2. build intermediate nodes from bottom up
for ; len(fields) > 1; fields = fields[:len(fields)-1] {
field := fields[len(fields)-1]
next := New()
next.metadata = val.meta()
v := cfgSub{next}
if err := field.SetValue(opt, v, val); err != nil {
return err
}
val = v
}
// 3. insert new sub-tree into config
return fields[0].SetValue(opt, node, val)
}
func (n namedField) SetValue(opts *options, elem value, v value) Error {
sub, ok := elem.(cfgSub)
if !ok {
return raiseExpectedObject(opts, elem)
}
sub.c.fields.set(n.name, v)
v.SetContext(context{parent: elem, field: n.name})
return nil
}
func (i idxField) SetValue(opts *options, elem value, v value) Error {
sub, ok := elem.(cfgSub)
if !ok {
return raiseExpectedObject(opts, elem)
}
sub.c.fields.setAt(i.i, elem, v)
v.SetContext(context{parent: elem, field: i.String()})
return nil
}
func (p cfgPath) Remove(cfg *Config, opt *options) (bool, error) {
fields := p.fields
// Loop over intermediate objects. Returns an error if any intermediate is
// actually no object.
cur := value(cfgSub{cfg})
for ; len(fields) > 1; fields = fields[1:] {
field := fields[0]
next, err := field.GetValue(opt, cur)
if err != nil {
// Ignore ErrMissing when walking down a config tree. If intermediary is
// missing we can't remove our setting.
if err.Reason() == ErrMissing {
err = nil
}
return false, err
}
if next == nil {
return false, err
}
cur = next
}
// resolve config object in case we deal with references
tmp, err := cur.toConfig(opt)
if err != nil {
return false, err
}
cur = cfgSub{tmp}
field := fields[0]
return field.Remove(opt, cur)
}
func (n namedField) Remove(opts *options, elem value) (bool, Error) {
sub, ok := elem.(cfgSub)
if !ok {
return false, raiseExpectedObject(opts, elem)
}
removed := sub.c.fields.del(n.name)
return removed, nil
}
func (i idxField) Remove(opts *options, elem value) (bool, Error) {
sub, ok := elem.(cfgSub)
if !ok {
return false, raiseExpectedObject(opts, elem)
}
removed := sub.c.fields.delAt(i.i)
return removed, nil
}