Skip to content

Commit 8afa67b

Browse files
authored
Merge pull request #607 from nginx-proxy/group-by-with-default
feat: add WithDefault variants to groupBy template functions
2 parents cc8ff2a + e46faee commit 8afa67b

File tree

4 files changed

+170
-145
lines changed

4 files changed

+170
-145
lines changed

README.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -382,9 +382,11 @@ For example, this is a JSON version of an emitted RuntimeContainer struct:
382382
- _`exists $path`_: Returns `true` if `$path` refers to an existing file or directory. Takes a string.
383383
- _`eval $templateName [$data]`_: Evaluates the named template like Go's built-in `template` action, but instead of writing out the result it returns the result as a string so that it can be post-processed. The `$data` argument may be omitted, which is equivalent to passing `nil`.
384384
- _`groupBy $containers $fieldPath`_: Groups an array of `RuntimeContainer` instances based on the values of a field path expression `$fieldPath`. A field path expression is a dot-delimited list of map keys or struct member names specifying the path from container to a nested value, which must be a string. Returns a map from the value of the field path expression to an array of containers having that value. Containers that do not have a value for the field path in question are omitted.
385+
- _`groupByWithDefault $containers $fieldPath $defaultValue`_: Returns the same as `groupBy`, but containers that do not have a value for the field path are instead included in the map under the `$defaultValue` key.
385386
- _`groupByKeys $containers $fieldPath`_: Returns the same as `groupBy` but only returns the keys of the map.
386387
- _`groupByMulti $containers $fieldPath $sep`_: Like `groupBy`, but the string value specified by `$fieldPath` is first split by `$sep` into a list of strings. A container whose `$fieldPath` value contains a list of strings will show up in the map output under each of those strings.
387-
- _`groupByLabel $containers $label`_: Returns the same as `groupBy` but grouping by the given label's value.
388+
- _`groupByLabel $containers $label`_: Returns the same as `groupBy` but grouping by the given label's value. Containers that do not have the `$label` set are omitted.
389+
- _`groupByLabelWithDefault $containers $label $defaultValue`_: Returns the same as `groupBy` but grouping by the given label's value. Containers that do not have the `$label` set are included in the map under the `$defaultValue` key.
388390
- _`include $file`_: Returns content of `$file`, and empty string if file reading error.
389391
- _`intersect $slice1 $slice2`_: Returns the strings that exist in both string slices.
390392
- _`json $value`_: Returns the JSON representation of `$value` as a `string`.

internal/template/groupby.go

+30
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,20 @@ func groupBy(entries interface{}, key string) (map[string][]interface{}, error)
5252
})
5353
}
5454

55+
// groupByWithDefault is the same as groupBy but allows a default value to be set
56+
func groupByWithDefault(entries interface{}, key string, defaultValue string) (map[string][]interface{}, error) {
57+
getValueWithDefault := func(v interface{}) (interface{}, error) {
58+
value := deepGet(v, key)
59+
if value == nil {
60+
return defaultValue, nil
61+
}
62+
return value, nil
63+
}
64+
return generalizedGroupBy("groupByWithDefault", entries, getValueWithDefault, func(groups map[string][]interface{}, value interface{}, v interface{}) {
65+
groups[value.(string)] = append(groups[value.(string)], v)
66+
})
67+
}
68+
5569
// groupByKeys is the same as groupBy but only returns a list of keys
5670
func groupByKeys(entries interface{}, key string) ([]string, error) {
5771
keys, err := generalizedGroupByKey("groupByKeys", entries, key, func(groups map[string][]interface{}, value interface{}, v interface{}) {
@@ -84,3 +98,19 @@ func groupByLabel(entries interface{}, label string) (map[string][]interface{},
8498
groups[value.(string)] = append(groups[value.(string)], v)
8599
})
86100
}
101+
102+
// groupByLabelWithDefault is the same as groupByLabel but allows a default value to be set
103+
func groupByLabelWithDefault(entries interface{}, label string, defaultValue string) (map[string][]interface{}, error) {
104+
getLabel := func(v interface{}) (interface{}, error) {
105+
if container, ok := v.(*context.RuntimeContainer); ok {
106+
if value, ok := container.Labels[label]; ok {
107+
return value, nil
108+
}
109+
return defaultValue, nil
110+
}
111+
return nil, fmt.Errorf("must pass an array or slice of *RuntimeContainer to 'groupByLabel'; received %v", v)
112+
}
113+
return generalizedGroupBy("groupByLabelWithDefault", entries, getLabel, func(groups map[string][]interface{}, value interface{}, v interface{}) {
114+
groups[value.(string)] = append(groups[value.(string)], v)
115+
})
116+
}

internal/template/groupby_test.go

+92-101
Original file line numberDiff line numberDiff line change
@@ -7,29 +7,34 @@ import (
77
"github.com/stretchr/testify/assert"
88
)
99

10-
func TestGroupByExistingKey(t *testing.T) {
11-
containers := []*context.RuntimeContainer{
12-
{
13-
Env: map[string]string{
14-
"VIRTUAL_HOST": "demo1.localhost",
15-
},
16-
ID: "1",
17-
},
18-
{
19-
Env: map[string]string{
20-
"VIRTUAL_HOST": "demo1.localhost",
21-
},
22-
ID: "2",
23-
},
24-
{
25-
Env: map[string]string{
26-
"VIRTUAL_HOST": "demo2.localhost",
27-
},
28-
ID: "3",
29-
},
30-
}
10+
var groupByContainers = []*context.RuntimeContainer{
11+
{
12+
Env: map[string]string{
13+
"VIRTUAL_HOST": "demo1.localhost",
14+
"EXTERNAL": "true",
15+
},
16+
ID: "1",
17+
},
18+
{
19+
Env: map[string]string{
20+
"VIRTUAL_HOST": "demo1.localhost",
21+
},
22+
ID: "2",
23+
},
24+
{
25+
Env: map[string]string{
26+
"VIRTUAL_HOST": "demo2.localhost",
27+
"EXTERNAL": "true",
28+
},
29+
ID: "3",
30+
},
31+
{
32+
ID: "4",
33+
},
34+
}
3135

32-
groups, err := groupBy(containers, "Env.VIRTUAL_HOST")
36+
func TestGroupByExistingKey(t *testing.T) {
37+
groups, err := groupBy(groupByContainers, "Env.VIRTUAL_HOST")
3338

3439
assert.NoError(t, err)
3540
assert.Len(t, groups, 2)
@@ -39,30 +44,7 @@ func TestGroupByExistingKey(t *testing.T) {
3944
}
4045

4146
func TestGroupByAfterWhere(t *testing.T) {
42-
containers := []*context.RuntimeContainer{
43-
{
44-
Env: map[string]string{
45-
"VIRTUAL_HOST": "demo1.localhost",
46-
"EXTERNAL": "true",
47-
},
48-
ID: "1",
49-
},
50-
{
51-
Env: map[string]string{
52-
"VIRTUAL_HOST": "demo1.localhost",
53-
},
54-
ID: "2",
55-
},
56-
{
57-
Env: map[string]string{
58-
"VIRTUAL_HOST": "demo2.localhost",
59-
"EXTERNAL": "true",
60-
},
61-
ID: "3",
62-
},
63-
}
64-
65-
filtered, _ := where(containers, "Env.EXTERNAL", "true")
47+
filtered, _ := where(groupByContainers, "Env.EXTERNAL", "true")
6648
groups, err := groupBy(filtered, "Env.VIRTUAL_HOST")
6749

6850
assert.NoError(t, err)
@@ -72,35 +54,25 @@ func TestGroupByAfterWhere(t *testing.T) {
7254
assert.Equal(t, "3", groups["demo2.localhost"][0].(*context.RuntimeContainer).ID)
7355
}
7456

75-
func TestGroupByKeys(t *testing.T) {
76-
containers := []*context.RuntimeContainer{
77-
{
78-
Env: map[string]string{
79-
"VIRTUAL_HOST": "demo1.localhost",
80-
},
81-
ID: "1",
82-
},
83-
{
84-
Env: map[string]string{
85-
"VIRTUAL_HOST": "demo1.localhost",
86-
},
87-
ID: "2",
88-
},
89-
{
90-
Env: map[string]string{
91-
"VIRTUAL_HOST": "demo2.localhost",
92-
},
93-
ID: "3",
94-
},
95-
}
57+
func TestGroupByWithDefault(t *testing.T) {
58+
groups, err := groupByWithDefault(groupByContainers, "Env.VIRTUAL_HOST", "default.localhost")
9659

60+
assert.NoError(t, err)
61+
assert.Len(t, groups, 3)
62+
assert.Len(t, groups["demo1.localhost"], 2)
63+
assert.Len(t, groups["demo2.localhost"], 1)
64+
assert.Len(t, groups["default.localhost"], 1)
65+
assert.Equal(t, "4", groups["default.localhost"][0].(*context.RuntimeContainer).ID)
66+
}
67+
68+
func TestGroupByKeys(t *testing.T) {
9769
expected := []string{"demo1.localhost", "demo2.localhost"}
98-
groups, err := groupByKeys(containers, "Env.VIRTUAL_HOST")
70+
groups, err := groupByKeys(groupByContainers, "Env.VIRTUAL_HOST")
9971
assert.NoError(t, err)
10072
assert.ElementsMatch(t, expected, groups)
10173

102-
expected = []string{"1", "2", "3"}
103-
groups, err = groupByKeys(containers, "ID")
74+
expected = []string{"1", "2", "3", "4"}
75+
groups, err = groupByKeys(groupByContainers, "ID")
10476
assert.NoError(t, err)
10577
assert.ElementsMatch(t, expected, groups)
10678
}
@@ -111,38 +83,38 @@ func TestGeneralizedGroupByError(t *testing.T) {
11183
assert.Nil(t, groups)
11284
}
11385

114-
func TestGroupByLabel(t *testing.T) {
115-
containers := []*context.RuntimeContainer{
116-
{
117-
Labels: map[string]string{
118-
"com.docker.compose.project": "one",
119-
},
120-
ID: "1",
121-
},
122-
{
123-
Labels: map[string]string{
124-
"com.docker.compose.project": "two",
125-
},
126-
ID: "2",
127-
},
128-
{
129-
Labels: map[string]string{
130-
"com.docker.compose.project": "one",
131-
},
132-
ID: "3",
133-
},
134-
{
135-
ID: "4",
136-
},
137-
{
138-
Labels: map[string]string{
139-
"com.docker.compose.project": "",
140-
},
141-
ID: "5",
142-
},
143-
}
86+
var groupByLabelContainers = []*context.RuntimeContainer{
87+
{
88+
Labels: map[string]string{
89+
"com.docker.compose.project": "one",
90+
},
91+
ID: "1",
92+
},
93+
{
94+
Labels: map[string]string{
95+
"com.docker.compose.project": "two",
96+
},
97+
ID: "2",
98+
},
99+
{
100+
Labels: map[string]string{
101+
"com.docker.compose.project": "one",
102+
},
103+
ID: "3",
104+
},
105+
{
106+
ID: "4",
107+
},
108+
{
109+
Labels: map[string]string{
110+
"com.docker.compose.project": "",
111+
},
112+
ID: "5",
113+
},
114+
}
144115

145-
groups, err := groupByLabel(containers, "com.docker.compose.project")
116+
func TestGroupByLabel(t *testing.T) {
117+
groups, err := groupByLabel(groupByLabelContainers, "com.docker.compose.project")
146118

147119
assert.NoError(t, err)
148120
assert.Len(t, groups, 3)
@@ -159,6 +131,25 @@ func TestGroupByLabelError(t *testing.T) {
159131
assert.Nil(t, groups)
160132
}
161133

134+
func TestGroupByLabelWithDefault(t *testing.T) {
135+
groups, err := groupByLabelWithDefault(groupByLabelContainers, "com.docker.compose.project", "default")
136+
137+
assert.NoError(t, err)
138+
assert.Len(t, groups, 4)
139+
assert.Len(t, groups["one"], 2)
140+
assert.Len(t, groups["two"], 1)
141+
assert.Len(t, groups[""], 1)
142+
assert.Len(t, groups["default"], 1)
143+
assert.Equal(t, "4", groups["default"][0].(*context.RuntimeContainer).ID)
144+
}
145+
146+
func TestGroupByLabelWithDefaultError(t *testing.T) {
147+
strings := []string{"foo", "bar", "baz"}
148+
groups, err := groupByLabelWithDefault(strings, "", "")
149+
assert.Error(t, err)
150+
assert.Nil(t, groups)
151+
}
152+
162153
func TestGroupByMulti(t *testing.T) {
163154
containers := []*context.RuntimeContainer{
164155
{

internal/template/template.go

+45-43
Original file line numberDiff line numberDiff line change
@@ -58,49 +58,51 @@ func newTemplate(name string) *template.Template {
5858
return buf.String(), nil
5959
}
6060
tmpl.Funcs(sprig.TxtFuncMap()).Funcs(template.FuncMap{
61-
"closest": arrayClosest,
62-
"coalesce": coalesce,
63-
"contains": contains,
64-
"dir": dirList,
65-
"eval": eval,
66-
"exists": utils.PathExists,
67-
"groupBy": groupBy,
68-
"groupByKeys": groupByKeys,
69-
"groupByMulti": groupByMulti,
70-
"groupByLabel": groupByLabel,
71-
"json": marshalJson,
72-
"include": include,
73-
"intersect": intersect,
74-
"keys": keys,
75-
"replace": strings.Replace,
76-
"parseBool": strconv.ParseBool,
77-
"parseJson": unmarshalJson,
78-
"fromYaml": fromYaml,
79-
"toYaml": toYaml,
80-
"mustFromYaml": mustFromYaml,
81-
"mustToYaml": mustToYaml,
82-
"queryEscape": url.QueryEscape,
83-
"sha1": hashSha1,
84-
"split": strings.Split,
85-
"splitN": strings.SplitN,
86-
"sortStringsAsc": sortStringsAsc,
87-
"sortStringsDesc": sortStringsDesc,
88-
"sortObjectsByKeysAsc": sortObjectsByKeysAsc,
89-
"sortObjectsByKeysDesc": sortObjectsByKeysDesc,
90-
"trimPrefix": trimPrefix,
91-
"trimSuffix": trimSuffix,
92-
"toLower": toLower,
93-
"toUpper": toUpper,
94-
"when": when,
95-
"where": where,
96-
"whereNot": whereNot,
97-
"whereExist": whereExist,
98-
"whereNotExist": whereNotExist,
99-
"whereAny": whereAny,
100-
"whereAll": whereAll,
101-
"whereLabelExists": whereLabelExists,
102-
"whereLabelDoesNotExist": whereLabelDoesNotExist,
103-
"whereLabelValueMatches": whereLabelValueMatches,
61+
"closest": arrayClosest,
62+
"coalesce": coalesce,
63+
"contains": contains,
64+
"dir": dirList,
65+
"eval": eval,
66+
"exists": utils.PathExists,
67+
"groupBy": groupBy,
68+
"groupByWithDefault": groupByWithDefault,
69+
"groupByKeys": groupByKeys,
70+
"groupByMulti": groupByMulti,
71+
"groupByLabel": groupByLabel,
72+
"groupByLabelWithDefault": groupByLabelWithDefault,
73+
"json": marshalJson,
74+
"include": include,
75+
"intersect": intersect,
76+
"keys": keys,
77+
"replace": strings.Replace,
78+
"parseBool": strconv.ParseBool,
79+
"parseJson": unmarshalJson,
80+
"fromYaml": fromYaml,
81+
"toYaml": toYaml,
82+
"mustFromYaml": mustFromYaml,
83+
"mustToYaml": mustToYaml,
84+
"queryEscape": url.QueryEscape,
85+
"sha1": hashSha1,
86+
"split": strings.Split,
87+
"splitN": strings.SplitN,
88+
"sortStringsAsc": sortStringsAsc,
89+
"sortStringsDesc": sortStringsDesc,
90+
"sortObjectsByKeysAsc": sortObjectsByKeysAsc,
91+
"sortObjectsByKeysDesc": sortObjectsByKeysDesc,
92+
"trimPrefix": trimPrefix,
93+
"trimSuffix": trimSuffix,
94+
"toLower": toLower,
95+
"toUpper": toUpper,
96+
"when": when,
97+
"where": where,
98+
"whereNot": whereNot,
99+
"whereExist": whereExist,
100+
"whereNotExist": whereNotExist,
101+
"whereAny": whereAny,
102+
"whereAll": whereAll,
103+
"whereLabelExists": whereLabelExists,
104+
"whereLabelDoesNotExist": whereLabelDoesNotExist,
105+
"whereLabelValueMatches": whereLabelValueMatches,
104106
})
105107
return tmpl
106108
}

0 commit comments

Comments
 (0)