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