Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Define dyn.Mapping to represent maps #1301

Merged
merged 5 commits into from
Mar 25, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion libs/dyn/convert/end_to_end_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"testing"

"github.com/databricks/cli/libs/dyn"
"github.com/stretchr/testify/assert"
assert "github.com/databricks/cli/libs/dyn/dynassert"
"github.com/stretchr/testify/require"
)

Expand Down
33 changes: 27 additions & 6 deletions libs/dyn/convert/from_typed.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,17 +71,28 @@ func fromTypedStruct(src reflect.Value, ref dyn.Value) (dyn.Value, error) {
return dyn.InvalidValue, fmt.Errorf("unhandled type: %s", ref.Kind())
}

out := make(map[string]dyn.Value)
refm, _ := ref.AsMapping()
out := dyn.NewMapping()
info := getStructInfo(src.Type())
for k, v := range info.FieldValues(src) {
pair, ok := refm.GetPairByString(k)
refk := pair.Key
refv := pair.Value

// Use nil reference if there is no reference for this key
if !ok {
refk = dyn.V(k)
refv = dyn.NilValue
}

// Convert the field taking into account the reference value (may be equal to config.NilValue).
nv, err := fromTyped(v.Interface(), ref.Get(k))
nv, err := fromTyped(v.Interface(), refv)
if err != nil {
return dyn.InvalidValue, err
}

if nv != dyn.NilValue {
out[k] = nv
out.Set(refk, nv)
}
}

Expand All @@ -101,21 +112,31 @@ func fromTypedMap(src reflect.Value, ref dyn.Value) (dyn.Value, error) {
return dyn.NilValue, nil
}

out := make(map[string]dyn.Value)
refm, _ := ref.AsMapping()
out := dyn.NewMapping()
iter := src.MapRange()
for iter.Next() {
k := iter.Key().String()
v := iter.Value()
pair, ok := refm.GetPairByString(k)
refk := pair.Key
refv := pair.Value

// Use nil reference if there is no reference for this key
if !ok {
refk = dyn.V(k)
refv = dyn.NilValue
}

// Convert entry taking into account the reference value (may be equal to dyn.NilValue).
nv, err := fromTyped(v.Interface(), ref.Get(k), includeZeroValues)
nv, err := fromTyped(v.Interface(), refv, includeZeroValues)
if err != nil {
return dyn.InvalidValue, err
}

// Every entry is represented, even if it is a nil.
// Otherwise, a map with zero-valued structs would yield a nil as well.
out[k] = nv
out.Set(refk, nv)
}

return dyn.NewValue(out, ref.Location()), nil
Expand Down
2 changes: 1 addition & 1 deletion libs/dyn/convert/from_typed_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"testing"

"github.com/databricks/cli/libs/dyn"
"github.com/stretchr/testify/assert"
assert "github.com/databricks/cli/libs/dyn/dynassert"
"github.com/stretchr/testify/require"
)

Expand Down
35 changes: 20 additions & 15 deletions libs/dyn/convert/normalize.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,30 +74,32 @@ func (n normalizeOptions) normalizeStruct(typ reflect.Type, src dyn.Value, seen

switch src.Kind() {
case dyn.KindMap:
out := make(map[string]dyn.Value)
out := dyn.NewMapping()
info := getStructInfo(typ)
for k, v := range src.MustMap() {
index, ok := info.Fields[k]
for _, pair := range src.MustMapping().Pairs() {
pk := pair.Key
pv := pair.Value
index, ok := info.Fields[pk.MustString()]
if !ok {
diags = diags.Append(diag.Diagnostic{
Severity: diag.Warning,
Summary: fmt.Sprintf("unknown field: %s", k),
Location: src.Location(),
Summary: fmt.Sprintf("unknown field: %s", pk.MustString()),
Location: pk.Location(),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This previously pointed to the location of the parent map value. Now it points to the unknown key.

})
continue
}

// Normalize the value according to the field type.
v, err := n.normalizeType(typ.FieldByIndex(index).Type, v, seen)
nv, err := n.normalizeType(typ.FieldByIndex(index).Type, pv, seen)
if err != nil {
diags = diags.Extend(err)
// Skip the element if it cannot be normalized.
if !v.IsValid() {
if !nv.IsValid() {
continue
}
}

out[k] = v
out.Set(pk, nv)
}

// Return the normalized value if missing fields are not included.
Expand All @@ -107,7 +109,7 @@ func (n normalizeOptions) normalizeStruct(typ reflect.Type, src dyn.Value, seen

// Populate missing fields with their zero values.
for k, index := range info.Fields {
if _, ok := out[k]; ok {
if _, ok := out.GetByString(k); ok {
continue
}

Expand Down Expand Up @@ -143,7 +145,7 @@ func (n normalizeOptions) normalizeStruct(typ reflect.Type, src dyn.Value, seen
continue
}
if v.IsValid() {
out[k] = v
out.Set(dyn.V(k), v)
}
}

Expand All @@ -160,19 +162,22 @@ func (n normalizeOptions) normalizeMap(typ reflect.Type, src dyn.Value, seen []r

switch src.Kind() {
case dyn.KindMap:
out := make(map[string]dyn.Value)
for k, v := range src.MustMap() {
out := dyn.NewMapping()
for _, pair := range src.MustMapping().Pairs() {
pk := pair.Key
pv := pair.Value

// Normalize the value according to the map element type.
v, err := n.normalizeType(typ.Elem(), v, seen)
nv, err := n.normalizeType(typ.Elem(), pv, seen)
if err != nil {
diags = diags.Extend(err)
// Skip the element if it cannot be normalized.
if !v.IsValid() {
if !nv.IsValid() {
continue
}
}

out[k] = v
out.Set(pk, nv)
}

return dyn.NewValue(out, src.Location()), diags
Expand Down
2 changes: 1 addition & 1 deletion libs/dyn/convert/normalize_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (

"github.com/databricks/cli/libs/diag"
"github.com/databricks/cli/libs/dyn"
"github.com/stretchr/testify/assert"
assert "github.com/databricks/cli/libs/dyn/dynassert"
)

func TestNormalizeStruct(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion libs/dyn/convert/struct_info_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"testing"

"github.com/databricks/cli/libs/dyn"
"github.com/stretchr/testify/assert"
assert "github.com/databricks/cli/libs/dyn/dynassert"
)

func TestStructInfoPlain(t *testing.T) {
Expand Down
15 changes: 9 additions & 6 deletions libs/dyn/convert/to_typed.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,10 @@ func toTypedStruct(dst reflect.Value, src dyn.Value) error {
dst.SetZero()

info := getStructInfo(dst.Type())
for k, v := range src.MustMap() {
for _, pair := range src.MustMapping().Pairs() {
k := pair.Key.MustString()
v := pair.Value

index, ok := info.Fields[k]
if !ok {
// Ignore unknown fields.
Expand Down Expand Up @@ -109,15 +112,15 @@ func toTypedStruct(dst reflect.Value, src dyn.Value) error {
func toTypedMap(dst reflect.Value, src dyn.Value) error {
switch src.Kind() {
case dyn.KindMap:
m := src.MustMap()
m := src.MustMapping()

// Always overwrite.
dst.Set(reflect.MakeMapWithSize(dst.Type(), len(m)))
for k, v := range m {
kv := reflect.ValueOf(k)
dst.Set(reflect.MakeMapWithSize(dst.Type(), m.Len()))
for _, pair := range m.Pairs() {
kv := reflect.ValueOf(pair.Key.MustString())
kt := dst.Type().Key()
vv := reflect.New(dst.Type().Elem())
err := ToTyped(vv.Interface(), v)
err := ToTyped(vv.Interface(), pair.Value)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion libs/dyn/convert/to_typed_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"testing"

"github.com/databricks/cli/libs/dyn"
"github.com/stretchr/testify/assert"
assert "github.com/databricks/cli/libs/dyn/dynassert"
"github.com/stretchr/testify/require"
)

Expand Down
113 changes: 113 additions & 0 deletions libs/dyn/dynassert/assert.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package dynassert

import (
"github.com/databricks/cli/libs/dyn"
"github.com/stretchr/testify/assert"
)

func Equal(t assert.TestingT, expected interface{}, actual interface{}, msgAndArgs ...interface{}) bool {
ev, eok := expected.(dyn.Value)
av, aok := actual.(dyn.Value)
if eok && aok && ev.IsValid() && av.IsValid() {
if !assert.Equal(t, ev.AsAny(), av.AsAny(), msgAndArgs...) {
return false
}

// The values are equal on contents. Now compare the locations.
if !assert.Equal(t, ev.Location(), av.Location(), msgAndArgs...) {
return false
}

// Walk ev and av and compare the locations of each element.
_, err := dyn.Walk(ev, func(p dyn.Path, evv dyn.Value) (dyn.Value, error) {
avv, err := dyn.GetByPath(av, p)
if assert.NoError(t, err, "unable to get value from actual value at path %v", p.String()) {
assert.Equal(t, evv.Location(), avv.Location())
}
return evv, nil
})
return assert.NoError(t, err)
}

return assert.Equal(t, expected, actual, msgAndArgs...)
}

func EqualValues(t assert.TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool {
return assert.EqualValues(t, expected, actual, msgAndArgs...)
}

func NotEqual(t assert.TestingT, expected interface{}, actual interface{}, msgAndArgs ...interface{}) bool {
return assert.NotEqual(t, expected, actual, msgAndArgs...)
}

func Len(t assert.TestingT, object interface{}, length int, msgAndArgs ...interface{}) bool {
return assert.Len(t, object, length, msgAndArgs...)
}

func Empty(t assert.TestingT, object interface{}, msgAndArgs ...interface{}) bool {
return assert.Empty(t, object, msgAndArgs...)
}

func Nil(t assert.TestingT, object interface{}, msgAndArgs ...interface{}) bool {
return assert.Nil(t, object, msgAndArgs...)
}

func NotNil(t assert.TestingT, object interface{}, msgAndArgs ...interface{}) bool {
return assert.NotNil(t, object, msgAndArgs...)
}

func NoError(t assert.TestingT, err error, msgAndArgs ...interface{}) bool {
return assert.NoError(t, err, msgAndArgs...)
}

func Error(t assert.TestingT, err error, msgAndArgs ...interface{}) bool {
return assert.Error(t, err, msgAndArgs...)
}

func EqualError(t assert.TestingT, theError error, errString string, msgAndArgs ...interface{}) bool {
return assert.EqualError(t, theError, errString, msgAndArgs...)
}

func ErrorContains(t assert.TestingT, theError error, contains string, msgAndArgs ...interface{}) bool {
return assert.ErrorContains(t, theError, contains, msgAndArgs...)
}

func ErrorIs(t assert.TestingT, theError, target error, msgAndArgs ...interface{}) bool {
return assert.ErrorIs(t, theError, target, msgAndArgs...)
}

func True(t assert.TestingT, value bool, msgAndArgs ...interface{}) bool {
return assert.True(t, value, msgAndArgs...)
}

func False(t assert.TestingT, value bool, msgAndArgs ...interface{}) bool {
return assert.False(t, value, msgAndArgs...)
}

func Contains(t assert.TestingT, list interface{}, element interface{}, msgAndArgs ...interface{}) bool {
return assert.Contains(t, list, element, msgAndArgs...)
}

func NotContains(t assert.TestingT, list interface{}, element interface{}, msgAndArgs ...interface{}) bool {
return assert.NotContains(t, list, element, msgAndArgs...)
}

func ElementsMatch(t assert.TestingT, listA, listB interface{}, msgAndArgs ...interface{}) bool {
return assert.ElementsMatch(t, listA, listB, msgAndArgs...)
}

func Panics(t assert.TestingT, f func(), msgAndArgs ...interface{}) bool {
return assert.Panics(t, f, msgAndArgs...)
}

func PanicsWithValue(t assert.TestingT, expected interface{}, f func(), msgAndArgs ...interface{}) bool {
return assert.PanicsWithValue(t, expected, f, msgAndArgs...)
}

func PanicsWithError(t assert.TestingT, errString string, f func(), msgAndArgs ...interface{}) bool {
return assert.PanicsWithError(t, errString, f, msgAndArgs...)
}

func NotPanics(t assert.TestingT, f func(), msgAndArgs ...interface{}) bool {
return assert.NotPanics(t, f, msgAndArgs...)
}
44 changes: 44 additions & 0 deletions libs/dyn/dynassert/assert_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package dynassert

import (
"go/parser"
"go/token"
"io/fs"
"os"
"path/filepath"
"strings"
"testing"

"github.com/stretchr/testify/require"
)

func TestThatThisTestPackageIsUsedWhereNecessary(t *testing.T) {
var base = ".."
var files []string
err := fs.WalkDir(os.DirFS(base), ".", func(path string, d fs.DirEntry, err error) error {
if d.IsDir() {
// Filter this directory.
if filepath.Base(path) == "dynassert" {
return fs.SkipDir
}
}
if ok, _ := filepath.Match("*_test.go", d.Name()); ok {
files = append(files, filepath.Join(base, path))
}
return nil
})
require.NoError(t, err)

// Check that all test files import this package.
fset := token.NewFileSet()
for _, file := range files {
f, err := parser.ParseFile(fset, file, nil, parser.ParseComments)
require.NoError(t, err)

for _, imp := range f.Imports {
if strings.Contains(imp.Path.Value, `github.com/stretchr/testify/assert`) {
t.Errorf("File %s should not import github.com/stretchr/testify/assert", file)
}
}
}
}
Loading
Loading