diff --git a/README.md b/README.md index d1e23cfb..c8adcf4a 100644 --- a/README.md +++ b/README.md @@ -356,6 +356,7 @@ For example, this is a JSON version of an emitted RuntimeContainer struct: * *`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. * *`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. +* *`groupByMultiKeyValuePairs $containers $fieldPath $listSep $kvpSep [$defaultKey]`*: Like `groupByMulti`, but the string value specified by `$fieldPath` is split by `splitKeyValuePairs` into a list of key value pairs. The container grouping is done based on the keys.A container will show up in the map output under each of the keys. * *`groupByLabel $containers $label`*: Returns the same as `groupBy` but grouping by the given label's value. * *`hasPrefix $prefix $string`*: Returns whether `$prefix` is a prefix of `$string`. * *`hasSuffix $suffix $string`*: Returns whether `$suffix` is a suffix of `$string`. @@ -368,6 +369,8 @@ For example, this is a JSON version of an emitted RuntimeContainer struct: * *`sha1 $string`*: Returns the hexadecimal representation of the SHA1 hash of `$string`. * *`split $string $sep`*: Splits `$string` into a slice of substrings delimited by `$sep`. Alias for [`strings.Split`](http://golang.org/pkg/strings/#Split) * *`splitN $string $sep $count`*: Splits `$string` into a slice of substrings delimited by `$sep`, with number of substrings returned determined by `$count`. Alias for [`strings.SplitN`](https://golang.org/pkg/strings/#SplitN) +* *`splitKeyValuePairs $string $listSep $kvpSep [$defaultKey]`*: Splits `$string` into a slice of substrings delimited by `$listSep`, each substring is then splitted by `$kvpSep`, the result is a map of key value pairs. `$defaultKey` is used for substrings which do not contain `$kvpSep` and therfore the substring cannot be splitted into a key value pair. +E.g `$string` = `key1=value1,value2`, first the string is splitted by e.g `$listSep` = `,`, which results in two strings `key1=value1` and `value2`. In a next step each string is splitted by e.g. `$kvpSep`= `=`: first string is splitted into `key1` and `value1`. The second string does not contain the `$kvpSep` = `=`: If `$defaultKey` is omitted or empty the string is splitted into `value1` as key and `value1` as value. If `$defaultKey` is set the string is splitted into value of`$defaultKey` as key and `value1`. * *`trimPrefix $prefix $string`*: If `$prefix` is a prefix of `$string`, return `$string` with `$prefix` trimmed from the beginning. Otherwise, return `$string` unchanged. * *`trimSuffix $suffix $string`*: If `$suffix` is a suffix of `$string`, return `$string` with `$suffix` trimmed from the end. Otherwise, return `$string` unchanged. * *`trim $string`*: Removes whitespace from both sides of `$string`. diff --git a/template.go b/template.go index 793396d5..34e74fa2 100644 --- a/template.go +++ b/template.go @@ -79,6 +79,49 @@ func generalizedGroupByKey(funcName string, entries interface{}, key string, add return generalizedGroupBy(funcName, entries, getKey, addEntry) } +// splitKeyValuePairs splits a input string into a map of key value pairs, first string is split by listSep into list items, then each list item is split by kvpSep into key value pair +// if a list item does not contai the kvpSep a defaultKey can be provided, where these values are grouped, or if omitted these values are used as key and value +func splitKeyValuePairs(input string, listSep string, kvpSep string, defaultKey ...string) map[string]string { + keyValuePairs := strings.Split(input, listSep) + + output := map[string]string{} + for _, kvp := range keyValuePairs { + var key string + var value string + if strings.Contains(kvp, kvpSep) { + splitted := strings.Split(kvp, kvpSep) + key = splitted[0] + value = splitted[1] + } else if len(defaultKey) == 0 || defaultKey[0] == "" { + // no key found, no default key specified + key = kvp + value = kvp + } else { + // no key found, use default key specified instead + key = defaultKey[0] + value = kvp + } + + output[key] = value + } + + return output +} + +// groupByMultiKeyValuePairs similar to groupByMulti, but the key value ist split into a list (delimited by listSep) of key value pairs (seperated by kvpSep: kvpSep) +// An array or slice entry will show up in the output map under all of the list key value pair keys +func groupByMultiKeyValuePairs(entries interface{}, key, listSep string, kvpSep string, defaultKey string) (map[string][]interface{}, error) { + return generalizedGroupByKey("groupByMultiKeyValuePairs", entries, key, func(groups map[string][]interface{}, value interface{}, v interface{}) { + + keyValuePairs := splitKeyValuePairs(value.(string), listSep, kvpSep, defaultKey) + for key := range keyValuePairs { + groups[key] = append(groups[key], v) + } + }) +} + +// groupByMulti groups a generic array or slice by the path property keys value, where the path value is first split by sep into a list of key strings. +// An array or slice entry will show up in the output map under all of the list keys func groupByMulti(entries interface{}, key, sep string) (map[string][]interface{}, error) { return generalizedGroupByKey("groupByMulti", entries, key, func(groups map[string][]interface{}, value interface{}, v interface{}) { items := strings.Split(value.(string), sep) @@ -420,43 +463,45 @@ func when(condition bool, trueValue, falseValue interface{}) interface{} { func newTemplate(name string) *template.Template { tmpl := template.New(name).Funcs(template.FuncMap{ - "closest": arrayClosest, - "coalesce": coalesce, - "contains": contains, - "dict": dict, - "dir": dirList, - "exists": exists, - "first": arrayFirst, - "groupBy": groupBy, - "groupByKeys": groupByKeys, - "groupByMulti": groupByMulti, - "groupByLabel": groupByLabel, - "hasPrefix": hasPrefix, - "hasSuffix": hasSuffix, - "json": marshalJson, - "intersect": intersect, - "keys": keys, - "last": arrayLast, - "replace": strings.Replace, - "parseBool": strconv.ParseBool, - "parseJson": unmarshalJson, - "queryEscape": url.QueryEscape, - "sha1": hashSha1, - "split": strings.Split, - "splitN": strings.SplitN, - "trimPrefix": trimPrefix, - "trimSuffix": trimSuffix, - "trim": trim, - "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, + "dict": dict, + "dir": dirList, + "exists": exists, + "first": arrayFirst, + "groupBy": groupBy, + "groupByKeys": groupByKeys, + "groupByMulti": groupByMulti, + "groupByMultiKeyValuePairs": groupByMultiKeyValuePairs, + "groupByLabel": groupByLabel, + "hasPrefix": hasPrefix, + "hasSuffix": hasSuffix, + "json": marshalJson, + "intersect": intersect, + "keys": keys, + "last": arrayLast, + "replace": strings.Replace, + "parseBool": strconv.ParseBool, + "parseJson": unmarshalJson, + "queryEscape": url.QueryEscape, + "sha1": hashSha1, + "split": strings.Split, + "splitN": strings.SplitN, + "splitKeyValuePairs": splitKeyValuePairs, + "trimPrefix": trimPrefix, + "trimSuffix": trimSuffix, + "trim": trim, + "when": when, + "where": where, + "whereNot": whereNot, + "whereExist": whereExist, + "whereNotExist": whereNotExist, + "whereAny": whereAny, + "whereAll": whereAll, + "whereLabelExists": whereLabelExists, + "whereLabelDoesNotExist": whereLabelDoesNotExist, + "whereLabelValueMatches": whereLabelValueMatches, }) return tmpl } diff --git a/template_test.go b/template_test.go index 71501ada..751900b1 100644 --- a/template_test.go +++ b/template_test.go @@ -286,6 +286,73 @@ func TestGroupByMulti(t *testing.T) { } } +func TestGroupByMultiKeyValuePairs(t *testing.T) { + containers := []*RuntimeContainer{ + &RuntimeContainer{ + Env: map[string]string{ + "VIRTUAL_PORT": "443:3000,3000:3000", + }, + ID: "1", + }, + &RuntimeContainer{ + Env: map[string]string{ + "VIRTUAL_PORT": "1111,250:360", + }, + ID: "2", + }, + &RuntimeContainer{ + Env: map[string]string{ + "VIRTUAL_PORT": "123", + }, + ID: "3", + }, + } + + groups, _ := groupByMultiKeyValuePairs(containers, "Env.VIRTUAL_PORT", ",", ":", "445") + if len(groups) != 4 { + t.Fatalf("expected 4 got %d", len(groups)) + } + + if len(groups["445"]) != 2 { + t.Fatalf("expected 2 got %d", len(groups["445"])) + } + + if len(groups["443"]) != 1 { + t.Fatalf("expected 1 got %d", len(groups["443"])) + } + if groups["445"][1].(RuntimeContainer).ID != "3" { + t.Fatalf("expected 1 got %s", groups["445"][0].(RuntimeContainer).ID) + } + if len(groups["3000"]) != 1 { + t.Fatalf("expect 1 got %d", len(groups["3000"])) + } + if groups["250"][0].(RuntimeContainer).ID != "2" { + t.Fatalf("expected 1 got %s", groups["250"][0].(RuntimeContainer).ID) + } +} + +func TestSplitKeyValuePairs1(t *testing.T) { + list := splitKeyValuePairs("key=value,1=2,test", ",", "=") + + if len(list) != 3 { + t.Fatalf("expected 3 got %d", len(list)) + } + if list["test"] != "test" { + t.Fatalf("expected value 'test' got '%s'", list["test"]) + } +} + +func TestSplitKeyValuePairs2(t *testing.T) { + list := splitKeyValuePairs("key:value/1:2/test", "/", ":") + + if len(list) != 3 { + t.Fatalf("expected 3 got %d", len(list)) + } + if list["test"] != "test" { + t.Fatalf("expected value 'test' got '%s'", list["test"]) + } +} + func TestWhere(t *testing.T) { containers := []*RuntimeContainer{ &RuntimeContainer{