Skip to content

feat: add WithDefault variants to groupBy template functions #607

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -382,9 +382,11 @@ For example, this is a JSON version of an emitted RuntimeContainer struct:
- _`exists $path`_: Returns `true` if `$path` refers to an existing file or directory. Takes a string.
- _`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`.
- _`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.
- _`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.
- _`groupByKeys $containers $fieldPath`_: Returns the same as `groupBy` but only returns the keys of the map.
- _`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.
- _`groupByLabel $containers $label`_: Returns the same as `groupBy` but grouping by the given label's value.
- _`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.
- _`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.
- _`include $file`_: Returns content of `$file`, and empty string if file reading error.
- _`intersect $slice1 $slice2`_: Returns the strings that exist in both string slices.
- _`json $value`_: Returns the JSON representation of `$value` as a `string`.
Expand Down
30 changes: 30 additions & 0 deletions internal/template/groupby.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,20 @@ func groupBy(entries interface{}, key string) (map[string][]interface{}, error)
})
}

// groupByWithDefault is the same as groupBy but allows a default value to be set
func groupByWithDefault(entries interface{}, key string, defaultValue string) (map[string][]interface{}, error) {
getValueWithDefault := func(v interface{}) (interface{}, error) {
value := deepGet(v, key)
if value == nil {
return defaultValue, nil
}
return value, nil
}
return generalizedGroupBy("groupByWithDefault", entries, getValueWithDefault, func(groups map[string][]interface{}, value interface{}, v interface{}) {
groups[value.(string)] = append(groups[value.(string)], v)
})
}

// groupByKeys is the same as groupBy but only returns a list of keys
func groupByKeys(entries interface{}, key string) ([]string, error) {
keys, err := generalizedGroupByKey("groupByKeys", entries, key, func(groups map[string][]interface{}, value interface{}, v interface{}) {
Expand Down Expand Up @@ -84,3 +98,19 @@ func groupByLabel(entries interface{}, label string) (map[string][]interface{},
groups[value.(string)] = append(groups[value.(string)], v)
})
}

// groupByLabelWithDefault is the same as groupByLabel but allows a default value to be set
func groupByLabelWithDefault(entries interface{}, label string, defaultValue string) (map[string][]interface{}, error) {
getLabel := func(v interface{}) (interface{}, error) {
if container, ok := v.(*context.RuntimeContainer); ok {
if value, ok := container.Labels[label]; ok {
return value, nil
}
return defaultValue, nil
}
return nil, fmt.Errorf("must pass an array or slice of *RuntimeContainer to 'groupByLabel'; received %v", v)
}
return generalizedGroupBy("groupByLabelWithDefault", entries, getLabel, func(groups map[string][]interface{}, value interface{}, v interface{}) {
groups[value.(string)] = append(groups[value.(string)], v)
})
}
193 changes: 92 additions & 101 deletions internal/template/groupby_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,34 @@ import (
"github.com/stretchr/testify/assert"
)

func TestGroupByExistingKey(t *testing.T) {
containers := []*context.RuntimeContainer{
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
},
ID: "1",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
},
ID: "2",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost",
},
ID: "3",
},
}
var groupByContainers = []*context.RuntimeContainer{
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
"EXTERNAL": "true",
},
ID: "1",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
},
ID: "2",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost",
"EXTERNAL": "true",
},
ID: "3",
},
{
ID: "4",
},
}

groups, err := groupBy(containers, "Env.VIRTUAL_HOST")
func TestGroupByExistingKey(t *testing.T) {
groups, err := groupBy(groupByContainers, "Env.VIRTUAL_HOST")

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

func TestGroupByAfterWhere(t *testing.T) {
containers := []*context.RuntimeContainer{
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
"EXTERNAL": "true",
},
ID: "1",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
},
ID: "2",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost",
"EXTERNAL": "true",
},
ID: "3",
},
}

filtered, _ := where(containers, "Env.EXTERNAL", "true")
filtered, _ := where(groupByContainers, "Env.EXTERNAL", "true")
groups, err := groupBy(filtered, "Env.VIRTUAL_HOST")

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

func TestGroupByKeys(t *testing.T) {
containers := []*context.RuntimeContainer{
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
},
ID: "1",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
},
ID: "2",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost",
},
ID: "3",
},
}
func TestGroupByWithDefault(t *testing.T) {
groups, err := groupByWithDefault(groupByContainers, "Env.VIRTUAL_HOST", "default.localhost")

assert.NoError(t, err)
assert.Len(t, groups, 3)
assert.Len(t, groups["demo1.localhost"], 2)
assert.Len(t, groups["demo2.localhost"], 1)
assert.Len(t, groups["default.localhost"], 1)
assert.Equal(t, "4", groups["default.localhost"][0].(*context.RuntimeContainer).ID)
}

func TestGroupByKeys(t *testing.T) {
expected := []string{"demo1.localhost", "demo2.localhost"}
groups, err := groupByKeys(containers, "Env.VIRTUAL_HOST")
groups, err := groupByKeys(groupByContainers, "Env.VIRTUAL_HOST")
assert.NoError(t, err)
assert.ElementsMatch(t, expected, groups)

expected = []string{"1", "2", "3"}
groups, err = groupByKeys(containers, "ID")
expected = []string{"1", "2", "3", "4"}
groups, err = groupByKeys(groupByContainers, "ID")
assert.NoError(t, err)
assert.ElementsMatch(t, expected, groups)
}
Expand All @@ -111,38 +83,38 @@ func TestGeneralizedGroupByError(t *testing.T) {
assert.Nil(t, groups)
}

func TestGroupByLabel(t *testing.T) {
containers := []*context.RuntimeContainer{
{
Labels: map[string]string{
"com.docker.compose.project": "one",
},
ID: "1",
},
{
Labels: map[string]string{
"com.docker.compose.project": "two",
},
ID: "2",
},
{
Labels: map[string]string{
"com.docker.compose.project": "one",
},
ID: "3",
},
{
ID: "4",
},
{
Labels: map[string]string{
"com.docker.compose.project": "",
},
ID: "5",
},
}
var groupByLabelContainers = []*context.RuntimeContainer{
{
Labels: map[string]string{
"com.docker.compose.project": "one",
},
ID: "1",
},
{
Labels: map[string]string{
"com.docker.compose.project": "two",
},
ID: "2",
},
{
Labels: map[string]string{
"com.docker.compose.project": "one",
},
ID: "3",
},
{
ID: "4",
},
{
Labels: map[string]string{
"com.docker.compose.project": "",
},
ID: "5",
},
}

groups, err := groupByLabel(containers, "com.docker.compose.project")
func TestGroupByLabel(t *testing.T) {
groups, err := groupByLabel(groupByLabelContainers, "com.docker.compose.project")

assert.NoError(t, err)
assert.Len(t, groups, 3)
Expand All @@ -159,6 +131,25 @@ func TestGroupByLabelError(t *testing.T) {
assert.Nil(t, groups)
}

func TestGroupByLabelWithDefault(t *testing.T) {
groups, err := groupByLabelWithDefault(groupByLabelContainers, "com.docker.compose.project", "default")

assert.NoError(t, err)
assert.Len(t, groups, 4)
assert.Len(t, groups["one"], 2)
assert.Len(t, groups["two"], 1)
assert.Len(t, groups[""], 1)
assert.Len(t, groups["default"], 1)
assert.Equal(t, "4", groups["default"][0].(*context.RuntimeContainer).ID)
}

func TestGroupByLabelWithDefaultError(t *testing.T) {
strings := []string{"foo", "bar", "baz"}
groups, err := groupByLabelWithDefault(strings, "", "")
assert.Error(t, err)
assert.Nil(t, groups)
}

func TestGroupByMulti(t *testing.T) {
containers := []*context.RuntimeContainer{
{
Expand Down
88 changes: 45 additions & 43 deletions internal/template/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,49 +58,51 @@ func newTemplate(name string) *template.Template {
return buf.String(), nil
}
tmpl.Funcs(sprig.TxtFuncMap()).Funcs(template.FuncMap{
"closest": arrayClosest,
"coalesce": coalesce,
"contains": contains,
"dir": dirList,
"eval": eval,
"exists": utils.PathExists,
"groupBy": groupBy,
"groupByKeys": groupByKeys,
"groupByMulti": groupByMulti,
"groupByLabel": groupByLabel,
"json": marshalJson,
"include": include,
"intersect": intersect,
"keys": keys,
"replace": strings.Replace,
"parseBool": strconv.ParseBool,
"parseJson": unmarshalJson,
"fromYaml": fromYaml,
"toYaml": toYaml,
"mustFromYaml": mustFromYaml,
"mustToYaml": mustToYaml,
"queryEscape": url.QueryEscape,
"sha1": hashSha1,
"split": strings.Split,
"splitN": strings.SplitN,
"sortStringsAsc": sortStringsAsc,
"sortStringsDesc": sortStringsDesc,
"sortObjectsByKeysAsc": sortObjectsByKeysAsc,
"sortObjectsByKeysDesc": sortObjectsByKeysDesc,
"trimPrefix": trimPrefix,
"trimSuffix": trimSuffix,
"toLower": toLower,
"toUpper": toUpper,
"when": when,
"where": where,
"whereNot": whereNot,
"whereExist": whereExist,
"whereNotExist": whereNotExist,
"whereAny": whereAny,
"whereAll": whereAll,
"whereLabelExists": whereLabelExists,
"whereLabelDoesNotExist": whereLabelDoesNotExist,
"whereLabelValueMatches": whereLabelValueMatches,
"closest": arrayClosest,
"coalesce": coalesce,
"contains": contains,
"dir": dirList,
"eval": eval,
"exists": utils.PathExists,
"groupBy": groupBy,
"groupByWithDefault": groupByWithDefault,
"groupByKeys": groupByKeys,
"groupByMulti": groupByMulti,
"groupByLabel": groupByLabel,
"groupByLabelWithDefault": groupByLabelWithDefault,
"json": marshalJson,
"include": include,
"intersect": intersect,
"keys": keys,
"replace": strings.Replace,
"parseBool": strconv.ParseBool,
"parseJson": unmarshalJson,
"fromYaml": fromYaml,
"toYaml": toYaml,
"mustFromYaml": mustFromYaml,
"mustToYaml": mustToYaml,
"queryEscape": url.QueryEscape,
"sha1": hashSha1,
"split": strings.Split,
"splitN": strings.SplitN,
"sortStringsAsc": sortStringsAsc,
"sortStringsDesc": sortStringsDesc,
"sortObjectsByKeysAsc": sortObjectsByKeysAsc,
"sortObjectsByKeysDesc": sortObjectsByKeysDesc,
"trimPrefix": trimPrefix,
"trimSuffix": trimSuffix,
"toLower": toLower,
"toUpper": toUpper,
"when": when,
"where": where,
"whereNot": whereNot,
"whereExist": whereExist,
"whereNotExist": whereNotExist,
"whereAny": whereAny,
"whereAll": whereAll,
"whereLabelExists": whereLabelExists,
"whereLabelDoesNotExist": whereLabelDoesNotExist,
"whereLabelValueMatches": whereLabelValueMatches,
})
return tmpl
}
Expand Down