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 )
} )
}
}