package interpolation import ( "context" "errors" "fmt" "reflect" "regexp" "strings" "github.com/databricks/bricks/bundle" ) const Delimiter = "." var re = regexp.MustCompile(`\$\{(\w+(\.\w+)*)\}`) 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 { 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, Delimiter) a.strings[path] = newStringField(path, anyGetter{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.Get() } return out, nil } 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.walk([]string{}, rv, nilSetter{}) } func (a *accumulator) expand(fns ...LookupFunction) error { for path, v := range a.strings { ds := v.dependsOn() if len(ds) == 0 { continue } // Create map to be used for interpolation m, err := a.gather(ds) if err != nil { return fmt.Errorf("cannot interpolate %s: %w", path, err) } v.interpolate(fns, m) } 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) ([]bundle.Mutator, error) { return nil, m.expand(&b.Config) }