From c42f1b7a503004dc54f121e848193e58b59bbe94 Mon Sep 17 00:00:00 2001 From: Fernandez Ludovic Date: Sun, 12 Jul 2020 12:56:22 +0200 Subject: [PATCH] feat: raw map parser. --- pkg/config/file/file_test.go | 49 ++++ pkg/config/label/label_test.go | 43 ++- pkg/config/parser/element_fill.go | 31 ++ pkg/config/parser/element_fill_test.go | 78 +++++ pkg/config/parser/element_nodes.go | 5 + pkg/config/parser/element_nodes_test.go | 36 +++ pkg/config/parser/labels_encode.go | 37 +++ pkg/config/parser/labels_encode_test.go | 54 ++++ pkg/config/parser/node.go | 1 + pkg/config/parser/nodes_metadata.go | 82 ++++++ pkg/config/parser/nodes_metadata_test.go | 150 ++++++++++ pkg/config/parser/parser.go | 7 +- pkg/config/parser/parser_test.go | 346 +++++++++++++++++++++++ 13 files changed, 901 insertions(+), 18 deletions(-) create mode 100644 pkg/config/parser/parser_test.go diff --git a/pkg/config/file/file_test.go b/pkg/config/file/file_test.go index 138ecebce..0fb348636 100644 --- a/pkg/config/file/file_test.go +++ b/pkg/config/file/file_test.go @@ -68,6 +68,30 @@ fii = "bir" assert.Equal(t, expected, element) } +func TestDecodeContent_TOML_rawValue(t *testing.T) { + content := ` +name = "test" +[[meta.aaa]] + bbb = 1 +` + + type Foo struct { + Name string + Meta map[string]interface{} + } + + element := &Foo{} + + err := DecodeContent(content, ".toml", element) + require.NoError(t, err) + + expected := &Foo{ + Name: "test", + Meta: map[string]interface{}{"aaa": []interface{}{map[string]interface{}{"bbb": "1"}}}, + } + assert.Equal(t, expected, element) +} + func TestDecode_YAML(t *testing.T) { f, err := ioutil.TempFile("", "traefik-config-*.yaml") require.NoError(t, err) @@ -126,3 +150,28 @@ yi: {} } assert.Equal(t, expected, element) } + +func TestDecodeContent_YAML_rawValue(t *testing.T) { + content := ` +name: test +meta: + aaa: + - bbb: 1 +` + + type Foo struct { + Name string + Meta map[string]interface{} + } + + element := &Foo{} + + err := DecodeContent(content, ".yaml", element) + require.NoError(t, err) + + expected := &Foo{ + Name: "test", + Meta: map[string]interface{}{"aaa": []interface{}{map[string]interface{}{"bbb": "1"}}}, + } + assert.Equal(t, expected, element) +} diff --git a/pkg/config/label/label_test.go b/pkg/config/label/label_test.go index 0fe71e318..ba1bff2bb 100644 --- a/pkg/config/label/label_test.go +++ b/pkg/config/label/label_test.go @@ -123,18 +123,19 @@ func TestDecodeConfiguration(t *testing.T) { "traefik.http.middlewares.Middleware17.stripprefix.prefixes": "foobar, fiibar", "traefik.http.middlewares.Middleware18.stripprefixregex.regex": "foobar, fiibar", "traefik.http.middlewares.Middleware19.compress": "true", - - "traefik.http.routers.Router0.entrypoints": "foobar, fiibar", - "traefik.http.routers.Router0.middlewares": "foobar, fiibar", - "traefik.http.routers.Router0.priority": "42", - "traefik.http.routers.Router0.rule": "foobar", - "traefik.http.routers.Router0.tls": "true", - "traefik.http.routers.Router0.service": "foobar", - "traefik.http.routers.Router1.entrypoints": "foobar, fiibar", - "traefik.http.routers.Router1.middlewares": "foobar, fiibar", - "traefik.http.routers.Router1.priority": "42", - "traefik.http.routers.Router1.rule": "foobar", - "traefik.http.routers.Router1.service": "foobar", + "traefik.http.middlewares.Middleware20.plugin.tomato.aaa": "foo1", + "traefik.http.middlewares.Middleware20.plugin.tomato.bbb": "foo2", + "traefik.http.routers.Router0.entrypoints": "foobar, fiibar", + "traefik.http.routers.Router0.middlewares": "foobar, fiibar", + "traefik.http.routers.Router0.priority": "42", + "traefik.http.routers.Router0.rule": "foobar", + "traefik.http.routers.Router0.tls": "true", + "traefik.http.routers.Router0.service": "foobar", + "traefik.http.routers.Router1.entrypoints": "foobar, fiibar", + "traefik.http.routers.Router1.middlewares": "foobar, fiibar", + "traefik.http.routers.Router1.priority": "42", + "traefik.http.routers.Router1.rule": "foobar", + "traefik.http.routers.Router1.service": "foobar", "traefik.http.services.Service0.loadbalancer.healthcheck.headers.name0": "foobar", "traefik.http.services.Service0.loadbalancer.healthcheck.headers.name1": "foobar", @@ -574,6 +575,14 @@ func TestDecodeConfiguration(t *testing.T) { }, }, }, + "Middleware20": { + Plugin: map[string]dynamic.PluginConf{ + "tomato": { + "aaa": "foo1", + "bbb": "foo2", + }, + }, + }, }, Services: map[string]*dynamic.Service{ "Service0": { @@ -897,6 +906,14 @@ func TestEncodeConfiguration(t *testing.T) { RetryExpression: "foobar", }, }, + "Middleware20": { + Plugin: map[string]dynamic.PluginConf{ + "tomato": { + "aaa": "foo1", + "bbb": "foo2", + }, + }, + }, "Middleware3": { Chain: &dynamic.Chain{ Middlewares: []string{ @@ -1205,6 +1222,8 @@ func TestEncodeConfiguration(t *testing.T) { "traefik.HTTP.Middlewares.Middleware17.StripPrefix.ForceSlash": "true", "traefik.HTTP.Middlewares.Middleware18.StripPrefixRegex.Regex": "foobar, fiibar", "traefik.HTTP.Middlewares.Middleware19.Compress": "true", + "traefik.HTTP.Middlewares.Middleware20.Plugin.tomato.aaa": "foo1", + "traefik.HTTP.Middlewares.Middleware20.Plugin.tomato.bbb": "foo2", "traefik.HTTP.Routers.Router0.EntryPoints": "foobar, fiibar", "traefik.HTTP.Routers.Router0.Middlewares": "foobar, fiibar", diff --git a/pkg/config/parser/element_fill.go b/pkg/config/parser/element_fill.go index 59b147dea..3a1840c8d 100644 --- a/pkg/config/parser/element_fill.go +++ b/pkg/config/parser/element_fill.go @@ -271,6 +271,16 @@ func (f filler) setMap(field reflect.Value, node *Node) error { field.Set(reflect.MakeMap(field.Type())) } + if field.Type().Elem().Kind() == reflect.Interface { + fillRawValue(field, node, false) + + for _, child := range node.Children { + fillRawValue(field, child, true) + } + + return nil + } + for _, child := range node.Children { ptrValue := reflect.New(reflect.PtrTo(field.Type().Elem())) @@ -284,6 +294,7 @@ func (f filler) setMap(field reflect.Value, node *Node) error { key := reflect.ValueOf(child.Name) field.SetMapIndex(key, value) } + return nil } @@ -339,3 +350,23 @@ func setFloat(field reflect.Value, value string, bitSize int) error { field.Set(reflect.ValueOf(val).Convert(field.Type())) return nil } + +func fillRawValue(field reflect.Value, node *Node, subMap bool) { + m, ok := node.RawValue.(map[string]interface{}) + if !ok { + return + } + + if _, self := m[node.Name]; self || !subMap { + for k, v := range m { + field.SetMapIndex(reflect.ValueOf(k), reflect.ValueOf(v)) + } + + return + } + + p := map[string]interface{}{node.Name: m} + node.RawValue = p + + field.SetMapIndex(reflect.ValueOf(node.Name), reflect.ValueOf(p[node.Name])) +} diff --git a/pkg/config/parser/element_fill_test.go b/pkg/config/parser/element_fill_test.go index 968d8edc8..628127416 100644 --- a/pkg/config/parser/element_fill_test.go +++ b/pkg/config/parser/element_fill_test.go @@ -1413,6 +1413,84 @@ func TestFill(t *testing.T) { }, }}, }, + { + desc: "raw value", + node: &Node{ + Name: "traefik", + Kind: reflect.Ptr, + Children: []*Node{ + {Name: "meta", FieldName: "Meta", Kind: reflect.Map, RawValue: map[string]interface{}{ + "aaa": "test", + "bbb": map[string]interface{}{ + "ccc": "test", + "ddd": map[string]interface{}{ + "eee": "test", + }, + }, + }}, + {Name: "name", FieldName: "Name", Value: "test", Kind: reflect.String}, + }, + }, + element: &struct { + Name string + Meta map[string]interface{} + }{}, + expected: expected{element: &struct { + Name string + Meta map[string]interface{} + }{ + Name: "test", + Meta: map[string]interface{}{ + "aaa": "test", + "bbb": map[string]interface{}{ + "ccc": "test", + "ddd": map[string]interface{}{ + "eee": "test", + }, + }, + }, + }}, + }, + { + desc: "explicit map of map, raw value", + node: &Node{ + Name: "traefik", + Kind: reflect.Ptr, + Children: []*Node{ + {Name: "meta", FieldName: "Meta", Kind: reflect.Map, Children: []*Node{ + {Name: "aaa", Kind: reflect.Map, Children: []*Node{ + {Name: "bbb", RawValue: map[string]interface{}{ + "ccc": "test1", + "ddd": "test2", + }}, + {Name: "eee", Value: "test3", RawValue: map[string]interface{}{ + "eee": "test3", + }}, + }}, + }}, + {Name: "name", FieldName: "Name", Value: "test", Kind: reflect.String}, + }, + }, + element: &struct { + Name string + Meta map[string]map[string]interface{} + }{}, + expected: expected{element: &struct { + Name string + Meta map[string]map[string]interface{} + }{ + Name: "test", + Meta: map[string]map[string]interface{}{ + "aaa": { + "bbb": map[string]interface{}{ + "ccc": "test1", + "ddd": "test2", + }, + "eee": "test3", + }, + }, + }}, + }, } for _, test := range testCases { diff --git a/pkg/config/parser/element_nodes.go b/pkg/config/parser/element_nodes.go index a66c108d1..788975cd2 100644 --- a/pkg/config/parser/element_nodes.go +++ b/pkg/config/parser/element_nodes.go @@ -123,6 +123,11 @@ func (e encoderToNode) setStructValue(node *Node, rValue reflect.Value) error { } func (e encoderToNode) setMapValue(node *Node, rValue reflect.Value) error { + if rValue.Type().Elem().Kind() == reflect.Interface { + node.RawValue = rValue.Interface() + return nil + } + for _, key := range rValue.MapKeys() { child := &Node{Name: key.String(), FieldName: key.String()} node.Children = append(node.Children, child) diff --git a/pkg/config/parser/element_nodes_test.go b/pkg/config/parser/element_nodes_test.go index f809f46e3..b48d5201c 100644 --- a/pkg/config/parser/element_nodes_test.go +++ b/pkg/config/parser/element_nodes_test.go @@ -755,6 +755,42 @@ func TestEncodeToNode(t *testing.T) { }}, }, }, + { + desc: "raw value", + element: struct { + Foo *struct { + Bar map[string]interface{} + } + }{ + Foo: &struct { + Bar map[string]interface{} + }{ + Bar: map[string]interface{}{ + "AAA": "valueA", + "BBB": map[string]interface{}{ + "CCC": map[string]interface{}{ + "DDD": "valueD", + }, + }, + }, + }, + }, + expected: expected{node: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Children: []*Node{ + {Name: "Bar", FieldName: "Bar", RawValue: map[string]interface{}{ + "AAA": "valueA", + "BBB": map[string]interface{}{ + "CCC": map[string]interface{}{ + "DDD": "valueD", + }, + }, + }}, + }}, + }, + }}, + }, } for _, test := range testCases { diff --git a/pkg/config/parser/labels_encode.go b/pkg/config/parser/labels_encode.go index 91cda15a2..9e9199679 100644 --- a/pkg/config/parser/labels_encode.go +++ b/pkg/config/parser/labels_encode.go @@ -1,5 +1,10 @@ package parser +import ( + "fmt" + "reflect" +) + // EncodeNode Converts a node to labels. // nodes -> labels. func EncodeNode(node *Node) map[string]string { @@ -21,6 +26,11 @@ func encodeNode(labels map[string]string, root string, node *Node) { childName := root + sep + child.Name + if child.RawValue != nil { + encodeRawValue(labels, childName, child.RawValue) + continue + } + if len(child.Children) > 0 { encodeNode(labels, childName, child) } else if len(child.Name) > 0 { @@ -28,3 +38,30 @@ func encodeNode(labels map[string]string, root string, node *Node) { } } } + +func encodeRawValue(labels map[string]string, root string, rawValue interface{}) { + if rawValue == nil { + return + } + + tValue := reflect.TypeOf(rawValue) + + if tValue.Kind() == reflect.Map && tValue.Elem().Kind() == reflect.Interface { + r := reflect.ValueOf(rawValue). + Convert(reflect.TypeOf((map[string]interface{})(nil))). + Interface().(map[string]interface{}) + + for k, v := range r { + switch tv := v.(type) { + case string: + labels[root+"."+k] = tv + case []interface{}: + for i, e := range tv { + encodeRawValue(labels, fmt.Sprintf("%s.%s[%d]", root, k, i), e) + } + default: + encodeRawValue(labels, root+"."+k, v) + } + } + } +} diff --git a/pkg/config/parser/labels_encode_test.go b/pkg/config/parser/labels_encode_test.go index cc8fa6930..3c6b6184d 100644 --- a/pkg/config/parser/labels_encode_test.go +++ b/pkg/config/parser/labels_encode_test.go @@ -165,6 +165,60 @@ func TestEncodeNode(t *testing.T) { "traefik.foo[1].bbb": "bur1", }, }, + { + desc: "raw value, level 1", + node: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "aaa", RawValue: map[string]interface{}{ + "bbb": "test1", + "ccc": "test2", + }}, + }, + }, + expected: map[string]string{ + "traefik.aaa.bbb": "test1", + "traefik.aaa.ccc": "test2", + }, + }, + { + desc: "raw value, level 2", + node: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "aaa", RawValue: map[string]interface{}{ + "bbb": "test1", + "ccc": map[string]interface{}{ + "ddd": "test2", + }, + }}, + }, + }, + expected: map[string]string{ + "traefik.aaa.bbb": "test1", + "traefik.aaa.ccc.ddd": "test2", + }, + }, + { + desc: "raw value, slice of struct", + node: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "aaa", RawValue: map[string]interface{}{ + "bbb": []interface{}{ + map[string]interface{}{ + "ccc": "test1", + "ddd": "test2", + }, + }, + }}, + }, + }, + expected: map[string]string{ + "traefik.aaa.bbb[0].ccc": "test1", + "traefik.aaa.bbb[0].ddd": "test2", + }, + }, } for _, test := range testCases { diff --git a/pkg/config/parser/node.go b/pkg/config/parser/node.go index 88f0c7998..8e3b58f09 100644 --- a/pkg/config/parser/node.go +++ b/pkg/config/parser/node.go @@ -14,6 +14,7 @@ type Node struct { Description string `json:"description,omitempty"` FieldName string `json:"fieldName"` Value string `json:"value,omitempty"` + RawValue interface{} `json:"rawValue,omitempty"` Disabled bool `json:"disabled,omitempty"` Kind reflect.Kind `json:"kind,omitempty"` Tag reflect.StructTag `json:"tag,omitempty"` diff --git a/pkg/config/parser/nodes_metadata.go b/pkg/config/parser/nodes_metadata.go index 3357a0bd2..a5a2b3f08 100644 --- a/pkg/config/parser/nodes_metadata.go +++ b/pkg/config/parser/nodes_metadata.go @@ -57,6 +57,11 @@ func (m metadata) add(rootType reflect.Type, node *Node) error { rType = rootType.Elem() } + if rType.Kind() == reflect.Map && rType.Elem().Kind() == reflect.Interface { + addRawValue(node) + return nil + } + field, err := m.findTypedField(rType, node) if err != nil { return err @@ -88,6 +93,11 @@ func (m metadata) add(rootType reflect.Type, node *Node) error { } if fType.Kind() == reflect.Map { + if fType.Elem().Kind() == reflect.Interface { + addRawValue(node) + return nil + } + for _, child := range node.Children { // elem is a map entry value type elem := fType.Elem() @@ -194,3 +204,75 @@ func isSupportedType(field reflect.StructField) error { return nil } + +/* +RawMap section +*/ + +func addRawValue(node *Node) { + if node.RawValue == nil { + node.RawValue = nodeToRawMap(node) + } + + node.Children = nil +} + +func nodeToRawMap(node *Node) map[string]interface{} { + result := map[string]interface{}{} + + squashNode(node, result, true) + + return result +} + +func squashNode(node *Node, acc map[string]interface{}, root bool) { + if len(node.Children) == 0 { + acc[node.Name] = node.Value + + return + } + + // slice + if isArrayKey(node.Children[0].Name) { + var accChild []interface{} + + for _, child := range node.Children { + tmp := map[string]interface{}{} + squashNode(child, tmp, false) + accChild = append(accChild, tmp[child.Name]) + } + + acc[node.Name] = accChild + + return + } + + // map + var accChild map[string]interface{} + if root { + accChild = acc + } else { + accChild = typedRawMap(acc, node.Name) + } + + for _, child := range node.Children { + squashNode(child, accChild, false) + } +} + +func typedRawMap(m map[string]interface{}, k string) map[string]interface{} { + if m[k] == nil { + m[k] = map[string]interface{}{} + } + + r, ok := m[k].(map[string]interface{}) + if !ok { + panic(fmt.Sprintf("unsupported value (key: %s): %T", k, m[k])) + } + + return r +} + +func isArrayKey(name string) bool { + return name[0] == '[' && name[len(name)-1] == ']' +} diff --git a/pkg/config/parser/nodes_metadata_test.go b/pkg/config/parser/nodes_metadata_test.go index 419b9eafe..642cf3837 100644 --- a/pkg/config/parser/nodes_metadata_test.go +++ b/pkg/config/parser/nodes_metadata_test.go @@ -991,6 +991,51 @@ func TestAddMetadata(t *testing.T) { }, }}, }, + { + desc: "raw value", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Children: []*Node{ + {Name: "Bar", FieldName: "Bar", Children: []*Node{ + {Name: "AAA", FieldName: "AAA", Value: "valueA"}, + {Name: "BBB", FieldName: "BBB", Children: []*Node{ + {Name: "CCC", FieldName: "CCC", Children: []*Node{ + {Name: "DDD", FieldName: "DDD", Value: "valueD"}, + }}, + }}, + }}, + }}, + }, + }, + structure: struct { + Foo *struct { + Bar map[string]interface{} + } + }{ + Foo: &struct { + Bar map[string]interface{} + }{}, + }, + expected: expected{ + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Kind: reflect.Ptr, Children: []*Node{ + {Name: "Bar", FieldName: "Bar", Kind: reflect.Map, RawValue: map[string]interface{}{ + "AAA": "valueA", + "BBB": map[string]interface{}{ + "CCC": map[string]interface{}{ + "DDD": "valueD", + }, + }, + }}, + }}, + }, + }, + }, + }, } for _, test := range testCases { @@ -1015,4 +1060,109 @@ func TestAddMetadata(t *testing.T) { } } +func Test_nodeToRawMap(t *testing.T) { + testCases := []struct { + desc string + root *Node + expected map[string]interface{} + }{ + { + desc: "simple", + root: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "meta", Children: []*Node{ + {Name: "aaa", Value: "test1"}, + {Name: "bbb", Children: []*Node{ + {Name: "ccc", Value: "test2"}, + {Name: "ddd", Children: []*Node{ + {Name: "eee", Value: "test3"}, + }}, + }}, + }}, + {Name: "name", Value: "bla"}, + }, + }, + expected: map[string]interface{}{ + "meta": map[string]interface{}{ + "aaa": "test1", + "bbb": map[string]interface{}{ + "ccc": "test2", + "ddd": map[string]interface{}{ + "eee": "test3", + }, + }, + }, + "name": "bla", + }, + }, + { + desc: "slice of struct, level 1", + root: &Node{ + Name: "aaa", + Children: []*Node{ + {Name: "[0]", Children: []*Node{ + {Name: "bbb", Value: "test1"}, + {Name: "ccc", Value: "test2"}, + }}, + }, + }, + expected: map[string]interface{}{ + "aaa": []interface{}{ + map[string]interface{}{ + "bbb": "test1", + "ccc": "test2", + }, + }, + }, + }, + { + desc: "slice of struct, level 2", + root: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "meta", Children: []*Node{{ + Name: "aaa", Children: []*Node{ + {Name: "[0]", Children: []*Node{ + {Name: "bbb", Value: "test2"}, + {Name: "ccc", Value: "test3"}, + }}, + {Name: "[1]", Children: []*Node{ + {Name: "bbb", Value: "test4"}, + {Name: "ccc", Value: "test5"}, + }}, + }, + }}}, + {Name: "name", Value: "test1"}, + }, + }, + expected: map[string]interface{}{ + "meta": map[string]interface{}{ + "aaa": []interface{}{ + map[string]interface{}{ + "bbb": "test2", + "ccc": "test3", + }, + map[string]interface{}{ + "bbb": "test4", + "ccc": "test5", + }, + }, + }, + "name": "test1", + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := nodeToRawMap(test.root) + assert.Equal(t, test.expected, actual) + }) + } +} + type MySliceType []string diff --git a/pkg/config/parser/parser.go b/pkg/config/parser/parser.go index af38c2a02..a693152fd 100644 --- a/pkg/config/parser/parser.go +++ b/pkg/config/parser/parser.go @@ -19,12 +19,7 @@ func Decode(labels map[string]string, element interface{}, rootName string, filt return err } - err = Fill(element, node, FillerOpts{AllowSliceAsStruct: true}) - if err != nil { - return err - } - - return nil + return Fill(element, node, FillerOpts{AllowSliceAsStruct: true}) } // Encode converts an element to labels. diff --git a/pkg/config/parser/parser_test.go b/pkg/config/parser/parser_test.go new file mode 100644 index 000000000..2e1393d8f --- /dev/null +++ b/pkg/config/parser/parser_test.go @@ -0,0 +1,346 @@ +package parser + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type Tomato struct { + Name string + Meta map[string]interface{} +} + +type Potato struct { + Name string + Meta map[string]map[string]interface{} +} + +func TestDecode_RawValue(t *testing.T) { + testCases := []struct { + desc string + labels map[string]string + elt interface{} + expected interface{} + }{ + { + desc: "level 1", + elt: &Tomato{}, + labels: map[string]string{ + "traefik.name": "test", + "traefik.meta.aaa": "test", + }, + expected: &Tomato{ + Name: "test", + Meta: map[string]interface{}{ + "aaa": "test", + }, + }, + }, + { + desc: "level 2", + labels: map[string]string{ + "traefik.name": "test", + "traefik.meta.aaa": "test", + "traefik.meta.bbb.ccc": "test", + }, + elt: &Tomato{}, + expected: &Tomato{ + Name: "test", + Meta: map[string]interface{}{ + "aaa": "test", + "bbb": map[string]interface{}{ + "ccc": "test", + }, + }, + }, + }, + { + desc: "level 3", + labels: map[string]string{ + "traefik.name": "test", + "traefik.meta.aaa": "test", + "traefik.meta.bbb.ccc": "test", + "traefik.meta.bbb.ddd.eee": "test", + }, + elt: &Tomato{}, + expected: &Tomato{ + Name: "test", + Meta: map[string]interface{}{ + "aaa": "test", + "bbb": map[string]interface{}{ + "ccc": "test", + "ddd": map[string]interface{}{ + "eee": "test", + }, + }, + }, + }, + }, + { + desc: "struct slice, one entry", + elt: &Tomato{}, + labels: map[string]string{ + "traefik.name": "test1", + "traefik.meta.aaa[0].bbb": "test2", + "traefik.meta.aaa[0].ccc": "test3", + }, + expected: &Tomato{ + Name: "test1", + Meta: map[string]interface{}{ + "aaa": []interface{}{ + map[string]interface{}{ + "bbb": "test2", + "ccc": "test3", + }, + }, + }, + }, + }, + { + desc: "struct slice, multiple entries", + elt: &Tomato{}, + labels: map[string]string{ + "traefik.name": "test1", + "traefik.meta.aaa[0].bbb": "test2", + "traefik.meta.aaa[0].ccc": "test3", + "traefik.meta.aaa[1].bbb": "test4", + "traefik.meta.aaa[1].ccc": "test5", + "traefik.meta.aaa[2].bbb": "test6", + "traefik.meta.aaa[2].ccc": "test7", + }, + expected: &Tomato{ + Name: "test1", + Meta: map[string]interface{}{ + "aaa": []interface{}{ + map[string]interface{}{ + "bbb": "test2", + "ccc": "test3", + }, + map[string]interface{}{ + "bbb": "test4", + "ccc": "test5", + }, + map[string]interface{}{ + "bbb": "test6", + "ccc": "test7", + }, + }, + }, + }, + }, + { + desc: "explicit map of map, level 1", + elt: &Potato{}, + labels: map[string]string{ + "traefik.name": "test", + "traefik.meta.aaa.bbb": "test1", + }, + expected: &Potato{ + Name: "test", + Meta: map[string]map[string]interface{}{ + "aaa": { + "bbb": "test1", + }, + }, + }, + }, + { + desc: "explicit map of map, level 2", + elt: &Potato{}, + labels: map[string]string{ + "traefik.name": "test", + "traefik.meta.aaa.bbb": "test1", + "traefik.meta.aaa.ccc": "test2", + }, + expected: &Potato{ + Name: "test", + Meta: map[string]map[string]interface{}{ + "aaa": { + "bbb": "test1", + "ccc": "test2", + }, + }, + }, + }, + { + desc: "explicit map of map, level 3", + elt: &Potato{}, + labels: map[string]string{ + "traefik.name": "test", + "traefik.meta.aaa.bbb.ccc": "test1", + "traefik.meta.aaa.bbb.ddd": "test2", + "traefik.meta.aaa.eee": "test3", + }, + expected: &Potato{ + Name: "test", + Meta: map[string]map[string]interface{}{ + "aaa": { + "bbb": map[string]interface{}{ + "ccc": "test1", + "ddd": "test2", + }, + "eee": "test3", + }, + }, + }, + }, + { + desc: "explicit map of map, level 4", + elt: &Potato{}, + labels: map[string]string{ + "traefik.name": "test", + "traefik.meta.aaa.bbb.ccc.ddd": "test1", + "traefik.meta.aaa.bbb.ccc.eee": "test2", + "traefik.meta.aaa.bbb.fff": "test3", + "traefik.meta.aaa.ggg": "test4", + }, + expected: &Potato{ + Name: "test", + Meta: map[string]map[string]interface{}{ + "aaa": { + "bbb": map[string]interface{}{ + "ccc": map[string]interface{}{ + "ddd": "test1", + "eee": "test2", + }, + "fff": "test3", + }, + "ggg": "test4", + }, + }, + }, + }, + { + desc: "explicit map of map, struct slice, level 1, one entry", + elt: &Potato{}, + labels: map[string]string{ + "traefik.name": "test1", + "traefik.meta.aaa.bbb[0].ccc": "test2", + "traefik.meta.aaa.bbb[0].ddd": "test3", + }, + expected: &Potato{ + Name: "test1", + Meta: map[string]map[string]interface{}{ + "aaa": { + "bbb": []interface{}{ + map[string]interface{}{ + "ccc": "test2", + "ddd": "test3", + }, + }, + }, + }, + }, + }, + { + desc: "explicit map of map, struct slice, level 1, multiple entries", + elt: &Potato{}, + labels: map[string]string{ + "traefik.name": "test1", + "traefik.meta.aaa.bbb[0].ccc": "test2", + "traefik.meta.aaa.bbb[0].ddd": "test3", + "traefik.meta.aaa.bbb[1].ccc": "test4", + "traefik.meta.aaa.bbb[1].ddd": "test5", + "traefik.meta.aaa.bbb[2].ccc": "test6", + "traefik.meta.aaa.bbb[2].ddd": "test7", + }, + expected: &Potato{ + Name: "test1", + Meta: map[string]map[string]interface{}{ + "aaa": { + "bbb": []interface{}{ + map[string]interface{}{ + "ccc": "test2", + "ddd": "test3", + }, + map[string]interface{}{ + "ccc": "test4", + "ddd": "test5", + }, + map[string]interface{}{ + "ccc": "test6", + "ddd": "test7", + }, + }, + }, + }, + }, + }, + { + desc: "explicit map of map, struct slice, level 2, one entry", + elt: &Potato{}, + labels: map[string]string{ + "traefik.name": "test1", + "traefik.meta.aaa.bbb.ccc[0].ddd": "test2", + "traefik.meta.aaa.bbb.ccc[0].eee": "test3", + }, + expected: &Potato{ + Name: "test1", + Meta: map[string]map[string]interface{}{ + "aaa": { + "bbb": map[string]interface{}{ + "ccc": []interface{}{ + map[string]interface{}{ + "ddd": "test2", + "eee": "test3", + }, + }, + }, + }, + }, + }, + }, + { + desc: "explicit map of map, struct slice, level 2, multiple entries", + elt: &Potato{}, + labels: map[string]string{ + "traefik.name": "test1", + "traefik.meta.aaa.bbb.ccc[0].ddd": "test2", + "traefik.meta.aaa.bbb.ccc[0].eee": "test3", + "traefik.meta.aaa.bbb.ccc[1].ddd": "test4", + "traefik.meta.aaa.bbb.ccc[1].eee": "test5", + "traefik.meta.aaa.bbb.ccc[2].ddd": "test6", + "traefik.meta.aaa.bbb.ccc[2].eee": "test7", + }, + expected: &Potato{ + Name: "test1", + Meta: map[string]map[string]interface{}{ + "aaa": { + "bbb": map[string]interface{}{ + "ccc": []interface{}{ + map[string]interface{}{ + "ddd": "test2", + "eee": "test3", + }, + map[string]interface{}{ + "ddd": "test4", + "eee": "test5", + }, + map[string]interface{}{ + "ddd": "test6", + "eee": "test7", + }, + }, + }, + }, + }, + }, + }, + } + + for _, test := range testCases { + if test.desc != "level 3" { + continue + } + + test := test + t.Run(test.desc, func(t *testing.T) { + err := Decode(test.labels, test.elt, "traefik") + require.NoError(t, err) + + assert.Equal(t, test.expected, test.elt) + }) + } +}