Populate struct field with `config.Value` instance if possible (#1010)

## Changes

If a struct has a field of type `config.Value`, then we set it to the
source value while converting a `config.Value` instance to a struct as
part of a call to `convert.ToTyped`.

This is convenient when dealing with deeply nested structs where
functions on inner structs need access to the metadata provided by their
corresponding `config.Value` (e.g. where they were defined).

## Tests

Added unit tests pass.
This commit is contained in:
Pieter Noordhuis 2023-11-27 11:06:29 +01:00 committed by GitHub
parent ef97e249ec
commit f5f57b6bf9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 72 additions and 0 deletions

View File

@ -4,6 +4,8 @@ import (
"reflect"
"strings"
"sync"
"github.com/databricks/cli/libs/config"
)
// structInfo holds the type information we need to efficiently
@ -11,6 +13,10 @@ import (
type structInfo struct {
// Fields maps the JSON-name of the field to the field's index for use with [FieldByIndex].
Fields map[string][]int
// ValueField maps to the field with a [config.Value].
// The underlying type is expected to only have one of these.
ValueField []int
}
// structInfoCache caches type information.
@ -68,6 +74,15 @@ func buildStructInfo(typ reflect.Type) structInfo {
continue
}
// If this field has type [config.Value], we populate it with the source [config.Value] from [ToTyped].
if sf.IsExported() && sf.Type == configValueType {
if out.ValueField != nil {
panic("multiple config.Value fields")
}
out.ValueField = append(prefix, sf.Index...)
continue
}
name, _, _ := strings.Cut(sf.Tag.Get("json"), ",")
if name == "" || name == "-" {
continue
@ -113,3 +128,6 @@ func (s *structInfo) FieldValues(v reflect.Value) map[string]reflect.Value {
return out
}
// Type of [config.Value].
var configValueType = reflect.TypeOf((*config.Value)(nil)).Elem()

View File

@ -4,6 +4,7 @@ import (
"reflect"
"testing"
"github.com/databricks/cli/libs/config"
"github.com/stretchr/testify/assert"
)
@ -194,3 +195,32 @@ func TestStructInfoFieldValuesAnonymousByPointer(t *testing.T) {
assert.Empty(t, fv)
})
}
func TestStructInfoValueFieldAbsent(t *testing.T) {
type Tmp struct {
Foo string `json:"foo"`
}
si := getStructInfo(reflect.TypeOf(Tmp{}))
assert.Nil(t, si.ValueField)
}
func TestStructInfoValueFieldPresent(t *testing.T) {
type Tmp struct {
Foo config.Value
}
si := getStructInfo(reflect.TypeOf(Tmp{}))
assert.NotNil(t, si.ValueField)
}
func TestStructInfoValueFieldMultiple(t *testing.T) {
type Tmp struct {
Foo config.Value
Bar config.Value
}
assert.Panics(t, func() {
getStructInfo(reflect.TypeOf(Tmp{}))
})
}

View File

@ -83,6 +83,12 @@ func toTypedStruct(dst reflect.Value, src config.Value) error {
}
}
// Populate field(s) for [config.Value], if any.
if info.ValueField != nil {
vv := dst.FieldByIndex(info.ValueField)
vv.Set(reflect.ValueOf(src))
}
return nil
case config.KindNil:
dst.SetZero()

View File

@ -133,6 +133,24 @@ func TestToTypedStructNilOverwrite(t *testing.T) {
assert.Equal(t, Tmp{}, out)
}
func TestToTypedStructWithValueField(t *testing.T) {
type Tmp struct {
Foo string `json:"foo"`
ConfigValue config.Value
}
var out Tmp
v := config.V(map[string]config.Value{
"foo": config.V("bar"),
})
err := ToTyped(&out, v)
require.NoError(t, err)
assert.Equal(t, "bar", out.Foo)
assert.Equal(t, v, out.ConfigValue)
}
func TestToTypedMap(t *testing.T) {
var out = map[string]string{}