mirror of https://github.com/docker/cli.git
165 lines
4.2 KiB
Go
165 lines
4.2 KiB
Go
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
|
//go:build go1.21
|
|
|
|
package interpolation
|
|
|
|
import (
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/docker/cli/cli/compose/template"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
// Options supported by Interpolate
|
|
type Options struct {
|
|
// LookupValue from a key
|
|
LookupValue LookupValue
|
|
// TypeCastMapping maps key paths to functions to cast to a type
|
|
TypeCastMapping map[Path]Cast
|
|
// Substitution function to use
|
|
Substitute func(string, template.Mapping) (string, error)
|
|
}
|
|
|
|
// LookupValue is a function which maps from variable names to values.
|
|
// Returns the value as a string and a bool indicating whether
|
|
// the value is present, to distinguish between an empty string
|
|
// and the absence of a value.
|
|
type LookupValue func(key string) (string, bool)
|
|
|
|
// Cast a value to a new type, or return an error if the value can't be cast
|
|
type Cast func(value string) (any, error)
|
|
|
|
// Interpolate replaces variables in a string with the values from a mapping
|
|
func Interpolate(config map[string]any, opts Options) (map[string]any, error) {
|
|
if opts.LookupValue == nil {
|
|
opts.LookupValue = os.LookupEnv
|
|
}
|
|
if opts.TypeCastMapping == nil {
|
|
opts.TypeCastMapping = make(map[Path]Cast)
|
|
}
|
|
if opts.Substitute == nil {
|
|
opts.Substitute = template.Substitute
|
|
}
|
|
|
|
out := map[string]any{}
|
|
|
|
for key, value := range config {
|
|
interpolatedValue, err := recursiveInterpolate(value, NewPath(key), opts)
|
|
if err != nil {
|
|
return out, err
|
|
}
|
|
out[key] = interpolatedValue
|
|
}
|
|
|
|
return out, nil
|
|
}
|
|
|
|
func recursiveInterpolate(value any, path Path, opts Options) (any, error) {
|
|
switch value := value.(type) {
|
|
case string:
|
|
newValue, err := opts.Substitute(value, template.Mapping(opts.LookupValue))
|
|
if err != nil || newValue == value {
|
|
return value, newPathError(path, err)
|
|
}
|
|
caster, ok := opts.getCasterForPath(path)
|
|
if !ok {
|
|
return newValue, nil
|
|
}
|
|
casted, err := caster(newValue)
|
|
return casted, newPathError(path, errors.Wrap(err, "failed to cast to expected type"))
|
|
|
|
case map[string]any:
|
|
out := map[string]any{}
|
|
for key, elem := range value {
|
|
interpolatedElem, err := recursiveInterpolate(elem, path.Next(key), opts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out[key] = interpolatedElem
|
|
}
|
|
return out, nil
|
|
|
|
case []any:
|
|
out := make([]any, len(value))
|
|
for i, elem := range value {
|
|
interpolatedElem, err := recursiveInterpolate(elem, path.Next(PathMatchList), opts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out[i] = interpolatedElem
|
|
}
|
|
return out, nil
|
|
|
|
default:
|
|
return value, nil
|
|
}
|
|
}
|
|
|
|
func newPathError(path Path, err error) error {
|
|
switch err := err.(type) {
|
|
case nil:
|
|
return nil
|
|
case *template.InvalidTemplateError:
|
|
return errors.Errorf(
|
|
"invalid interpolation format for %s: %#v; you may need to escape any $ with another $",
|
|
path, err.Template)
|
|
default:
|
|
return errors.Wrapf(err, "error while interpolating %s", path)
|
|
}
|
|
}
|
|
|
|
const pathSeparator = "."
|
|
|
|
// PathMatchAll is a token used as part of a Path to match any key at that level
|
|
// in the nested structure
|
|
const PathMatchAll = "*"
|
|
|
|
// PathMatchList is a token used as part of a Path to match items in a list
|
|
const PathMatchList = "[]"
|
|
|
|
// Path is a dotted path of keys to a value in a nested mapping structure. A *
|
|
// section in a path will match any key in the mapping structure.
|
|
type Path string
|
|
|
|
// NewPath returns a new Path
|
|
func NewPath(items ...string) Path {
|
|
return Path(strings.Join(items, pathSeparator))
|
|
}
|
|
|
|
// Next returns a new path by append part to the current path
|
|
func (p Path) Next(part string) Path {
|
|
return Path(string(p) + pathSeparator + part)
|
|
}
|
|
|
|
func (p Path) parts() []string {
|
|
return strings.Split(string(p), pathSeparator)
|
|
}
|
|
|
|
func (p Path) matches(pattern Path) bool {
|
|
patternParts := pattern.parts()
|
|
parts := p.parts()
|
|
|
|
if len(patternParts) != len(parts) {
|
|
return false
|
|
}
|
|
for index, part := range parts {
|
|
switch patternParts[index] {
|
|
case PathMatchAll, part:
|
|
continue
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (o Options) getCasterForPath(path Path) (Cast, bool) {
|
|
for pattern, caster := range o.TypeCastMapping {
|
|
if path.matches(pattern) {
|
|
return caster, true
|
|
}
|
|
}
|
|
return nil, false
|
|
}
|