mirror of https://github.com/databricks/cli.git
255 lines
6.0 KiB
Go
255 lines
6.0 KiB
Go
package interpolation
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
|
|
"slices"
|
|
|
|
"github.com/databricks/cli/bundle"
|
|
"github.com/databricks/cli/bundle/config/variable"
|
|
"golang.org/x/exp/maps"
|
|
)
|
|
|
|
const Delimiter = "."
|
|
|
|
// must start with alphabet, support hyphens and underscores in middle but must end with character
|
|
var re = regexp.MustCompile(`\$\{([a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\.[a-zA-Z]+([-_]?[a-zA-Z0-9]+)*)*)\}`)
|
|
|
|
type stringField struct {
|
|
path string
|
|
|
|
getter
|
|
setter
|
|
}
|
|
|
|
func newStringField(path string, g getter, s setter) *stringField {
|
|
return &stringField{
|
|
path: path,
|
|
|
|
getter: g,
|
|
setter: s,
|
|
}
|
|
}
|
|
|
|
func (s *stringField) dependsOn() []string {
|
|
var out []string
|
|
m := re.FindAllStringSubmatch(s.Get(), -1)
|
|
for i := range m {
|
|
out = append(out, m[i][1])
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (s *stringField) interpolate(fns []LookupFunction, lookup map[string]string) {
|
|
out := re.ReplaceAllStringFunc(s.Get(), func(s string) string {
|
|
// Turn the whole match into the submatch.
|
|
match := re.FindStringSubmatch(s)
|
|
for _, fn := range fns {
|
|
v, err := fn(match[1], lookup)
|
|
if errors.Is(err, ErrSkipInterpolation) {
|
|
continue
|
|
}
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return v
|
|
}
|
|
|
|
// No substitution.
|
|
return s
|
|
})
|
|
|
|
s.Set(out)
|
|
}
|
|
|
|
type accumulator struct {
|
|
// all string fields in the bundle config
|
|
strings map[string]*stringField
|
|
|
|
// contains path -> resolved_string mapping for string fields in the config
|
|
// The resolved strings will NOT contain any variable references that could
|
|
// have been resolved, however there might still be references that cannot
|
|
// be resolved
|
|
memo map[string]string
|
|
}
|
|
|
|
// jsonFieldName returns the name in a field's `json` tag.
|
|
// Returns the empty string if it isn't set.
|
|
func jsonFieldName(sf reflect.StructField) string {
|
|
tag, ok := sf.Tag.Lookup("json")
|
|
if !ok {
|
|
return ""
|
|
}
|
|
parts := strings.Split(tag, ",")
|
|
if parts[0] == "-" {
|
|
return ""
|
|
}
|
|
return parts[0]
|
|
}
|
|
|
|
func (a *accumulator) walkStruct(scope []string, rv reflect.Value) {
|
|
num := rv.NumField()
|
|
for i := 0; i < num; i++ {
|
|
sf := rv.Type().Field(i)
|
|
f := rv.Field(i)
|
|
|
|
// Walk field with the same scope for anonymous (embedded) fields.
|
|
if sf.Anonymous {
|
|
a.walk(scope, f, anySetter{f})
|
|
continue
|
|
}
|
|
|
|
// Skip unnamed fields.
|
|
fieldName := jsonFieldName(rv.Type().Field(i))
|
|
if fieldName == "" {
|
|
continue
|
|
}
|
|
|
|
a.walk(append(scope, fieldName), f, anySetter{f})
|
|
}
|
|
}
|
|
|
|
func (a *accumulator) walk(scope []string, rv reflect.Value, s setter) {
|
|
// Dereference pointer.
|
|
if rv.Type().Kind() == reflect.Pointer {
|
|
// Skip nil pointers.
|
|
if rv.IsNil() {
|
|
return
|
|
}
|
|
rv = rv.Elem()
|
|
s = anySetter{rv}
|
|
}
|
|
|
|
switch rv.Type().Kind() {
|
|
case reflect.String:
|
|
path := strings.Join(scope, Delimiter)
|
|
a.strings[path] = newStringField(path, anyGetter{rv}, s)
|
|
|
|
// register alias for variable value. `var.foo` would be the alias for
|
|
// `variables.foo.value`
|
|
if len(scope) == 3 && scope[0] == "variables" && scope[2] == "value" {
|
|
aliasPath := strings.Join([]string{variable.VariableReferencePrefix, scope[1]}, Delimiter)
|
|
a.strings[aliasPath] = a.strings[path]
|
|
}
|
|
case reflect.Struct:
|
|
a.walkStruct(scope, rv)
|
|
case reflect.Map:
|
|
if rv.Type().Key().Kind() != reflect.String {
|
|
panic("only support string keys in map")
|
|
}
|
|
keys := rv.MapKeys()
|
|
for _, key := range keys {
|
|
a.walk(append(scope, key.String()), rv.MapIndex(key), mapSetter{rv, key})
|
|
}
|
|
case reflect.Slice:
|
|
n := rv.Len()
|
|
name := scope[len(scope)-1]
|
|
base := scope[:len(scope)-1]
|
|
for i := 0; i < n; i++ {
|
|
element := rv.Index(i)
|
|
a.walk(append(base, fmt.Sprintf("%s[%d]", name, i)), element, anySetter{element})
|
|
}
|
|
}
|
|
}
|
|
|
|
// walk and gather all string fields in the config
|
|
func (a *accumulator) start(v any) {
|
|
rv := reflect.ValueOf(v)
|
|
if rv.Type().Kind() != reflect.Pointer {
|
|
panic("expect pointer")
|
|
}
|
|
rv = rv.Elem()
|
|
if rv.Type().Kind() != reflect.Struct {
|
|
panic("expect struct")
|
|
}
|
|
|
|
a.strings = make(map[string]*stringField)
|
|
a.memo = make(map[string]string)
|
|
a.walk([]string{}, rv, nilSetter{})
|
|
}
|
|
|
|
// recursively interpolate variables in a depth first manner
|
|
func (a *accumulator) Resolve(path string, seenPaths []string, fns ...LookupFunction) error {
|
|
// return early if the path is already resolved
|
|
if _, ok := a.memo[path]; ok {
|
|
return nil
|
|
}
|
|
|
|
// fetch the string node to resolve
|
|
field, ok := a.strings[path]
|
|
if !ok {
|
|
return fmt.Errorf("no value found for interpolation reference: ${%s}", path)
|
|
}
|
|
|
|
// return early if the string field has no variables to interpolate
|
|
if len(field.dependsOn()) == 0 {
|
|
a.memo[path] = field.Get()
|
|
return nil
|
|
}
|
|
|
|
// resolve all variables refered in the root string field
|
|
for _, childFieldPath := range field.dependsOn() {
|
|
// error if there is a loop in variable interpolation
|
|
if slices.Contains(seenPaths, childFieldPath) {
|
|
return fmt.Errorf("cycle detected in field resolution: %s", strings.Join(append(seenPaths, childFieldPath), " -> "))
|
|
}
|
|
|
|
// recursive resolve variables in the child fields
|
|
err := a.Resolve(childFieldPath, append(seenPaths, childFieldPath), fns...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// interpolate root string once all variable references in it have been resolved
|
|
field.interpolate(fns, a.memo)
|
|
|
|
// record interpolated string in memo
|
|
a.memo[path] = field.Get()
|
|
return nil
|
|
}
|
|
|
|
// Interpolate all string fields in the config
|
|
func (a *accumulator) expand(fns ...LookupFunction) error {
|
|
// sorting paths for stable order of iteration
|
|
paths := maps.Keys(a.strings)
|
|
sort.Strings(paths)
|
|
|
|
// iterate over paths for all strings fields in the config
|
|
for _, path := range paths {
|
|
err := a.Resolve(path, []string{path}, fns...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type interpolate struct {
|
|
fns []LookupFunction
|
|
}
|
|
|
|
func (m *interpolate) expand(v any) error {
|
|
a := accumulator{}
|
|
a.start(v)
|
|
return a.expand(m.fns...)
|
|
}
|
|
|
|
func Interpolate(fns ...LookupFunction) bundle.Mutator {
|
|
return &interpolate{fns: fns}
|
|
}
|
|
|
|
func (m *interpolate) Name() string {
|
|
return "Interpolate"
|
|
}
|
|
|
|
func (m *interpolate) Apply(_ context.Context, b *bundle.Bundle) error {
|
|
return m.expand(&b.Config)
|
|
}
|