pulumi/sdk/go/common/util/mapper/mapper_test.go

724 lines
19 KiB
Go

// Copyright 2016-2018, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package mapper
import (
"reflect"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type bag struct {
Bool bool
BoolP *bool
String string
StringP *string
Float64 float64
Float64P *float64
Strings []string
StringsP *[]string
}
func TestFieldMapper(t *testing.T) {
t.Parallel()
md := New(nil)
tree := map[string]interface{}{
"b": true,
"s": "hello",
"f": float64(3.14159265359),
"ss": []string{"a", "b", "c"},
}
// Try some simple primitive decodes.
var s bag
err := md.DecodeValue(tree, reflect.TypeOf(bag{}), "b", &s.Bool, false)
assert.NoError(t, err)
assert.Equal(t, tree["b"], s.Bool)
err = md.DecodeValue(tree, reflect.TypeOf(bag{}), "b", &s.BoolP, false)
assert.NoError(t, err)
assert.Equal(t, tree["b"], *s.BoolP)
err = md.DecodeValue(tree, reflect.TypeOf(bag{}), "s", &s.String, false)
assert.NoError(t, err)
assert.Equal(t, tree["s"], s.String)
err = md.DecodeValue(tree, reflect.TypeOf(bag{}), "s", &s.StringP, false)
assert.NoError(t, err)
assert.Equal(t, tree["s"], *s.StringP)
err = md.DecodeValue(tree, reflect.TypeOf(bag{}), "f", &s.Float64, false)
assert.NoError(t, err)
assert.Equal(t, tree["f"], s.Float64)
err = md.DecodeValue(tree, reflect.TypeOf(bag{}), "f", &s.Float64P, false)
assert.NoError(t, err)
assert.Equal(t, tree["f"], *s.Float64P)
err = md.DecodeValue(tree, reflect.TypeOf(bag{}), "ss", &s.Strings, false)
assert.NoError(t, err)
assert.Equal(t, tree["ss"], s.Strings)
err = md.DecodeValue(tree, reflect.TypeOf(bag{}), "ss", &s.StringsP, false)
assert.NoError(t, err)
assert.Equal(t, tree["ss"], *s.StringsP)
// Ensure interface{} conversions work:
var sif string
err = md.DecodeValue(map[string]interface{}{"x": interface{}("hello")},
reflect.TypeOf(bag{}), "x", &sif, false)
assert.NoError(t, err)
assert.Equal(t, "hello", sif)
var sifs []string
err = md.DecodeValue(map[string]interface{}{"arr": []interface{}{"a", "b", "c"}},
reflect.TypeOf(bag{}), "arr", &sifs, false)
assert.NoError(t, err)
assert.Equal(t, []string{"a", "b", "c"}, sifs)
// Ensure missing optional fields are ignored:
s.String = "x"
err = md.DecodeValue(tree, reflect.TypeOf(bag{}), "missing", &s.String, true)
assert.NoError(t, err)
assert.Equal(t, "x", s.String)
// Try some error conditions; first, wrong type:
s.String = "x"
err = md.DecodeValue(tree, reflect.TypeOf(bag{}), "b", &s.String, false)
assert.EqualError(t, err, "Field 'b' on 'mapper.bag' must be a 'string'; got 'bool' instead")
assert.Equal(t, "x", s.String)
// Next, missing required field:
s.String = "x"
err = md.DecodeValue(tree, reflect.TypeOf(bag{}), "missing", &s.String, false)
assert.EqualError(t, err, "Missing required field 'missing' on 'mapper.bag'")
assert.Equal(t, "x", s.String)
}
type bagtag struct {
String string `pulumi:"s"`
StringSkip string `pulumi:"sc,skip"`
StringOpt string `pulumi:"so,optional"`
StringSkipOpt string `pulumi:"sco,skip,optional"`
MapOpt map[string]interface{} `pulumi:"mo,optional"`
}
type AnInterface interface {
isAnInterface()
}
func TestMapperEncode(t *testing.T) {
t.Parallel()
bag := bagtag{
String: "something",
StringOpt: "ohmv",
MapOpt: map[string]interface{}{
"a": "something",
"b": nil,
},
}
md := &mapper{}
var err error
var m map[string]interface{}
// Nils
m, err = md.Encode(nil)
require.NoError(t, err)
assert.Len(t, m, 0)
// Nil (interface)
m, err = md.Encode((AnInterface)(nil))
require.NoError(t, err)
assert.Len(t, m, 0)
// Structs
m, err = md.encode(reflect.ValueOf(bag))
require.NoError(t, err)
assert.Equal(t, "something", m["s"])
assert.Equal(t, "ohmv", m["so"])
assert.Equal(t, map[string]interface{}{"a": "something", "b": nil}, m["mo"])
// Pointers
m, err = md.encode(reflect.Zero(reflect.TypeOf(&bag)))
require.NoError(t, err)
assert.Nil(t, m)
m, err = md.encode(reflect.ValueOf(&bag))
require.NoError(t, err)
assert.Equal(t, "something", m["s"])
assert.Equal(t, "ohmv", m["so"])
assert.Equal(t, map[string]interface{}{"a": "something", "b": nil}, m["mo"])
}
func TestMapperEncodeValue(t *testing.T) {
t.Parallel()
strdata := "something"
bag := bagtag{
String: "something",
StringOpt: "ohmv",
}
slice := []string{"something"}
mapdata := map[string]interface{}{
"a": "something",
"b": nil,
}
anyType := reflect.TypeOf((*any)(nil)).Elem()
assert.Equal(t, reflect.Interface, anyType.Kind())
md := &mapper{}
var err error
var v any
// Nils
v, err = md.EncodeValue(nil)
require.NoError(t, err)
assert.Nil(t, v)
// Bools
v, err = md.encodeValue(reflect.ValueOf(true))
require.NoError(t, err)
assert.Equal(t, true, v)
// Ints
v, err = md.encodeValue(reflect.ValueOf(int(1)))
require.NoError(t, err)
assert.Equal(t, float64(1), v)
// Uints
v, err = md.encodeValue(reflect.ValueOf(uint(1)))
require.NoError(t, err)
assert.Equal(t, float64(1), v)
// Floats
v, err = md.encodeValue(reflect.ValueOf(float32(1.0)))
require.NoError(t, err)
assert.Equal(t, float64(1.0), v)
// Pointers
v, err = md.encodeValue(reflect.Zero(reflect.TypeOf(&strdata)))
require.NoError(t, err)
assert.Nil(t, v)
v, err = md.encodeValue(reflect.ValueOf(&strdata))
require.NoError(t, err)
assert.Equal(t, "something", v)
// Slices
v, err = md.encodeValue(reflect.Zero(reflect.TypeOf(slice)))
require.NoError(t, err)
assert.Nil(t, v)
v, err = md.encodeValue(reflect.ValueOf(slice))
require.NoError(t, err)
assert.Equal(t, []interface{}{"something"}, v)
// Maps
v, err = md.encodeValue(reflect.Zero(reflect.TypeOf(mapdata)))
require.NoError(t, err)
assert.Nil(t, v)
v, err = md.encodeValue(reflect.ValueOf(mapdata))
require.NoError(t, err)
assert.Equal(t, map[string]interface{}{"a": "something", "b": nil}, v)
// Structs
v, err = md.encodeValue(reflect.ValueOf(bag))
require.NoError(t, err)
assert.Equal(t, map[string]interface{}{"s": "something", "so": "ohmv"}, v)
// Interfaces
v, err = md.encodeValue(reflect.Zero(anyType))
require.NoError(t, err)
assert.Nil(t, v)
v, err = md.encodeValue(reflect.ValueOf("something").Convert(anyType))
require.NoError(t, err)
assert.Equal(t, "something", v)
}
func TestMapperDecode(t *testing.T) {
t.Parallel()
var err error
md := New(nil)
// First, test the fully populated case.
var b1 bagtag
err = md.Decode(map[string]interface{}{
"s": "something",
"sc": "nothing",
"so": "ohmy",
"sco": "ohmynada",
"mo": map[string]interface{}{
"a": "something",
"b": nil,
},
}, &b1)
assert.NoError(t, err)
assert.Equal(t, "something", b1.String)
assert.Equal(t, "", b1.StringSkip)
assert.Equal(t, "ohmy", b1.StringOpt)
assert.Equal(t, "", b1.StringSkipOpt)
assert.Equal(t, map[string]interface{}{"a": "something", "b": nil}, b1.MapOpt)
// Now let optional fields go missing.
var b2 bagtag
err = md.Decode(map[string]interface{}{
"s": "something",
"sc": "nothing",
}, &b2)
assert.NoError(t, err)
assert.Equal(t, "something", b2.String)
assert.Equal(t, "", b2.StringSkip)
assert.Equal(t, "", b2.StringOpt)
assert.Equal(t, "", b2.StringSkipOpt)
// Try some error conditions; first, wrong type:
var b3 bagtag
err = md.Decode(map[string]interface{}{
"s": true,
"sc": "",
}, &b3)
assert.EqualError(t, err, "1 failures decoding:\n"+
"\ts: Field 's' on 'mapper.bagtag' must be a 'string'; got 'bool' instead")
assert.Equal(t, "", b3.String)
// Next, missing required field:
var b4 bagtag
err = md.Decode(map[string]interface{}{}, &b4)
assert.EqualError(t, err, "1 failures decoding:\n"+
"\ts: Missing required field 's' on 'mapper.bagtag'")
assert.Equal(t, "", b4.String)
}
type bog struct {
Boggy bogger `pulumi:"boggy"`
BoggyP *bogger `pulumi:"boggyp"`
Boggers []bogger `pulumi:"boggers"`
BoggersP *[]*bogger `pulumi:"boggersp"`
}
type bogger struct {
Num float64 `pulumi:"num"`
}
func TestNestedMapper(t *testing.T) {
t.Parallel()
md := New(nil)
// Test one level deep nesting (fields, arrays, pointers).
var b bog
err := md.Decode(map[string]interface{}{
"boggy": map[string]interface{}{"num": float64(99)},
"boggyp": map[string]interface{}{"num": float64(180)},
"boggers": []map[string]interface{}{
{"num": float64(1)},
{"num": float64(2)},
{"num": float64(42)},
},
"boggersp": []map[string]interface{}{
{"num": float64(4)},
{"num": float64(8)},
{"num": float64(84)},
},
}, &b)
assert.NoError(t, err)
assert.Equal(t, float64(99), b.Boggy.Num)
assert.NotNil(t, b.BoggyP)
assert.Equal(t, float64(180), b.BoggyP.Num)
assert.Equal(t, 3, len(b.Boggers))
assert.Equal(t, float64(1), b.Boggers[0].Num)
assert.Equal(t, float64(2), b.Boggers[1].Num)
assert.Equal(t, float64(42), b.Boggers[2].Num)
assert.NotNil(t, b.BoggersP)
assert.Equal(t, 3, len(*b.BoggersP))
assert.NotNil(t, (*b.BoggersP)[0])
assert.Equal(t, float64(4), (*b.BoggersP)[0].Num)
assert.NotNil(t, (*b.BoggersP)[1])
assert.Equal(t, float64(8), (*b.BoggersP)[1].Num)
assert.NotNil(t, (*b.BoggersP)[2])
assert.Equal(t, float64(84), (*b.BoggersP)[2].Num)
}
type boggerdybogger struct {
Bogs map[string]bog `pulumi:"bogs"`
BogsP *map[string]*bog `pulumi:"bogsp"`
}
func TestMultiplyNestedMapper(t *testing.T) {
t.Parallel()
md := New(nil)
// Test multilevel nesting (maps, fields, arrays, pointers).
var ber boggerdybogger
err := md.Decode(map[string]interface{}{
"bogs": map[string]interface{}{
"a": map[string]interface{}{
"boggy": map[string]interface{}{"num": float64(99)},
"boggyp": map[string]interface{}{"num": float64(180)},
"boggers": []map[string]interface{}{
{"num": float64(1)},
{"num": float64(2)},
{"num": float64(42)},
},
"boggersp": []map[string]interface{}{
{"num": float64(4)},
{"num": float64(8)},
{"num": float64(84)},
},
},
},
"bogsp": map[string]interface{}{
"z": map[string]interface{}{
"boggy": map[string]interface{}{"num": float64(188)},
"boggyp": map[string]interface{}{"num": float64(360)},
"boggers": []map[string]interface{}{
{"num": float64(2)},
{"num": float64(4)},
{"num": float64(84)},
},
"boggersp": []map[string]interface{}{
{"num": float64(8)},
{"num": float64(16)},
{"num": float64(168)},
},
},
},
}, &ber)
assert.NoError(t, err)
assert.Equal(t, 1, len(ber.Bogs))
b := ber.Bogs["a"]
assert.Equal(t, float64(99), b.Boggy.Num)
assert.NotNil(t, b.BoggyP)
assert.Equal(t, float64(180), b.BoggyP.Num)
assert.Equal(t, 3, len(b.Boggers))
assert.Equal(t, float64(1), b.Boggers[0].Num)
assert.Equal(t, float64(2), b.Boggers[1].Num)
assert.Equal(t, float64(42), b.Boggers[2].Num)
assert.NotNil(t, b.BoggersP)
assert.Equal(t, 3, len(*b.BoggersP))
assert.NotNil(t, (*b.BoggersP)[0])
assert.Equal(t, float64(4), (*b.BoggersP)[0].Num)
assert.NotNil(t, (*b.BoggersP)[1])
assert.Equal(t, float64(8), (*b.BoggersP)[1].Num)
assert.NotNil(t, (*b.BoggersP)[2])
assert.Equal(t, float64(84), (*b.BoggersP)[2].Num)
assert.NotNil(t, ber.BogsP)
assert.Equal(t, 1, len(*ber.BogsP))
p := (*ber.BogsP)["z"]
assert.NotNil(t, p)
assert.Equal(t, float64(188), p.Boggy.Num)
assert.NotNil(t, p.BoggyP)
assert.Equal(t, float64(360), p.BoggyP.Num)
assert.Equal(t, 3, len(p.Boggers))
assert.Equal(t, float64(2), p.Boggers[0].Num)
assert.Equal(t, float64(4), p.Boggers[1].Num)
assert.Equal(t, float64(84), p.Boggers[2].Num)
assert.NotNil(t, p.BoggersP)
assert.Equal(t, 3, len(*p.BoggersP))
assert.NotNil(t, (*p.BoggersP)[0])
assert.Equal(t, float64(8), (*p.BoggersP)[0].Num)
assert.NotNil(t, (*p.BoggersP)[1])
assert.Equal(t, float64(16), (*p.BoggersP)[1].Num)
assert.NotNil(t, (*p.BoggersP)[2])
assert.Equal(t, float64(168), (*p.BoggersP)[2].Num)
}
type hasmap struct {
Entries map[string]mapentry `pulumi:"entries"`
EntriesP map[string]*mapentry `pulumi:"entriesp"`
}
type mapentry struct {
Title string `pulumi:"title"`
}
func TestMapMapper(t *testing.T) {
t.Parallel()
md := New(nil)
// Ensure we can decode both maps of structs and maps of pointers to structs.
var hm hasmap
err := md.Decode(map[string]interface{}{
"entries": map[string]interface{}{
"a": map[string]interface{}{"title": "first"},
"b": map[string]interface{}{"title": "second"},
},
"entriesp": map[string]interface{}{
"x": map[string]interface{}{"title": "firstp"},
"y": map[string]interface{}{"title": "secondp"},
},
}, &hm)
assert.NoError(t, err)
assert.Equal(t, 2, len(hm.Entries))
assert.Equal(t, "first", hm.Entries["a"].Title)
assert.Equal(t, "second", hm.Entries["b"].Title)
assert.Equal(t, 2, len(hm.EntriesP))
assert.NotNil(t, hm.EntriesP["x"])
assert.NotNil(t, hm.EntriesP["y"])
assert.Equal(t, "firstp", hm.EntriesP["x"].Title)
assert.Equal(t, "secondp", hm.EntriesP["y"].Title)
}
type wrap struct {
C customStruct `pulumi:"c"`
CI customInterface `pulumi:"ci"`
}
type customInterface interface {
GetX() float64
GetY() float64
}
type customStruct struct {
X float64 `pulumi:"x"`
Y float64 `pulumi:"y"`
}
func (s *customStruct) GetX() float64 { return s.X }
func (s *customStruct) GetY() float64 { return s.Y }
func TestCustomMapper(t *testing.T) {
t.Parallel()
md := New(&Opts{
CustomDecoders: Decoders{
reflect.TypeOf((*customInterface)(nil)).Elem(): decodeCustomInterface,
reflect.TypeOf(customStruct{}): decodeCustomStruct,
},
})
var w wrap
err := md.Decode(map[string]interface{}{
"c": map[string]interface{}{
"x": float64(-99.2),
"y": float64(127.127),
},
"ci": map[string]interface{}{
"x": float64(42.6),
"y": float64(247.9),
},
}, &w)
assert.NoError(t, err)
assert.Equal(t, float64(-99.2), w.C.X)
assert.Equal(t, float64(127.127), w.C.Y)
assert.NotNil(t, w.CI)
assert.Equal(t, float64(42.6), w.CI.GetX())
assert.Equal(t, float64(247.9), w.CI.GetY())
}
func decodeCustomInterface(m Mapper, tree map[string]interface{}) (interface{}, error) {
var s customStruct
if err := m.DecodeValue(tree, reflect.TypeOf(s), "x", &s.X, false); err != nil {
return nil, err
}
if err := m.DecodeValue(tree, reflect.TypeOf(s), "y", &s.Y, false); err != nil {
return nil, err
}
return customInterface(&s), nil
}
func decodeCustomStruct(m Mapper, tree map[string]interface{}) (interface{}, error) {
var s customStruct
if err := m.DecodeValue(tree, reflect.TypeOf(s), "x", &s.X, false); err != nil {
return nil, err
}
if err := m.DecodeValue(tree, reflect.TypeOf(s), "y", &s.Y, false); err != nil {
return nil, err
}
return s, nil
}
type outer struct {
Inners *[]inner `pulumi:"inners,optional"`
}
type inner struct {
A string `pulumi:"a"`
B *string `pulumi:"b,optional"`
C *string `pulumi:"c,optional"`
D float64 `pulumi:"d"`
E *float64 `pulumi:"e,optional"`
F *float64 `pulumi:"f,optional"`
G *inner `pulumi:"g,optional"`
H *[]inner `pulumi:"h,optional"`
}
func TestBasicUnmap(t *testing.T) {
t.Parallel()
v2 := "v2"
v5 := float64(5)
i1v2 := "i1v2"
i1v5 := float64(15)
i2v2 := "i2v2"
i2v5 := float64(25)
i3v2 := "i3v2"
i3v5 := float64(35)
o := outer{
Inners: &[]inner{
{
A: "v1",
B: &v2,
C: nil,
D: float64(4),
E: &v5,
F: nil,
G: &inner{
A: "i1v1",
B: &i1v2,
C: nil,
D: float64(14),
E: &i1v5,
F: nil,
G: nil,
H: nil,
},
H: &[]inner{
{
A: "i2v1",
B: &i2v2,
C: nil,
D: float64(24),
E: &i2v5,
F: nil,
G: nil,
H: nil,
},
{
A: "i3v1",
B: &i3v2,
C: nil,
D: float64(34),
E: &i3v5,
F: nil,
G: nil,
H: nil,
},
},
},
},
}
// Unmap returns a JSON-like dictionary object representing the above structure.
for _, e := range []interface{}{o, &o} {
um, err := Unmap(e)
assert.NoError(t, err)
assert.NotNil(t, um)
// check outer:
assert.NotNil(t, um["inners"])
arr := um["inners"].([]interface{})
assert.Equal(t, len(arr), 1)
// check outer.inner:
inn := arr[0].(map[string]interface{})
assert.Equal(t, inn["a"], "v1")
assert.Equal(t, inn["b"], "v2")
_, hasc := inn["c"]
assert.False(t, hasc)
assert.Equal(t, inn["d"], float64(4))
assert.Equal(t, inn["e"], float64(5))
_, hasf := inn["f"]
assert.False(t, hasf)
assert.NotNil(t, inn["g"])
// check outer.inner.inner:
inng := inn["g"].(map[string]interface{})
assert.Equal(t, inng["a"], "i1v1")
assert.Equal(t, inng["b"], "i1v2")
_, hasgc := inng["c"]
assert.False(t, hasgc)
assert.Equal(t, inng["d"], float64(14))
assert.Equal(t, inng["e"], float64(15))
_, hasgf := inng["f"]
assert.False(t, hasgf)
_, hasgg := inng["g"]
assert.False(t, hasgg)
_, hasgh := inng["h"]
assert.False(t, hasgh)
// check outer.inner.inners[0]:
innh := inn["h"].([]interface{})
assert.Equal(t, len(innh), 2)
innh0 := innh[0].(map[string]interface{})
assert.Equal(t, innh0["a"], "i2v1")
assert.Equal(t, innh0["b"], "i2v2")
_, hash0c := inng["c"]
assert.False(t, hash0c)
assert.Equal(t, innh0["d"], float64(24))
assert.Equal(t, innh0["e"], float64(25))
_, hash0f := inng["f"]
assert.False(t, hash0f)
_, hash0g := inng["g"]
assert.False(t, hash0g)
_, hash0h := inng["h"]
assert.False(t, hash0h)
// check outer.inner.inners[1]:
innh1 := innh[1].(map[string]interface{})
assert.Equal(t, innh1["a"], "i3v1")
assert.Equal(t, innh1["b"], "i3v2")
_, hash1c := inng["c"]
assert.False(t, hash1c)
assert.Equal(t, innh1["d"], float64(34))
assert.Equal(t, innh1["e"], float64(35))
_, hash1f := inng["f"]
assert.False(t, hash1f)
_, hash1g := inng["g"]
assert.False(t, hash1g)
_, hash1h := inng["h"]
assert.False(t, hash1h)
}
}
func TestReproduceMapStringPointerTurnaroundIssue(t *testing.T) {
t.Parallel()
type X struct {
Args map[string]*string `pulumi:"args,optional"`
}
xToMap := func(build X) (map[string]interface{}, error) {
m, err := New(nil).Encode(build)
if err != nil {
return nil, err
}
return m, nil
}
xFromMap := func(pm map[string]interface{}) (X, error) {
var build X
err := New(nil).Decode(pm, &build)
if err != nil {
return X{}, err
}
return build, nil
}
value := "value"
expected := X{
Args: map[string]*string{
"key": &value,
},
}
encodedMap, err := xToMap(expected)
require.NoError(t, err)
t.Logf("encodedMap: %v", encodedMap)
back, err2 := xFromMap(encodedMap)
require.NoError(t, err2)
require.Equal(t, expected, back)
}