databricks-cli/libs/dyn/value.go

217 lines
4.0 KiB
Go
Raw Permalink Normal View History

package dyn
2023-10-20 12:56:59 +00:00
import (
"fmt"
"slices"
)
2023-10-20 12:56:59 +00:00
type Value struct {
v any
k Kind
// List of locations this value is defined at. The first location in the slice
// is the location returned by the `.Location()` method and is typically used
// for reporting errors and warnings associated with the value.
l []Location
2023-10-20 12:56:59 +00:00
// Whether or not this value is an anchor.
// If this node doesn't map to a type, we don't need to warn about it.
anchor bool
}
// InvalidValue is equal to the zero-value of Value.
var InvalidValue = Value{
k: KindInvalid,
}
// NilValue is a convenient constant for a nil value.
var NilValue = Value{
k: KindNil,
}
// V constructs a new Value with the given value.
func V(v any) Value {
return NewValue(v, []Location{})
}
2023-10-20 12:56:59 +00:00
// NewValue constructs a new Value with the given value and location.
func NewValue(v any, loc []Location) Value {
switch vin := v.(type) {
case map[string]Value:
v = newMappingFromGoMap(vin)
}
2023-10-20 12:56:59 +00:00
return Value{
v: v,
k: kindOf(v),
// create a copy of the locations, so that mutations to the original slice
// don't affect new value.
l: slices.Clone(loc),
}
}
// WithLocations returns a new Value with its location set to the given value.
func (v Value) WithLocations(loc []Location) Value {
return Value{
v: v.v,
k: v.k,
// create a copy of the locations, so that mutations to the original slice
// don't affect new value.
l: slices.Clone(loc),
2023-10-20 12:56:59 +00:00
}
}
func (v Value) AppendLocationsFromValue(w Value) Value {
Use dynamic configuration model in bundles (#1098) ## Changes This is a fundamental change to how we load and process bundle configuration. We now depend on the configuration being represented as a `dyn.Value`. This representation is functionally equivalent to Go's `any` (it is variadic) and allows us to capture metadata associated with a value, such as where it was defined (e.g. file, line, and column). It also allows us to represent Go's zero values properly (e.g. empty string, integer equal to 0, or boolean false). Using this representation allows us to let the configuration model deviate from the typed structure we have been relying on so far (`config.Root`). We need to deviate from these types when using variables for fields that are not a string themselves. For example, using `${var.num_workers}` for an integer `workers` field was impossible until now (though not implemented in this change). The loader for a `dyn.Value` includes functionality to capture any and all type mismatches between the user-defined configuration and the expected types. These mismatches can be surfaced as validation errors in future PRs. Given that many mutators expect the typed struct to be the source of truth, this change converts between the dynamic representation and the typed representation on mutator entry and exit. Existing mutators can continue to modify the typed representation and these modifications are reflected in the dynamic representation (see `MarkMutatorEntry` and `MarkMutatorExit` in `bundle/config/root.go`). Required changes included in this change: * The existing interpolation package is removed in favor of `libs/dyn/dynvar`. * Functionality to merge job clusters, job tasks, and pipeline clusters are now all broken out into their own mutators. To be implemented later: * Allow variable references for non-string types. * Surface diagnostics about the configuration provided by the user in the validation output. * Some mutators use a resource's configuration file path to resolve related relative paths. These depend on `bundle/config/paths.Path` being set and populated through `ConfigureConfigFilePath`. Instead, they should interact with the dynamically typed configuration directly. Doing this also unlocks being able to differentiate different base paths used within a job (e.g. a task override with a relative path defined in a directory other than the base job). ## Tests * Existing unit tests pass (some have been modified to accommodate) * Integration tests pass
2024-02-16 19:41:58 +00:00
return Value{
v: v.v,
k: v.k,
l: append(v.l, w.l...),
Use dynamic configuration model in bundles (#1098) ## Changes This is a fundamental change to how we load and process bundle configuration. We now depend on the configuration being represented as a `dyn.Value`. This representation is functionally equivalent to Go's `any` (it is variadic) and allows us to capture metadata associated with a value, such as where it was defined (e.g. file, line, and column). It also allows us to represent Go's zero values properly (e.g. empty string, integer equal to 0, or boolean false). Using this representation allows us to let the configuration model deviate from the typed structure we have been relying on so far (`config.Root`). We need to deviate from these types when using variables for fields that are not a string themselves. For example, using `${var.num_workers}` for an integer `workers` field was impossible until now (though not implemented in this change). The loader for a `dyn.Value` includes functionality to capture any and all type mismatches between the user-defined configuration and the expected types. These mismatches can be surfaced as validation errors in future PRs. Given that many mutators expect the typed struct to be the source of truth, this change converts between the dynamic representation and the typed representation on mutator entry and exit. Existing mutators can continue to modify the typed representation and these modifications are reflected in the dynamic representation (see `MarkMutatorEntry` and `MarkMutatorExit` in `bundle/config/root.go`). Required changes included in this change: * The existing interpolation package is removed in favor of `libs/dyn/dynvar`. * Functionality to merge job clusters, job tasks, and pipeline clusters are now all broken out into their own mutators. To be implemented later: * Allow variable references for non-string types. * Surface diagnostics about the configuration provided by the user in the validation output. * Some mutators use a resource's configuration file path to resolve related relative paths. These depend on `bundle/config/paths.Path` being set and populated through `ConfigureConfigFilePath`. Instead, they should interact with the dynamically typed configuration directly. Doing this also unlocks being able to differentiate different base paths used within a job (e.g. a task override with a relative path defined in a directory other than the base job). ## Tests * Existing unit tests pass (some have been modified to accommodate) * Integration tests pass
2024-02-16 19:41:58 +00:00
}
}
func (v Value) Kind() Kind {
return v.k
}
func (v Value) Value() any {
return v.v
}
func (v Value) Locations() []Location {
2023-10-20 12:56:59 +00:00
return v.l
}
func (v Value) Location() Location {
if len(v.l) == 0 {
return Location{}
}
return v.l[0]
}
func (v Value) IsValid() bool {
return v.k != KindInvalid
}
2023-10-20 12:56:59 +00:00
func (v Value) AsAny() any {
switch v.k {
case KindInvalid:
panic("invoked AsAny on invalid value")
case KindMap:
m := v.v.(Mapping)
out := make(map[string]any, m.Len())
for _, pair := range m.pairs {
pk := pair.Key
pv := pair.Value
out[pk.MustString()] = pv.AsAny()
2023-10-20 12:56:59 +00:00
}
return out
case KindSequence:
vv := v.v.([]Value)
2023-10-20 12:56:59 +00:00
a := make([]any, len(vv))
for i, v := range vv {
a[i] = v.AsAny()
}
return a
case KindNil:
return v.v
case KindString:
return v.v
case KindBool:
return v.v
case KindInt:
return v.v
case KindFloat:
return v.v
case KindTime:
t := v.v.(Time)
return t.Time()
2023-10-20 12:56:59 +00:00
default:
// Panic because we only want to deal with known types.
panic(fmt.Sprintf("invalid kind: %d", v.k))
2023-10-20 12:56:59 +00:00
}
}
func (v Value) Get(key string) Value {
m, ok := v.AsMap()
if !ok {
return InvalidValue
2023-10-20 12:56:59 +00:00
}
vv, ok := m.GetByString(key)
2023-10-20 12:56:59 +00:00
if !ok {
return InvalidValue
2023-10-20 12:56:59 +00:00
}
return vv
}
func (v Value) Index(i int) Value {
s, ok := v.v.([]Value)
if !ok {
return InvalidValue
2023-10-20 12:56:59 +00:00
}
if i < 0 || i >= len(s) {
return InvalidValue
2023-10-20 12:56:59 +00:00
}
return s[i]
}
func (v Value) MarkAnchor() Value {
return Value{
v: v.v,
k: v.k,
2023-10-20 12:56:59 +00:00
l: v.l,
anchor: true,
}
}
func (v Value) IsAnchor() bool {
return v.anchor
}
// eq is an internal only method that compares two values.
// It is used to determine if a value has changed during a visit.
// We need a custom implementation because maps and slices
// cannot be compared with the regular == operator.
func (v Value) eq(w Value) bool {
if v.k != w.k {
return false
}
if !slices.Equal(v.l, w.l) {
return false
}
switch v.k {
case KindMap:
// Compare pointers to the underlying map.
// This is safe because we don't allow maps to be mutated.
return &v.v == &w.v
case KindSequence:
vs := v.v.([]Value)
ws := w.v.([]Value)
lv := len(vs)
lw := len(ws)
// If both slices are empty, they are equal.
if lv == 0 && lw == 0 {
return true
}
// If they have different lengths, they are not equal.
if lv != lw {
return false
}
// They are both non-empty and have the same length.
// Compare pointers to the underlying slice.
// This is safe because we don't allow slices to be mutated.
return &vs[0] == &ws[0]
default:
return v.v == w.v
}
}