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)
}
// 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)

View File

@ -59,7 +59,7 @@ func (m *expandPipelineGlobPaths) expandLibrary(v dyn.Value) ([]dyn.Value, error
if err != nil {
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 {
return nil, err
}
@ -90,7 +90,7 @@ func (m *expandPipelineGlobPaths) expandSequence(p dyn.Path, v dyn.Value) (dyn.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 {

View File

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

View File

@ -38,7 +38,7 @@ func (m *rewriteSyncPaths) makeRelativeTo(root string) dyn.MapFunc {
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.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) {

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.
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 {
return err
}
@ -404,7 +404,7 @@ func (r *Root) MergeTargetOverrides(name string) error {
if v := target.Get("git"); v.Kind() != dyn.KindInvalid {
ref, err := dyn.GetByPath(root, dyn.NewPath(dyn.Key("bundle"), dyn.Key("git")))
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.
@ -415,7 +415,7 @@ func (r *Root) MergeTargetOverrides(name string) error {
// If the branch was overridden, we need to clear the inferred flag.
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 {
return err
}
@ -456,7 +456,7 @@ func rewriteShorthands(v dyn.Value) (dyn.Value, error) {
// configuration will convert this to a string if necessary.
return dyn.NewValue(map[string]dyn.Value{
"default": variable,
}, variable.Location()), nil
}, variable.Locations()), nil
case dyn.KindMap, dyn.KindSequence:
// 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{
"type": typeV,
"default": variable,
}, variable.Location()), nil
}, variable.Locations()), 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) {
// If the path has the given prefix, set the location.
if p.HasPrefix(start) {
return v.WithLocation(dyn.Location{
return v.WithLocations([]dyn.Location{{
File: filePath,
}), nil
}}), nil
}
// 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
for srcv.Kind() == reflect.Pointer {
if srcv.IsNil() {
return dyn.NilValue.WithLocation(ref.Location()), nil
return dyn.NilValue.WithLocations(ref.Locations()), nil
}
srcv = srcv.Elem()
@ -83,7 +83,7 @@ func fromTyped(src any, ref dyn.Value, options ...fromTypedOptions) (dyn.Value,
if err != nil {
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) {

View File

@ -115,16 +115,16 @@ func TestFromTypedStructSetFieldsRetainLocation(t *testing.T) {
}
ref := dyn.V(map[string]dyn.Value{
"foo": dyn.NewValue("bar", dyn.Location{File: "foo"}),
"bar": dyn.NewValue("baz", dyn.Location{File: "bar"}),
"foo": dyn.NewValue("bar", []dyn.Location{{File: "foo"}}),
"bar": dyn.NewValue("baz", []dyn.Location{{File: "bar"}}),
})
nv, err := FromTyped(src, ref)
require.NoError(t, err)
// 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("qux", dyn.Location{File: "bar"}), nv.Get("bar"))
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"))
}
func TestFromTypedStringMapWithZeroValue(t *testing.T) {
@ -359,16 +359,16 @@ func TestFromTypedMapNonEmptyRetainLocation(t *testing.T) {
}
ref := dyn.V(map[string]dyn.Value{
"foo": dyn.NewValue("bar", dyn.Location{File: "foo"}),
"bar": dyn.NewValue("baz", dyn.Location{File: "bar"}),
"foo": dyn.NewValue("bar", []dyn.Location{{File: "foo"}}),
"bar": dyn.NewValue("baz", []dyn.Location{{File: "bar"}}),
})
nv, err := FromTyped(src, ref)
require.NoError(t, err)
// 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("qux", dyn.Location{File: "bar"}), nv.Get("bar"))
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"))
}
func TestFromTypedMapFieldWithZeroValue(t *testing.T) {
@ -432,16 +432,16 @@ func TestFromTypedSliceNonEmptyRetainLocation(t *testing.T) {
}
ref := dyn.V([]dyn.Value{
dyn.NewValue("foo", dyn.Location{File: "foo"}),
dyn.NewValue("bar", dyn.Location{File: "bar"}),
dyn.NewValue("foo", []dyn.Location{{File: "foo"}}),
dyn.NewValue("bar", []dyn.Location{{File: "bar"}}),
})
nv, err := FromTyped(src, ref)
require.NoError(t, err)
// 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("bar", dyn.Location{File: "bar"}), nv.Index(1))
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))
}
func TestFromTypedStringEmpty(t *testing.T) {
@ -477,19 +477,19 @@ func TestFromTypedStringNonEmptyOverwrite(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
var src string = "foo"
nv, err := FromTyped(src, ref)
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
src = "bar"
nv, err = FromTyped(src, ref)
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) {
@ -532,19 +532,19 @@ func TestFromTypedBoolNonEmptyOverwrite(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
var src bool = true
nv, err := FromTyped(src, ref)
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
src = false
nv, err = FromTyped(src, ref)
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) {
@ -595,19 +595,19 @@ func TestFromTypedIntNonEmptyOverwrite(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
var src int = 1234
nv, err := FromTyped(src, ref)
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
src = 1235
nv, err = FromTyped(src, ref)
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) {
@ -659,19 +659,19 @@ func TestFromTypedFloatNonEmptyOverwrite(t *testing.T) {
func TestFromTypedFloatRetainsLocations(t *testing.T) {
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
src = 1.23
nv, err := FromTyped(src, ref)
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
src = 1.24
nv, err = FromTyped(src, ref)
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) {
@ -740,27 +740,27 @@ func TestFromTypedNilPointerRetainsLocations(t *testing.T) {
}
var src *Tmp
ref := dyn.NewValue(nil, dyn.Location{File: "foobar"})
ref := dyn.NewValue(nil, []dyn.Location{{File: "foobar"}})
nv, err := FromTyped(src, ref)
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) {
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)
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) {
var src []string
ref := dyn.NewValue(nil, dyn.Location{File: "foobar"})
ref := dyn.NewValue(nil, []dyn.Location{{File: "foobar"}})
nv, err := FromTyped(src, ref)
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.
if !n.includeMissingFields {
return dyn.NewValue(out, src.Location()), diags
return dyn.NewValue(out, src.Locations()), diags
}
// 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:
return src, diags
@ -203,7 +203,7 @@ func (n normalizeOptions) normalizeMap(typ reflect.Type, src dyn.Value, seen []r
out.Set(pk, nv)
}
return dyn.NewValue(out, src.Location()), diags
return dyn.NewValue(out, src.Locations()), diags
case dyn.KindNil:
return src, diags
@ -238,7 +238,7 @@ func (n normalizeOptions) normalizeSlice(typ reflect.Type, src dyn.Value, seen [
out = append(out, v)
}
return dyn.NewValue(out, src.Location()), diags
return dyn.NewValue(out, src.Locations()), diags
case dyn.KindNil:
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.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) {
@ -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.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) {
@ -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.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) {
@ -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.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) {

View File

@ -229,7 +229,7 @@ func TestNormalizeStructVariableReference(t *testing.T) {
}
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)
assert.Empty(t, err)
assert.Equal(t, vin, vout)
@ -241,7 +241,7 @@ func TestNormalizeStructRandomStringError(t *testing.T) {
}
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)
assert.Len(t, err, 1)
assert.Equal(t, diag.Diagnostic{
@ -258,7 +258,7 @@ func TestNormalizeStructIntError(t *testing.T) {
}
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)
assert.Len(t, err, 1)
assert.Equal(t, diag.Diagnostic{
@ -360,7 +360,7 @@ func TestNormalizeMapNestedError(t *testing.T) {
func TestNormalizeMapVariableReference(t *testing.T) {
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)
assert.Empty(t, err)
assert.Equal(t, vin, vout)
@ -368,7 +368,7 @@ func TestNormalizeMapVariableReference(t *testing.T) {
func TestNormalizeMapRandomStringError(t *testing.T) {
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)
assert.Len(t, err, 1)
assert.Equal(t, diag.Diagnostic{
@ -381,7 +381,7 @@ func TestNormalizeMapRandomStringError(t *testing.T) {
func TestNormalizeMapIntError(t *testing.T) {
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)
assert.Len(t, err, 1)
assert.Equal(t, diag.Diagnostic{
@ -482,7 +482,7 @@ func TestNormalizeSliceNestedError(t *testing.T) {
func TestNormalizeSliceVariableReference(t *testing.T) {
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)
assert.Empty(t, err)
assert.Equal(t, vin, vout)
@ -490,7 +490,7 @@ func TestNormalizeSliceVariableReference(t *testing.T) {
func TestNormalizeSliceRandomStringError(t *testing.T) {
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)
assert.Len(t, err, 1)
assert.Equal(t, diag.Diagnostic{
@ -503,7 +503,7 @@ func TestNormalizeSliceRandomStringError(t *testing.T) {
func TestNormalizeSliceIntError(t *testing.T) {
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)
assert.Len(t, err, 1)
assert.Equal(t, diag.Diagnostic{
@ -524,7 +524,7 @@ func TestNormalizeString(t *testing.T) {
func TestNormalizeStringNil(t *testing.T) {
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)
assert.Len(t, err, 1)
assert.Equal(t, diag.Diagnostic{
@ -537,26 +537,26 @@ func TestNormalizeStringNil(t *testing.T) {
func TestNormalizeStringFromBool(t *testing.T) {
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)
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) {
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)
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) {
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)
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) {
@ -582,7 +582,7 @@ func TestNormalizeBool(t *testing.T) {
func TestNormalizeBoolNil(t *testing.T) {
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)
assert.Len(t, err, 1)
assert.Equal(t, diag.Diagnostic{
@ -658,7 +658,7 @@ func TestNormalizeInt(t *testing.T) {
func TestNormalizeIntNil(t *testing.T) {
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)
assert.Len(t, err, 1)
assert.Equal(t, diag.Diagnostic{
@ -742,7 +742,7 @@ func TestNormalizeFloat(t *testing.T) {
func TestNormalizeFloatNil(t *testing.T) {
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)
assert.Len(t, err, 1)
assert.Equal(t, diag.Diagnostic{
@ -842,26 +842,26 @@ func TestNormalizeAnchors(t *testing.T) {
func TestNormalizeBoolToAny(t *testing.T) {
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)
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) {
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)
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) {
var typ any
v1 := dyn.NewValue(1, 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})
v1 := dyn.NewValue(1, []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}})
vout, err := Normalize(&typ, vin)
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
// 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.
@ -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)
}
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) {

View File

@ -52,7 +52,7 @@ func (e elementsByKey) Map(_ dyn.Path, v dyn.Value) (dyn.Value, error) {
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

View File

@ -12,6 +12,26 @@ import (
// * Merging x with nil or nil with x always yields x.
// * Merging maps a and b means entries from map b take precedence.
// * 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) {
return merge(a, b)
}
@ -22,12 +42,12 @@ func merge(a, b dyn.Value) (dyn.Value, error) {
// If a is nil, return b.
if ak == dyn.KindNil {
return b, nil
return b.AppendLocationsFromValue(a), nil
}
// If b is nil, return a.
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.
@ -75,8 +95,8 @@ func mergeMap(a, b dyn.Value) (dyn.Value, error) {
}
}
// Preserve the location of the first value.
return dyn.NewValue(out, a.Location()), nil
// Preserve the location of the first value. Accumulate the locations of the second value.
return dyn.NewValue(out, a.Locations()).AppendLocationsFromValue(b), nil
}
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[len(as):], bs)
// Preserve the location of the first value.
return dyn.NewValue(out, a.Location()), nil
// Preserve the location of the first value. Accumulate the locations of the second value.
return dyn.NewValue(out, a.Locations()).AppendLocationsFromValue(b), nil
}
func mergePrimitive(a, b dyn.Value) (dyn.Value, error) {
// 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) {
v1 := dyn.V(map[string]dyn.Value{
"foo": dyn.V("bar"),
"bar": dyn.V("baz"),
})
l1 := dyn.Location{File: "file1", Line: 1, Column: 2}
v1 := dyn.NewValue(map[string]dyn.Value{
"foo": dyn.NewValue("bar", []dyn.Location{l1}),
"bar": dyn.NewValue("baz", []dyn.Location{l1}),
}, []dyn.Location{l1})
v2 := dyn.V(map[string]dyn.Value{
"bar": dyn.V("qux"),
"qux": dyn.V("foo"),
})
l2 := dyn.Location{File: "file2", Line: 3, Column: 4}
v2 := dyn.NewValue(map[string]dyn.Value{
"bar": dyn.NewValue("qux", []dyn.Location{l2}),
"qux": dyn.NewValue("foo", []dyn.Location{l2}),
}, []dyn.Location{l2})
// Merge v2 into v1.
{
@ -27,6 +29,23 @@ func TestMergeMaps(t *testing.T) {
"bar": "qux",
"qux": "foo",
}, 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.
@ -38,30 +57,64 @@ func TestMergeMaps(t *testing.T) {
"bar": "baz",
"qux": "foo",
}, 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) {
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"),
})
}, []dyn.Location{l})
nilL := dyn.Location{File: "file", Line: 3, Column: 4}
nilV := dyn.NewValue(nil, []dyn.Location{nilL})
// Merge nil into v.
{
out, err := Merge(v, dyn.NilValue)
out, err := Merge(v, nilV)
assert.NoError(t, err)
assert.Equal(t, map[string]any{
"foo": "bar",
}, 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.
{
out, err := Merge(dyn.NilValue, v)
out, err := Merge(nilV, v)
assert.NoError(t, err)
assert.Equal(t, map[string]any{
"foo": "bar",
}, 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) {
v1 := dyn.V([]dyn.Value{
dyn.V("bar"),
dyn.V("baz"),
})
l1 := dyn.Location{File: "file1", Line: 1, Column: 2}
v1 := dyn.NewValue([]dyn.Value{
dyn.NewValue("bar", []dyn.Location{l1}),
dyn.NewValue("baz", []dyn.Location{l1}),
}, []dyn.Location{l1})
v2 := dyn.V([]dyn.Value{
dyn.V("qux"),
dyn.V("foo"),
})
l2 := dyn.Location{File: "file2", Line: 3, Column: 4}
l3 := dyn.Location{File: "file3", Line: 5, Column: 6}
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.
{
@ -101,6 +157,18 @@ func TestMergeSequences(t *testing.T) {
"qux",
"foo",
}, 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.
@ -113,6 +181,18 @@ func TestMergeSequences(t *testing.T) {
"bar",
"baz",
}, 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) {
v1 := dyn.V("bar")
v2 := dyn.V("baz")
l1 := dyn.Location{File: "file1", Line: 1, Column: 2}
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.
{
out, err := Merge(v1, v2)
assert.NoError(t, err)
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.
@ -171,6 +259,12 @@ func TestMergePrimitives(t *testing.T) {
out, err := Merge(v2, v1)
assert.NoError(t, err)
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.NewValue(merged, left.Location()), nil
return dyn.NewValue(merged, left.Locations()), nil
case dyn.KindSequence:
// 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.NewValue(merged, left.Location()), nil
return dyn.NewValue(merged, left.Locations()), nil
case dyn.KindString:
if left.MustString() == right.MustString() {

View File

@ -27,79 +27,79 @@ func TestOverride_Primitive(t *testing.T) {
{
name: "string (updated)",
state: visitorState{updated: []string{"root"}},
left: dyn.NewValue("a", leftLocation),
right: dyn.NewValue("b", rightLocation),
expected: dyn.NewValue("b", rightLocation),
left: dyn.NewValue("a", []dyn.Location{leftLocation}),
right: dyn.NewValue("b", []dyn.Location{rightLocation}),
expected: dyn.NewValue("b", []dyn.Location{rightLocation}),
},
{
name: "string (not updated)",
state: visitorState{},
left: dyn.NewValue("a", leftLocation),
right: dyn.NewValue("a", rightLocation),
expected: dyn.NewValue("a", leftLocation),
left: dyn.NewValue("a", []dyn.Location{leftLocation}),
right: dyn.NewValue("a", []dyn.Location{rightLocation}),
expected: dyn.NewValue("a", []dyn.Location{leftLocation}),
},
{
name: "bool (updated)",
state: visitorState{updated: []string{"root"}},
left: dyn.NewValue(true, leftLocation),
right: dyn.NewValue(false, rightLocation),
expected: dyn.NewValue(false, rightLocation),
left: dyn.NewValue(true, []dyn.Location{leftLocation}),
right: dyn.NewValue(false, []dyn.Location{rightLocation}),
expected: dyn.NewValue(false, []dyn.Location{rightLocation}),
},
{
name: "bool (not updated)",
state: visitorState{},
left: dyn.NewValue(true, leftLocation),
right: dyn.NewValue(true, rightLocation),
expected: dyn.NewValue(true, leftLocation),
left: dyn.NewValue(true, []dyn.Location{leftLocation}),
right: dyn.NewValue(true, []dyn.Location{rightLocation}),
expected: dyn.NewValue(true, []dyn.Location{leftLocation}),
},
{
name: "int (updated)",
state: visitorState{updated: []string{"root"}},
left: dyn.NewValue(1, leftLocation),
right: dyn.NewValue(2, rightLocation),
expected: dyn.NewValue(2, rightLocation),
left: dyn.NewValue(1, []dyn.Location{leftLocation}),
right: dyn.NewValue(2, []dyn.Location{rightLocation}),
expected: dyn.NewValue(2, []dyn.Location{rightLocation}),
},
{
name: "int (not updated)",
state: visitorState{},
left: dyn.NewValue(int32(1), leftLocation),
right: dyn.NewValue(int64(1), rightLocation),
expected: dyn.NewValue(int32(1), leftLocation),
left: dyn.NewValue(int32(1), []dyn.Location{leftLocation}),
right: dyn.NewValue(int64(1), []dyn.Location{rightLocation}),
expected: dyn.NewValue(int32(1), []dyn.Location{leftLocation}),
},
{
name: "float (updated)",
state: visitorState{updated: []string{"root"}},
left: dyn.NewValue(1.0, leftLocation),
right: dyn.NewValue(2.0, rightLocation),
expected: dyn.NewValue(2.0, rightLocation),
left: dyn.NewValue(1.0, []dyn.Location{leftLocation}),
right: dyn.NewValue(2.0, []dyn.Location{rightLocation}),
expected: dyn.NewValue(2.0, []dyn.Location{rightLocation}),
},
{
name: "float (not updated)",
state: visitorState{},
left: dyn.NewValue(float32(1.0), leftLocation),
right: dyn.NewValue(float64(1.0), rightLocation),
expected: dyn.NewValue(float32(1.0), leftLocation),
left: dyn.NewValue(float32(1.0), []dyn.Location{leftLocation}),
right: dyn.NewValue(float64(1.0), []dyn.Location{rightLocation}),
expected: dyn.NewValue(float32(1.0), []dyn.Location{leftLocation}),
},
{
name: "time (updated)",
state: visitorState{updated: []string{"root"}},
left: dyn.NewValue(time.UnixMilli(10000), leftLocation),
right: dyn.NewValue(time.UnixMilli(10001), rightLocation),
expected: dyn.NewValue(time.UnixMilli(10001), rightLocation),
left: dyn.NewValue(time.UnixMilli(10000), []dyn.Location{leftLocation}),
right: dyn.NewValue(time.UnixMilli(10001), []dyn.Location{rightLocation}),
expected: dyn.NewValue(time.UnixMilli(10001), []dyn.Location{rightLocation}),
},
{
name: "time (not updated)",
state: visitorState{},
left: dyn.NewValue(time.UnixMilli(10000), leftLocation),
right: dyn.NewValue(time.UnixMilli(10000), rightLocation),
expected: dyn.NewValue(time.UnixMilli(10000), leftLocation),
left: dyn.NewValue(time.UnixMilli(10000), []dyn.Location{leftLocation}),
right: dyn.NewValue(time.UnixMilli(10000), []dyn.Location{rightLocation}),
expected: dyn.NewValue(time.UnixMilli(10000), []dyn.Location{leftLocation}),
},
{
name: "different types (updated)",
state: visitorState{updated: []string{"root"}},
left: dyn.NewValue("a", leftLocation),
right: dyn.NewValue(42, rightLocation),
expected: dyn.NewValue(42, rightLocation),
left: dyn.NewValue("a", []dyn.Location{leftLocation}),
right: dyn.NewValue(42, []dyn.Location{rightLocation}),
expected: dyn.NewValue(42, []dyn.Location{rightLocation}),
},
{
name: "map - remove 'a', update 'b'",
@ -109,23 +109,22 @@ func TestOverride_Primitive(t *testing.T) {
},
left: dyn.NewValue(
map[string]dyn.Value{
"a": dyn.NewValue(42, leftLocation),
"b": dyn.NewValue(10, leftLocation),
"a": dyn.NewValue(42, []dyn.Location{leftLocation}),
"b": dyn.NewValue(10, []dyn.Location{leftLocation}),
},
leftLocation,
),
[]dyn.Location{leftLocation}),
right: dyn.NewValue(
map[string]dyn.Value{
"b": dyn.NewValue(20, rightLocation),
"b": dyn.NewValue(20, []dyn.Location{rightLocation}),
},
rightLocation,
),
[]dyn.Location{rightLocation}),
expected: dyn.NewValue(
map[string]dyn.Value{
"b": dyn.NewValue(20, rightLocation),
"b": dyn.NewValue(20, []dyn.Location{rightLocation}),
},
leftLocation,
),
[]dyn.Location{leftLocation}),
},
{
name: "map - add 'a'",
@ -134,24 +133,26 @@ func TestOverride_Primitive(t *testing.T) {
},
left: dyn.NewValue(
map[string]dyn.Value{
"b": dyn.NewValue(10, leftLocation),
"b": dyn.NewValue(10, []dyn.Location{leftLocation}),
},
leftLocation,
[]dyn.Location{leftLocation},
),
right: dyn.NewValue(
map[string]dyn.Value{
"a": dyn.NewValue(42, rightLocation),
"b": dyn.NewValue(10, rightLocation),
"a": dyn.NewValue(42, []dyn.Location{rightLocation}),
"b": dyn.NewValue(10, []dyn.Location{rightLocation}),
},
leftLocation,
[]dyn.Location{leftLocation},
),
expected: dyn.NewValue(
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
"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(
map[string]dyn.Value{
"a": dyn.NewValue(42, leftLocation),
"b": dyn.NewValue(10, leftLocation),
"a": dyn.NewValue(42, []dyn.Location{leftLocation}),
"b": dyn.NewValue(10, []dyn.Location{leftLocation}),
},
leftLocation,
[]dyn.Location{leftLocation},
),
right: dyn.NewValue(
map[string]dyn.Value{
"b": dyn.NewValue(10, rightLocation),
"b": dyn.NewValue(10, []dyn.Location{rightLocation}),
},
leftLocation,
[]dyn.Location{leftLocation},
),
expected: dyn.NewValue(
map[string]dyn.Value{
// 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{
"jobs": dyn.NewValue(
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(
map[string]dyn.Value{
"jobs": dyn.NewValue(
map[string]dyn.Value{
"job_0": dyn.NewValue(42, rightLocation),
"job_1": dyn.NewValue(1337, rightLocation),
"job_0": dyn.NewValue(42, []dyn.Location{rightLocation}),
"job_1": dyn.NewValue(1337, []dyn.Location{rightLocation}),
},
rightLocation,
[]dyn.Location{rightLocation},
),
},
rightLocation,
[]dyn.Location{rightLocation},
),
expected: dyn.NewValue(
map[string]dyn.Value{
"jobs": dyn.NewValue(
map[string]dyn.Value{
"job_0": dyn.NewValue(42, leftLocation),
"job_1": dyn.NewValue(1337, rightLocation),
"job_0": dyn.NewValue(42, []dyn.Location{leftLocation}),
"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{
"jobs": dyn.NewValue(
map[string]dyn.Value{
"job_0": dyn.NewValue(42, leftLocation),
"job_1": dyn.NewValue(1337, rightLocation),
"job_0": dyn.NewValue(42, []dyn.Location{leftLocation}),
"job_1": dyn.NewValue(1337, []dyn.Location{rightLocation}),
},
leftLocation,
[]dyn.Location{leftLocation},
),
},
leftLocation,
[]dyn.Location{leftLocation},
),
right: dyn.NewValue(
map[string]dyn.Value{
"jobs": dyn.NewValue(
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(
map[string]dyn.Value{
"jobs": dyn.NewValue(
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]"}},
left: dyn.NewValue(
[]dyn.Value{
dyn.NewValue(42, leftLocation),
dyn.NewValue(42, []dyn.Location{leftLocation}),
},
leftLocation,
[]dyn.Location{leftLocation},
),
right: dyn.NewValue(
[]dyn.Value{
dyn.NewValue(42, rightLocation),
dyn.NewValue(10, rightLocation),
dyn.NewValue(42, []dyn.Location{rightLocation}),
dyn.NewValue(10, []dyn.Location{rightLocation}),
},
rightLocation,
[]dyn.Location{rightLocation},
),
expected: dyn.NewValue(
[]dyn.Value{
dyn.NewValue(42, leftLocation),
dyn.NewValue(10, rightLocation),
dyn.NewValue(42, []dyn.Location{leftLocation}),
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]"}},
left: dyn.NewValue(
[]dyn.Value{
dyn.NewValue(42, leftLocation),
dyn.NewValue(10, leftLocation),
dyn.NewValue(42, []dyn.Location{leftLocation}),
dyn.NewValue(10, []dyn.Location{leftLocation}),
},
leftLocation,
[]dyn.Location{leftLocation},
),
right: dyn.NewValue(
[]dyn.Value{
dyn.NewValue(42, rightLocation),
dyn.NewValue(42, []dyn.Location{rightLocation}),
},
rightLocation,
[]dyn.Location{rightLocation},
),
expected: dyn.NewValue(
[]dyn.Value{
// location hasn't changed because value hasn't changed
dyn.NewValue(42, leftLocation),
dyn.NewValue(42, []dyn.Location{leftLocation}),
},
leftLocation,
[]dyn.Location{leftLocation},
),
// location hasn't changed because value hasn't changed
},
{
name: "sequence (not updated)",
state: visitorState{},
left: dyn.NewValue(
[]dyn.Value{
dyn.NewValue(42, leftLocation),
dyn.NewValue(42, []dyn.Location{leftLocation}),
},
leftLocation,
[]dyn.Location{leftLocation},
),
right: dyn.NewValue(
[]dyn.Value{
dyn.NewValue(42, rightLocation),
dyn.NewValue(42, []dyn.Location{rightLocation}),
},
rightLocation,
[]dyn.Location{rightLocation},
),
expected: dyn.NewValue(
[]dyn.Value{
dyn.NewValue(42, leftLocation),
dyn.NewValue(42, []dyn.Location{leftLocation}),
},
leftLocation,
[]dyn.Location{leftLocation},
),
},
{
name: "nil (not updated)",
state: visitorState{},
left: dyn.NilValue.WithLocation(leftLocation),
right: dyn.NilValue.WithLocation(rightLocation),
expected: dyn.NilValue.WithLocation(leftLocation),
left: dyn.NilValue.WithLocations([]dyn.Location{leftLocation}),
right: dyn.NilValue.WithLocations([]dyn.Location{rightLocation}),
expected: dyn.NilValue.WithLocations([]dyn.Location{leftLocation}),
},
{
name: "nil (updated)",
state: visitorState{updated: []string{"root"}},
left: dyn.NilValue,
right: dyn.NewValue(42, rightLocation),
expected: dyn.NewValue(42, rightLocation),
right: dyn.NewValue(42, []dyn.Location{rightLocation}),
expected: dyn.NewValue(42, []dyn.Location{rightLocation}),
},
{
name: "change kind (updated)",
state: visitorState{updated: []string{"root"}},
left: dyn.NewValue(42.0, leftLocation),
right: dyn.NewValue(42, rightLocation),
expected: dyn.NewValue(42, rightLocation),
left: dyn.NewValue(42.0, []dyn.Location{leftLocation}),
right: dyn.NewValue(42, []dyn.Location{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) {
expected := dyn.NewValue("return value", dyn.Location{})
expected := dyn.V("return value")
s, visitor := createVisitor(visitorOpts{returnValue: &expected})
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}
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.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{})
out, err := override(
dyn.EmptyPath,
dyn.NewValue(left, leftLocation),
dyn.NewValue(right, rightLocation),
dyn.NewValue(left, []dyn.Location{leftLocation}),
dyn.NewValue(right, []dyn.Location{rightLocation}),
visitor,
)

View File

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

View File

@ -2,13 +2,18 @@ package dyn
import (
"fmt"
"slices"
)
type Value struct {
v any
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.
// 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.
func V(v any) Value {
return NewValue(v, Location{})
return NewValue(v, []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) {
case map[string]Value:
v = newMappingFromGoMap(vin)
@ -40,16 +45,30 @@ func NewValue(v any, loc Location) Value {
return Value{
v: 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.
func (v Value) WithLocation(loc Location) Value {
// 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,
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
}
func (v Value) Location() Location {
func (v Value) Locations() []Location {
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
}
@ -153,7 +180,10 @@ func (v Value) IsAnchor() bool {
// 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 || v.l != w.l {
if v.k != w.k {
return false
}
if !slices.Equal(v.l, w.l) {
return false
}

View File

@ -25,16 +25,19 @@ func TestValueAsMap(t *testing.T) {
_, ok := zeroValue.AsMap()
assert.False(t, ok)
var intValue = dyn.NewValue(1, dyn.Location{})
var intValue = dyn.V(1)
_, ok = intValue.AsMap()
assert.False(t, ok)
var mapValue = dyn.NewValue(
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()
assert.True(t, ok)
assert.Equal(t, 1, m.Len())
@ -43,6 +46,6 @@ func TestValueAsMap(t *testing.T) {
func TestValueIsValid(t *testing.T) {
var zeroValue dyn.Value
assert.False(t, zeroValue.IsValid())
var intValue = dyn.NewValue(1, dyn.Location{})
var intValue = dyn.V(1)
assert.True(t, intValue.IsValid())
}

View File

@ -11,7 +11,7 @@ import (
func TestValueUnderlyingMap(t *testing.T) {
v := dyn.V(
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) {
v := dyn.V(
[]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)
}
return NewValue(m, v.Location()), nil
return NewValue(m, v.Locations()), nil
case KindSequence:
s := slices.Clone(v.MustSequence())
for i, value := range s {
@ -37,7 +37,7 @@ func Foreach(fn MapFunc) MapFunc {
return InvalidValue, err
}
}
return NewValue(s, v.Location()), nil
return NewValue(s, v.Locations()), nil
default:
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
}
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) {
@ -130,7 +130,7 @@ func (d *loader) loadMapping(node *yaml.Node, loc dyn.Location) (dyn.Value, erro
}
if merge == nil {
return dyn.NewValue(acc, loc), nil
return dyn.NewValue(acc, []dyn.Location{loc}), nil
}
// 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)
}
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) {
st := node.ShortTag()
switch st {
case "!!str":
return dyn.NewValue(node.Value, loc), nil
return dyn.NewValue(node.Value, []dyn.Location{loc}), nil
case "!!bool":
switch strings.ToLower(node.Value) {
case "true":
return dyn.NewValue(true, loc), nil
return dyn.NewValue(true, []dyn.Location{loc}), nil
case "false":
return dyn.NewValue(false, loc), nil
return dyn.NewValue(false, []dyn.Location{loc}), nil
default:
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.
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":
f64, err := strconv.ParseFloat(node.Value, 64)
if err != nil {
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":
return dyn.NewValue(nil, loc), nil
return dyn.NewValue(nil, []dyn.Location{loc}), nil
case "!!timestamp":
// Try a couple of layouts
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)
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)

View File

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

View File

@ -44,7 +44,7 @@ func skipAndOrder(mv dyn.Value, order *Order, skipFields []string, dst map[strin
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

View File

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