Track multiple locations associated with a `dyn.Value` (#1510)

## Changes
This PR changes the location metadata associated with a `dyn.Value` to a
slice of locations. This will allow us to keep track of location
metadata across merges and overrides.

The convention is to treat the first location in the slice as the
primary location. Also, the semantics are the same as before if there's
only one location associated with a value, that is:
1. For complex values (maps, sequences) the location of the v1 is
primary in Merge(v1, v2)
2. For primitive values the location of v2 is primary in Merge(v1, v2)

## Tests
Modifying existing merge unit tests. Other existing unit tests and
integration tests pass.

---------

Co-authored-by: Pieter Noordhuis <pieter.noordhuis@databricks.com>
This commit is contained in:
shreyas-goenka 2024-07-16 16:57:27 +05:30 committed by GitHub
parent 39c2633773
commit 8ed9964482
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 472 additions and 313 deletions

View File

@ -22,7 +22,7 @@ func ConvertJobToValue(job *jobs.Job) (dyn.Value, error) {
tasks = append(tasks, v) tasks = append(tasks, v)
} }
// We're using location lines to define the order of keys in exported YAML. // We're using location lines to define the order of keys in exported YAML.
value["tasks"] = dyn.NewValue(tasks, dyn.Location{Line: jobOrder.Get("tasks")}) value["tasks"] = dyn.NewValue(tasks, []dyn.Location{{Line: jobOrder.Get("tasks")}})
} }
return yamlsaver.ConvertToMapValue(job.Settings, jobOrder, []string{"format", "new_cluster", "existing_cluster_id"}, value) return yamlsaver.ConvertToMapValue(job.Settings, jobOrder, []string{"format", "new_cluster", "existing_cluster_id"}, value)

View File

@ -59,7 +59,7 @@ func (m *expandPipelineGlobPaths) expandLibrary(v dyn.Value) ([]dyn.Value, error
if err != nil { if err != nil {
return nil, err return nil, err
} }
nv, err := dyn.SetByPath(v, p, dyn.NewValue(m, pv.Location())) nv, err := dyn.SetByPath(v, p, dyn.NewValue(m, pv.Locations()))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -90,7 +90,7 @@ func (m *expandPipelineGlobPaths) expandSequence(p dyn.Path, v dyn.Value) (dyn.V
vs = append(vs, v...) vs = append(vs, v...)
} }
return dyn.NewValue(vs, v.Location()), nil return dyn.NewValue(vs, v.Locations()), nil
} }
func (m *expandPipelineGlobPaths) Apply(_ context.Context, b *bundle.Bundle) diag.Diagnostics { func (m *expandPipelineGlobPaths) Apply(_ context.Context, b *bundle.Bundle) diag.Diagnostics {

View File

@ -305,8 +305,8 @@ type createOverrideVisitorTestCase struct {
} }
func TestCreateOverrideVisitor(t *testing.T) { func TestCreateOverrideVisitor(t *testing.T) {
left := dyn.NewValue(42, dyn.Location{}) left := dyn.V(42)
right := dyn.NewValue(1337, dyn.Location{}) right := dyn.V(1337)
testCases := []createOverrideVisitorTestCase{ testCases := []createOverrideVisitorTestCase{
{ {
@ -470,21 +470,21 @@ func TestCreateOverrideVisitor_omitempty(t *testing.T) {
// this is not happening, but adding for completeness // this is not happening, but adding for completeness
name: "undo delete of empty variables", name: "undo delete of empty variables",
path: dyn.MustPathFromString("variables"), path: dyn.MustPathFromString("variables"),
left: dyn.NewValue([]dyn.Value{}, location), left: dyn.NewValue([]dyn.Value{}, []dyn.Location{location}),
expectedErr: merge.ErrOverrideUndoDelete, expectedErr: merge.ErrOverrideUndoDelete,
phases: allPhases, phases: allPhases,
}, },
{ {
name: "undo delete of empty job clusters", name: "undo delete of empty job clusters",
path: dyn.MustPathFromString("resources.jobs.job0.job_clusters"), path: dyn.MustPathFromString("resources.jobs.job0.job_clusters"),
left: dyn.NewValue([]dyn.Value{}, location), left: dyn.NewValue([]dyn.Value{}, []dyn.Location{location}),
expectedErr: merge.ErrOverrideUndoDelete, expectedErr: merge.ErrOverrideUndoDelete,
phases: allPhases, phases: allPhases,
}, },
{ {
name: "allow delete of non-empty job clusters", name: "allow delete of non-empty job clusters",
path: dyn.MustPathFromString("resources.jobs.job0.job_clusters"), path: dyn.MustPathFromString("resources.jobs.job0.job_clusters"),
left: dyn.NewValue([]dyn.Value{dyn.NewValue("abc", location)}, location), left: dyn.NewValue([]dyn.Value{dyn.NewValue("abc", []dyn.Location{location})}, []dyn.Location{location}),
expectedErr: nil, expectedErr: nil,
// deletions aren't allowed in 'load' phase // deletions aren't allowed in 'load' phase
phases: []phase{PythonMutatorPhaseInit}, phases: []phase{PythonMutatorPhaseInit},
@ -492,17 +492,15 @@ func TestCreateOverrideVisitor_omitempty(t *testing.T) {
{ {
name: "undo delete of empty tags", name: "undo delete of empty tags",
path: dyn.MustPathFromString("resources.jobs.job0.tags"), path: dyn.MustPathFromString("resources.jobs.job0.tags"),
left: dyn.NewValue(map[string]dyn.Value{}, location), left: dyn.NewValue(map[string]dyn.Value{}, []dyn.Location{location}),
expectedErr: merge.ErrOverrideUndoDelete, expectedErr: merge.ErrOverrideUndoDelete,
phases: allPhases, phases: allPhases,
}, },
{ {
name: "allow delete of non-empty tags", name: "allow delete of non-empty tags",
path: dyn.MustPathFromString("resources.jobs.job0.tags"), path: dyn.MustPathFromString("resources.jobs.job0.tags"),
left: dyn.NewValue( left: dyn.NewValue(map[string]dyn.Value{"dev": dyn.NewValue("true", []dyn.Location{location})}, []dyn.Location{location}),
map[string]dyn.Value{"dev": dyn.NewValue("true", location)},
location,
),
expectedErr: nil, expectedErr: nil,
// deletions aren't allowed in 'load' phase // deletions aren't allowed in 'load' phase
phases: []phase{PythonMutatorPhaseInit}, phases: []phase{PythonMutatorPhaseInit},
@ -510,7 +508,7 @@ func TestCreateOverrideVisitor_omitempty(t *testing.T) {
{ {
name: "undo delete of nil", name: "undo delete of nil",
path: dyn.MustPathFromString("resources.jobs.job0.tags"), path: dyn.MustPathFromString("resources.jobs.job0.tags"),
left: dyn.NilValue.WithLocation(location), left: dyn.NilValue.WithLocations([]dyn.Location{location}),
expectedErr: merge.ErrOverrideUndoDelete, expectedErr: merge.ErrOverrideUndoDelete,
phases: allPhases, phases: allPhases,
}, },

View File

@ -38,7 +38,7 @@ func (m *rewriteSyncPaths) makeRelativeTo(root string) dyn.MapFunc {
return dyn.InvalidValue, err return dyn.InvalidValue, err
} }
return dyn.NewValue(filepath.Join(rel, v.MustString()), v.Location()), nil return dyn.NewValue(filepath.Join(rel, v.MustString()), v.Locations()), nil
} }
} }

View File

@ -182,7 +182,7 @@ func (t *translateContext) rewriteValue(p dyn.Path, v dyn.Value, fn rewriteFunc,
return dyn.InvalidValue, err return dyn.InvalidValue, err
} }
return dyn.NewValue(out, v.Location()), nil return dyn.NewValue(out, v.Locations()), nil
} }
func (t *translateContext) rewriteRelativeTo(p dyn.Path, v dyn.Value, fn rewriteFunc, dir, fallback string) (dyn.Value, error) { func (t *translateContext) rewriteRelativeTo(p dyn.Path, v dyn.Value, fn rewriteFunc, dir, fallback string) (dyn.Value, error) {

View File

@ -378,7 +378,7 @@ func (r *Root) MergeTargetOverrides(name string) error {
// Below, we're setting fields on the bundle key, so make sure it exists. // Below, we're setting fields on the bundle key, so make sure it exists.
if root.Get("bundle").Kind() == dyn.KindInvalid { if root.Get("bundle").Kind() == dyn.KindInvalid {
root, err = dyn.Set(root, "bundle", dyn.NewValue(map[string]dyn.Value{}, dyn.Location{})) root, err = dyn.Set(root, "bundle", dyn.V(map[string]dyn.Value{}))
if err != nil { if err != nil {
return err return err
} }
@ -404,7 +404,7 @@ func (r *Root) MergeTargetOverrides(name string) error {
if v := target.Get("git"); v.Kind() != dyn.KindInvalid { if v := target.Get("git"); v.Kind() != dyn.KindInvalid {
ref, err := dyn.GetByPath(root, dyn.NewPath(dyn.Key("bundle"), dyn.Key("git"))) ref, err := dyn.GetByPath(root, dyn.NewPath(dyn.Key("bundle"), dyn.Key("git")))
if err != nil { if err != nil {
ref = dyn.NewValue(map[string]dyn.Value{}, dyn.Location{}) ref = dyn.V(map[string]dyn.Value{})
} }
// Merge the override into the reference. // Merge the override into the reference.
@ -415,7 +415,7 @@ func (r *Root) MergeTargetOverrides(name string) error {
// If the branch was overridden, we need to clear the inferred flag. // If the branch was overridden, we need to clear the inferred flag.
if branch := v.Get("branch"); branch.Kind() != dyn.KindInvalid { if branch := v.Get("branch"); branch.Kind() != dyn.KindInvalid {
out, err = dyn.SetByPath(out, dyn.NewPath(dyn.Key("inferred")), dyn.NewValue(false, dyn.Location{})) out, err = dyn.SetByPath(out, dyn.NewPath(dyn.Key("inferred")), dyn.V(false))
if err != nil { if err != nil {
return err return err
} }
@ -456,7 +456,7 @@ func rewriteShorthands(v dyn.Value) (dyn.Value, error) {
// configuration will convert this to a string if necessary. // configuration will convert this to a string if necessary.
return dyn.NewValue(map[string]dyn.Value{ return dyn.NewValue(map[string]dyn.Value{
"default": variable, "default": variable,
}, variable.Location()), nil }, variable.Locations()), nil
case dyn.KindMap, dyn.KindSequence: case dyn.KindMap, dyn.KindSequence:
// Check if the original definition of variable has a type field. // Check if the original definition of variable has a type field.
@ -469,7 +469,7 @@ func rewriteShorthands(v dyn.Value) (dyn.Value, error) {
return dyn.NewValue(map[string]dyn.Value{ return dyn.NewValue(map[string]dyn.Value{
"type": typeV, "type": typeV,
"default": variable, "default": variable,
}, variable.Location()), nil }, variable.Locations()), nil
} }
return variable, nil return variable, nil

View File

@ -14,9 +14,9 @@ func SetLocation(b *bundle.Bundle, prefix string, filePath string) {
return dyn.Walk(root, func(p dyn.Path, v dyn.Value) (dyn.Value, error) { return dyn.Walk(root, func(p dyn.Path, v dyn.Value) (dyn.Value, error) {
// If the path has the given prefix, set the location. // If the path has the given prefix, set the location.
if p.HasPrefix(start) { if p.HasPrefix(start) {
return v.WithLocation(dyn.Location{ return v.WithLocations([]dyn.Location{{
File: filePath, File: filePath,
}), nil }}), nil
} }
// The path is not nested under the given prefix. // The path is not nested under the given prefix.

View File

@ -42,7 +42,7 @@ func fromTyped(src any, ref dyn.Value, options ...fromTypedOptions) (dyn.Value,
// Dereference pointer if necessary // Dereference pointer if necessary
for srcv.Kind() == reflect.Pointer { for srcv.Kind() == reflect.Pointer {
if srcv.IsNil() { if srcv.IsNil() {
return dyn.NilValue.WithLocation(ref.Location()), nil return dyn.NilValue.WithLocations(ref.Locations()), nil
} }
srcv = srcv.Elem() srcv = srcv.Elem()
@ -83,7 +83,7 @@ func fromTyped(src any, ref dyn.Value, options ...fromTypedOptions) (dyn.Value,
if err != nil { if err != nil {
return dyn.InvalidValue, err return dyn.InvalidValue, err
} }
return v.WithLocation(ref.Location()), err return v.WithLocations(ref.Locations()), err
} }
func fromTypedStruct(src reflect.Value, ref dyn.Value, options ...fromTypedOptions) (dyn.Value, error) { func fromTypedStruct(src reflect.Value, ref dyn.Value, options ...fromTypedOptions) (dyn.Value, error) {

View File

@ -115,16 +115,16 @@ func TestFromTypedStructSetFieldsRetainLocation(t *testing.T) {
} }
ref := dyn.V(map[string]dyn.Value{ ref := dyn.V(map[string]dyn.Value{
"foo": dyn.NewValue("bar", dyn.Location{File: "foo"}), "foo": dyn.NewValue("bar", []dyn.Location{{File: "foo"}}),
"bar": dyn.NewValue("baz", dyn.Location{File: "bar"}), "bar": dyn.NewValue("baz", []dyn.Location{{File: "bar"}}),
}) })
nv, err := FromTyped(src, ref) nv, err := FromTyped(src, ref)
require.NoError(t, err) require.NoError(t, err)
// Assert foo and bar have retained their location. // Assert foo and bar have retained their location.
assert.Equal(t, dyn.NewValue("bar", dyn.Location{File: "foo"}), nv.Get("foo")) assert.Equal(t, dyn.NewValue("bar", []dyn.Location{{File: "foo"}}), nv.Get("foo"))
assert.Equal(t, dyn.NewValue("qux", dyn.Location{File: "bar"}), nv.Get("bar")) assert.Equal(t, dyn.NewValue("qux", []dyn.Location{{File: "bar"}}), nv.Get("bar"))
} }
func TestFromTypedStringMapWithZeroValue(t *testing.T) { func TestFromTypedStringMapWithZeroValue(t *testing.T) {
@ -359,16 +359,16 @@ func TestFromTypedMapNonEmptyRetainLocation(t *testing.T) {
} }
ref := dyn.V(map[string]dyn.Value{ ref := dyn.V(map[string]dyn.Value{
"foo": dyn.NewValue("bar", dyn.Location{File: "foo"}), "foo": dyn.NewValue("bar", []dyn.Location{{File: "foo"}}),
"bar": dyn.NewValue("baz", dyn.Location{File: "bar"}), "bar": dyn.NewValue("baz", []dyn.Location{{File: "bar"}}),
}) })
nv, err := FromTyped(src, ref) nv, err := FromTyped(src, ref)
require.NoError(t, err) require.NoError(t, err)
// Assert foo and bar have retained their locations. // Assert foo and bar have retained their locations.
assert.Equal(t, dyn.NewValue("bar", dyn.Location{File: "foo"}), nv.Get("foo")) assert.Equal(t, dyn.NewValue("bar", []dyn.Location{{File: "foo"}}), nv.Get("foo"))
assert.Equal(t, dyn.NewValue("qux", dyn.Location{File: "bar"}), nv.Get("bar")) assert.Equal(t, dyn.NewValue("qux", []dyn.Location{{File: "bar"}}), nv.Get("bar"))
} }
func TestFromTypedMapFieldWithZeroValue(t *testing.T) { func TestFromTypedMapFieldWithZeroValue(t *testing.T) {
@ -432,16 +432,16 @@ func TestFromTypedSliceNonEmptyRetainLocation(t *testing.T) {
} }
ref := dyn.V([]dyn.Value{ ref := dyn.V([]dyn.Value{
dyn.NewValue("foo", dyn.Location{File: "foo"}), dyn.NewValue("foo", []dyn.Location{{File: "foo"}}),
dyn.NewValue("bar", dyn.Location{File: "bar"}), dyn.NewValue("bar", []dyn.Location{{File: "bar"}}),
}) })
nv, err := FromTyped(src, ref) nv, err := FromTyped(src, ref)
require.NoError(t, err) require.NoError(t, err)
// Assert foo and bar have retained their locations. // Assert foo and bar have retained their locations.
assert.Equal(t, dyn.NewValue("foo", dyn.Location{File: "foo"}), nv.Index(0)) assert.Equal(t, dyn.NewValue("foo", []dyn.Location{{File: "foo"}}), nv.Index(0))
assert.Equal(t, dyn.NewValue("bar", dyn.Location{File: "bar"}), nv.Index(1)) assert.Equal(t, dyn.NewValue("bar", []dyn.Location{{File: "bar"}}), nv.Index(1))
} }
func TestFromTypedStringEmpty(t *testing.T) { func TestFromTypedStringEmpty(t *testing.T) {
@ -477,19 +477,19 @@ func TestFromTypedStringNonEmptyOverwrite(t *testing.T) {
} }
func TestFromTypedStringRetainsLocations(t *testing.T) { func TestFromTypedStringRetainsLocations(t *testing.T) {
var ref = dyn.NewValue("foo", dyn.Location{File: "foo"}) var ref = dyn.NewValue("foo", []dyn.Location{{File: "foo"}})
// case: value has not been changed // case: value has not been changed
var src string = "foo" var src string = "foo"
nv, err := FromTyped(src, ref) nv, err := FromTyped(src, ref)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, dyn.NewValue("foo", dyn.Location{File: "foo"}), nv) assert.Equal(t, dyn.NewValue("foo", []dyn.Location{{File: "foo"}}), nv)
// case: value has been changed // case: value has been changed
src = "bar" src = "bar"
nv, err = FromTyped(src, ref) nv, err = FromTyped(src, ref)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, dyn.NewValue("bar", dyn.Location{File: "foo"}), nv) assert.Equal(t, dyn.NewValue("bar", []dyn.Location{{File: "foo"}}), nv)
} }
func TestFromTypedStringTypeError(t *testing.T) { func TestFromTypedStringTypeError(t *testing.T) {
@ -532,19 +532,19 @@ func TestFromTypedBoolNonEmptyOverwrite(t *testing.T) {
} }
func TestFromTypedBoolRetainsLocations(t *testing.T) { func TestFromTypedBoolRetainsLocations(t *testing.T) {
var ref = dyn.NewValue(true, dyn.Location{File: "foo"}) var ref = dyn.NewValue(true, []dyn.Location{{File: "foo"}})
// case: value has not been changed // case: value has not been changed
var src bool = true var src bool = true
nv, err := FromTyped(src, ref) nv, err := FromTyped(src, ref)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, dyn.NewValue(true, dyn.Location{File: "foo"}), nv) assert.Equal(t, dyn.NewValue(true, []dyn.Location{{File: "foo"}}), nv)
// case: value has been changed // case: value has been changed
src = false src = false
nv, err = FromTyped(src, ref) nv, err = FromTyped(src, ref)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, dyn.NewValue(false, dyn.Location{File: "foo"}), nv) assert.Equal(t, dyn.NewValue(false, []dyn.Location{{File: "foo"}}), nv)
} }
func TestFromTypedBoolVariableReference(t *testing.T) { func TestFromTypedBoolVariableReference(t *testing.T) {
@ -595,19 +595,19 @@ func TestFromTypedIntNonEmptyOverwrite(t *testing.T) {
} }
func TestFromTypedIntRetainsLocations(t *testing.T) { func TestFromTypedIntRetainsLocations(t *testing.T) {
var ref = dyn.NewValue(1234, dyn.Location{File: "foo"}) var ref = dyn.NewValue(1234, []dyn.Location{{File: "foo"}})
// case: value has not been changed // case: value has not been changed
var src int = 1234 var src int = 1234
nv, err := FromTyped(src, ref) nv, err := FromTyped(src, ref)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, dyn.NewValue(1234, dyn.Location{File: "foo"}), nv) assert.Equal(t, dyn.NewValue(1234, []dyn.Location{{File: "foo"}}), nv)
// case: value has been changed // case: value has been changed
src = 1235 src = 1235
nv, err = FromTyped(src, ref) nv, err = FromTyped(src, ref)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, dyn.NewValue(int64(1235), dyn.Location{File: "foo"}), nv) assert.Equal(t, dyn.NewValue(int64(1235), []dyn.Location{{File: "foo"}}), nv)
} }
func TestFromTypedIntVariableReference(t *testing.T) { func TestFromTypedIntVariableReference(t *testing.T) {
@ -659,19 +659,19 @@ func TestFromTypedFloatNonEmptyOverwrite(t *testing.T) {
func TestFromTypedFloatRetainsLocations(t *testing.T) { func TestFromTypedFloatRetainsLocations(t *testing.T) {
var src float64 var src float64
var ref = dyn.NewValue(1.23, dyn.Location{File: "foo"}) var ref = dyn.NewValue(1.23, []dyn.Location{{File: "foo"}})
// case: value has not been changed // case: value has not been changed
src = 1.23 src = 1.23
nv, err := FromTyped(src, ref) nv, err := FromTyped(src, ref)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, dyn.NewValue(1.23, dyn.Location{File: "foo"}), nv) assert.Equal(t, dyn.NewValue(1.23, []dyn.Location{{File: "foo"}}), nv)
// case: value has been changed // case: value has been changed
src = 1.24 src = 1.24
nv, err = FromTyped(src, ref) nv, err = FromTyped(src, ref)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, dyn.NewValue(1.24, dyn.Location{File: "foo"}), nv) assert.Equal(t, dyn.NewValue(1.24, []dyn.Location{{File: "foo"}}), nv)
} }
func TestFromTypedFloatVariableReference(t *testing.T) { func TestFromTypedFloatVariableReference(t *testing.T) {
@ -740,27 +740,27 @@ func TestFromTypedNilPointerRetainsLocations(t *testing.T) {
} }
var src *Tmp var src *Tmp
ref := dyn.NewValue(nil, dyn.Location{File: "foobar"}) ref := dyn.NewValue(nil, []dyn.Location{{File: "foobar"}})
nv, err := FromTyped(src, ref) nv, err := FromTyped(src, ref)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, dyn.NewValue(nil, dyn.Location{File: "foobar"}), nv) assert.Equal(t, dyn.NewValue(nil, []dyn.Location{{File: "foobar"}}), nv)
} }
func TestFromTypedNilMapRetainsLocation(t *testing.T) { func TestFromTypedNilMapRetainsLocation(t *testing.T) {
var src map[string]string var src map[string]string
ref := dyn.NewValue(nil, dyn.Location{File: "foobar"}) ref := dyn.NewValue(nil, []dyn.Location{{File: "foobar"}})
nv, err := FromTyped(src, ref) nv, err := FromTyped(src, ref)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, dyn.NewValue(nil, dyn.Location{File: "foobar"}), nv) assert.Equal(t, dyn.NewValue(nil, []dyn.Location{{File: "foobar"}}), nv)
} }
func TestFromTypedNilSliceRetainsLocation(t *testing.T) { func TestFromTypedNilSliceRetainsLocation(t *testing.T) {
var src []string var src []string
ref := dyn.NewValue(nil, dyn.Location{File: "foobar"}) ref := dyn.NewValue(nil, []dyn.Location{{File: "foobar"}})
nv, err := FromTyped(src, ref) nv, err := FromTyped(src, ref)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, dyn.NewValue(nil, dyn.Location{File: "foobar"}), nv) assert.Equal(t, dyn.NewValue(nil, []dyn.Location{{File: "foobar"}}), nv)
} }

View File

@ -120,7 +120,7 @@ func (n normalizeOptions) normalizeStruct(typ reflect.Type, src dyn.Value, seen
// Return the normalized value if missing fields are not included. // Return the normalized value if missing fields are not included.
if !n.includeMissingFields { if !n.includeMissingFields {
return dyn.NewValue(out, src.Location()), diags return dyn.NewValue(out, src.Locations()), diags
} }
// Populate missing fields with their zero values. // Populate missing fields with their zero values.
@ -165,7 +165,7 @@ func (n normalizeOptions) normalizeStruct(typ reflect.Type, src dyn.Value, seen
} }
} }
return dyn.NewValue(out, src.Location()), diags return dyn.NewValue(out, src.Locations()), diags
case dyn.KindNil: case dyn.KindNil:
return src, diags return src, diags
@ -203,7 +203,7 @@ func (n normalizeOptions) normalizeMap(typ reflect.Type, src dyn.Value, seen []r
out.Set(pk, nv) out.Set(pk, nv)
} }
return dyn.NewValue(out, src.Location()), diags return dyn.NewValue(out, src.Locations()), diags
case dyn.KindNil: case dyn.KindNil:
return src, diags return src, diags
@ -238,7 +238,7 @@ func (n normalizeOptions) normalizeSlice(typ reflect.Type, src dyn.Value, seen [
out = append(out, v) out = append(out, v)
} }
return dyn.NewValue(out, src.Location()), diags return dyn.NewValue(out, src.Locations()), diags
case dyn.KindNil: case dyn.KindNil:
return src, diags return src, diags
@ -273,7 +273,7 @@ func (n normalizeOptions) normalizeString(typ reflect.Type, src dyn.Value, path
return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindString, src, path)) return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindString, src, path))
} }
return dyn.NewValue(out, src.Location()), diags return dyn.NewValue(out, src.Locations()), diags
} }
func (n normalizeOptions) normalizeBool(typ reflect.Type, src dyn.Value, path dyn.Path) (dyn.Value, diag.Diagnostics) { func (n normalizeOptions) normalizeBool(typ reflect.Type, src dyn.Value, path dyn.Path) (dyn.Value, diag.Diagnostics) {
@ -306,7 +306,7 @@ func (n normalizeOptions) normalizeBool(typ reflect.Type, src dyn.Value, path dy
return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindBool, src, path)) return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindBool, src, path))
} }
return dyn.NewValue(out, src.Location()), diags return dyn.NewValue(out, src.Locations()), diags
} }
func (n normalizeOptions) normalizeInt(typ reflect.Type, src dyn.Value, path dyn.Path) (dyn.Value, diag.Diagnostics) { func (n normalizeOptions) normalizeInt(typ reflect.Type, src dyn.Value, path dyn.Path) (dyn.Value, diag.Diagnostics) {
@ -349,7 +349,7 @@ func (n normalizeOptions) normalizeInt(typ reflect.Type, src dyn.Value, path dyn
return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindInt, src, path)) return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindInt, src, path))
} }
return dyn.NewValue(out, src.Location()), diags return dyn.NewValue(out, src.Locations()), diags
} }
func (n normalizeOptions) normalizeFloat(typ reflect.Type, src dyn.Value, path dyn.Path) (dyn.Value, diag.Diagnostics) { func (n normalizeOptions) normalizeFloat(typ reflect.Type, src dyn.Value, path dyn.Path) (dyn.Value, diag.Diagnostics) {
@ -392,7 +392,7 @@ func (n normalizeOptions) normalizeFloat(typ reflect.Type, src dyn.Value, path d
return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindFloat, src, path)) return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindFloat, src, path))
} }
return dyn.NewValue(out, src.Location()), diags return dyn.NewValue(out, src.Locations()), diags
} }
func (n normalizeOptions) normalizeInterface(typ reflect.Type, src dyn.Value, path dyn.Path) (dyn.Value, diag.Diagnostics) { func (n normalizeOptions) normalizeInterface(typ reflect.Type, src dyn.Value, path dyn.Path) (dyn.Value, diag.Diagnostics) {

View File

@ -229,7 +229,7 @@ func TestNormalizeStructVariableReference(t *testing.T) {
} }
var typ Tmp var typ Tmp
vin := dyn.NewValue("${var.foo}", dyn.Location{File: "file", Line: 1, Column: 1}) vin := dyn.NewValue("${var.foo}", []dyn.Location{{File: "file", Line: 1, Column: 1}})
vout, err := Normalize(typ, vin) vout, err := Normalize(typ, vin)
assert.Empty(t, err) assert.Empty(t, err)
assert.Equal(t, vin, vout) assert.Equal(t, vin, vout)
@ -241,7 +241,7 @@ func TestNormalizeStructRandomStringError(t *testing.T) {
} }
var typ Tmp var typ Tmp
vin := dyn.NewValue("var foo", dyn.Location{File: "file", Line: 1, Column: 1}) vin := dyn.NewValue("var foo", []dyn.Location{{File: "file", Line: 1, Column: 1}})
_, err := Normalize(typ, vin) _, err := Normalize(typ, vin)
assert.Len(t, err, 1) assert.Len(t, err, 1)
assert.Equal(t, diag.Diagnostic{ assert.Equal(t, diag.Diagnostic{
@ -258,7 +258,7 @@ func TestNormalizeStructIntError(t *testing.T) {
} }
var typ Tmp var typ Tmp
vin := dyn.NewValue(1, dyn.Location{File: "file", Line: 1, Column: 1}) vin := dyn.NewValue(1, []dyn.Location{{File: "file", Line: 1, Column: 1}})
_, err := Normalize(typ, vin) _, err := Normalize(typ, vin)
assert.Len(t, err, 1) assert.Len(t, err, 1)
assert.Equal(t, diag.Diagnostic{ assert.Equal(t, diag.Diagnostic{
@ -360,7 +360,7 @@ func TestNormalizeMapNestedError(t *testing.T) {
func TestNormalizeMapVariableReference(t *testing.T) { func TestNormalizeMapVariableReference(t *testing.T) {
var typ map[string]string var typ map[string]string
vin := dyn.NewValue("${var.foo}", dyn.Location{File: "file", Line: 1, Column: 1}) vin := dyn.NewValue("${var.foo}", []dyn.Location{{File: "file", Line: 1, Column: 1}})
vout, err := Normalize(typ, vin) vout, err := Normalize(typ, vin)
assert.Empty(t, err) assert.Empty(t, err)
assert.Equal(t, vin, vout) assert.Equal(t, vin, vout)
@ -368,7 +368,7 @@ func TestNormalizeMapVariableReference(t *testing.T) {
func TestNormalizeMapRandomStringError(t *testing.T) { func TestNormalizeMapRandomStringError(t *testing.T) {
var typ map[string]string var typ map[string]string
vin := dyn.NewValue("var foo", dyn.Location{File: "file", Line: 1, Column: 1}) vin := dyn.NewValue("var foo", []dyn.Location{{File: "file", Line: 1, Column: 1}})
_, err := Normalize(typ, vin) _, err := Normalize(typ, vin)
assert.Len(t, err, 1) assert.Len(t, err, 1)
assert.Equal(t, diag.Diagnostic{ assert.Equal(t, diag.Diagnostic{
@ -381,7 +381,7 @@ func TestNormalizeMapRandomStringError(t *testing.T) {
func TestNormalizeMapIntError(t *testing.T) { func TestNormalizeMapIntError(t *testing.T) {
var typ map[string]string var typ map[string]string
vin := dyn.NewValue(1, dyn.Location{File: "file", Line: 1, Column: 1}) vin := dyn.NewValue(1, []dyn.Location{{File: "file", Line: 1, Column: 1}})
_, err := Normalize(typ, vin) _, err := Normalize(typ, vin)
assert.Len(t, err, 1) assert.Len(t, err, 1)
assert.Equal(t, diag.Diagnostic{ assert.Equal(t, diag.Diagnostic{
@ -482,7 +482,7 @@ func TestNormalizeSliceNestedError(t *testing.T) {
func TestNormalizeSliceVariableReference(t *testing.T) { func TestNormalizeSliceVariableReference(t *testing.T) {
var typ []string var typ []string
vin := dyn.NewValue("${var.foo}", dyn.Location{File: "file", Line: 1, Column: 1}) vin := dyn.NewValue("${var.foo}", []dyn.Location{{File: "file", Line: 1, Column: 1}})
vout, err := Normalize(typ, vin) vout, err := Normalize(typ, vin)
assert.Empty(t, err) assert.Empty(t, err)
assert.Equal(t, vin, vout) assert.Equal(t, vin, vout)
@ -490,7 +490,7 @@ func TestNormalizeSliceVariableReference(t *testing.T) {
func TestNormalizeSliceRandomStringError(t *testing.T) { func TestNormalizeSliceRandomStringError(t *testing.T) {
var typ []string var typ []string
vin := dyn.NewValue("var foo", dyn.Location{File: "file", Line: 1, Column: 1}) vin := dyn.NewValue("var foo", []dyn.Location{{File: "file", Line: 1, Column: 1}})
_, err := Normalize(typ, vin) _, err := Normalize(typ, vin)
assert.Len(t, err, 1) assert.Len(t, err, 1)
assert.Equal(t, diag.Diagnostic{ assert.Equal(t, diag.Diagnostic{
@ -503,7 +503,7 @@ func TestNormalizeSliceRandomStringError(t *testing.T) {
func TestNormalizeSliceIntError(t *testing.T) { func TestNormalizeSliceIntError(t *testing.T) {
var typ []string var typ []string
vin := dyn.NewValue(1, dyn.Location{File: "file", Line: 1, Column: 1}) vin := dyn.NewValue(1, []dyn.Location{{File: "file", Line: 1, Column: 1}})
_, err := Normalize(typ, vin) _, err := Normalize(typ, vin)
assert.Len(t, err, 1) assert.Len(t, err, 1)
assert.Equal(t, diag.Diagnostic{ assert.Equal(t, diag.Diagnostic{
@ -524,7 +524,7 @@ func TestNormalizeString(t *testing.T) {
func TestNormalizeStringNil(t *testing.T) { func TestNormalizeStringNil(t *testing.T) {
var typ string var typ string
vin := dyn.NewValue(nil, dyn.Location{File: "file", Line: 1, Column: 1}) vin := dyn.NewValue(nil, []dyn.Location{{File: "file", Line: 1, Column: 1}})
_, err := Normalize(&typ, vin) _, err := Normalize(&typ, vin)
assert.Len(t, err, 1) assert.Len(t, err, 1)
assert.Equal(t, diag.Diagnostic{ assert.Equal(t, diag.Diagnostic{
@ -537,26 +537,26 @@ func TestNormalizeStringNil(t *testing.T) {
func TestNormalizeStringFromBool(t *testing.T) { func TestNormalizeStringFromBool(t *testing.T) {
var typ string var typ string
vin := dyn.NewValue(true, dyn.Location{File: "file", Line: 1, Column: 1}) vin := dyn.NewValue(true, []dyn.Location{{File: "file", Line: 1, Column: 1}})
vout, err := Normalize(&typ, vin) vout, err := Normalize(&typ, vin)
assert.Empty(t, err) assert.Empty(t, err)
assert.Equal(t, dyn.NewValue("true", vin.Location()), vout) assert.Equal(t, dyn.NewValue("true", vin.Locations()), vout)
} }
func TestNormalizeStringFromInt(t *testing.T) { func TestNormalizeStringFromInt(t *testing.T) {
var typ string var typ string
vin := dyn.NewValue(123, dyn.Location{File: "file", Line: 1, Column: 1}) vin := dyn.NewValue(123, []dyn.Location{{File: "file", Line: 1, Column: 1}})
vout, err := Normalize(&typ, vin) vout, err := Normalize(&typ, vin)
assert.Empty(t, err) assert.Empty(t, err)
assert.Equal(t, dyn.NewValue("123", vin.Location()), vout) assert.Equal(t, dyn.NewValue("123", vin.Locations()), vout)
} }
func TestNormalizeStringFromFloat(t *testing.T) { func TestNormalizeStringFromFloat(t *testing.T) {
var typ string var typ string
vin := dyn.NewValue(1.20, dyn.Location{File: "file", Line: 1, Column: 1}) vin := dyn.NewValue(1.20, []dyn.Location{{File: "file", Line: 1, Column: 1}})
vout, err := Normalize(&typ, vin) vout, err := Normalize(&typ, vin)
assert.Empty(t, err) assert.Empty(t, err)
assert.Equal(t, dyn.NewValue("1.2", vin.Location()), vout) assert.Equal(t, dyn.NewValue("1.2", vin.Locations()), vout)
} }
func TestNormalizeStringError(t *testing.T) { func TestNormalizeStringError(t *testing.T) {
@ -582,7 +582,7 @@ func TestNormalizeBool(t *testing.T) {
func TestNormalizeBoolNil(t *testing.T) { func TestNormalizeBoolNil(t *testing.T) {
var typ bool var typ bool
vin := dyn.NewValue(nil, dyn.Location{File: "file", Line: 1, Column: 1}) vin := dyn.NewValue(nil, []dyn.Location{{File: "file", Line: 1, Column: 1}})
_, err := Normalize(&typ, vin) _, err := Normalize(&typ, vin)
assert.Len(t, err, 1) assert.Len(t, err, 1)
assert.Equal(t, diag.Diagnostic{ assert.Equal(t, diag.Diagnostic{
@ -658,7 +658,7 @@ func TestNormalizeInt(t *testing.T) {
func TestNormalizeIntNil(t *testing.T) { func TestNormalizeIntNil(t *testing.T) {
var typ int var typ int
vin := dyn.NewValue(nil, dyn.Location{File: "file", Line: 1, Column: 1}) vin := dyn.NewValue(nil, []dyn.Location{{File: "file", Line: 1, Column: 1}})
_, err := Normalize(&typ, vin) _, err := Normalize(&typ, vin)
assert.Len(t, err, 1) assert.Len(t, err, 1)
assert.Equal(t, diag.Diagnostic{ assert.Equal(t, diag.Diagnostic{
@ -742,7 +742,7 @@ func TestNormalizeFloat(t *testing.T) {
func TestNormalizeFloatNil(t *testing.T) { func TestNormalizeFloatNil(t *testing.T) {
var typ float64 var typ float64
vin := dyn.NewValue(nil, dyn.Location{File: "file", Line: 1, Column: 1}) vin := dyn.NewValue(nil, []dyn.Location{{File: "file", Line: 1, Column: 1}})
_, err := Normalize(&typ, vin) _, err := Normalize(&typ, vin)
assert.Len(t, err, 1) assert.Len(t, err, 1)
assert.Equal(t, diag.Diagnostic{ assert.Equal(t, diag.Diagnostic{
@ -842,26 +842,26 @@ func TestNormalizeAnchors(t *testing.T) {
func TestNormalizeBoolToAny(t *testing.T) { func TestNormalizeBoolToAny(t *testing.T) {
var typ any var typ any
vin := dyn.NewValue(false, dyn.Location{File: "file", Line: 1, Column: 1}) vin := dyn.NewValue(false, []dyn.Location{{File: "file", Line: 1, Column: 1}})
vout, err := Normalize(&typ, vin) vout, err := Normalize(&typ, vin)
assert.Len(t, err, 0) assert.Len(t, err, 0)
assert.Equal(t, dyn.NewValue(false, dyn.Location{File: "file", Line: 1, Column: 1}), vout) assert.Equal(t, dyn.NewValue(false, []dyn.Location{{File: "file", Line: 1, Column: 1}}), vout)
} }
func TestNormalizeIntToAny(t *testing.T) { func TestNormalizeIntToAny(t *testing.T) {
var typ any var typ any
vin := dyn.NewValue(10, dyn.Location{File: "file", Line: 1, Column: 1}) vin := dyn.NewValue(10, []dyn.Location{{File: "file", Line: 1, Column: 1}})
vout, err := Normalize(&typ, vin) vout, err := Normalize(&typ, vin)
assert.Len(t, err, 0) assert.Len(t, err, 0)
assert.Equal(t, dyn.NewValue(10, dyn.Location{File: "file", Line: 1, Column: 1}), vout) assert.Equal(t, dyn.NewValue(10, []dyn.Location{{File: "file", Line: 1, Column: 1}}), vout)
} }
func TestNormalizeSliceToAny(t *testing.T) { func TestNormalizeSliceToAny(t *testing.T) {
var typ any var typ any
v1 := dyn.NewValue(1, dyn.Location{File: "file", Line: 1, Column: 1}) v1 := dyn.NewValue(1, []dyn.Location{{File: "file", Line: 1, Column: 1}})
v2 := dyn.NewValue(2, dyn.Location{File: "file", Line: 1, Column: 1}) v2 := dyn.NewValue(2, []dyn.Location{{File: "file", Line: 1, Column: 1}})
vin := dyn.NewValue([]dyn.Value{v1, v2}, dyn.Location{File: "file", Line: 1, Column: 1}) vin := dyn.NewValue([]dyn.Value{v1, v2}, []dyn.Location{{File: "file", Line: 1, Column: 1}})
vout, err := Normalize(&typ, vin) vout, err := Normalize(&typ, vin)
assert.Len(t, err, 0) assert.Len(t, err, 0)
assert.Equal(t, dyn.NewValue([]dyn.Value{v1, v2}, dyn.Location{File: "file", Line: 1, Column: 1}), vout) assert.Equal(t, dyn.NewValue([]dyn.Value{v1, v2}, []dyn.Location{{File: "file", Line: 1, Column: 1}}), vout)
} }

View File

@ -155,7 +155,7 @@ func (r *resolver) resolveRef(ref ref, seen []string) (dyn.Value, error) {
// of where it is used. This also means that relative path resolution is done // 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. // relative to where a variable is used, not where it is defined.
// //
return dyn.NewValue(resolved[0].Value(), ref.value.Location()), nil return dyn.NewValue(resolved[0].Value(), ref.value.Locations()), nil
} }
// Not pure; perform string interpolation. // Not pure; perform string interpolation.
@ -178,7 +178,7 @@ func (r *resolver) resolveRef(ref ref, seen []string) (dyn.Value, error) {
ref.str = strings.Replace(ref.str, ref.matches[j][0], s, 1) ref.str = strings.Replace(ref.str, ref.matches[j][0], s, 1)
} }
return dyn.NewValue(ref.str, ref.value.Location()), nil return dyn.NewValue(ref.str, ref.value.Locations()), nil
} }
func (r *resolver) resolveKey(key string, seen []string) (dyn.Value, error) { func (r *resolver) resolveKey(key string, seen []string) (dyn.Value, error) {

View File

@ -52,7 +52,7 @@ func (e elementsByKey) Map(_ dyn.Path, v dyn.Value) (dyn.Value, error) {
out = append(out, nv) out = append(out, nv)
} }
return dyn.NewValue(out, v.Location()), nil return dyn.NewValue(out, v.Locations()), nil
} }
// ElementsByKey returns a [dyn.MapFunc] that operates on a sequence // ElementsByKey returns a [dyn.MapFunc] that operates on a sequence

View File

@ -12,6 +12,26 @@ import (
// * Merging x with nil or nil with x always yields x. // * Merging x with nil or nil with x always yields x.
// * Merging maps a and b means entries from map b take precedence. // * Merging maps a and b means entries from map b take precedence.
// * Merging sequences a and b means concatenating them. // * Merging sequences a and b means concatenating them.
//
// Merging retains and accumulates the locations metadata associated with the values.
// This allows users of the module to track the provenance of values across merging of
// configuration trees, which is useful for reporting errors and warnings.
//
// Semantics for location metadata in the merged value are similar to the semantics
// for the values themselves:
//
// - When merging x with nil or nil with x, the location of x is retained.
//
// - When merging maps or sequences, the combined value retains the location of a and
// accumulates the location of b. The individual elements of the map or sequence retain
// their original locations, i.e., whether they were originally defined in a or b.
//
// The rationale for retaining location of a is that we would like to return
// the first location a bit of configuration showed up when reporting errors and warnings.
//
// - Merging primitive values means using the incoming value `b`. The location of the
// incoming value is retained and the location of the existing value `a` is accumulated.
// This is because the incoming value overwrites the existing value.
func Merge(a, b dyn.Value) (dyn.Value, error) { func Merge(a, b dyn.Value) (dyn.Value, error) {
return merge(a, b) return merge(a, b)
} }
@ -22,12 +42,12 @@ func merge(a, b dyn.Value) (dyn.Value, error) {
// If a is nil, return b. // If a is nil, return b.
if ak == dyn.KindNil { if ak == dyn.KindNil {
return b, nil return b.AppendLocationsFromValue(a), nil
} }
// If b is nil, return a. // If b is nil, return a.
if bk == dyn.KindNil { if bk == dyn.KindNil {
return a, nil return a.AppendLocationsFromValue(b), nil
} }
// Call the appropriate merge function based on the kind of a and b. // Call the appropriate merge function based on the kind of a and b.
@ -75,8 +95,8 @@ func mergeMap(a, b dyn.Value) (dyn.Value, error) {
} }
} }
// Preserve the location of the first value. // Preserve the location of the first value. Accumulate the locations of the second value.
return dyn.NewValue(out, a.Location()), nil return dyn.NewValue(out, a.Locations()).AppendLocationsFromValue(b), nil
} }
func mergeSequence(a, b dyn.Value) (dyn.Value, error) { func mergeSequence(a, b dyn.Value) (dyn.Value, error) {
@ -88,11 +108,10 @@ func mergeSequence(a, b dyn.Value) (dyn.Value, error) {
copy(out[:], as) copy(out[:], as)
copy(out[len(as):], bs) copy(out[len(as):], bs)
// Preserve the location of the first value. // Preserve the location of the first value. Accumulate the locations of the second value.
return dyn.NewValue(out, a.Location()), nil return dyn.NewValue(out, a.Locations()).AppendLocationsFromValue(b), nil
} }
func mergePrimitive(a, b dyn.Value) (dyn.Value, error) { func mergePrimitive(a, b dyn.Value) (dyn.Value, error) {
// Merging primitive values means using the incoming value. // Merging primitive values means using the incoming value.
return b, nil return b.AppendLocationsFromValue(a), nil
} }

View File

@ -8,15 +8,17 @@ import (
) )
func TestMergeMaps(t *testing.T) { func TestMergeMaps(t *testing.T) {
v1 := dyn.V(map[string]dyn.Value{ l1 := dyn.Location{File: "file1", Line: 1, Column: 2}
"foo": dyn.V("bar"), v1 := dyn.NewValue(map[string]dyn.Value{
"bar": dyn.V("baz"), "foo": dyn.NewValue("bar", []dyn.Location{l1}),
}) "bar": dyn.NewValue("baz", []dyn.Location{l1}),
}, []dyn.Location{l1})
v2 := dyn.V(map[string]dyn.Value{ l2 := dyn.Location{File: "file2", Line: 3, Column: 4}
"bar": dyn.V("qux"), v2 := dyn.NewValue(map[string]dyn.Value{
"qux": dyn.V("foo"), "bar": dyn.NewValue("qux", []dyn.Location{l2}),
}) "qux": dyn.NewValue("foo", []dyn.Location{l2}),
}, []dyn.Location{l2})
// Merge v2 into v1. // Merge v2 into v1.
{ {
@ -27,6 +29,23 @@ func TestMergeMaps(t *testing.T) {
"bar": "qux", "bar": "qux",
"qux": "foo", "qux": "foo",
}, out.AsAny()) }, out.AsAny())
// Locations of both values should be preserved.
assert.Equal(t, []dyn.Location{l1, l2}, out.Locations())
assert.Equal(t, []dyn.Location{l2, l1}, out.Get("bar").Locations())
assert.Equal(t, []dyn.Location{l1}, out.Get("foo").Locations())
assert.Equal(t, []dyn.Location{l2}, out.Get("qux").Locations())
// Location of the merged value should be the location of v1.
assert.Equal(t, l1, out.Location())
// Value of bar is "qux" which comes from v2. This .Location() should
// return the location of v2.
assert.Equal(t, l2, out.Get("bar").Location())
// Original locations of keys that were not overwritten should be preserved.
assert.Equal(t, l1, out.Get("foo").Location())
assert.Equal(t, l2, out.Get("qux").Location())
} }
// Merge v1 into v2. // Merge v1 into v2.
@ -38,30 +57,64 @@ func TestMergeMaps(t *testing.T) {
"bar": "baz", "bar": "baz",
"qux": "foo", "qux": "foo",
}, out.AsAny()) }, out.AsAny())
// Locations of both values should be preserved.
assert.Equal(t, []dyn.Location{l2, l1}, out.Locations())
assert.Equal(t, []dyn.Location{l1, l2}, out.Get("bar").Locations())
assert.Equal(t, []dyn.Location{l1}, out.Get("foo").Locations())
assert.Equal(t, []dyn.Location{l2}, out.Get("qux").Locations())
// Location of the merged value should be the location of v2.
assert.Equal(t, l2, out.Location())
// Value of bar is "baz" which comes from v1. This .Location() should
// return the location of v1.
assert.Equal(t, l1, out.Get("bar").Location())
// Original locations of keys that were not overwritten should be preserved.
assert.Equal(t, l1, out.Get("foo").Location())
assert.Equal(t, l2, out.Get("qux").Location())
} }
} }
func TestMergeMapsNil(t *testing.T) { func TestMergeMapsNil(t *testing.T) {
v := dyn.V(map[string]dyn.Value{ l := dyn.Location{File: "file", Line: 1, Column: 2}
v := dyn.NewValue(map[string]dyn.Value{
"foo": dyn.V("bar"), "foo": dyn.V("bar"),
}) }, []dyn.Location{l})
nilL := dyn.Location{File: "file", Line: 3, Column: 4}
nilV := dyn.NewValue(nil, []dyn.Location{nilL})
// Merge nil into v. // Merge nil into v.
{ {
out, err := Merge(v, dyn.NilValue) out, err := Merge(v, nilV)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, map[string]any{ assert.Equal(t, map[string]any{
"foo": "bar", "foo": "bar",
}, out.AsAny()) }, out.AsAny())
// Locations of both values should be preserved.
assert.Equal(t, []dyn.Location{l, nilL}, out.Locations())
// Location of the non-nil value should be returned by .Location().
assert.Equal(t, l, out.Location())
} }
// Merge v into nil. // Merge v into nil.
{ {
out, err := Merge(dyn.NilValue, v) out, err := Merge(nilV, v)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, map[string]any{ assert.Equal(t, map[string]any{
"foo": "bar", "foo": "bar",
}, out.AsAny()) }, out.AsAny())
// Locations of both values should be preserved.
assert.Equal(t, []dyn.Location{l, nilL}, out.Locations())
// Location of the non-nil value should be returned by .Location().
assert.Equal(t, l, out.Location())
} }
} }
@ -81,15 +134,18 @@ func TestMergeMapsError(t *testing.T) {
} }
func TestMergeSequences(t *testing.T) { func TestMergeSequences(t *testing.T) {
v1 := dyn.V([]dyn.Value{ l1 := dyn.Location{File: "file1", Line: 1, Column: 2}
dyn.V("bar"), v1 := dyn.NewValue([]dyn.Value{
dyn.V("baz"), dyn.NewValue("bar", []dyn.Location{l1}),
}) dyn.NewValue("baz", []dyn.Location{l1}),
}, []dyn.Location{l1})
v2 := dyn.V([]dyn.Value{ l2 := dyn.Location{File: "file2", Line: 3, Column: 4}
dyn.V("qux"), l3 := dyn.Location{File: "file3", Line: 5, Column: 6}
dyn.V("foo"), v2 := dyn.NewValue([]dyn.Value{
}) dyn.NewValue("qux", []dyn.Location{l2}),
dyn.NewValue("foo", []dyn.Location{l3}),
}, []dyn.Location{l2, l3})
// Merge v2 into v1. // Merge v2 into v1.
{ {
@ -101,6 +157,18 @@ func TestMergeSequences(t *testing.T) {
"qux", "qux",
"foo", "foo",
}, out.AsAny()) }, out.AsAny())
// Locations of both values should be preserved.
assert.Equal(t, []dyn.Location{l1, l2, l3}, out.Locations())
// Location of the merged value should be the location of v1.
assert.Equal(t, l1, out.Location())
// Location of the individual values should be preserved.
assert.Equal(t, l1, out.Index(0).Location()) // "bar"
assert.Equal(t, l1, out.Index(1).Location()) // "baz"
assert.Equal(t, l2, out.Index(2).Location()) // "qux"
assert.Equal(t, l3, out.Index(3).Location()) // "foo"
} }
// Merge v1 into v2. // Merge v1 into v2.
@ -113,6 +181,18 @@ func TestMergeSequences(t *testing.T) {
"bar", "bar",
"baz", "baz",
}, out.AsAny()) }, out.AsAny())
// Locations of both values should be preserved.
assert.Equal(t, []dyn.Location{l2, l3, l1}, out.Locations())
// Location of the merged value should be the location of v2.
assert.Equal(t, l2, out.Location())
// Location of the individual values should be preserved.
assert.Equal(t, l2, out.Index(0).Location()) // "qux"
assert.Equal(t, l3, out.Index(1).Location()) // "foo"
assert.Equal(t, l1, out.Index(2).Location()) // "bar"
assert.Equal(t, l1, out.Index(3).Location()) // "baz"
} }
} }
@ -156,14 +236,22 @@ func TestMergeSequencesError(t *testing.T) {
} }
func TestMergePrimitives(t *testing.T) { func TestMergePrimitives(t *testing.T) {
v1 := dyn.V("bar") l1 := dyn.Location{File: "file1", Line: 1, Column: 2}
v2 := dyn.V("baz") l2 := dyn.Location{File: "file2", Line: 3, Column: 4}
v1 := dyn.NewValue("bar", []dyn.Location{l1})
v2 := dyn.NewValue("baz", []dyn.Location{l2})
// Merge v2 into v1. // Merge v2 into v1.
{ {
out, err := Merge(v1, v2) out, err := Merge(v1, v2)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "baz", out.AsAny()) assert.Equal(t, "baz", out.AsAny())
// Locations of both values should be preserved.
assert.Equal(t, []dyn.Location{l2, l1}, out.Locations())
// Location of the merged value should be the location of v2, the second value.
assert.Equal(t, l2, out.Location())
} }
// Merge v1 into v2. // Merge v1 into v2.
@ -171,6 +259,12 @@ func TestMergePrimitives(t *testing.T) {
out, err := Merge(v2, v1) out, err := Merge(v2, v1)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "bar", out.AsAny()) assert.Equal(t, "bar", out.AsAny())
// Locations of both values should be preserved.
assert.Equal(t, []dyn.Location{l1, l2}, out.Locations())
// Location of the merged value should be the location of v1, the second value.
assert.Equal(t, l1, out.Location())
} }
} }

View File

@ -51,7 +51,7 @@ func override(basePath dyn.Path, left dyn.Value, right dyn.Value, visitor Overri
return dyn.InvalidValue, err return dyn.InvalidValue, err
} }
return dyn.NewValue(merged, left.Location()), nil return dyn.NewValue(merged, left.Locations()), nil
case dyn.KindSequence: case dyn.KindSequence:
// some sequences are keyed, and we can detect which elements are added/removed/updated, // some sequences are keyed, and we can detect which elements are added/removed/updated,
@ -62,7 +62,7 @@ func override(basePath dyn.Path, left dyn.Value, right dyn.Value, visitor Overri
return dyn.InvalidValue, err return dyn.InvalidValue, err
} }
return dyn.NewValue(merged, left.Location()), nil return dyn.NewValue(merged, left.Locations()), nil
case dyn.KindString: case dyn.KindString:
if left.MustString() == right.MustString() { if left.MustString() == right.MustString() {

View File

@ -27,79 +27,79 @@ func TestOverride_Primitive(t *testing.T) {
{ {
name: "string (updated)", name: "string (updated)",
state: visitorState{updated: []string{"root"}}, state: visitorState{updated: []string{"root"}},
left: dyn.NewValue("a", leftLocation), left: dyn.NewValue("a", []dyn.Location{leftLocation}),
right: dyn.NewValue("b", rightLocation), right: dyn.NewValue("b", []dyn.Location{rightLocation}),
expected: dyn.NewValue("b", rightLocation), expected: dyn.NewValue("b", []dyn.Location{rightLocation}),
}, },
{ {
name: "string (not updated)", name: "string (not updated)",
state: visitorState{}, state: visitorState{},
left: dyn.NewValue("a", leftLocation), left: dyn.NewValue("a", []dyn.Location{leftLocation}),
right: dyn.NewValue("a", rightLocation), right: dyn.NewValue("a", []dyn.Location{rightLocation}),
expected: dyn.NewValue("a", leftLocation), expected: dyn.NewValue("a", []dyn.Location{leftLocation}),
}, },
{ {
name: "bool (updated)", name: "bool (updated)",
state: visitorState{updated: []string{"root"}}, state: visitorState{updated: []string{"root"}},
left: dyn.NewValue(true, leftLocation), left: dyn.NewValue(true, []dyn.Location{leftLocation}),
right: dyn.NewValue(false, rightLocation), right: dyn.NewValue(false, []dyn.Location{rightLocation}),
expected: dyn.NewValue(false, rightLocation), expected: dyn.NewValue(false, []dyn.Location{rightLocation}),
}, },
{ {
name: "bool (not updated)", name: "bool (not updated)",
state: visitorState{}, state: visitorState{},
left: dyn.NewValue(true, leftLocation), left: dyn.NewValue(true, []dyn.Location{leftLocation}),
right: dyn.NewValue(true, rightLocation), right: dyn.NewValue(true, []dyn.Location{rightLocation}),
expected: dyn.NewValue(true, leftLocation), expected: dyn.NewValue(true, []dyn.Location{leftLocation}),
}, },
{ {
name: "int (updated)", name: "int (updated)",
state: visitorState{updated: []string{"root"}}, state: visitorState{updated: []string{"root"}},
left: dyn.NewValue(1, leftLocation), left: dyn.NewValue(1, []dyn.Location{leftLocation}),
right: dyn.NewValue(2, rightLocation), right: dyn.NewValue(2, []dyn.Location{rightLocation}),
expected: dyn.NewValue(2, rightLocation), expected: dyn.NewValue(2, []dyn.Location{rightLocation}),
}, },
{ {
name: "int (not updated)", name: "int (not updated)",
state: visitorState{}, state: visitorState{},
left: dyn.NewValue(int32(1), leftLocation), left: dyn.NewValue(int32(1), []dyn.Location{leftLocation}),
right: dyn.NewValue(int64(1), rightLocation), right: dyn.NewValue(int64(1), []dyn.Location{rightLocation}),
expected: dyn.NewValue(int32(1), leftLocation), expected: dyn.NewValue(int32(1), []dyn.Location{leftLocation}),
}, },
{ {
name: "float (updated)", name: "float (updated)",
state: visitorState{updated: []string{"root"}}, state: visitorState{updated: []string{"root"}},
left: dyn.NewValue(1.0, leftLocation), left: dyn.NewValue(1.0, []dyn.Location{leftLocation}),
right: dyn.NewValue(2.0, rightLocation), right: dyn.NewValue(2.0, []dyn.Location{rightLocation}),
expected: dyn.NewValue(2.0, rightLocation), expected: dyn.NewValue(2.0, []dyn.Location{rightLocation}),
}, },
{ {
name: "float (not updated)", name: "float (not updated)",
state: visitorState{}, state: visitorState{},
left: dyn.NewValue(float32(1.0), leftLocation), left: dyn.NewValue(float32(1.0), []dyn.Location{leftLocation}),
right: dyn.NewValue(float64(1.0), rightLocation), right: dyn.NewValue(float64(1.0), []dyn.Location{rightLocation}),
expected: dyn.NewValue(float32(1.0), leftLocation), expected: dyn.NewValue(float32(1.0), []dyn.Location{leftLocation}),
}, },
{ {
name: "time (updated)", name: "time (updated)",
state: visitorState{updated: []string{"root"}}, state: visitorState{updated: []string{"root"}},
left: dyn.NewValue(time.UnixMilli(10000), leftLocation), left: dyn.NewValue(time.UnixMilli(10000), []dyn.Location{leftLocation}),
right: dyn.NewValue(time.UnixMilli(10001), rightLocation), right: dyn.NewValue(time.UnixMilli(10001), []dyn.Location{rightLocation}),
expected: dyn.NewValue(time.UnixMilli(10001), rightLocation), expected: dyn.NewValue(time.UnixMilli(10001), []dyn.Location{rightLocation}),
}, },
{ {
name: "time (not updated)", name: "time (not updated)",
state: visitorState{}, state: visitorState{},
left: dyn.NewValue(time.UnixMilli(10000), leftLocation), left: dyn.NewValue(time.UnixMilli(10000), []dyn.Location{leftLocation}),
right: dyn.NewValue(time.UnixMilli(10000), rightLocation), right: dyn.NewValue(time.UnixMilli(10000), []dyn.Location{rightLocation}),
expected: dyn.NewValue(time.UnixMilli(10000), leftLocation), expected: dyn.NewValue(time.UnixMilli(10000), []dyn.Location{leftLocation}),
}, },
{ {
name: "different types (updated)", name: "different types (updated)",
state: visitorState{updated: []string{"root"}}, state: visitorState{updated: []string{"root"}},
left: dyn.NewValue("a", leftLocation), left: dyn.NewValue("a", []dyn.Location{leftLocation}),
right: dyn.NewValue(42, rightLocation), right: dyn.NewValue(42, []dyn.Location{rightLocation}),
expected: dyn.NewValue(42, rightLocation), expected: dyn.NewValue(42, []dyn.Location{rightLocation}),
}, },
{ {
name: "map - remove 'a', update 'b'", name: "map - remove 'a', update 'b'",
@ -109,23 +109,22 @@ func TestOverride_Primitive(t *testing.T) {
}, },
left: dyn.NewValue( left: dyn.NewValue(
map[string]dyn.Value{ map[string]dyn.Value{
"a": dyn.NewValue(42, leftLocation), "a": dyn.NewValue(42, []dyn.Location{leftLocation}),
"b": dyn.NewValue(10, leftLocation), "b": dyn.NewValue(10, []dyn.Location{leftLocation}),
}, },
leftLocation, []dyn.Location{leftLocation}),
),
right: dyn.NewValue( right: dyn.NewValue(
map[string]dyn.Value{ map[string]dyn.Value{
"b": dyn.NewValue(20, rightLocation), "b": dyn.NewValue(20, []dyn.Location{rightLocation}),
}, },
rightLocation, []dyn.Location{rightLocation}),
),
expected: dyn.NewValue( expected: dyn.NewValue(
map[string]dyn.Value{ map[string]dyn.Value{
"b": dyn.NewValue(20, rightLocation), "b": dyn.NewValue(20, []dyn.Location{rightLocation}),
}, },
leftLocation, []dyn.Location{leftLocation}),
),
}, },
{ {
name: "map - add 'a'", name: "map - add 'a'",
@ -134,24 +133,26 @@ func TestOverride_Primitive(t *testing.T) {
}, },
left: dyn.NewValue( left: dyn.NewValue(
map[string]dyn.Value{ map[string]dyn.Value{
"b": dyn.NewValue(10, leftLocation), "b": dyn.NewValue(10, []dyn.Location{leftLocation}),
}, },
leftLocation, []dyn.Location{leftLocation},
), ),
right: dyn.NewValue( right: dyn.NewValue(
map[string]dyn.Value{ map[string]dyn.Value{
"a": dyn.NewValue(42, rightLocation), "a": dyn.NewValue(42, []dyn.Location{rightLocation}),
"b": dyn.NewValue(10, rightLocation), "b": dyn.NewValue(10, []dyn.Location{rightLocation}),
}, },
leftLocation, []dyn.Location{leftLocation},
), ),
expected: dyn.NewValue( expected: dyn.NewValue(
map[string]dyn.Value{ map[string]dyn.Value{
"a": dyn.NewValue(42, rightLocation), "a": dyn.NewValue(42, []dyn.Location{rightLocation}),
// location hasn't changed because value hasn't changed // location hasn't changed because value hasn't changed
"b": dyn.NewValue(10, leftLocation), "b": dyn.NewValue(10, []dyn.Location{leftLocation}),
}, },
leftLocation, []dyn.Location{leftLocation},
), ),
}, },
{ {
@ -161,23 +162,25 @@ func TestOverride_Primitive(t *testing.T) {
}, },
left: dyn.NewValue( left: dyn.NewValue(
map[string]dyn.Value{ map[string]dyn.Value{
"a": dyn.NewValue(42, leftLocation), "a": dyn.NewValue(42, []dyn.Location{leftLocation}),
"b": dyn.NewValue(10, leftLocation), "b": dyn.NewValue(10, []dyn.Location{leftLocation}),
}, },
leftLocation, []dyn.Location{leftLocation},
), ),
right: dyn.NewValue( right: dyn.NewValue(
map[string]dyn.Value{ map[string]dyn.Value{
"b": dyn.NewValue(10, rightLocation), "b": dyn.NewValue(10, []dyn.Location{rightLocation}),
}, },
leftLocation, []dyn.Location{leftLocation},
), ),
expected: dyn.NewValue( expected: dyn.NewValue(
map[string]dyn.Value{ map[string]dyn.Value{
// location hasn't changed because value hasn't changed // location hasn't changed because value hasn't changed
"b": dyn.NewValue(10, leftLocation), "b": dyn.NewValue(10, []dyn.Location{leftLocation}),
}, },
leftLocation, []dyn.Location{leftLocation},
), ),
}, },
{ {
@ -189,36 +192,38 @@ func TestOverride_Primitive(t *testing.T) {
map[string]dyn.Value{ map[string]dyn.Value{
"jobs": dyn.NewValue( "jobs": dyn.NewValue(
map[string]dyn.Value{ map[string]dyn.Value{
"job_0": dyn.NewValue(42, leftLocation), "job_0": dyn.NewValue(42, []dyn.Location{leftLocation}),
}, },
leftLocation, []dyn.Location{leftLocation},
), ),
}, },
leftLocation, []dyn.Location{leftLocation},
), ),
right: dyn.NewValue( right: dyn.NewValue(
map[string]dyn.Value{ map[string]dyn.Value{
"jobs": dyn.NewValue( "jobs": dyn.NewValue(
map[string]dyn.Value{ map[string]dyn.Value{
"job_0": dyn.NewValue(42, rightLocation), "job_0": dyn.NewValue(42, []dyn.Location{rightLocation}),
"job_1": dyn.NewValue(1337, rightLocation), "job_1": dyn.NewValue(1337, []dyn.Location{rightLocation}),
}, },
rightLocation, []dyn.Location{rightLocation},
), ),
}, },
rightLocation, []dyn.Location{rightLocation},
), ),
expected: dyn.NewValue( expected: dyn.NewValue(
map[string]dyn.Value{ map[string]dyn.Value{
"jobs": dyn.NewValue( "jobs": dyn.NewValue(
map[string]dyn.Value{ map[string]dyn.Value{
"job_0": dyn.NewValue(42, leftLocation), "job_0": dyn.NewValue(42, []dyn.Location{leftLocation}),
"job_1": dyn.NewValue(1337, rightLocation), "job_1": dyn.NewValue(1337, []dyn.Location{rightLocation}),
}, },
leftLocation, []dyn.Location{leftLocation},
), ),
}, },
leftLocation, []dyn.Location{leftLocation},
), ),
}, },
{ {
@ -228,35 +233,35 @@ func TestOverride_Primitive(t *testing.T) {
map[string]dyn.Value{ map[string]dyn.Value{
"jobs": dyn.NewValue( "jobs": dyn.NewValue(
map[string]dyn.Value{ map[string]dyn.Value{
"job_0": dyn.NewValue(42, leftLocation), "job_0": dyn.NewValue(42, []dyn.Location{leftLocation}),
"job_1": dyn.NewValue(1337, rightLocation), "job_1": dyn.NewValue(1337, []dyn.Location{rightLocation}),
}, },
leftLocation, []dyn.Location{leftLocation},
), ),
}, },
leftLocation, []dyn.Location{leftLocation},
), ),
right: dyn.NewValue( right: dyn.NewValue(
map[string]dyn.Value{ map[string]dyn.Value{
"jobs": dyn.NewValue( "jobs": dyn.NewValue(
map[string]dyn.Value{ map[string]dyn.Value{
"job_0": dyn.NewValue(42, rightLocation), "job_0": dyn.NewValue(42, []dyn.Location{rightLocation}),
}, },
rightLocation, []dyn.Location{rightLocation},
), ),
}, },
rightLocation, []dyn.Location{rightLocation},
), ),
expected: dyn.NewValue( expected: dyn.NewValue(
map[string]dyn.Value{ map[string]dyn.Value{
"jobs": dyn.NewValue( "jobs": dyn.NewValue(
map[string]dyn.Value{ map[string]dyn.Value{
"job_0": dyn.NewValue(42, leftLocation), "job_0": dyn.NewValue(42, []dyn.Location{leftLocation}),
}, },
leftLocation, []dyn.Location{leftLocation},
), ),
}, },
leftLocation, []dyn.Location{leftLocation},
), ),
}, },
{ {
@ -264,23 +269,23 @@ func TestOverride_Primitive(t *testing.T) {
state: visitorState{added: []string{"root[1]"}}, state: visitorState{added: []string{"root[1]"}},
left: dyn.NewValue( left: dyn.NewValue(
[]dyn.Value{ []dyn.Value{
dyn.NewValue(42, leftLocation), dyn.NewValue(42, []dyn.Location{leftLocation}),
}, },
leftLocation, []dyn.Location{leftLocation},
), ),
right: dyn.NewValue( right: dyn.NewValue(
[]dyn.Value{ []dyn.Value{
dyn.NewValue(42, rightLocation), dyn.NewValue(42, []dyn.Location{rightLocation}),
dyn.NewValue(10, rightLocation), dyn.NewValue(10, []dyn.Location{rightLocation}),
}, },
rightLocation, []dyn.Location{rightLocation},
), ),
expected: dyn.NewValue( expected: dyn.NewValue(
[]dyn.Value{ []dyn.Value{
dyn.NewValue(42, leftLocation), dyn.NewValue(42, []dyn.Location{leftLocation}),
dyn.NewValue(10, rightLocation), dyn.NewValue(10, []dyn.Location{rightLocation}),
}, },
leftLocation, []dyn.Location{leftLocation},
), ),
}, },
{ {
@ -288,67 +293,67 @@ func TestOverride_Primitive(t *testing.T) {
state: visitorState{removed: []string{"root[1]"}}, state: visitorState{removed: []string{"root[1]"}},
left: dyn.NewValue( left: dyn.NewValue(
[]dyn.Value{ []dyn.Value{
dyn.NewValue(42, leftLocation), dyn.NewValue(42, []dyn.Location{leftLocation}),
dyn.NewValue(10, leftLocation), dyn.NewValue(10, []dyn.Location{leftLocation}),
}, },
leftLocation, []dyn.Location{leftLocation},
), ),
right: dyn.NewValue( right: dyn.NewValue(
[]dyn.Value{ []dyn.Value{
dyn.NewValue(42, rightLocation), dyn.NewValue(42, []dyn.Location{rightLocation}),
}, },
rightLocation, []dyn.Location{rightLocation},
), ),
expected: dyn.NewValue( expected: dyn.NewValue(
[]dyn.Value{ []dyn.Value{
// location hasn't changed because value hasn't changed dyn.NewValue(42, []dyn.Location{leftLocation}),
dyn.NewValue(42, leftLocation),
}, },
leftLocation, []dyn.Location{leftLocation},
), ),
// location hasn't changed because value hasn't changed
}, },
{ {
name: "sequence (not updated)", name: "sequence (not updated)",
state: visitorState{}, state: visitorState{},
left: dyn.NewValue( left: dyn.NewValue(
[]dyn.Value{ []dyn.Value{
dyn.NewValue(42, leftLocation), dyn.NewValue(42, []dyn.Location{leftLocation}),
}, },
leftLocation, []dyn.Location{leftLocation},
), ),
right: dyn.NewValue( right: dyn.NewValue(
[]dyn.Value{ []dyn.Value{
dyn.NewValue(42, rightLocation), dyn.NewValue(42, []dyn.Location{rightLocation}),
}, },
rightLocation, []dyn.Location{rightLocation},
), ),
expected: dyn.NewValue( expected: dyn.NewValue(
[]dyn.Value{ []dyn.Value{
dyn.NewValue(42, leftLocation), dyn.NewValue(42, []dyn.Location{leftLocation}),
}, },
leftLocation, []dyn.Location{leftLocation},
), ),
}, },
{ {
name: "nil (not updated)", name: "nil (not updated)",
state: visitorState{}, state: visitorState{},
left: dyn.NilValue.WithLocation(leftLocation), left: dyn.NilValue.WithLocations([]dyn.Location{leftLocation}),
right: dyn.NilValue.WithLocation(rightLocation), right: dyn.NilValue.WithLocations([]dyn.Location{rightLocation}),
expected: dyn.NilValue.WithLocation(leftLocation), expected: dyn.NilValue.WithLocations([]dyn.Location{leftLocation}),
}, },
{ {
name: "nil (updated)", name: "nil (updated)",
state: visitorState{updated: []string{"root"}}, state: visitorState{updated: []string{"root"}},
left: dyn.NilValue, left: dyn.NilValue,
right: dyn.NewValue(42, rightLocation), right: dyn.NewValue(42, []dyn.Location{rightLocation}),
expected: dyn.NewValue(42, rightLocation), expected: dyn.NewValue(42, []dyn.Location{rightLocation}),
}, },
{ {
name: "change kind (updated)", name: "change kind (updated)",
state: visitorState{updated: []string{"root"}}, state: visitorState{updated: []string{"root"}},
left: dyn.NewValue(42.0, leftLocation), left: dyn.NewValue(42.0, []dyn.Location{leftLocation}),
right: dyn.NewValue(42, rightLocation), right: dyn.NewValue(42, []dyn.Location{rightLocation}),
expected: dyn.NewValue(42, rightLocation), expected: dyn.NewValue(42, []dyn.Location{rightLocation}),
}, },
} }
@ -375,7 +380,7 @@ func TestOverride_Primitive(t *testing.T) {
}) })
t.Run(tc.name+" - visitor overrides value", func(t *testing.T) { t.Run(tc.name+" - visitor overrides value", func(t *testing.T) {
expected := dyn.NewValue("return value", dyn.Location{}) expected := dyn.V("return value")
s, visitor := createVisitor(visitorOpts{returnValue: &expected}) s, visitor := createVisitor(visitorOpts{returnValue: &expected})
out, err := override(dyn.EmptyPath, tc.left, tc.right, visitor) out, err := override(dyn.EmptyPath, tc.left, tc.right, visitor)
@ -427,17 +432,17 @@ func TestOverride_PreserveMappingKeys(t *testing.T) {
rightValueLocation := dyn.Location{File: "right.yml", Line: 3, Column: 1} rightValueLocation := dyn.Location{File: "right.yml", Line: 3, Column: 1}
left := dyn.NewMapping() left := dyn.NewMapping()
left.Set(dyn.NewValue("a", leftKeyLocation), dyn.NewValue(42, leftValueLocation)) left.Set(dyn.NewValue("a", []dyn.Location{leftKeyLocation}), dyn.NewValue(42, []dyn.Location{leftValueLocation}))
right := dyn.NewMapping() right := dyn.NewMapping()
right.Set(dyn.NewValue("a", rightKeyLocation), dyn.NewValue(7, rightValueLocation)) right.Set(dyn.NewValue("a", []dyn.Location{rightKeyLocation}), dyn.NewValue(7, []dyn.Location{rightValueLocation}))
state, visitor := createVisitor(visitorOpts{}) state, visitor := createVisitor(visitorOpts{})
out, err := override( out, err := override(
dyn.EmptyPath, dyn.EmptyPath,
dyn.NewValue(left, leftLocation), dyn.NewValue(left, []dyn.Location{leftLocation}),
dyn.NewValue(right, rightLocation), dyn.NewValue(right, []dyn.Location{rightLocation}),
visitor, visitor,
) )

View File

@ -72,7 +72,7 @@ func (c anyKeyComponent) visit(v Value, prefix Path, suffix Pattern, opts visitO
m.Set(pk, nv) m.Set(pk, nv)
} }
return NewValue(m, v.Location()), nil return NewValue(m, v.Locations()), nil
} }
type anyIndexComponent struct{} type anyIndexComponent struct{}
@ -103,5 +103,5 @@ func (c anyIndexComponent) visit(v Value, prefix Path, suffix Pattern, opts visi
s[i] = nv s[i] = nv
} }
return NewValue(s, v.Location()), nil return NewValue(s, v.Locations()), nil
} }

View File

@ -2,13 +2,18 @@ package dyn
import ( import (
"fmt" "fmt"
"slices"
) )
type Value struct { type Value struct {
v any v any
k Kind k Kind
l Location
// 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
// Whether or not this value is an anchor. // 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. // If this node doesn't map to a type, we don't need to warn about it.
@ -27,11 +32,11 @@ var NilValue = Value{
// V constructs a new Value with the given value. // V constructs a new Value with the given value.
func V(v any) Value { func V(v any) Value {
return NewValue(v, Location{}) return NewValue(v, []Location{})
} }
// NewValue constructs a new Value with the given value and location. // NewValue constructs a new Value with the given value and location.
func NewValue(v any, loc Location) Value { func NewValue(v any, loc []Location) Value {
switch vin := v.(type) { switch vin := v.(type) {
case map[string]Value: case map[string]Value:
v = newMappingFromGoMap(vin) v = newMappingFromGoMap(vin)
@ -40,16 +45,30 @@ func NewValue(v any, loc Location) Value {
return Value{ return Value{
v: v, v: v,
k: kindOf(v), k: kindOf(v),
l: loc,
// create a copy of the locations, so that mutations to the original slice
// don't affect new value.
l: slices.Clone(loc),
} }
} }
// WithLocation returns a new Value with its location set to the given value. // WithLocations returns a new Value with its location set to the given value.
func (v Value) WithLocation(loc Location) Value { func (v Value) WithLocations(loc []Location) Value {
return Value{ return Value{
v: v.v, v: v.v,
k: v.k, k: v.k,
l: loc,
// create a copy of the locations, so that mutations to the original slice
// don't affect new value.
l: slices.Clone(loc),
}
}
func (v Value) AppendLocationsFromValue(w Value) Value {
return Value{
v: v.v,
k: v.k,
l: append(v.l, w.l...),
} }
} }
@ -61,10 +80,18 @@ func (v Value) Value() any {
return v.v return v.v
} }
func (v Value) Location() Location { func (v Value) Locations() []Location {
return v.l return v.l
} }
func (v Value) Location() Location {
if len(v.l) == 0 {
return Location{}
}
return v.l[0]
}
func (v Value) IsValid() bool { func (v Value) IsValid() bool {
return v.k != KindInvalid return v.k != KindInvalid
} }
@ -153,7 +180,10 @@ func (v Value) IsAnchor() bool {
// We need a custom implementation because maps and slices // We need a custom implementation because maps and slices
// cannot be compared with the regular == operator. // cannot be compared with the regular == operator.
func (v Value) eq(w Value) bool { func (v Value) eq(w Value) bool {
if v.k != w.k || v.l != w.l { if v.k != w.k {
return false
}
if !slices.Equal(v.l, w.l) {
return false return false
} }

View File

@ -25,16 +25,19 @@ func TestValueAsMap(t *testing.T) {
_, ok := zeroValue.AsMap() _, ok := zeroValue.AsMap()
assert.False(t, ok) assert.False(t, ok)
var intValue = dyn.NewValue(1, dyn.Location{}) var intValue = dyn.V(1)
_, ok = intValue.AsMap() _, ok = intValue.AsMap()
assert.False(t, ok) assert.False(t, ok)
var mapValue = dyn.NewValue( var mapValue = dyn.NewValue(
map[string]dyn.Value{ map[string]dyn.Value{
"key": dyn.NewValue("value", dyn.Location{File: "file", Line: 1, Column: 2}), "key": dyn.NewValue(
"value",
[]dyn.Location{{File: "file", Line: 1, Column: 2}}),
}, },
dyn.Location{File: "file", Line: 1, Column: 2}, []dyn.Location{{File: "file", Line: 1, Column: 2}},
) )
m, ok := mapValue.AsMap() m, ok := mapValue.AsMap()
assert.True(t, ok) assert.True(t, ok)
assert.Equal(t, 1, m.Len()) assert.Equal(t, 1, m.Len())
@ -43,6 +46,6 @@ func TestValueAsMap(t *testing.T) {
func TestValueIsValid(t *testing.T) { func TestValueIsValid(t *testing.T) {
var zeroValue dyn.Value var zeroValue dyn.Value
assert.False(t, zeroValue.IsValid()) assert.False(t, zeroValue.IsValid())
var intValue = dyn.NewValue(1, dyn.Location{}) var intValue = dyn.V(1)
assert.True(t, intValue.IsValid()) assert.True(t, intValue.IsValid())
} }

View File

@ -11,7 +11,7 @@ import (
func TestValueUnderlyingMap(t *testing.T) { func TestValueUnderlyingMap(t *testing.T) {
v := dyn.V( v := dyn.V(
map[string]dyn.Value{ map[string]dyn.Value{
"key": dyn.NewValue("value", dyn.Location{File: "file", Line: 1, Column: 2}), "key": dyn.NewValue("value", []dyn.Location{{File: "file", Line: 1, Column: 2}}),
}, },
) )
@ -33,7 +33,7 @@ func TestValueUnderlyingMap(t *testing.T) {
func TestValueUnderlyingSequence(t *testing.T) { func TestValueUnderlyingSequence(t *testing.T) {
v := dyn.V( v := dyn.V(
[]dyn.Value{ []dyn.Value{
dyn.NewValue("value", dyn.Location{File: "file", Line: 1, Column: 2}), dyn.NewValue("value", []dyn.Location{{File: "file", Line: 1, Column: 2}}),
}, },
) )

View File

@ -27,7 +27,7 @@ func Foreach(fn MapFunc) MapFunc {
} }
m.Set(pk, nv) m.Set(pk, nv)
} }
return NewValue(m, v.Location()), nil return NewValue(m, v.Locations()), nil
case KindSequence: case KindSequence:
s := slices.Clone(v.MustSequence()) s := slices.Clone(v.MustSequence())
for i, value := range s { for i, value := range s {
@ -37,7 +37,7 @@ func Foreach(fn MapFunc) MapFunc {
return InvalidValue, err return InvalidValue, err
} }
} }
return NewValue(s, v.Location()), nil return NewValue(s, v.Locations()), nil
default: default:
return InvalidValue, fmt.Errorf("expected a map or sequence, found %s", v.Kind()) return InvalidValue, fmt.Errorf("expected a map or sequence, found %s", v.Kind())
} }

View File

@ -86,7 +86,7 @@ func (d *loader) loadSequence(node *yaml.Node, loc dyn.Location) (dyn.Value, err
acc[i] = v acc[i] = v
} }
return dyn.NewValue(acc, loc), nil return dyn.NewValue(acc, []dyn.Location{loc}), nil
} }
func (d *loader) loadMapping(node *yaml.Node, loc dyn.Location) (dyn.Value, error) { func (d *loader) loadMapping(node *yaml.Node, loc dyn.Location) (dyn.Value, error) {
@ -130,7 +130,7 @@ func (d *loader) loadMapping(node *yaml.Node, loc dyn.Location) (dyn.Value, erro
} }
if merge == nil { if merge == nil {
return dyn.NewValue(acc, loc), nil return dyn.NewValue(acc, []dyn.Location{loc}), nil
} }
// Build location for the merge node. // Build location for the merge node.
@ -171,20 +171,20 @@ func (d *loader) loadMapping(node *yaml.Node, loc dyn.Location) (dyn.Value, erro
out.Merge(m) out.Merge(m)
} }
return dyn.NewValue(out, loc), nil return dyn.NewValue(out, []dyn.Location{loc}), nil
} }
func (d *loader) loadScalar(node *yaml.Node, loc dyn.Location) (dyn.Value, error) { func (d *loader) loadScalar(node *yaml.Node, loc dyn.Location) (dyn.Value, error) {
st := node.ShortTag() st := node.ShortTag()
switch st { switch st {
case "!!str": case "!!str":
return dyn.NewValue(node.Value, loc), nil return dyn.NewValue(node.Value, []dyn.Location{loc}), nil
case "!!bool": case "!!bool":
switch strings.ToLower(node.Value) { switch strings.ToLower(node.Value) {
case "true": case "true":
return dyn.NewValue(true, loc), nil return dyn.NewValue(true, []dyn.Location{loc}), nil
case "false": case "false":
return dyn.NewValue(false, loc), nil return dyn.NewValue(false, []dyn.Location{loc}), nil
default: default:
return dyn.InvalidValue, errorf(loc, "invalid bool value: %v", node.Value) return dyn.InvalidValue, errorf(loc, "invalid bool value: %v", node.Value)
} }
@ -195,17 +195,17 @@ func (d *loader) loadScalar(node *yaml.Node, loc dyn.Location) (dyn.Value, error
} }
// Use regular int type instead of int64 if possible. // Use regular int type instead of int64 if possible.
if i64 >= math.MinInt32 && i64 <= math.MaxInt32 { if i64 >= math.MinInt32 && i64 <= math.MaxInt32 {
return dyn.NewValue(int(i64), loc), nil return dyn.NewValue(int(i64), []dyn.Location{loc}), nil
} }
return dyn.NewValue(i64, loc), nil return dyn.NewValue(i64, []dyn.Location{loc}), nil
case "!!float": case "!!float":
f64, err := strconv.ParseFloat(node.Value, 64) f64, err := strconv.ParseFloat(node.Value, 64)
if err != nil { if err != nil {
return dyn.InvalidValue, errorf(loc, "invalid float value: %v", node.Value) return dyn.InvalidValue, errorf(loc, "invalid float value: %v", node.Value)
} }
return dyn.NewValue(f64, loc), nil return dyn.NewValue(f64, []dyn.Location{loc}), nil
case "!!null": case "!!null":
return dyn.NewValue(nil, loc), nil return dyn.NewValue(nil, []dyn.Location{loc}), nil
case "!!timestamp": case "!!timestamp":
// Try a couple of layouts // Try a couple of layouts
for _, layout := range []string{ for _, layout := range []string{
@ -216,7 +216,7 @@ func (d *loader) loadScalar(node *yaml.Node, loc dyn.Location) (dyn.Value, error
} { } {
t, terr := time.Parse(layout, node.Value) t, terr := time.Parse(layout, node.Value)
if terr == nil { if terr == nil {
return dyn.NewValue(t, loc), nil return dyn.NewValue(t, []dyn.Location{loc}), nil
} }
} }
return dyn.InvalidValue, errorf(loc, "invalid timestamp value: %v", node.Value) return dyn.InvalidValue, errorf(loc, "invalid timestamp value: %v", node.Value)

View File

@ -19,7 +19,7 @@ func TestMarshalNilValue(t *testing.T) {
func TestMarshalIntValue(t *testing.T) { func TestMarshalIntValue(t *testing.T) {
s := NewSaver() s := NewSaver()
var intValue = dyn.NewValue(1, dyn.Location{}) var intValue = dyn.V(1)
v, err := s.toYamlNode(intValue) v, err := s.toYamlNode(intValue)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "1", v.Value) assert.Equal(t, "1", v.Value)
@ -28,7 +28,7 @@ func TestMarshalIntValue(t *testing.T) {
func TestMarshalFloatValue(t *testing.T) { func TestMarshalFloatValue(t *testing.T) {
s := NewSaver() s := NewSaver()
var floatValue = dyn.NewValue(1.0, dyn.Location{}) var floatValue = dyn.V(1.0)
v, err := s.toYamlNode(floatValue) v, err := s.toYamlNode(floatValue)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "1", v.Value) assert.Equal(t, "1", v.Value)
@ -37,7 +37,7 @@ func TestMarshalFloatValue(t *testing.T) {
func TestMarshalBoolValue(t *testing.T) { func TestMarshalBoolValue(t *testing.T) {
s := NewSaver() s := NewSaver()
var boolValue = dyn.NewValue(true, dyn.Location{}) var boolValue = dyn.V(true)
v, err := s.toYamlNode(boolValue) v, err := s.toYamlNode(boolValue)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "true", v.Value) assert.Equal(t, "true", v.Value)
@ -46,7 +46,7 @@ func TestMarshalBoolValue(t *testing.T) {
func TestMarshalTimeValue(t *testing.T) { func TestMarshalTimeValue(t *testing.T) {
s := NewSaver() s := NewSaver()
var timeValue = dyn.NewValue(time.Unix(0, 0), dyn.Location{}) var timeValue = dyn.V(time.Unix(0, 0))
v, err := s.toYamlNode(timeValue) v, err := s.toYamlNode(timeValue)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "1970-01-01 00:00:00 +0000 UTC", v.Value) assert.Equal(t, "1970-01-01 00:00:00 +0000 UTC", v.Value)
@ -57,10 +57,10 @@ func TestMarshalSequenceValue(t *testing.T) {
s := NewSaver() s := NewSaver()
var sequenceValue = dyn.NewValue( var sequenceValue = dyn.NewValue(
[]dyn.Value{ []dyn.Value{
dyn.NewValue("value1", dyn.Location{File: "file", Line: 1, Column: 2}), dyn.NewValue("value1", []dyn.Location{{File: "file", Line: 1, Column: 2}}),
dyn.NewValue("value2", dyn.Location{File: "file", Line: 2, Column: 2}), dyn.NewValue("value2", []dyn.Location{{File: "file", Line: 2, Column: 2}}),
}, },
dyn.Location{File: "file", Line: 1, Column: 2}, []dyn.Location{{File: "file", Line: 1, Column: 2}},
) )
v, err := s.toYamlNode(sequenceValue) v, err := s.toYamlNode(sequenceValue)
assert.NoError(t, err) assert.NoError(t, err)
@ -71,7 +71,7 @@ func TestMarshalSequenceValue(t *testing.T) {
func TestMarshalStringValue(t *testing.T) { func TestMarshalStringValue(t *testing.T) {
s := NewSaver() s := NewSaver()
var stringValue = dyn.NewValue("value", dyn.Location{}) var stringValue = dyn.V("value")
v, err := s.toYamlNode(stringValue) v, err := s.toYamlNode(stringValue)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "value", v.Value) assert.Equal(t, "value", v.Value)
@ -82,12 +82,13 @@ func TestMarshalMapValue(t *testing.T) {
s := NewSaver() s := NewSaver()
var mapValue = dyn.NewValue( var mapValue = dyn.NewValue(
map[string]dyn.Value{ map[string]dyn.Value{
"key3": dyn.NewValue("value3", dyn.Location{File: "file", Line: 3, Column: 2}), "key3": dyn.NewValue("value3", []dyn.Location{{File: "file", Line: 3, Column: 2}}),
"key2": dyn.NewValue("value2", dyn.Location{File: "file", Line: 2, Column: 2}), "key2": dyn.NewValue("value2", []dyn.Location{{File: "file", Line: 2, Column: 2}}),
"key1": dyn.NewValue("value1", dyn.Location{File: "file", Line: 1, Column: 2}), "key1": dyn.NewValue("value1", []dyn.Location{{File: "file", Line: 1, Column: 2}}),
}, },
dyn.Location{File: "file", Line: 1, Column: 2}, []dyn.Location{{File: "file", Line: 1, Column: 2}},
) )
v, err := s.toYamlNode(mapValue) v, err := s.toYamlNode(mapValue)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, yaml.MappingNode, v.Kind) assert.Equal(t, yaml.MappingNode, v.Kind)
@ -107,12 +108,12 @@ func TestMarshalNestedValues(t *testing.T) {
map[string]dyn.Value{ map[string]dyn.Value{
"key1": dyn.NewValue( "key1": dyn.NewValue(
map[string]dyn.Value{ map[string]dyn.Value{
"key2": dyn.NewValue("value", dyn.Location{File: "file", Line: 1, Column: 2}), "key2": dyn.NewValue("value", []dyn.Location{{File: "file", Line: 1, Column: 2}}),
}, },
dyn.Location{File: "file", Line: 1, Column: 2}, []dyn.Location{{File: "file", Line: 1, Column: 2}},
), ),
}, },
dyn.Location{File: "file", Line: 1, Column: 2}, []dyn.Location{{File: "file", Line: 1, Column: 2}},
) )
v, err := s.toYamlNode(mapValue) v, err := s.toYamlNode(mapValue)
assert.NoError(t, err) assert.NoError(t, err)
@ -125,14 +126,14 @@ func TestMarshalNestedValues(t *testing.T) {
func TestMarshalHexadecimalValueIsQuoted(t *testing.T) { func TestMarshalHexadecimalValueIsQuoted(t *testing.T) {
s := NewSaver() s := NewSaver()
var hexValue = dyn.NewValue(0x123, dyn.Location{}) var hexValue = dyn.V(0x123)
v, err := s.toYamlNode(hexValue) v, err := s.toYamlNode(hexValue)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "291", v.Value) assert.Equal(t, "291", v.Value)
assert.Equal(t, yaml.Style(0), v.Style) assert.Equal(t, yaml.Style(0), v.Style)
assert.Equal(t, yaml.ScalarNode, v.Kind) assert.Equal(t, yaml.ScalarNode, v.Kind)
var stringValue = dyn.NewValue("0x123", dyn.Location{}) var stringValue = dyn.V("0x123")
v, err = s.toYamlNode(stringValue) v, err = s.toYamlNode(stringValue)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "0x123", v.Value) assert.Equal(t, "0x123", v.Value)
@ -142,14 +143,14 @@ func TestMarshalHexadecimalValueIsQuoted(t *testing.T) {
func TestMarshalBinaryValueIsQuoted(t *testing.T) { func TestMarshalBinaryValueIsQuoted(t *testing.T) {
s := NewSaver() s := NewSaver()
var binaryValue = dyn.NewValue(0b101, dyn.Location{}) var binaryValue = dyn.V(0b101)
v, err := s.toYamlNode(binaryValue) v, err := s.toYamlNode(binaryValue)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "5", v.Value) assert.Equal(t, "5", v.Value)
assert.Equal(t, yaml.Style(0), v.Style) assert.Equal(t, yaml.Style(0), v.Style)
assert.Equal(t, yaml.ScalarNode, v.Kind) assert.Equal(t, yaml.ScalarNode, v.Kind)
var stringValue = dyn.NewValue("0b101", dyn.Location{}) var stringValue = dyn.V("0b101")
v, err = s.toYamlNode(stringValue) v, err = s.toYamlNode(stringValue)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "0b101", v.Value) assert.Equal(t, "0b101", v.Value)
@ -159,14 +160,14 @@ func TestMarshalBinaryValueIsQuoted(t *testing.T) {
func TestMarshalOctalValueIsQuoted(t *testing.T) { func TestMarshalOctalValueIsQuoted(t *testing.T) {
s := NewSaver() s := NewSaver()
var octalValue = dyn.NewValue(0123, dyn.Location{}) var octalValue = dyn.V(0123)
v, err := s.toYamlNode(octalValue) v, err := s.toYamlNode(octalValue)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "83", v.Value) assert.Equal(t, "83", v.Value)
assert.Equal(t, yaml.Style(0), v.Style) assert.Equal(t, yaml.Style(0), v.Style)
assert.Equal(t, yaml.ScalarNode, v.Kind) assert.Equal(t, yaml.ScalarNode, v.Kind)
var stringValue = dyn.NewValue("0123", dyn.Location{}) var stringValue = dyn.V("0123")
v, err = s.toYamlNode(stringValue) v, err = s.toYamlNode(stringValue)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "0123", v.Value) assert.Equal(t, "0123", v.Value)
@ -176,14 +177,14 @@ func TestMarshalOctalValueIsQuoted(t *testing.T) {
func TestMarshalFloatValueIsQuoted(t *testing.T) { func TestMarshalFloatValueIsQuoted(t *testing.T) {
s := NewSaver() s := NewSaver()
var floatValue = dyn.NewValue(1.0, dyn.Location{}) var floatValue = dyn.V(1.0)
v, err := s.toYamlNode(floatValue) v, err := s.toYamlNode(floatValue)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "1", v.Value) assert.Equal(t, "1", v.Value)
assert.Equal(t, yaml.Style(0), v.Style) assert.Equal(t, yaml.Style(0), v.Style)
assert.Equal(t, yaml.ScalarNode, v.Kind) assert.Equal(t, yaml.ScalarNode, v.Kind)
var stringValue = dyn.NewValue("1.0", dyn.Location{}) var stringValue = dyn.V("1.0")
v, err = s.toYamlNode(stringValue) v, err = s.toYamlNode(stringValue)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "1.0", v.Value) assert.Equal(t, "1.0", v.Value)
@ -193,14 +194,14 @@ func TestMarshalFloatValueIsQuoted(t *testing.T) {
func TestMarshalBoolValueIsQuoted(t *testing.T) { func TestMarshalBoolValueIsQuoted(t *testing.T) {
s := NewSaver() s := NewSaver()
var boolValue = dyn.NewValue(true, dyn.Location{}) var boolValue = dyn.V(true)
v, err := s.toYamlNode(boolValue) v, err := s.toYamlNode(boolValue)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "true", v.Value) assert.Equal(t, "true", v.Value)
assert.Equal(t, yaml.Style(0), v.Style) assert.Equal(t, yaml.Style(0), v.Style)
assert.Equal(t, yaml.ScalarNode, v.Kind) assert.Equal(t, yaml.ScalarNode, v.Kind)
var stringValue = dyn.NewValue("true", dyn.Location{}) var stringValue = dyn.V("true")
v, err = s.toYamlNode(stringValue) v, err = s.toYamlNode(stringValue)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "true", v.Value) assert.Equal(t, "true", v.Value)
@ -215,18 +216,18 @@ func TestCustomStylingWithNestedMap(t *testing.T) {
var styledMap = dyn.NewValue( var styledMap = dyn.NewValue(
map[string]dyn.Value{ map[string]dyn.Value{
"key1": dyn.NewValue("value1", dyn.Location{File: "file", Line: 1, Column: 2}), "key1": dyn.NewValue("value1", []dyn.Location{{File: "file", Line: 1, Column: 2}}),
"key2": dyn.NewValue("value2", dyn.Location{File: "file", Line: 2, Column: 2}), "key2": dyn.NewValue("value2", []dyn.Location{{File: "file", Line: 2, Column: 2}}),
}, },
dyn.Location{File: "file", Line: -2, Column: 2}, []dyn.Location{{File: "file", Line: -2, Column: 2}},
) )
var unstyledMap = dyn.NewValue( var unstyledMap = dyn.NewValue(
map[string]dyn.Value{ map[string]dyn.Value{
"key3": dyn.NewValue("value3", dyn.Location{File: "file", Line: 1, Column: 2}), "key3": dyn.NewValue("value3", []dyn.Location{{File: "file", Line: 1, Column: 2}}),
"key4": dyn.NewValue("value4", dyn.Location{File: "file", Line: 2, Column: 2}), "key4": dyn.NewValue("value4", []dyn.Location{{File: "file", Line: 2, Column: 2}}),
}, },
dyn.Location{File: "file", Line: -1, Column: 2}, []dyn.Location{{File: "file", Line: -1, Column: 2}},
) )
var val = dyn.NewValue( var val = dyn.NewValue(
@ -234,7 +235,7 @@ func TestCustomStylingWithNestedMap(t *testing.T) {
"styled": styledMap, "styled": styledMap,
"unstyled": unstyledMap, "unstyled": unstyledMap,
}, },
dyn.Location{File: "file", Line: 1, Column: 2}, []dyn.Location{{File: "file", Line: 1, Column: 2}},
) )
mv, err := s.toYamlNode(val) mv, err := s.toYamlNode(val)

View File

@ -44,7 +44,7 @@ func skipAndOrder(mv dyn.Value, order *Order, skipFields []string, dst map[strin
continue continue
} }
dst[k] = dyn.NewValue(v.Value(), dyn.Location{Line: order.Get(k)}) dst[k] = dyn.NewValue(v.Value(), []dyn.Location{{Line: order.Get(k)}})
} }
return dyn.V(dst), nil return dyn.V(dst), nil

View File

@ -33,16 +33,25 @@ func TestConvertToMapValueWithOrder(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, dyn.V(map[string]dyn.Value{ assert.Equal(t, dyn.V(map[string]dyn.Value{
"list": dyn.NewValue([]dyn.Value{ "list": dyn.NewValue(
[]dyn.Value{
dyn.V("a"), dyn.V("a"),
dyn.V("b"), dyn.V("b"),
dyn.V("c"), dyn.V("c"),
}, dyn.Location{Line: -3}), },
"name": dyn.NewValue("test", dyn.Location{Line: -2}), []dyn.Location{{Line: -3}},
"map": dyn.NewValue(map[string]dyn.Value{ ),
"name": dyn.NewValue(
"test",
[]dyn.Location{{Line: -2}},
),
"map": dyn.NewValue(
map[string]dyn.Value{
"key1": dyn.V("value1"), "key1": dyn.V("value1"),
"key2": dyn.V("value2"), "key2": dyn.V("value2"),
}, dyn.Location{Line: -1}), },
"long_name_field": dyn.NewValue("long name goes here", dyn.Location{Line: 1}), []dyn.Location{{Line: -1}},
),
"long_name_field": dyn.NewValue("long name goes here", []dyn.Location{{Line: 1}}),
}), result) }), result)
} }