mirror of https://github.com/databricks/cli.git
Rudimentary interpolation support (#108)
Performs interpolation on string field. It looks for patterns `${foo.bar}` where `foo.bar` points to a string field in the configuration data model. It does not support traversal (e.g. `${foo}` with `foo` equal to`${bar}`), hence "rudimentary".
This commit is contained in:
parent
4064a21797
commit
c4d63eac70
|
@ -0,0 +1,195 @@
|
|||
package interpolation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/databricks/bricks/bundle"
|
||||
)
|
||||
|
||||
var re = regexp.MustCompile(`\$\{(\w+(\.\w+)*)\}`)
|
||||
|
||||
type stringField struct {
|
||||
rv reflect.Value
|
||||
s setter
|
||||
}
|
||||
|
||||
func newStringField(path string, rv reflect.Value, s setter) *stringField {
|
||||
return &stringField{
|
||||
rv: rv,
|
||||
s: s,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *stringField) String() string {
|
||||
return s.rv.String()
|
||||
}
|
||||
|
||||
func (s *stringField) dependsOn() []string {
|
||||
var out []string
|
||||
m := re.FindAllStringSubmatch(s.String(), -1)
|
||||
for i := range m {
|
||||
out = append(out, m[i][1])
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (s *stringField) interpolate(lookup map[string]string) {
|
||||
out := re.ReplaceAllStringFunc(s.String(), func(s string) string {
|
||||
// Turn the whole match into the submatch.
|
||||
match := re.FindStringSubmatch(s)
|
||||
path := match[1]
|
||||
v, ok := lookup[path]
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("expected to find value for path: %s", path))
|
||||
}
|
||||
return v
|
||||
})
|
||||
|
||||
s.s.Set(out)
|
||||
}
|
||||
|
||||
type accumulator struct {
|
||||
strings map[string]*stringField
|
||||
}
|
||||
|
||||
// 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, ".")
|
||||
a.strings[path] = newStringField(path, rv, s)
|
||||
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})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Gathers the strings for a list of paths.
|
||||
// The fields in these paths may not depend on other fields,
|
||||
// as we don't support full DAG lookup yet (only single level).
|
||||
func (a *accumulator) gather(paths []string) (map[string]string, error) {
|
||||
var out = make(map[string]string)
|
||||
for _, path := range paths {
|
||||
f, ok := a.strings[path]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%s is not defined", path)
|
||||
}
|
||||
deps := f.dependsOn()
|
||||
if len(deps) > 0 {
|
||||
return nil, fmt.Errorf("%s depends on %s", path, strings.Join(deps, ", "))
|
||||
}
|
||||
out[path] = f.rv.String()
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func expand(v any) error {
|
||||
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")
|
||||
}
|
||||
acc := &accumulator{
|
||||
strings: make(map[string]*stringField),
|
||||
}
|
||||
acc.walk([]string{}, rv, nilSetter{})
|
||||
|
||||
for path, v := range acc.strings {
|
||||
ds := v.dependsOn()
|
||||
if len(ds) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Create map to be used for interpolation
|
||||
m, err := acc.gather(ds)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot interpolate %s: %w", path, err)
|
||||
}
|
||||
|
||||
v.interpolate(m)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type interpolate struct{}
|
||||
|
||||
func Interpolate() bundle.Mutator {
|
||||
return &interpolate{}
|
||||
}
|
||||
|
||||
func (m *interpolate) Name() string {
|
||||
return "Interpolate"
|
||||
}
|
||||
|
||||
func (m *interpolate) Apply(_ context.Context, b *bundle.Bundle) ([]bundle.Mutator, error) {
|
||||
err := expand(&b.Config)
|
||||
return nil, err
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
package interpolation
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type nest struct {
|
||||
X string `json:"x"`
|
||||
Y *string `json:"y"`
|
||||
Z map[string]string `json:"z"`
|
||||
}
|
||||
|
||||
type foo struct {
|
||||
A string `json:"a"`
|
||||
B string `json:"b"`
|
||||
C string `json:"c"`
|
||||
|
||||
// Pointer field
|
||||
D *string `json:"d"`
|
||||
|
||||
// Struct field
|
||||
E nest `json:"e"`
|
||||
|
||||
// Map field
|
||||
F map[string]string `json:"f"`
|
||||
}
|
||||
|
||||
func TestInterpolationVariables(t *testing.T) {
|
||||
f := foo{
|
||||
A: "a",
|
||||
B: "${a}",
|
||||
C: "${a}",
|
||||
}
|
||||
|
||||
err := expand(&f)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "a", f.A)
|
||||
assert.Equal(t, "a", f.B)
|
||||
assert.Equal(t, "a", f.C)
|
||||
}
|
||||
|
||||
func TestInterpolationWithPointers(t *testing.T) {
|
||||
fd := "${a}"
|
||||
f := foo{
|
||||
A: "a",
|
||||
D: &fd,
|
||||
}
|
||||
|
||||
err := expand(&f)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "a", f.A)
|
||||
assert.Equal(t, "a", *f.D)
|
||||
}
|
||||
|
||||
func TestInterpolationWithStruct(t *testing.T) {
|
||||
fy := "${e.x}"
|
||||
f := foo{
|
||||
A: "${e.x}",
|
||||
E: nest{
|
||||
X: "x",
|
||||
Y: &fy,
|
||||
},
|
||||
}
|
||||
|
||||
err := expand(&f)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "x", f.A)
|
||||
assert.Equal(t, "x", f.E.X)
|
||||
assert.Equal(t, "x", *f.E.Y)
|
||||
}
|
||||
|
||||
func TestInterpolationWithMap(t *testing.T) {
|
||||
f := foo{
|
||||
A: "${f.a}",
|
||||
F: map[string]string{
|
||||
"a": "a",
|
||||
"b": "${f.a}",
|
||||
},
|
||||
}
|
||||
|
||||
err := expand(&f)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "a", f.A)
|
||||
assert.Equal(t, "a", f.F["a"])
|
||||
assert.Equal(t, "a", f.F["b"])
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package interpolation
|
||||
|
||||
import "reflect"
|
||||
|
||||
// String values in maps are not addressable and therefore not settable
|
||||
// through Go's reflection mechanism. This interface solves this limitation
|
||||
// by wrapping the setter differently for addressable values and map values.
|
||||
type setter interface {
|
||||
Set(string)
|
||||
}
|
||||
|
||||
type nilSetter struct{}
|
||||
|
||||
func (nilSetter) Set(_ string) {
|
||||
panic("nil setter")
|
||||
}
|
||||
|
||||
type anySetter struct {
|
||||
rv reflect.Value
|
||||
}
|
||||
|
||||
func (s anySetter) Set(str string) {
|
||||
s.rv.SetString(str)
|
||||
}
|
||||
|
||||
type mapSetter struct {
|
||||
// map[string]string
|
||||
m reflect.Value
|
||||
|
||||
// key
|
||||
k reflect.Value
|
||||
}
|
||||
|
||||
func (s mapSetter) Set(str string) {
|
||||
s.m.SetMapIndex(s.k, reflect.ValueOf(str))
|
||||
}
|
Loading…
Reference in New Issue