7ffd1560aa
The decode hook is not call for the embedded squashed struct, so we need to recurse when we find squash tags. See https://github.com/mitchellh/mapstructure/issues/226
438 lines
9.7 KiB
Go
438 lines
9.7 KiB
Go
package decode
|
|
|
|
import (
|
|
"fmt"
|
|
"reflect"
|
|
"testing"
|
|
|
|
"github.com/hashicorp/hcl"
|
|
"github.com/mitchellh/mapstructure"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestHookTranslateKeys(t *testing.T) {
|
|
var testcases = []struct {
|
|
name string
|
|
data interface{}
|
|
expected interface{}
|
|
}{
|
|
{
|
|
name: "target of type struct, with struct receiver",
|
|
data: map[string]interface{}{
|
|
"S": map[string]interface{}{
|
|
"None": "no translation",
|
|
"OldOne": "value1",
|
|
"oldtwo": "value2",
|
|
},
|
|
},
|
|
expected: Config{
|
|
S: TypeStruct{
|
|
One: "value1",
|
|
Two: "value2",
|
|
None: "no translation",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "target of type ptr, with struct receiver",
|
|
data: map[string]interface{}{
|
|
"PS": map[string]interface{}{
|
|
"None": "no translation",
|
|
"OldOne": "value1",
|
|
"oldtwo": "value2",
|
|
},
|
|
},
|
|
expected: Config{
|
|
PS: &TypeStruct{
|
|
One: "value1",
|
|
Two: "value2",
|
|
None: "no translation",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "target of type ptr, with ptr receiver",
|
|
data: map[string]interface{}{
|
|
"PTR": map[string]interface{}{
|
|
"None": "no translation",
|
|
"old_THREE": "value3",
|
|
"oldfour": "value4",
|
|
},
|
|
},
|
|
expected: Config{
|
|
PTR: &TypePtrToStruct{
|
|
Three: "value3",
|
|
Four: "value4",
|
|
None: "no translation",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "target of type ptr, with struct receiver",
|
|
data: map[string]interface{}{
|
|
"PTRS": map[string]interface{}{
|
|
"None": "no translation",
|
|
"old_THREE": "value3",
|
|
"old_four": "value4",
|
|
},
|
|
},
|
|
expected: Config{
|
|
PTRS: TypePtrToStruct{
|
|
Three: "value3",
|
|
Four: "value4",
|
|
None: "no translation",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "target of type map",
|
|
data: map[string]interface{}{
|
|
"Blob": map[string]interface{}{
|
|
"one": 1,
|
|
"two": 2,
|
|
},
|
|
},
|
|
expected: Config{
|
|
Blob: map[string]interface{}{
|
|
"one": 1,
|
|
"two": 2,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "value already exists for canonical key",
|
|
data: map[string]interface{}{
|
|
"PS": map[string]interface{}{
|
|
"OldOne": "value1",
|
|
"One": "original1",
|
|
"oldTWO": "value2",
|
|
"two": "original2",
|
|
},
|
|
},
|
|
expected: Config{
|
|
PS: &TypeStruct{
|
|
One: "original1",
|
|
Two: "original2",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testcases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
cfg := Config{}
|
|
md := new(mapstructure.Metadata)
|
|
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
|
|
DecodeHook: HookTranslateKeys,
|
|
Metadata: md,
|
|
Result: &cfg,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, decoder.Decode(tc.data))
|
|
require.Equal(t, cfg, tc.expected, "decode metadata: %#v", md)
|
|
})
|
|
}
|
|
}
|
|
|
|
type Config struct {
|
|
S TypeStruct
|
|
PS *TypeStruct
|
|
PTR *TypePtrToStruct
|
|
PTRS TypePtrToStruct
|
|
Blob map[string]interface{}
|
|
}
|
|
|
|
type TypeStruct struct {
|
|
One string `alias:"oldone"`
|
|
Two string `alias:"oldtwo"`
|
|
None string
|
|
}
|
|
|
|
type TypePtrToStruct struct {
|
|
Three string `alias:"old_three"`
|
|
Four string `alias:"old_four,oldfour"`
|
|
None string
|
|
}
|
|
|
|
func TestHookTranslateKeys_TargetStructHasPointerReceiver(t *testing.T) {
|
|
target := &TypePtrToStruct{}
|
|
md := new(mapstructure.Metadata)
|
|
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
|
|
DecodeHook: HookTranslateKeys,
|
|
Metadata: md,
|
|
Result: target,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
data := map[string]interface{}{
|
|
"None": "no translation",
|
|
"Old_Three": "value3",
|
|
"OldFour": "value4",
|
|
}
|
|
expected := &TypePtrToStruct{
|
|
None: "no translation",
|
|
Three: "value3",
|
|
Four: "value4",
|
|
}
|
|
require.NoError(t, decoder.Decode(data))
|
|
require.Equal(t, expected, target, "decode metadata: %#v", md)
|
|
}
|
|
|
|
func TestHookTranslateKeys_DoesNotModifySourceData(t *testing.T) {
|
|
raw := map[string]interface{}{
|
|
"S": map[string]interface{}{
|
|
"None": "no translation",
|
|
"OldOne": "value1",
|
|
"oldtwo": "value2",
|
|
},
|
|
}
|
|
|
|
cfg := Config{}
|
|
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
|
|
DecodeHook: HookTranslateKeys,
|
|
Result: &cfg,
|
|
})
|
|
require.NoError(t, err)
|
|
require.NoError(t, decoder.Decode(raw))
|
|
|
|
expected := map[string]interface{}{
|
|
"S": map[string]interface{}{
|
|
"None": "no translation",
|
|
"OldOne": "value1",
|
|
"oldtwo": "value2",
|
|
},
|
|
}
|
|
require.Equal(t, raw, expected)
|
|
}
|
|
|
|
type translateExample struct {
|
|
FieldDefaultCanonical string `alias:"first"`
|
|
FieldWithMapstructureTag string `alias:"second" mapstructure:"field_with_mapstruct_tag"`
|
|
FieldWithMapstructureTagOmit string `mapstructure:"field_with_mapstruct_omit,omitempty" alias:"third"`
|
|
FieldWithEmptyTag string `mapstructure:"" alias:"forth"`
|
|
EmbeddedStruct `mapstructure:",squash"`
|
|
*PtrEmbeddedStruct `mapstructure:",squash"`
|
|
BadField string `mapstructure:",squash"`
|
|
}
|
|
|
|
type EmbeddedStruct struct {
|
|
NextField string `alias:"next"`
|
|
}
|
|
|
|
type PtrEmbeddedStruct struct {
|
|
OtherNextField string `alias:"othernext"`
|
|
}
|
|
|
|
func TestTranslationsForType(t *testing.T) {
|
|
to := reflect.TypeOf(translateExample{})
|
|
actual := translationsForType(to)
|
|
expected := map[string]string{
|
|
"first": "fielddefaultcanonical",
|
|
"second": "field_with_mapstruct_tag",
|
|
"third": "field_with_mapstruct_omit",
|
|
"forth": "fieldwithemptytag",
|
|
"next": "nextfield",
|
|
"othernext": "othernextfield",
|
|
}
|
|
require.Equal(t, expected, actual)
|
|
}
|
|
|
|
type nested struct {
|
|
O map[string]interface{}
|
|
Slice []Item
|
|
Item Item
|
|
OSlice []map[string]interface{}
|
|
Sub *nested
|
|
}
|
|
|
|
type Item struct {
|
|
Name string
|
|
}
|
|
|
|
func TestHookWeakDecodeFromSlice_DoesNotModifySliceTargets(t *testing.T) {
|
|
source := `
|
|
slice {
|
|
name = "first"
|
|
}
|
|
slice {
|
|
name = "second"
|
|
}
|
|
item {
|
|
name = "solo"
|
|
}
|
|
sub {
|
|
oslice {
|
|
something = "v1"
|
|
}
|
|
}
|
|
`
|
|
target := &nested{}
|
|
err := decodeHCLToMapStructure(source, target)
|
|
require.NoError(t, err)
|
|
|
|
expected := &nested{
|
|
Slice: []Item{{Name: "first"}, {Name: "second"}},
|
|
Item: Item{Name: "solo"},
|
|
Sub: &nested{
|
|
OSlice: []map[string]interface{}{
|
|
{"something": "v1"},
|
|
},
|
|
},
|
|
}
|
|
require.Equal(t, target, expected)
|
|
}
|
|
|
|
func decodeHCLToMapStructure(source string, target interface{}) error {
|
|
raw := map[string]interface{}{}
|
|
err := hcl.Decode(&raw, source)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
md := new(mapstructure.Metadata)
|
|
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
|
|
DecodeHook: HookWeakDecodeFromSlice,
|
|
Metadata: md,
|
|
Result: target,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return decoder.Decode(&raw)
|
|
}
|
|
|
|
func TestHookWeakDecodeFromSlice_DoesNotModifySliceTargetsFromSliceInterface(t *testing.T) {
|
|
raw := map[string]interface{}{
|
|
"slice": []interface{}{map[string]interface{}{"name": "first"}},
|
|
"item": []interface{}{map[string]interface{}{"name": "solo"}},
|
|
"sub": []interface{}{
|
|
map[string]interface{}{
|
|
"OSlice": []interface{}{
|
|
map[string]interface{}{"something": "v1"},
|
|
},
|
|
"item": []interface{}{map[string]interface{}{"name": "subitem"}},
|
|
},
|
|
},
|
|
}
|
|
target := &nested{}
|
|
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
|
|
DecodeHook: HookWeakDecodeFromSlice,
|
|
Result: target,
|
|
})
|
|
require.NoError(t, err)
|
|
err = decoder.Decode(&raw)
|
|
require.NoError(t, err)
|
|
|
|
expected := &nested{
|
|
Slice: []Item{{Name: "first"}},
|
|
Item: Item{Name: "solo"},
|
|
Sub: &nested{
|
|
OSlice: []map[string]interface{}{
|
|
{"something": "v1"},
|
|
},
|
|
Item: Item{Name: "subitem"},
|
|
},
|
|
}
|
|
require.Equal(t, target, expected)
|
|
}
|
|
|
|
func TestHookWeakDecodeFromSlice_ErrorsWithMultipleNestedBlocks(t *testing.T) {
|
|
source := `
|
|
item {
|
|
name = "first"
|
|
}
|
|
item {
|
|
name = "second"
|
|
}
|
|
`
|
|
target := &nested{}
|
|
err := decodeHCLToMapStructure(source, target)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "'Item' expected a map, got 'slice'")
|
|
}
|
|
|
|
func TestHookWeakDecodeFromSlice_UnpacksNestedBlocks(t *testing.T) {
|
|
source := `
|
|
item {
|
|
name = "first"
|
|
}
|
|
`
|
|
target := &nested{}
|
|
err := decodeHCLToMapStructure(source, target)
|
|
require.NoError(t, err)
|
|
|
|
expected := &nested{
|
|
Item: Item{Name: "first"},
|
|
}
|
|
require.Equal(t, target, expected)
|
|
}
|
|
|
|
func TestHookWeakDecodeFromSlice_NestedOpaqueConfig(t *testing.T) {
|
|
source := `
|
|
service {
|
|
proxy {
|
|
config {
|
|
envoy_gateway_bind_addresses {
|
|
all-interfaces {
|
|
address = "0.0.0.0"
|
|
port = 8443
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}`
|
|
|
|
target := map[string]interface{}{}
|
|
err := decodeHCLToMapStructure(source, &target)
|
|
require.NoError(t, err)
|
|
|
|
expected := map[string]interface{}{
|
|
"service": map[string]interface{}{
|
|
"proxy": map[string]interface{}{
|
|
"config": map[string]interface{}{
|
|
"envoy_gateway_bind_addresses": map[string]interface{}{
|
|
"all-interfaces": map[string]interface{}{
|
|
"address": "0.0.0.0",
|
|
"port": 8443,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
require.Equal(t, target, expected)
|
|
}
|
|
|
|
func TestFieldTags(t *testing.T) {
|
|
type testCase struct {
|
|
tags string
|
|
expected mapstructureFieldTags
|
|
}
|
|
|
|
fn := func(t *testing.T, tc testCase) {
|
|
tag := fmt.Sprintf(`mapstructure:"%v"`, tc.tags)
|
|
field := reflect.StructField{
|
|
Tag: reflect.StructTag(tag),
|
|
Name: "Original",
|
|
}
|
|
actual := fieldTags(field)
|
|
require.Equal(t, tc.expected, actual)
|
|
}
|
|
|
|
var testCases = []testCase{
|
|
{tags: "", expected: mapstructureFieldTags{name: "Original"}},
|
|
{tags: "just-a-name", expected: mapstructureFieldTags{name: "just-a-name"}},
|
|
{tags: "name,squash", expected: mapstructureFieldTags{name: "name", squash: true}},
|
|
{tags: ",squash", expected: mapstructureFieldTags{name: "Original", squash: true}},
|
|
{tags: ",omitempty,squash", expected: mapstructureFieldTags{name: "Original", squash: true}},
|
|
{tags: "named,omitempty,squash", expected: mapstructureFieldTags{name: "named", squash: true}},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.tags, func(t *testing.T) {
|
|
fn(t, tc)
|
|
})
|
|
}
|
|
}
|