diff --git a/docs/notifications.md b/docs/notifications.md index 30a3dc20b9..02bafdd020 100644 --- a/docs/notifications.md +++ b/docs/notifications.md @@ -89,7 +89,7 @@ templating. | date | string, time.Time | Returns the text representation of the time in the specified format. For documentation on formats refer to [pkg.go.dev/time](https://pkg.go.dev/time#pkg-constants). | | dict | values ...any | Returns a map of string to any, constructed from the variadic list of key-value pairs. The number of arguments must be even, and the keys must be strings. | | humanizeDuration | number or string | Returns a human-readable string representing the duration, and the error if it happened. | -| join | sep string, s []string | [strings.Join](http://golang.org/pkg/strings/#Join), concatenates the elements of s to create a single string. The separator string sep is placed between elements in the resulting string. (note: argument order inverted for easier pipelining in templates.) | +| join | sep string, any | [strings.Join](http://golang.org/pkg/strings/#Join), concatenates the elements of the provided slice to create a single string. The separator string sep is placed between elements in the resulting string. (note: argument order inverted for easier pipelining in templates.) | | list | ...any | Returns the passed arguments as a slice of interfaces. | | match | pattern, string | [Regexp.MatchString](https://golang.org/pkg/regexp/#MatchString). Match a string using Regexp. | | now | | [time.Now](https://pkg.go.dev/time#Now), returns the current local time. | diff --git a/template/template.go b/template/template.go index 73f1ac526f..d0a4ab34f1 100644 --- a/template/template.go +++ b/template/template.go @@ -185,8 +185,25 @@ var DefaultFuncs = FuncMap{ "trimSpace": strings.TrimSpace, // join is equal to strings.Join but inverts the argument order // for easier pipelining in templates. - "join": func(sep string, s []string) string { - return strings.Join(s, sep) + "join": func(sep string, v any) (string, error) { + if s, ok := v.([]string); ok { + return strings.Join(s, sep), nil + } + + value := reflect.ValueOf(v) + switch { + case !value.IsValid(): + return "", nil + case value.Kind() == reflect.Slice, value.Kind() == reflect.Array: + parts := make([]string, 0, value.Len()) + for i := 0; i < value.Len(); i++ { + elem := value.Index(i).Interface() + parts = append(parts, fmt.Sprint(elem)) + } + return strings.Join(parts, sep), nil + default: + return "", fmt.Errorf("join expects a slice or array, got %T", v) + } }, "match": regexp.MatchString, "safeHtml": func(text string) tmplhtml.HTML { diff --git a/template/template_test.go b/template/template_test.go index 3935f0bec0..f734d52db1 100644 --- a/template/template_test.go +++ b/template/template_test.go @@ -352,6 +352,16 @@ func TestTemplateExpansion(t *testing.T) { data: []string{"a", "b", "c"}, exp: "a,b,c", }, + { + title: "Template using join with list", + in: `{{ list "a" "b" "c" | join "," }}`, + exp: "a,b,c", + }, + { + title: "Template using join with mixed types", + in: `{{ list 1 true "x" | join "," }}`, + exp: "1,true,x", + }, { title: "Text template without HTML escaping", in: `{{ "" }}`, @@ -621,6 +631,10 @@ func TestTemplateFuncs(t *testing.T) { in: `{{ . | join "," }}`, data: []string{"abc", "def"}, exp: "abc,def", + }, { + title: "Template using join with list", + in: `{{ list "abc" "def" | join "," }}`, + exp: "abc,def", }, { title: "Template using match", in: `{{ match "[a-z]+" "abc" }}`,