mirror of https://github.com/databricks/cli.git
232 lines
5.9 KiB
Go
232 lines
5.9 KiB
Go
package dynvar
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/databricks/cli/libs/dyn"
|
|
"golang.org/x/exp/maps"
|
|
)
|
|
|
|
// Resolve resolves variable references in the given input value using the provided lookup function.
|
|
// It returns the resolved output value and any error encountered during the resolution process.
|
|
//
|
|
// For example, given the input value:
|
|
//
|
|
// {
|
|
// "a": "a",
|
|
// "b": "${a}",
|
|
// "c": "${b}${b}",
|
|
// }
|
|
//
|
|
// The output value will be:
|
|
//
|
|
// {
|
|
// "a": "a",
|
|
// "b": "a",
|
|
// "c": "aa",
|
|
// }
|
|
//
|
|
// If the input value contains a variable reference that cannot be resolved, an error is returned.
|
|
// If a cycle is detected in the variable references, an error is returned.
|
|
// If for some path the resolution function returns [ErrSkipResolution], the variable reference is left in place.
|
|
// This is useful when some variable references are not yet ready to be interpolated.
|
|
func Resolve(in dyn.Value, fn Lookup) (out dyn.Value, err error) {
|
|
return resolver{in: in, fn: fn}.run()
|
|
}
|
|
|
|
type lookupResult struct {
|
|
v dyn.Value
|
|
err error
|
|
}
|
|
|
|
type resolver struct {
|
|
in dyn.Value
|
|
fn Lookup
|
|
|
|
refs map[string]ref
|
|
resolved map[string]dyn.Value
|
|
|
|
// Memoization for lookups.
|
|
lookups map[string]lookupResult
|
|
}
|
|
|
|
func (r resolver) run() (out dyn.Value, err error) {
|
|
err = r.collectVariableReferences()
|
|
if err != nil {
|
|
return dyn.InvalidValue, err
|
|
}
|
|
|
|
err = r.resolveVariableReferences()
|
|
if err != nil {
|
|
return dyn.InvalidValue, err
|
|
}
|
|
|
|
out, err = r.replaceVariableReferences()
|
|
if err != nil {
|
|
return dyn.InvalidValue, err
|
|
}
|
|
|
|
return out, nil
|
|
}
|
|
|
|
func (r *resolver) collectVariableReferences() (err error) {
|
|
r.refs = make(map[string]ref)
|
|
|
|
// First walk the input to gather all values with a variable reference.
|
|
_, err = dyn.Walk(r.in, func(p dyn.Path, v dyn.Value) (dyn.Value, error) {
|
|
ref, ok := newRef(v)
|
|
if !ok {
|
|
// Skip values without variable references.
|
|
return v, nil
|
|
}
|
|
|
|
r.refs[p.String()] = ref
|
|
return v, nil
|
|
})
|
|
|
|
return err
|
|
}
|
|
|
|
func (r *resolver) resolveVariableReferences() (err error) {
|
|
// Initialize cache for lookups.
|
|
r.lookups = make(map[string]lookupResult)
|
|
|
|
// Initialize cache for resolved variable references.
|
|
r.resolved = make(map[string]dyn.Value)
|
|
|
|
// Resolve each variable reference (in order).
|
|
// We sort the keys here to ensure that we always resolve the same variable reference first.
|
|
// This is done such that the cycle detection error is deterministic. If we did not do this,
|
|
// we could enter the cycle at any point in the cycle and return varying errors.
|
|
keys := maps.Keys(r.refs)
|
|
sort.Strings(keys)
|
|
for _, key := range keys {
|
|
v, err := r.resolveRef(r.refs[key], []string{key})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
r.resolved[key] = v
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *resolver) resolveRef(ref ref, seen []string) (dyn.Value, error) {
|
|
// This is an unresolved variable reference.
|
|
deps := ref.references()
|
|
|
|
// Resolve each of the dependencies, then interpolate them in the ref.
|
|
resolved := make([]dyn.Value, len(deps))
|
|
complete := true
|
|
|
|
for j, dep := range deps {
|
|
// Cycle detection.
|
|
if slices.Contains(seen, dep) {
|
|
return dyn.InvalidValue, fmt.Errorf(
|
|
"cycle detected in field resolution: %s",
|
|
strings.Join(append(seen, dep), " -> "),
|
|
)
|
|
}
|
|
|
|
v, err := r.resolveKey(dep, append(seen, dep))
|
|
|
|
// If we should skip resolution of this key, index j will hold an invalid [dyn.Value].
|
|
if errors.Is(err, ErrSkipResolution) {
|
|
complete = false
|
|
continue
|
|
} else if err != nil {
|
|
// Otherwise, propagate the error.
|
|
return dyn.InvalidValue, err
|
|
}
|
|
|
|
resolved[j] = v
|
|
}
|
|
|
|
// Interpolate the resolved values.
|
|
if ref.isPure() && complete {
|
|
// If the variable reference is pure, we can substitute it.
|
|
// This is useful for interpolating values of non-string types.
|
|
//
|
|
// Note: we use the location of the variable reference to preserve the information
|
|
// of where it is used. This also means that relative path resolution is done
|
|
// relative to where a variable is used, not where it is defined.
|
|
//
|
|
return dyn.NewValue(resolved[0].Value(), ref.value.Locations()), nil
|
|
}
|
|
|
|
// Not pure; perform string interpolation.
|
|
for j := range ref.matches {
|
|
// The value is invalid if resolution returned [ErrSkipResolution].
|
|
// We must skip those and leave the original variable reference in place.
|
|
if !resolved[j].IsValid() {
|
|
continue
|
|
}
|
|
|
|
// Try to turn the resolved value into a string.
|
|
s, ok := resolved[j].AsString()
|
|
if !ok {
|
|
return dyn.InvalidValue, fmt.Errorf(
|
|
"cannot interpolate non-string value: %s",
|
|
ref.matches[j][0],
|
|
)
|
|
}
|
|
|
|
ref.str = strings.Replace(ref.str, ref.matches[j][0], s, 1)
|
|
}
|
|
|
|
return dyn.NewValue(ref.str, ref.value.Locations()), nil
|
|
}
|
|
|
|
func (r *resolver) resolveKey(key string, seen []string) (dyn.Value, error) {
|
|
// Check if we have already looked up this key.
|
|
if v, ok := r.lookups[key]; ok {
|
|
return v.v, v.err
|
|
}
|
|
|
|
// Parse the key into a path.
|
|
p, err := dyn.NewPathFromString(key)
|
|
if err != nil {
|
|
return dyn.InvalidValue, err
|
|
}
|
|
|
|
// Look up the value for the given key.
|
|
v, err := r.fn(p)
|
|
if err != nil {
|
|
if dyn.IsNoSuchKeyError(err) {
|
|
err = fmt.Errorf("reference does not exist: ${%s}", key)
|
|
}
|
|
|
|
// Cache the return value and return to the caller.
|
|
r.lookups[key] = lookupResult{v: dyn.InvalidValue, err: err}
|
|
return dyn.InvalidValue, err
|
|
}
|
|
|
|
// If the returned value is a valid variable reference, resolve it.
|
|
ref, ok := newRef(v)
|
|
if ok {
|
|
v, err = r.resolveRef(ref, seen)
|
|
}
|
|
|
|
// Cache the return value and return to the caller.
|
|
r.lookups[key] = lookupResult{v: v, err: err}
|
|
return v, err
|
|
}
|
|
|
|
func (r *resolver) replaceVariableReferences() (dyn.Value, error) {
|
|
// Walk the input and replace all variable references.
|
|
return dyn.Walk(r.in, func(p dyn.Path, v dyn.Value) (dyn.Value, error) {
|
|
nv, ok := r.resolved[p.String()]
|
|
if !ok {
|
|
// No variable reference; return the original value.
|
|
return v, nil
|
|
}
|
|
|
|
// We have a variable reference; return the resolved value.
|
|
return nv, nil
|
|
})
|
|
}
|