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

Library to convert config.Value to Go struct #904

Merged
merged 8 commits into from
Oct 24, 2023
Merged
Show file tree
Hide file tree
Changes from 7 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
16 changes: 16 additions & 0 deletions libs/config/convert/error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package convert

import (
"fmt"

"github.com/databricks/cli/libs/config"
)

type TypeError struct {
value config.Value
msg string
}

func (e TypeError) Error() string {
return fmt.Sprintf("%s: %s", e.value.Location(), e.msg)
}
87 changes: 87 additions & 0 deletions libs/config/convert/struct_info.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package convert

import (
"reflect"
"strings"
"sync"
)

// structInfo holds the type information we need to efficiently
// convert data from a [config.Value] to a Go struct.
type structInfo struct {
// Fields maps the JSON-name of the field to the field's index for use with [FieldByIndex].
Fields map[string][]int
}

// structInfoCache caches type information.
var structInfoCache = make(map[reflect.Type]structInfo)

// structInfoCacheLock guards concurrent access to structInfoCache.
var structInfoCacheLock sync.Mutex

// getStructInfo returns the [structInfo] for the given type.
// It lazily populates a cache, so the first call for a given
// type is slower than subsequent calls for that same type.
func getStructInfo(typ reflect.Type) structInfo {
structInfoCacheLock.Lock()
defer structInfoCacheLock.Unlock()

si, ok := structInfoCache[typ]
if !ok {
si = buildStructInfo(typ)
structInfoCache[typ] = si
}

return si
}

// buildStructInfo populates a new [structInfo] for the given type.
func buildStructInfo(typ reflect.Type) structInfo {
var out = structInfo{
Fields: make(map[string][]int),
}

// Queue holds the indexes of the structs to visit.
// It is initialized with a single empty slice to visit the top level struct.
var queue [][]int = [][]int{{}}
for i := 0; i < len(queue); i++ {
prefix := queue[i]

// Traverse embedded anonymous types (if prefix is non-empty).
styp := typ
if len(prefix) > 0 {
styp = styp.FieldByIndex(prefix).Type
}

// Dereference pointer type.
if styp.Kind() == reflect.Pointer {
styp = styp.Elem()
}

nf := styp.NumField()
for j := 0; j < nf; j++ {
sf := styp.Field(j)

// Recurse into anonymous fields.
if sf.Anonymous {
queue = append(queue, append(prefix, sf.Index...))
continue
}

name, _, _ := strings.Cut(sf.Tag.Get("json"), ",")
if name == "" || name == "-" {
continue
}

// Top level fields always take precedence.
// Therefore, if it is already set, we ignore it.
if _, ok := out.Fields[name]; ok {
continue
}

out.Fields[name] = append(prefix, sf.Index...)
}
}

return out
}
89 changes: 89 additions & 0 deletions libs/config/convert/struct_info_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package convert

import (
"reflect"
"testing"

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

func TestStructInfoPlain(t *testing.T) {
type Tmp struct {
Foo string `json:"foo"`
Bar string `json:"bar,omitempty"`

// Baz must be skipped.
Baz string `json:""`

// Qux must be skipped.
Qux string `json:"-"`
}

si := getStructInfo(reflect.TypeOf(Tmp{}))
assert.Len(t, si.Fields, 2)
assert.Equal(t, []int{0}, si.Fields["foo"])
assert.Equal(t, []int{1}, si.Fields["bar"])
}

func TestStructInfoAnonymousByValue(t *testing.T) {
type Bar struct {
Bar string `json:"bar"`
}

type Foo struct {
Foo string `json:"foo"`
Bar
}

type Tmp struct {
Foo
}

si := getStructInfo(reflect.TypeOf(Tmp{}))
assert.Len(t, si.Fields, 2)
assert.Equal(t, []int{0, 0}, si.Fields["foo"])
assert.Equal(t, []int{0, 1, 0}, si.Fields["bar"])
}

func TestStructInfoAnonymousByValuePrecedence(t *testing.T) {
type Bar struct {
Bar string `json:"bar"`
}

type Foo struct {
Foo string `json:"foo"`
Bar
}

type Tmp struct {
// "foo" comes from [Foo].
Foo
// "bar" comes from [Bar] directly, not through [Foo].
Bar
}

si := getStructInfo(reflect.TypeOf(Tmp{}))
assert.Len(t, si.Fields, 2)
assert.Equal(t, []int{0, 0}, si.Fields["foo"])
assert.Equal(t, []int{1, 0}, si.Fields["bar"])
}

func TestStructInfoAnonymousByPointer(t *testing.T) {
type Bar struct {
Bar string `json:"bar"`
}

type Foo struct {
Foo string `json:"foo"`
*Bar
}

type Tmp struct {
*Foo
}

si := getStructInfo(reflect.TypeOf(Tmp{}))
assert.Len(t, si.Fields, 2)
assert.Equal(t, []int{0, 0}, si.Fields["foo"])
assert.Equal(t, []int{0, 1, 0}, si.Fields["bar"])
}
Loading