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" "reflect"
"strings" "strings"
"sync" "sync"
"github.com/databricks/cli/libs/config"
) )
// structInfo holds the type information we need to efficiently // structInfo holds the type information we need to efficiently
@ -11,6 +13,10 @@ import (
type structInfo struct { type structInfo struct {
// Fields maps the JSON-name of the field to the field's index for use with [FieldByIndex]. // Fields maps the JSON-name of the field to the field's index for use with [FieldByIndex].
Fields map[string][]int 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. // structInfoCache caches type information.
@ -68,6 +74,15 @@ func buildStructInfo(typ reflect.Type) structInfo {
continue 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"), ",") name, _, _ := strings.Cut(sf.Tag.Get("json"), ",")
if name == "" || name == "-" { if name == "" || name == "-" {
continue continue
@ -113,3 +128,6 @@ func (s *structInfo) FieldValues(v reflect.Value) map[string]reflect.Value {
return out return out
} }
// Type of [config.Value].
var configValueType = reflect.TypeOf((*config.Value)(nil)).Elem()

View File

@ -4,6 +4,7 @@ import (
"reflect" "reflect"
"testing" "testing"
"github.com/databricks/cli/libs/config"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -194,3 +195,32 @@ func TestStructInfoFieldValuesAnonymousByPointer(t *testing.T) {
assert.Empty(t, fv) 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 return nil
case config.KindNil: case config.KindNil:
dst.SetZero() dst.SetZero()

View File

@ -133,6 +133,24 @@ func TestToTypedStructNilOverwrite(t *testing.T) {
assert.Equal(t, Tmp{}, out) 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) { func TestToTypedMap(t *testing.T) {
var out = map[string]string{} var out = map[string]string{}