From 1f2c76e63d3c256f335918f9653527ed5526c6a6 Mon Sep 17 00:00:00 2001 From: Hunter Kehoe Date: Mon, 7 Jul 2025 22:23:32 -0600 Subject: [PATCH] copy subset of Sprig template functions --- README.md | 1 + docs/publish.md | 5 +- docs/releases.md | 1 + docs/sprig.md | 24 ++ docs/sprig/conversion.md | 36 +++ docs/sprig/crypto.md | 41 +++ docs/sprig/date.md | 126 ++++++++ docs/sprig/defaults.md | 169 ++++++++++ docs/sprig/dicts.md | 172 ++++++++++ docs/sprig/encoding.md | 6 + docs/sprig/flow_control.md | 11 + docs/sprig/integer_slice.md | 41 +++ docs/sprig/lists.md | 188 +++++++++++ docs/sprig/math.md | 78 +++++ docs/sprig/os.md | 24 ++ docs/sprig/paths.md | 114 +++++++ docs/sprig/reflection.md | 50 +++ docs/sprig/semver.md | 151 +++++++++ docs/sprig/string_slice.md | 72 +++++ docs/sprig/strings.md | 309 ++++++++++++++++++ docs/sprig/url.md | 33 ++ docs/sprig/uuid.md | 9 + mkdocs.yml | 1 + server/server.go | 7 +- server/server_test.go | 45 +++ util/sprig/LICENSE.txt | 19 ++ util/sprig/crypto.go | 37 +++ util/sprig/crypto_test.go | 54 ++++ util/sprig/date.go | 152 +++++++++ util/sprig/date_test.go | 120 +++++++ util/sprig/defaults.go | 163 ++++++++++ util/sprig/defaults_test.go | 196 +++++++++++ util/sprig/dict.go | 118 +++++++ util/sprig/dict_test.go | 166 ++++++++++ util/sprig/doc.go | 19 ++ util/sprig/example_test.go | 25 ++ util/sprig/flow_control_test.go | 16 + util/sprig/functions.go | 302 +++++++++++++++++ util/sprig/functions_linux_test.go | 28 ++ util/sprig/functions_test.go | 70 ++++ util/sprig/functions_windows_test.go | 28 ++ util/sprig/list.go | 464 +++++++++++++++++++++++++++ util/sprig/list_test.go | 364 +++++++++++++++++++++ util/sprig/numeric.go | 228 +++++++++++++ util/sprig/numeric_test.go | 307 ++++++++++++++++++ util/sprig/reflect.go | 28 ++ util/sprig/reflect_test.go | 73 +++++ util/sprig/regex.go | 83 +++++ util/sprig/regex_test.go | 203 ++++++++++++ util/sprig/strings.go | 189 +++++++++++ util/sprig/strings_test.go | 233 ++++++++++++++ util/sprig/url.go | 66 ++++ util/sprig/url_test.go | 87 +++++ 53 files changed, 5550 insertions(+), 2 deletions(-) create mode 100644 docs/sprig.md create mode 100644 docs/sprig/conversion.md create mode 100644 docs/sprig/crypto.md create mode 100644 docs/sprig/date.md create mode 100644 docs/sprig/defaults.md create mode 100644 docs/sprig/dicts.md create mode 100644 docs/sprig/encoding.md create mode 100644 docs/sprig/flow_control.md create mode 100644 docs/sprig/integer_slice.md create mode 100644 docs/sprig/lists.md create mode 100644 docs/sprig/math.md create mode 100644 docs/sprig/os.md create mode 100644 docs/sprig/paths.md create mode 100644 docs/sprig/reflection.md create mode 100644 docs/sprig/semver.md create mode 100644 docs/sprig/string_slice.md create mode 100644 docs/sprig/strings.md create mode 100644 docs/sprig/url.md create mode 100644 docs/sprig/uuid.md create mode 100644 util/sprig/LICENSE.txt create mode 100644 util/sprig/crypto.go create mode 100644 util/sprig/crypto_test.go create mode 100644 util/sprig/date.go create mode 100644 util/sprig/date_test.go create mode 100644 util/sprig/defaults.go create mode 100644 util/sprig/defaults_test.go create mode 100644 util/sprig/dict.go create mode 100644 util/sprig/dict_test.go create mode 100644 util/sprig/doc.go create mode 100644 util/sprig/example_test.go create mode 100644 util/sprig/flow_control_test.go create mode 100644 util/sprig/functions.go create mode 100644 util/sprig/functions_linux_test.go create mode 100644 util/sprig/functions_test.go create mode 100644 util/sprig/functions_windows_test.go create mode 100644 util/sprig/list.go create mode 100644 util/sprig/list_test.go create mode 100644 util/sprig/numeric.go create mode 100644 util/sprig/numeric_test.go create mode 100644 util/sprig/reflect.go create mode 100644 util/sprig/reflect_test.go create mode 100644 util/sprig/regex.go create mode 100644 util/sprig/regex_test.go create mode 100644 util/sprig/strings.go create mode 100644 util/sprig/strings_test.go create mode 100644 util/sprig/url.go create mode 100644 util/sprig/url_test.go diff --git a/README.md b/README.md index 61591ca6..9942e138 100644 --- a/README.md +++ b/README.md @@ -253,3 +253,4 @@ Third-party libraries and resources: * [Statically linking go-sqlite3](https://www.arp242.net/static-go.html) * [Linked tabs in mkdocs](https://facelessuser.github.io/pymdown-extensions/extensions/tabbed/#linked-tabs) * [webpush-go](https://github.com/SherClockHolmes/webpush-go) (MIT) is used to send web push notifications +* [Sprig](https://github.com/Masterminds/sprig) (MIT) is used to add template parsing functions diff --git a/docs/publish.md b/docs/publish.md index 25bff035..91f75e3d 100644 --- a/docs/publish.md +++ b/docs/publish.md @@ -953,13 +953,16 @@ is valid JSON). You can enable templating by setting the `X-Template` header (or its aliases `Template` or `tpl`) to `yes` or `1`, or (more appropriately for webhooks) by setting the `?template=yes` query parameter. Then, include templates in your `message` and/or `title`, using the following stanzas (see [Go docs](https://pkg.go.dev/text/template) for detailed syntax): -* Variables,, e.g. `{{.alert.title}}` or `An error occurred: {{.error.desc}}` +* Variables, e.g. `{{.alert.title}}` or `An error occurred: {{.error.desc}}` * Conditionals (if/else, e.g. `{{if eq .action "opened"}}..{{else}}..{{end}}`, see [example](https://repeatit.io/#/share/eyJ0ZW1wbGF0ZSI6Ilt7ey5wdWxsX3JlcXVlc3QuaGVhZC5yZXBvLmZ1bGxfbmFtZX19XSBQdWxsIHJlcXVlc3Qge3tpZiBlcSAuYWN0aW9uIFwib3BlbmVkXCJ9fU9QRU5FRHt7ZWxzZX19Q0xPU0VEe3tlbmR9fToge3sucHVsbF9yZXF1ZXN0LnRpdGxlfX0iLCJpbnB1dCI6IntcbiAgXCJhY3Rpb25cIjogXCJvcGVuZWRcIixcbiAgXCJudW1iZXJcIjogMSxcbiAgXCJwdWxsX3JlcXVlc3RcIjoge1xuICAgIFwidXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9wdWxscy8xXCIsXG4gICAgXCJpZFwiOiAxNzgzNDIwOTcyLFxuICAgIFwibm9kZV9pZFwiOiBcIlBSX2t3RE9IQWJkbzg1cVROZ3NcIixcbiAgICBcImh0bWxfdXJsXCI6IFwiaHR0cHM6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXIvZGFiYmxlL3B1bGwvMVwiLFxuICAgIFwiZGlmZl91cmxcIjogXCJodHRwczovL2dpdGh1Yi5jb20vYmlud2llZGVyaGllci9kYWJibGUvcHVsbC8xLmRpZmZcIixcbiAgICBcInBhdGNoX3VybFwiOiBcImh0dHBzOi8vZ2l0aHViLmNvbS9iaW53aWVkZXJoaWVyL2RhYmJsZS9wdWxsLzEucGF0Y2hcIixcbiAgICBcImlzc3VlX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvaXNzdWVzLzFcIixcbiAgICBcIm51bWJlclwiOiAxLFxuICAgIFwic3RhdGVcIjogXCJvcGVuXCIsXG4gICAgXCJsb2NrZWRcIjogZmFsc2UsXG4gICAgXCJ0aXRsZVwiOiBcIkEgc2FtcGxlIFBSIGZyb20gUGhpbFwiLFxuICAgIFwidXNlclwiOiB7XG4gICAgICBcImxvZ2luXCI6IFwiYmlud2llZGVyaGllclwiLFxuICAgICAgXCJpZFwiOiA2NjQ1OTcsXG4gICAgICBcIm5vZGVfaWRcIjogXCJNRFE2VlhObGNqWTJORFU1Tnc9PVwiLFxuICAgICAgXCJhdmF0YXJfdXJsXCI6IFwiaHR0cHM6Ly9hdmF0YXJzLmdpdGh1YnVzZXJjb250ZW50LmNvbS91LzY2NDU5Nz92PTRcIixcbiAgICAgIFwiZ3JhdmF0YXJfaWRcIjogXCJcIixcbiAgICAgIFwidXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyXCIsXG4gICAgICBcImh0bWxfdXJsXCI6IFwiaHR0cHM6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXJcIixcbiAgICAgIFwiZm9sbG93ZXJzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9mb2xsb3dlcnNcIixcbiAgICAgIFwiZm9sbG93aW5nX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9mb2xsb3dpbmd7L290aGVyX3VzZXJ9XCIsXG4gICAgICBcImdpc3RzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9naXN0c3svZ2lzdF9pZH1cIixcbiAgICAgIFwic3RhcnJlZF91cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvc3RhcnJlZHsvb3duZXJ9ey9yZXBvfVwiLFxuICAgICAgXCJzdWJzY3JpcHRpb25zX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9zdWJzY3JpcHRpb25zXCIsXG4gICAgICBcIm9yZ2FuaXphdGlvbnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL29yZ3NcIixcbiAgICAgIFwicmVwb3NfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL3JlcG9zXCIsXG4gICAgICBcImV2ZW50c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZXZlbnRzey9wcml2YWN5fVwiLFxuICAgICAgXCJyZWNlaXZlZF9ldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL3JlY2VpdmVkX2V2ZW50c1wiLFxuICAgICAgXCJ0eXBlXCI6IFwiVXNlclwiLFxuICAgICAgXCJzaXRlX2FkbWluXCI6IGZhbHNlXG4gICAgfSxcbiAgICBcImJvZHlcIjogbnVsbCxcbiAgICBcImNyZWF0ZWRfYXRcIjogXCIyMDI0LTAzLTIxVDAyOjUyOjA5WlwiLFxuICAgIFwidXBkYXRlZF9hdFwiOiBcIjIwMjQtMDMtMjFUMDI6NTI6MDlaXCIsXG4gICAgXCJjbG9zZWRfYXRcIjogbnVsbCxcbiAgICBcIm1lcmdlZF9hdFwiOiBudWxsLFxuICAgIFwibWVyZ2VfY29tbWl0X3NoYVwiOiBudWxsLFxuICAgIFwiYXNzaWduZWVcIjogbnVsbCxcbiAgICBcImFzc2lnbmVlc1wiOiBbXSxcbiAgICBcInJlcXVlc3RlZF9yZXZpZXdlcnNcIjogW10sXG4gICAgXCJyZXF1ZXN0ZWRfdGVhbXNcIjogW10sXG4gICAgXCJsYWJlbHNcIjogW10sXG4gICAgXCJtaWxlc3RvbmVcIjogbnVsbCxcbiAgICBcImRyYWZ0XCI6IGZhbHNlLFxuICAgIFwiY29tbWl0c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3B1bGxzLzEvY29tbWl0c1wiLFxuICAgIFwicmV2aWV3X2NvbW1lbnRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvcHVsbHMvMS9jb21tZW50c1wiLFxuICAgIFwicmV2aWV3X2NvbW1lbnRfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9wdWxscy9jb21tZW50c3svbnVtYmVyfVwiLFxuICAgIFwiY29tbWVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9pc3N1ZXMvMS9jb21tZW50c1wiLFxuICAgIFwic3RhdHVzZXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9zdGF0dXNlcy81NzAzODQyY2M1NzE1ZWQxZTM1OGQyM2ViYjY5M2RiMDk3NDdhZTliXCIsXG4gICAgXCJoZWFkXCI6IHtcbiAgICAgIFwibGFiZWxcIjogXCJiaW53aWVkZXJoaWVyOmFhXCIsXG4gICAgICBcInJlZlwiOiBcImFhXCIsXG4gICAgICBcInNoYVwiOiBcIjU3MDM4NDJjYzU3MTVlZDFlMzU4ZDIzZWJiNjkzZGIwOTc0N2FlOWJcIixcbiAgICAgIFwidXNlclwiOiB7XG4gICAgICAgIFwibG9naW5cIjogXCJiaW53aWVkZXJoaWVyXCIsXG4gICAgICAgIFwiaWRcIjogNjY0NTk3LFxuICAgICAgICBcIm5vZGVfaWRcIjogXCJNRFE2VlhObGNqWTJORFU1Tnc9PVwiLFxuICAgICAgICBcImF2YXRhcl91cmxcIjogXCJodHRwczovL2F2YXRhcnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tL3UvNjY0NTk3P3Y9NFwiLFxuICAgICAgICBcImdyYXZhdGFyX2lkXCI6IFwiXCIsXG4gICAgICAgIFwidXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyXCIsXG4gICAgICAgIFwiaHRtbF91cmxcIjogXCJodHRwczovL2dpdGh1Yi5jb20vYmlud2llZGVyaGllclwiLFxuICAgICAgICBcImZvbGxvd2Vyc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZm9sbG93ZXJzXCIsXG4gICAgICAgIFwiZm9sbG93aW5nX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9mb2xsb3dpbmd7L290aGVyX3VzZXJ9XCIsXG4gICAgICAgIFwiZ2lzdHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL2dpc3Rzey9naXN0X2lkfVwiLFxuICAgICAgICBcInN0YXJyZWRfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL3N0YXJyZWR7L293bmVyfXsvcmVwb31cIixcbiAgICAgICAgXCJzdWJzY3JpcHRpb25zX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9zdWJzY3JpcHRpb25zXCIsXG4gICAgICAgIFwib3JnYW5pemF0aW9uc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvb3Jnc1wiLFxuICAgICAgICBcInJlcG9zX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9yZXBvc1wiLFxuICAgICAgICBcImV2ZW50c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZXZlbnRzey9wcml2YWN5fVwiLFxuICAgICAgICBcInJlY2VpdmVkX2V2ZW50c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvcmVjZWl2ZWRfZXZlbnRzXCIsXG4gICAgICAgIFwidHlwZVwiOiBcIlVzZXJcIixcbiAgICAgICAgXCJzaXRlX2FkbWluXCI6IGZhbHNlXG4gICAgICB9LFxuICAgICAgXCJyZXBvXCI6IHtcbiAgICAgICAgXCJpZFwiOiA0NzAyMTIwMDMsXG4gICAgICAgIFwibm9kZV9pZFwiOiBcIlJfa2dET0hBYmRvd1wiLFxuICAgICAgICBcIm5hbWVcIjogXCJkYWJibGVcIixcbiAgICAgICAgXCJmdWxsX25hbWVcIjogXCJiaW53aWVkZXJoaWVyL2RhYmJsZVwiLFxuICAgICAgICBcInByaXZhdGVcIjogZmFsc2UsXG4gICAgICAgIFwib3duZXJcIjoge1xuICAgICAgICAgIFwibG9naW5cIjogXCJiaW53aWVkZXJoaWVyXCIsXG4gICAgICAgICAgXCJpZFwiOiA2NjQ1OTcsXG4gICAgICAgICAgXCJub2RlX2lkXCI6IFwiTURRNlZYTmxjalkyTkRVNU53PT1cIixcbiAgICAgICAgICBcImF2YXRhcl91cmxcIjogXCJodHRwczovL2F2YXRhcnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tL3UvNjY0NTk3P3Y9NFwiLFxuICAgICAgICAgIFwiZ3JhdmF0YXJfaWRcIjogXCJcIixcbiAgICAgICAgICBcInVybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllclwiLFxuICAgICAgICAgIFwiaHRtbF91cmxcIjogXCJodHRwczovL2dpdGh1Yi5jb20vYmlud2llZGVyaGllclwiLFxuICAgICAgICAgIFwiZm9sbG93ZXJzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9mb2xsb3dlcnNcIixcbiAgICAgICAgICBcImZvbGxvd2luZ191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZm9sbG93aW5ney9vdGhlcl91c2VyfVwiLFxuICAgICAgICAgIFwiZ2lzdHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL2dpc3Rzey9naXN0X2lkfVwiLFxuICAgICAgICAgIFwic3RhcnJlZF91cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvc3RhcnJlZHsvb3duZXJ9ey9yZXBvfVwiLFxuICAgICAgICAgIFwic3Vic2NyaXB0aW9uc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvc3Vic2NyaXB0aW9uc1wiLFxuICAgICAgICAgIFwib3JnYW5pemF0aW9uc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvb3Jnc1wiLFxuICAgICAgICAgIFwicmVwb3NfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL3JlcG9zXCIsXG4gICAgICAgICAgXCJldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL2V2ZW50c3svcHJpdmFjeX1cIixcbiAgICAgICAgICBcInJlY2VpdmVkX2V2ZW50c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvcmVjZWl2ZWRfZXZlbnRzXCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwiVXNlclwiLFxuICAgICAgICAgIFwic2l0ZV9hZG1pblwiOiBmYWxzZVxuICAgICAgICB9LFxuICAgICAgICBcImh0bWxfdXJsXCI6IFwiaHR0cHM6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXIvZGFiYmxlXCIsXG4gICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJBIHJlcG8gZm9yIGRhYmJsaW5nXCIsXG4gICAgICAgIFwiZm9ya1wiOiBmYWxzZSxcbiAgICAgICAgXCJ1cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlXCIsXG4gICAgICAgIFwiZm9ya3NfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9mb3Jrc1wiLFxuICAgICAgICBcImtleXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9rZXlzey9rZXlfaWR9XCIsXG4gICAgICAgIFwiY29sbGFib3JhdG9yc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2NvbGxhYm9yYXRvcnN7L2NvbGxhYm9yYXRvcn1cIixcbiAgICAgICAgXCJ0ZWFtc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3RlYW1zXCIsXG4gICAgICAgIFwiaG9va3NfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9ob29rc1wiLFxuICAgICAgICBcImlzc3VlX2V2ZW50c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2lzc3Vlcy9ldmVudHN7L251bWJlcn1cIixcbiAgICAgICAgXCJldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9ldmVudHNcIixcbiAgICAgICAgXCJhc3NpZ25lZXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9hc3NpZ25lZXN7L3VzZXJ9XCIsXG4gICAgICAgIFwiYnJhbmNoZXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9icmFuY2hlc3svYnJhbmNofVwiLFxuICAgICAgICBcInRhZ3NfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS90YWdzXCIsXG4gICAgICAgIFwiYmxvYnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9naXQvYmxvYnN7L3NoYX1cIixcbiAgICAgICAgXCJnaXRfdGFnc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2dpdC90YWdzey9zaGF9XCIsXG4gICAgICAgIFwiZ2l0X3JlZnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9naXQvcmVmc3svc2hhfVwiLFxuICAgICAgICBcInRyZWVzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvZ2l0L3RyZWVzey9zaGF9XCIsXG4gICAgICAgIFwic3RhdHVzZXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9zdGF0dXNlcy97c2hhfVwiLFxuICAgICAgICBcImxhbmd1YWdlc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2xhbmd1YWdlc1wiLFxuICAgICAgICBcInN0YXJnYXplcnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9zdGFyZ2F6ZXJzXCIsXG4gICAgICAgIFwiY29udHJpYnV0b3JzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvY29udHJpYnV0b3JzXCIsXG4gICAgICAgIFwic3Vic2NyaWJlcnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9zdWJzY3JpYmVyc1wiLFxuICAgICAgICBcInN1YnNjcmlwdGlvbl91cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3N1YnNjcmlwdGlvblwiLFxuICAgICAgICBcImNvbW1pdHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9jb21taXRzey9zaGF9XCIsXG4gICAgICAgIFwiZ2l0X2NvbW1pdHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9naXQvY29tbWl0c3svc2hhfVwiLFxuICAgICAgICBcImNvbW1lbnRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvY29tbWVudHN7L251bWJlcn1cIixcbiAgICAgICAgXCJpc3N1ZV9jb21tZW50X3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvaXNzdWVzL2NvbW1lbnRzey9udW1iZXJ9XCIsXG4gICAgICAgIFwiY29udGVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9jb250ZW50cy97K3BhdGh9XCIsXG4gICAgICAgIFwiY29tcGFyZV91cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2NvbXBhcmUve2Jhc2V9Li4ue2hlYWR9XCIsXG4gICAgICAgIFwibWVyZ2VzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvbWVyZ2VzXCIsXG4gICAgICAgIFwiYXJjaGl2ZV91cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3thcmNoaXZlX2Zvcm1hdH17L3JlZn1cIixcbiAgICAgICAgXCJkb3dubG9hZHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9kb3dubG9hZHNcIixcbiAgICAgICAgXCJpc3N1ZXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9pc3N1ZXN7L251bWJlcn1cIixcbiAgICAgICAgXCJwdWxsc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3B1bGxzey9udW1iZXJ9XCIsXG4gICAgICAgIFwibWlsZXN0b25lc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL21pbGVzdG9uZXN7L251bWJlcn1cIixcbiAgICAgICAgXCJub3RpZmljYXRpb25zX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvbm90aWZpY2F0aW9uc3s/c2luY2UsYWxsLHBhcnRpY2lwYXRpbmd9XCIsXG4gICAgICAgIFwibGFiZWxzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvbGFiZWxzey9uYW1lfVwiLFxuICAgICAgICBcInJlbGVhc2VzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvcmVsZWFzZXN7L2lkfVwiLFxuICAgICAgICBcImRlcGxveW1lbnRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvZGVwbG95bWVudHNcIixcbiAgICAgICAgXCJjcmVhdGVkX2F0XCI6IFwiMjAyMi0wMy0xNVQxNTowNjoxN1pcIixcbiAgICAgICAgXCJ1cGRhdGVkX2F0XCI6IFwiMjAyMi0wMy0xNVQxNTowNjoxN1pcIixcbiAgICAgICAgXCJwdXNoZWRfYXRcIjogXCIyMDI0LTAzLTIxVDAyOjUyOjEwWlwiLFxuICAgICAgICBcImdpdF91cmxcIjogXCJnaXQ6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXIvZGFiYmxlLmdpdFwiLFxuICAgICAgICBcInNzaF91cmxcIjogXCJnaXRAZ2l0aHViLmNvbTpiaW53aWVkZXJoaWVyL2RhYmJsZS5naXRcIixcbiAgICAgICAgXCJjbG9uZV91cmxcIjogXCJodHRwczovL2dpdGh1Yi5jb20vYmlud2llZGVyaGllci9kYWJibGUuZ2l0XCIsXG4gICAgICAgIFwic3ZuX3VybFwiOiBcImh0dHBzOi8vZ2l0aHViLmNvbS9iaW53aWVkZXJoaWVyL2RhYmJsZVwiLFxuICAgICAgICBcImhvbWVwYWdlXCI6IG51bGwsXG4gICAgICAgIFwic2l6ZVwiOiAxLFxuICAgICAgICBcInN0YXJnYXplcnNfY291bnRcIjogMCxcbiAgICAgICAgXCJ3YXRjaGVyc19jb3VudFwiOiAwLFxuICAgICAgICBcImxhbmd1YWdlXCI6IG51bGwsXG4gICAgICAgIFwiaGFzX2lzc3Vlc1wiOiB0cnVlLFxuICAgICAgICBcImhhc19wcm9qZWN0c1wiOiB0cnVlLFxuICAgICAgICBcImhhc19kb3dubG9hZHNcIjogdHJ1ZSxcbiAgICAgICAgXCJoYXNfd2lraVwiOiB0cnVlLFxuICAgICAgICBcImhhc19wYWdlc1wiOiBmYWxzZSxcbiAgICAgICAgXCJoYXNfZGlzY3Vzc2lvbnNcIjogZmFsc2UsXG4gICAgICAgIFwiZm9ya3NfY291bnRcIjogMCxcbiAgICAgICAgXCJtaXJyb3JfdXJsXCI6IG51bGwsXG4gICAgICAgIFwiYXJjaGl2ZWRcIjogZmFsc2UsXG4gICAgICAgIFwiZGlzYWJsZWRcIjogZmFsc2UsXG4gICAgICAgIFwib3Blbl9pc3N1ZXNfY291bnRcIjogMSxcbiAgICAgICAgXCJsaWNlbnNlXCI6IG51bGwsXG4gICAgICAgIFwiYWxsb3dfZm9ya2luZ1wiOiB0cnVlLFxuICAgICAgICBcImlzX3RlbXBsYXRlXCI6IGZhbHNlLFxuICAgICAgICBcIndlYl9jb21taXRfc2lnbm9mZl9yZXF1aXJlZFwiOiBmYWxzZSxcbiAgICAgICAgXCJ0b3BpY3NcIjogW10sXG4gICAgICAgIFwidmlzaWJpbGl0eVwiOiBcInB1YmxpY1wiLFxuICAgICAgICBcImZvcmtzXCI6IDAsXG4gICAgICAgIFwib3Blbl9pc3N1ZXNcIjogMSxcbiAgICAgICAgXCJ3YXRjaGVyc1wiOiAwLFxuICAgICAgICBcImRlZmF1bHRfYnJhbmNoXCI6IFwibWFpblwiLFxuICAgICAgICBcImFsbG93X3NxdWFzaF9tZXJnZVwiOiB0cnVlLFxuICAgICAgICBcImFsbG93X21lcmdlX2NvbW1pdFwiOiB0cnVlLFxuICAgICAgICBcImFsbG93X3JlYmFzZV9tZXJnZVwiOiB0cnVlLFxuICAgICAgICBcImFsbG93X2F1dG9fbWVyZ2VcIjogZmFsc2UsXG4gICAgICAgIFwiZGVsZXRlX2JyYW5jaF9vbl9tZXJnZVwiOiBmYWxzZSxcbiAgICAgICAgXCJhbGxvd191cGRhdGVfYnJhbmNoXCI6IGZhbHNlLFxuICAgICAgICBcInVzZV9zcXVhc2hfcHJfdGl0bGVfYXNfZGVmYXVsdFwiOiBmYWxzZSxcbiAgICAgICAgXCJzcXVhc2hfbWVyZ2VfY29tbWl0X21lc3NhZ2VcIjogXCJDT01NSVRfTUVTU0FHRVNcIixcbiAgICAgICAgXCJzcXVhc2hfbWVyZ2VfY29tbWl0X3RpdGxlXCI6IFwiQ09NTUlUX09SX1BSX1RJVExFXCIsXG4gICAgICAgIFwibWVyZ2VfY29tbWl0X21lc3NhZ2VcIjogXCJQUl9USVRMRVwiLFxuICAgICAgICBcIm1lcmdlX2NvbW1pdF90aXRsZVwiOiBcIk1FUkdFX01FU1NBR0VcIlxuICAgICAgfVxuICAgIH0sXG4gICAgXCJiYXNlXCI6IHtcbiAgICAgIFwibGFiZWxcIjogXCJiaW53aWVkZXJoaWVyOm1haW5cIixcbiAgICAgIFwicmVmXCI6IFwibWFpblwiLFxuICAgICAgXCJzaGFcIjogXCI3MmQ5MzFhMjBiYjgzZDEyM2FiNDVhY2NhZjc2MTE1MGM4YjAxMjExXCIsXG4gICAgICBcInVzZXJcIjoge1xuICAgICAgICBcImxvZ2luXCI6IFwiYmlud2llZGVyaGllclwiLFxuICAgICAgICBcImlkXCI6IDY2NDU5NyxcbiAgICAgICAgXCJub2RlX2lkXCI6IFwiTURRNlZYTmxjalkyTkRVNU53PT1cIixcbiAgICAgICAgXCJhdmF0YXJfdXJsXCI6IFwiaHR0cHM6Ly9hdmF0YXJzLmdpdGh1YnVzZXJjb250ZW50LmNvbS91LzY2NDU5Nz92PTRcIixcbiAgICAgICAgXCJncmF2YXRhcl9pZFwiOiBcIlwiLFxuICAgICAgICBcInVybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllclwiLFxuICAgICAgICBcImh0bWxfdXJsXCI6IFwiaHR0cHM6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXJcIixcbiAgICAgICAgXCJmb2xsb3dlcnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL2ZvbGxvd2Vyc1wiLFxuICAgICAgICBcImZvbGxvd2luZ191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZm9sbG93aW5ney9vdGhlcl91c2VyfVwiLFxuICAgICAgICBcImdpc3RzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9naXN0c3svZ2lzdF9pZH1cIixcbiAgICAgICAgXCJzdGFycmVkX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9zdGFycmVkey9vd25lcn17L3JlcG99XCIsXG4gICAgICAgIFwic3Vic2NyaXB0aW9uc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvc3Vic2NyaXB0aW9uc1wiLFxuICAgICAgICBcIm9yZ2FuaXphdGlvbnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL29yZ3NcIixcbiAgICAgICAgXCJyZXBvc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvcmVwb3NcIixcbiAgICAgICAgXCJldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL2V2ZW50c3svcHJpdmFjeX1cIixcbiAgICAgICAgXCJyZWNlaXZlZF9ldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL3JlY2VpdmVkX2V2ZW50c1wiLFxuICAgICAgICBcInR5cGVcIjogXCJVc2VyXCIsXG4gICAgICAgIFwic2l0ZV9hZG1pblwiOiBmYWxzZVxuICAgICAgfSxcbiAgICAgIFwicmVwb1wiOiB7XG4gICAgICAgIFwiaWRcIjogNDcwMjEyMDAzLFxuICAgICAgICBcIm5vZGVfaWRcIjogXCJSX2tnRE9IQWJkb3dcIixcbiAgICAgICAgXCJuYW1lXCI6IFwiZGFiYmxlXCIsXG4gICAgICAgIFwiZnVsbF9uYW1lXCI6IFwiYmlud2llZGVyaGllci9kYWJibGVcIixcbiAgICAgICAgXCJwcml2YXRlXCI6IGZhbHNlLFxuICAgICAgICBcIm93bmVyXCI6IHtcbiAgICAgICAgICBcImxvZ2luXCI6IFwiYmlud2llZGVyaGllclwiLFxuICAgICAgICAgIFwiaWRcIjogNjY0NTk3LFxuICAgICAgICAgIFwibm9kZV9pZFwiOiBcIk1EUTZWWE5sY2pZMk5EVTVOdz09XCIsXG4gICAgICAgICAgXCJhdmF0YXJfdXJsXCI6IFwiaHR0cHM6Ly9hdmF0YXJzLmdpdGh1YnVzZXJjb250ZW50LmNvbS91LzY2NDU5Nz92PTRcIixcbiAgICAgICAgICBcImdyYXZhdGFyX2lkXCI6IFwiXCIsXG4gICAgICAgICAgXCJ1cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXJcIixcbiAgICAgICAgICBcImh0bWxfdXJsXCI6IFwiaHR0cHM6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXJcIixcbiAgICAgICAgICBcImZvbGxvd2Vyc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZm9sbG93ZXJzXCIsXG4gICAgICAgICAgXCJmb2xsb3dpbmdfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL2ZvbGxvd2luZ3svb3RoZXJfdXNlcn1cIixcbiAgICAgICAgICBcImdpc3RzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9naXN0c3svZ2lzdF9pZH1cIixcbiAgICAgICAgICBcInN0YXJyZWRfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL3N0YXJyZWR7L293bmVyfXsvcmVwb31cIixcbiAgICAgICAgICBcInN1YnNjcmlwdGlvbnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL3N1YnNjcmlwdGlvbnNcIixcbiAgICAgICAgICBcIm9yZ2FuaXphdGlvbnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL29yZ3NcIixcbiAgICAgICAgICBcInJlcG9zX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9yZXBvc1wiLFxuICAgICAgICAgIFwiZXZlbnRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9ldmVudHN7L3ByaXZhY3l9XCIsXG4gICAgICAgICAgXCJyZWNlaXZlZF9ldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL3JlY2VpdmVkX2V2ZW50c1wiLFxuICAgICAgICAgIFwidHlwZVwiOiBcIlVzZXJcIixcbiAgICAgICAgICBcInNpdGVfYWRtaW5cIjogZmFsc2VcbiAgICAgICAgfSxcbiAgICAgICAgXCJodG1sX3VybFwiOiBcImh0dHBzOi8vZ2l0aHViLmNvbS9iaW53aWVkZXJoaWVyL2RhYmJsZVwiLFxuICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiQSByZXBvIGZvciBkYWJibGluZ1wiLFxuICAgICAgICBcImZvcmtcIjogZmFsc2UsXG4gICAgICAgIFwidXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZVwiLFxuICAgICAgICBcImZvcmtzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvZm9ya3NcIixcbiAgICAgICAgXCJrZXlzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUva2V5c3sva2V5X2lkfVwiLFxuICAgICAgICBcImNvbGxhYm9yYXRvcnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9jb2xsYWJvcmF0b3Jzey9jb2xsYWJvcmF0b3J9XCIsXG4gICAgICAgIFwidGVhbXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS90ZWFtc1wiLFxuICAgICAgICBcImhvb2tzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvaG9va3NcIixcbiAgICAgICAgXCJpc3N1ZV9ldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9pc3N1ZXMvZXZlbnRzey9udW1iZXJ9XCIsXG4gICAgICAgIFwiZXZlbnRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvZXZlbnRzXCIsXG4gICAgICAgIFwiYXNzaWduZWVzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvYXNzaWduZWVzey91c2VyfVwiLFxuICAgICAgICBcImJyYW5jaGVzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvYnJhbmNoZXN7L2JyYW5jaH1cIixcbiAgICAgICAgXCJ0YWdzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvdGFnc1wiLFxuICAgICAgICBcImJsb2JzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvZ2l0L2Jsb2Jzey9zaGF9XCIsXG4gICAgICAgIFwiZ2l0X3RhZ3NfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9naXQvdGFnc3svc2hhfVwiLFxuICAgICAgICBcImdpdF9yZWZzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvZ2l0L3JlZnN7L3NoYX1cIixcbiAgICAgICAgXCJ0cmVlc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2dpdC90cmVlc3svc2hhfVwiLFxuICAgICAgICBcInN0YXR1c2VzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvc3RhdHVzZXMve3NoYX1cIixcbiAgICAgICAgXCJsYW5ndWFnZXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9sYW5ndWFnZXNcIixcbiAgICAgICAgXCJzdGFyZ2F6ZXJzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvc3RhcmdhemVyc1wiLFxuICAgICAgICBcImNvbnRyaWJ1dG9yc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2NvbnRyaWJ1dG9yc1wiLFxuICAgICAgICBcInN1YnNjcmliZXJzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvc3Vic2NyaWJlcnNcIixcbiAgICAgICAgXCJzdWJzY3JpcHRpb25fdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9zdWJzY3JpcHRpb25cIixcbiAgICAgICAgXCJjb21taXRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvY29tbWl0c3svc2hhfVwiLFxuICAgICAgICBcImdpdF9jb21taXRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvZ2l0L2NvbW1pdHN7L3NoYX1cIixcbiAgICAgICAgXCJjb21tZW50c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2NvbW1lbnRzey9udW1iZXJ9XCIsXG4gICAgICAgIFwiaXNzdWVfY29tbWVudF91cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2lzc3Vlcy9jb21tZW50c3svbnVtYmVyfVwiLFxuICAgICAgICBcImNvbnRlbnRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvY29udGVudHMveytwYXRofVwiLFxuICAgICAgICBcImNvbXBhcmVfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9jb21wYXJlL3tiYXNlfS4uLntoZWFkfVwiLFxuICAgICAgICBcIm1lcmdlc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL21lcmdlc1wiLFxuICAgICAgICBcImFyY2hpdmVfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS97YXJjaGl2ZV9mb3JtYXR9ey9yZWZ9XCIsXG4gICAgICAgIFwiZG93bmxvYWRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvZG93bmxvYWRzXCIsXG4gICAgICAgIFwiaXNzdWVzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvaXNzdWVzey9udW1iZXJ9XCIsXG4gICAgICAgIFwicHVsbHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9wdWxsc3svbnVtYmVyfVwiLFxuICAgICAgICBcIm1pbGVzdG9uZXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9taWxlc3RvbmVzey9udW1iZXJ9XCIsXG4gICAgICAgIFwibm90aWZpY2F0aW9uc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL25vdGlmaWNhdGlvbnN7P3NpbmNlLGFsbCxwYXJ0aWNpcGF0aW5nfVwiLFxuICAgICAgICBcImxhYmVsc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2xhYmVsc3svbmFtZX1cIixcbiAgICAgICAgXCJyZWxlYXNlc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3JlbGVhc2Vzey9pZH1cIixcbiAgICAgICAgXCJkZXBsb3ltZW50c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2RlcGxveW1lbnRzXCIsXG4gICAgICAgIFwiY3JlYXRlZF9hdFwiOiBcIjIwMjItMDMtMTVUMTU6MDY6MTdaXCIsXG4gICAgICAgIFwidXBkYXRlZF9hdFwiOiBcIjIwMjItMDMtMTVUMTU6MDY6MTdaXCIsXG4gICAgICAgIFwicHVzaGVkX2F0XCI6IFwiMjAyNC0wMy0yMVQwMjo1MjoxMFpcIixcbiAgICAgICAgXCJnaXRfdXJsXCI6IFwiZ2l0Oi8vZ2l0aHViLmNvbS9iaW53aWVkZXJoaWVyL2RhYmJsZS5naXRcIixcbiAgICAgICAgXCJzc2hfdXJsXCI6IFwiZ2l0QGdpdGh1Yi5jb206Ymlud2llZGVyaGllci9kYWJibGUuZ2l0XCIsXG4gICAgICAgIFwiY2xvbmVfdXJsXCI6IFwiaHR0cHM6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXIvZGFiYmxlLmdpdFwiLFxuICAgICAgICBcInN2bl91cmxcIjogXCJodHRwczovL2dpdGh1Yi5jb20vYmlud2llZGVyaGllci9kYWJibGVcIixcbiAgICAgICAgXCJob21lcGFnZVwiOiBudWxsLFxuICAgICAgICBcInNpemVcIjogMSxcbiAgICAgICAgXCJzdGFyZ2F6ZXJzX2NvdW50XCI6IDAsXG4gICAgICAgIFwid2F0Y2hlcnNfY291bnRcIjogMCxcbiAgICAgICAgXCJsYW5ndWFnZVwiOiBudWxsLFxuICAgICAgICBcImhhc19pc3N1ZXNcIjogdHJ1ZSxcbiAgICAgICAgXCJoYXNfcHJvamVjdHNcIjogdHJ1ZSxcbiAgICAgICAgXCJoYXNfZG93bmxvYWRzXCI6IHRydWUsXG4gICAgICAgIFwiaGFzX3dpa2lcIjogdHJ1ZSxcbiAgICAgICAgXCJoYXNfcGFnZXNcIjogZmFsc2UsXG4gICAgICAgIFwiaGFzX2Rpc2N1c3Npb25zXCI6IGZhbHNlLFxuICAgICAgICBcImZvcmtzX2NvdW50XCI6IDAsXG4gICAgICAgIFwibWlycm9yX3VybFwiOiBudWxsLFxuICAgICAgICBcImFyY2hpdmVkXCI6IGZhbHNlLFxuICAgICAgICBcImRpc2FibGVkXCI6IGZhbHNlLFxuICAgICAgICBcIm9wZW5faXNzdWVzX2NvdW50XCI6IDEsXG4gICAgICAgIFwibGljZW5zZVwiOiBudWxsLFxuICAgICAgICBcImFsbG93X2ZvcmtpbmdcIjogdHJ1ZSxcbiAgICAgICAgXCJpc190ZW1wbGF0ZVwiOiBmYWxzZSxcbiAgICAgICAgXCJ3ZWJfY29tbWl0X3NpZ25vZmZfcmVxdWlyZWRcIjogZmFsc2UsXG4gICAgICAgIFwidG9waWNzXCI6IFtdLFxuICAgICAgICBcInZpc2liaWxpdHlcIjogXCJwdWJsaWNcIixcbiAgICAgICAgXCJmb3Jrc1wiOiAwLFxuICAgICAgICBcIm9wZW5faXNzdWVzXCI6IDEsXG4gICAgICAgIFwid2F0Y2hlcnNcIjogMCxcbiAgICAgICAgXCJkZWZhdWx0X2JyYW5jaFwiOiBcIm1haW5cIixcbiAgICAgICAgXCJhbGxvd19zcXVhc2hfbWVyZ2VcIjogdHJ1ZSxcbiAgICAgICAgXCJhbGxvd19tZXJnZV9jb21taXRcIjogdHJ1ZSxcbiAgICAgICAgXCJhbGxvd19yZWJhc2VfbWVyZ2VcIjogdHJ1ZSxcbiAgICAgICAgXCJhbGxvd19hdXRvX21lcmdlXCI6IGZhbHNlLFxuICAgICAgICBcImRlbGV0ZV9icmFuY2hfb25fbWVyZ2VcIjogZmFsc2UsXG4gICAgICAgIFwiYWxsb3dfdXBkYXRlX2JyYW5jaFwiOiBmYWxzZSxcbiAgICAgICAgXCJ1c2Vfc3F1YXNoX3ByX3RpdGxlX2FzX2RlZmF1bHRcIjogZmFsc2UsXG4gICAgICAgIFwic3F1YXNoX21lcmdlX2NvbW1pdF9tZXNzYWdlXCI6IFwiQ09NTUlUX01FU1NBR0VTXCIsXG4gICAgICAgIFwic3F1YXNoX21lcmdlX2NvbW1pdF90aXRsZVwiOiBcIkNPTU1JVF9PUl9QUl9USVRMRVwiLFxuICAgICAgICBcIm1lcmdlX2NvbW1pdF9tZXNzYWdlXCI6IFwiUFJfVElUTEVcIixcbiAgICAgICAgXCJtZXJnZV9jb21taXRfdGl0bGVcIjogXCJNRVJHRV9NRVNTQUdFXCJcbiAgICAgIH1cbiAgICB9LFxuICAgIFwiX2xpbmtzXCI6IHtcbiAgICAgIFwic2VsZlwiOiB7XG4gICAgICAgIFwiaHJlZlwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvcHVsbHMvMVwiXG4gICAgICB9LFxuICAgICAgXCJodG1sXCI6IHtcbiAgICAgICAgXCJocmVmXCI6IFwiaHR0cHM6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXIvZGFiYmxlL3B1bGwvMVwiXG4gICAgICB9LFxuICAgICAgXCJpc3N1ZVwiOiB7XG4gICAgICAgIFwiaHJlZlwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvaXNzdWVzLzFcIlxuICAgICAgfSxcbiAgICAgIFwiY29tbWVudHNcIjoge1xuICAgICAgICBcImhyZWZcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2lzc3Vlcy8xL2NvbW1lbnRzXCJcbiAgICAgIH0sXG4gICAgICBcInJldmlld19jb21tZW50c1wiOiB7XG4gICAgICAgIFwiaHJlZlwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvcHVsbHMvMS9jb21tZW50c1wiXG4gICAgICB9LFxuICAgICAgXCJyZXZpZXdfY29tbWVudFwiOiB7XG4gICAgICAgIFwiaHJlZlwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvcHVsbHMvY29tbWVudHN7L251bWJlcn1cIlxuICAgICAgfSxcbiAgICAgIFwiY29tbWl0c1wiOiB7XG4gICAgICAgIFwiaHJlZlwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvcHVsbHMvMS9jb21taXRzXCJcbiAgICAgIH0sXG4gICAgICBcInN0YXR1c2VzXCI6IHtcbiAgICAgICAgXCJocmVmXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9zdGF0dXNlcy81NzAzODQyY2M1NzE1ZWQxZTM1OGQyM2ViYjY5M2RiMDk3NDdhZTliXCJcbiAgICAgIH1cbiAgICB9LFxuICAgIFwiYXV0aG9yX2Fzc29jaWF0aW9uXCI6IFwiT1dORVJcIixcbiAgICBcImF1dG9fbWVyZ2VcIjogbnVsbCxcbiAgICBcImFjdGl2ZV9sb2NrX3JlYXNvblwiOiBudWxsLFxuICAgIFwibWVyZ2VkXCI6IGZhbHNlLFxuICAgIFwibWVyZ2VhYmxlXCI6IG51bGwsXG4gICAgXCJyZWJhc2VhYmxlXCI6IG51bGwsXG4gICAgXCJtZXJnZWFibGVfc3RhdGVcIjogXCJ1bmtub3duXCIsXG4gICAgXCJtZXJnZWRfYnlcIjogbnVsbCxcbiAgICBcImNvbW1lbnRzXCI6IDAsXG4gICAgXCJyZXZpZXdfY29tbWVudHNcIjogMCxcbiAgICBcIm1haW50YWluZXJfY2FuX21vZGlmeVwiOiBmYWxzZSxcbiAgICBcImNvbW1pdHNcIjogMSxcbiAgICBcImFkZGl0aW9uc1wiOiAxLFxuICAgIFwiZGVsZXRpb25zXCI6IDEsXG4gICAgXCJjaGFuZ2VkX2ZpbGVzXCI6IDFcbiAgfSxcbiAgXCJyZXBvc2l0b3J5XCI6IHtcbiAgICBcImlkXCI6IDQ3MDIxMjAwMyxcbiAgICBcIm5vZGVfaWRcIjogXCJSX2tnRE9IQWJkb3dcIixcbiAgICBcIm5hbWVcIjogXCJkYWJibGVcIixcbiAgICBcImZ1bGxfbmFtZVwiOiBcImJpbndpZWRlcmhpZXIvZGFiYmxlXCIsXG4gICAgXCJwcml2YXRlXCI6IGZhbHNlLFxuICAgIFwib3duZXJcIjoge1xuICAgICAgXCJsb2dpblwiOiBcImJpbndpZWRlcmhpZXJcIixcbiAgICAgIFwiaWRcIjogNjY0NTk3LFxuICAgICAgXCJub2RlX2lkXCI6IFwiTURRNlZYTmxjalkyTkRVNU53PT1cIixcbiAgICAgIFwiYXZhdGFyX3VybFwiOiBcImh0dHBzOi8vYXZhdGFycy5naXRodWJ1c2VyY29udGVudC5jb20vdS82NjQ1OTc/dj00XCIsXG4gICAgICBcImdyYXZhdGFyX2lkXCI6IFwiXCIsXG4gICAgICBcInVybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllclwiLFxuICAgICAgXCJodG1sX3VybFwiOiBcImh0dHBzOi8vZ2l0aHViLmNvbS9iaW53aWVkZXJoaWVyXCIsXG4gICAgICBcImZvbGxvd2Vyc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZm9sbG93ZXJzXCIsXG4gICAgICBcImZvbGxvd2luZ191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZm9sbG93aW5ney9vdGhlcl91c2VyfVwiLFxuICAgICAgXCJnaXN0c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZ2lzdHN7L2dpc3RfaWR9XCIsXG4gICAgICBcInN0YXJyZWRfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL3N0YXJyZWR7L293bmVyfXsvcmVwb31cIixcbiAgICAgIFwic3Vic2NyaXB0aW9uc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvc3Vic2NyaXB0aW9uc1wiLFxuICAgICAgXCJvcmdhbml6YXRpb25zX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9vcmdzXCIsXG4gICAgICBcInJlcG9zX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9yZXBvc1wiLFxuICAgICAgXCJldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL2V2ZW50c3svcHJpdmFjeX1cIixcbiAgICAgIFwicmVjZWl2ZWRfZXZlbnRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9yZWNlaXZlZF9ldmVudHNcIixcbiAgICAgIFwidHlwZVwiOiBcIlVzZXJcIixcbiAgICAgIFwic2l0ZV9hZG1pblwiOiBmYWxzZVxuICAgIH0sXG4gICAgXCJodG1sX3VybFwiOiBcImh0dHBzOi8vZ2l0aHViLmNvbS9iaW53aWVkZXJoaWVyL2RhYmJsZVwiLFxuICAgIFwiZGVzY3JpcHRpb25cIjogXCJBIHJlcG8gZm9yIGRhYmJsaW5nXCIsXG4gICAgXCJmb3JrXCI6IGZhbHNlLFxuICAgIFwidXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZVwiLFxuICAgIFwiZm9ya3NfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9mb3Jrc1wiLFxuICAgIFwia2V5c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2tleXN7L2tleV9pZH1cIixcbiAgICBcImNvbGxhYm9yYXRvcnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9jb2xsYWJvcmF0b3Jzey9jb2xsYWJvcmF0b3J9XCIsXG4gICAgXCJ0ZWFtc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3RlYW1zXCIsXG4gICAgXCJob29rc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2hvb2tzXCIsXG4gICAgXCJpc3N1ZV9ldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9pc3N1ZXMvZXZlbnRzey9udW1iZXJ9XCIsXG4gICAgXCJldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9ldmVudHNcIixcbiAgICBcImFzc2lnbmVlc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2Fzc2lnbmVlc3svdXNlcn1cIixcbiAgICBcImJyYW5jaGVzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvYnJhbmNoZXN7L2JyYW5jaH1cIixcbiAgICBcInRhZ3NfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS90YWdzXCIsXG4gICAgXCJibG9ic191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2dpdC9ibG9ic3svc2hhfVwiLFxuICAgIFwiZ2l0X3RhZ3NfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9naXQvdGFnc3svc2hhfVwiLFxuICAgIFwiZ2l0X3JlZnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9naXQvcmVmc3svc2hhfVwiLFxuICAgIFwidHJlZXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9naXQvdHJlZXN7L3NoYX1cIixcbiAgICBcInN0YXR1c2VzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvc3RhdHVzZXMve3NoYX1cIixcbiAgICBcImxhbmd1YWdlc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2xhbmd1YWdlc1wiLFxuICAgIFwic3RhcmdhemVyc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3N0YXJnYXplcnNcIixcbiAgICBcImNvbnRyaWJ1dG9yc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2NvbnRyaWJ1dG9yc1wiLFxuICAgIFwic3Vic2NyaWJlcnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9zdWJzY3JpYmVyc1wiLFxuICAgIFwic3Vic2NyaXB0aW9uX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvc3Vic2NyaXB0aW9uXCIsXG4gICAgXCJjb21taXRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvY29tbWl0c3svc2hhfVwiLFxuICAgIFwiZ2l0X2NvbW1pdHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9naXQvY29tbWl0c3svc2hhfVwiLFxuICAgIFwiY29tbWVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9jb21tZW50c3svbnVtYmVyfVwiLFxuICAgIFwiaXNzdWVfY29tbWVudF91cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2lzc3Vlcy9jb21tZW50c3svbnVtYmVyfVwiLFxuICAgIFwiY29udGVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9jb250ZW50cy97K3BhdGh9XCIsXG4gICAgXCJjb21wYXJlX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvY29tcGFyZS97YmFzZX0uLi57aGVhZH1cIixcbiAgICBcIm1lcmdlc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL21lcmdlc1wiLFxuICAgIFwiYXJjaGl2ZV91cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3thcmNoaXZlX2Zvcm1hdH17L3JlZn1cIixcbiAgICBcImRvd25sb2Fkc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2Rvd25sb2Fkc1wiLFxuICAgIFwiaXNzdWVzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvaXNzdWVzey9udW1iZXJ9XCIsXG4gICAgXCJwdWxsc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3B1bGxzey9udW1iZXJ9XCIsXG4gICAgXCJtaWxlc3RvbmVzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvbWlsZXN0b25lc3svbnVtYmVyfVwiLFxuICAgIFwibm90aWZpY2F0aW9uc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL25vdGlmaWNhdGlvbnN7P3NpbmNlLGFsbCxwYXJ0aWNpcGF0aW5nfVwiLFxuICAgIFwibGFiZWxzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvbGFiZWxzey9uYW1lfVwiLFxuICAgIFwicmVsZWFzZXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9yZWxlYXNlc3svaWR9XCIsXG4gICAgXCJkZXBsb3ltZW50c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2RlcGxveW1lbnRzXCIsXG4gICAgXCJjcmVhdGVkX2F0XCI6IFwiMjAyMi0wMy0xNVQxNTowNjoxN1pcIixcbiAgICBcInVwZGF0ZWRfYXRcIjogXCIyMDIyLTAzLTE1VDE1OjA2OjE3WlwiLFxuICAgIFwicHVzaGVkX2F0XCI6IFwiMjAyNC0wMy0yMVQwMjo1MjoxMFpcIixcbiAgICBcImdpdF91cmxcIjogXCJnaXQ6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXIvZGFiYmxlLmdpdFwiLFxuICAgIFwic3NoX3VybFwiOiBcImdpdEBnaXRodWIuY29tOmJpbndpZWRlcmhpZXIvZGFiYmxlLmdpdFwiLFxuICAgIFwiY2xvbmVfdXJsXCI6IFwiaHR0cHM6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXIvZGFiYmxlLmdpdFwiLFxuICAgIFwic3ZuX3VybFwiOiBcImh0dHBzOi8vZ2l0aHViLmNvbS9iaW53aWVkZXJoaWVyL2RhYmJsZVwiLFxuICAgIFwiaG9tZXBhZ2VcIjogbnVsbCxcbiAgICBcInNpemVcIjogMSxcbiAgICBcInN0YXJnYXplcnNfY291bnRcIjogMCxcbiAgICBcIndhdGNoZXJzX2NvdW50XCI6IDAsXG4gICAgXCJsYW5ndWFnZVwiOiBudWxsLFxuICAgIFwiaGFzX2lzc3Vlc1wiOiB0cnVlLFxuICAgIFwiaGFzX3Byb2plY3RzXCI6IHRydWUsXG4gICAgXCJoYXNfZG93bmxvYWRzXCI6IHRydWUsXG4gICAgXCJoYXNfd2lraVwiOiB0cnVlLFxuICAgIFwiaGFzX3BhZ2VzXCI6IGZhbHNlLFxuICAgIFwiaGFzX2Rpc2N1c3Npb25zXCI6IGZhbHNlLFxuICAgIFwiZm9ya3NfY291bnRcIjogMCxcbiAgICBcIm1pcnJvcl91cmxcIjogbnVsbCxcbiAgICBcImFyY2hpdmVkXCI6IGZhbHNlLFxuICAgIFwiZGlzYWJsZWRcIjogZmFsc2UsXG4gICAgXCJvcGVuX2lzc3Vlc19jb3VudFwiOiAxLFxuICAgIFwibGljZW5zZVwiOiBudWxsLFxuICAgIFwiYWxsb3dfZm9ya2luZ1wiOiB0cnVlLFxuICAgIFwiaXNfdGVtcGxhdGVcIjogZmFsc2UsXG4gICAgXCJ3ZWJfY29tbWl0X3NpZ25vZmZfcmVxdWlyZWRcIjogZmFsc2UsXG4gICAgXCJ0b3BpY3NcIjogW10sXG4gICAgXCJ2aXNpYmlsaXR5XCI6IFwicHVibGljXCIsXG4gICAgXCJmb3Jrc1wiOiAwLFxuICAgIFwib3Blbl9pc3N1ZXNcIjogMSxcbiAgICBcIndhdGNoZXJzXCI6IDAsXG4gICAgXCJkZWZhdWx0X2JyYW5jaFwiOiBcIm1haW5cIlxuICB9LFxuICBcInNlbmRlclwiOiB7XG4gICAgXCJsb2dpblwiOiBcImJpbndpZWRlcmhpZXJcIixcbiAgICBcImlkXCI6IDY2NDU5NyxcbiAgICBcIm5vZGVfaWRcIjogXCJNRFE2VlhObGNqWTJORFU1Tnc9PVwiLFxuICAgIFwiYXZhdGFyX3VybFwiOiBcImh0dHBzOi8vYXZhdGFycy5naXRodWJ1c2VyY29udGVudC5jb20vdS82NjQ1OTc/dj00XCIsXG4gICAgXCJncmF2YXRhcl9pZFwiOiBcIlwiLFxuICAgIFwidXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyXCIsXG4gICAgXCJodG1sX3VybFwiOiBcImh0dHBzOi8vZ2l0aHViLmNvbS9iaW53aWVkZXJoaWVyXCIsXG4gICAgXCJmb2xsb3dlcnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL2ZvbGxvd2Vyc1wiLFxuICAgIFwiZm9sbG93aW5nX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9mb2xsb3dpbmd7L290aGVyX3VzZXJ9XCIsXG4gICAgXCJnaXN0c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZ2lzdHN7L2dpc3RfaWR9XCIsXG4gICAgXCJzdGFycmVkX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9zdGFycmVkey9vd25lcn17L3JlcG99XCIsXG4gICAgXCJzdWJzY3JpcHRpb25zX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9zdWJzY3JpcHRpb25zXCIsXG4gICAgXCJvcmdhbml6YXRpb25zX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9vcmdzXCIsXG4gICAgXCJyZXBvc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvcmVwb3NcIixcbiAgICBcImV2ZW50c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZXZlbnRzey9wcml2YWN5fVwiLFxuICAgIFwicmVjZWl2ZWRfZXZlbnRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9yZWNlaXZlZF9ldmVudHNcIixcbiAgICBcInR5cGVcIjogXCJVc2VyXCIsXG4gICAgXCJzaXRlX2FkbWluXCI6IGZhbHNlXG4gIH1cbn1cbiIsImNvbmZpZyI6eyJ0ZW1wbGF0ZSI6InRleHQiLCJmdWxsU2NyZWVuSFRNTCI6ZmFsc2UsImZ1bmN0aW9ucyI6WyJzcHJpZyJdLCJvcHRpb25zIjpbImxpdmUiXSwiaW5wdXRUeXBlIjoieWFtbCJ9fQ==)) * Loops (e.g. `{{range .errors}}..{{end}}`, see [example](https://repeatit.io/#/share/eyJ0ZW1wbGF0ZSI6IlNldmVyZSBVUkxzOlxue3tyYW5nZSAuZXJyb3JzfX17e2lmIGVxIC5sZXZlbCBcInNldmVyZVwifX0tIHt7LnVybH19XG57e2VuZH19e3tlbmR9fSIsImlucHV0Ijoie1wiZm9vXCI6IFwiYmFyXCIsIFwiZXJyb3JzXCI6IFt7XCJsZXZlbFwiOiBcInNldmVyZVwiLCBcInVybFwiOiBcImh0dHBzOi8vc2V2ZXJlMS5jb21cIn0se1wibGV2ZWxcIjogXCJ3YXJuaW5nXCIsIFwidXJsXCI6IFwiaHR0cHM6Ly93YXJuaW5nLmNvbVwifSx7XCJsZXZlbFwiOiBcInNldmVyZVwiLCBcInVybFwiOiBcImh0dHBzOi8vc2V2ZXJlMi5jb21cIn1dfSIsImNvbmZpZyI6eyJ0ZW1wbGF0ZSI6InRleHQiLCJmdWxsU2NyZWVuSFRNTCI6ZmFsc2UsImZ1bmN0aW9ucyI6WyJzcHJpZyJdLCJvcHRpb25zIjpbImxpdmUiXSwiaW5wdXRUeXBlIjoieWFtbCJ9fQ==)) A good way to experiment with Go templates is the **[Go Template Playground](https://repeatit.io)**. It is _highly recommended_ to test your templates there first ([example for Grafana alert](https://repeatit.io/#/share/eyJ0ZW1wbGF0ZSI6InRpdGxlPUdyYWZhbmErYWxlcnQ6K3t7LnRpdGxlfX0mbWVzc2FnZT17ey5tZXNzYWdlfX0iLCJpbnB1dCI6IntcbiAgXCJyZWNlaXZlclwiOiBcIm50ZnlcXFxcLmV4YW1wbGVcXFxcLmNvbS9hbGVydHNcIixcbiAgXCJzdGF0dXNcIjogXCJyZXNvbHZlZFwiLFxuICBcImFsZXJ0c1wiOiBbXG4gICAge1xuICAgICAgXCJzdGF0dXNcIjogXCJyZXNvbHZlZFwiLFxuICAgICAgXCJsYWJlbHNcIjoge1xuICAgICAgICBcImFsZXJ0bmFtZVwiOiBcIkxvYWQgYXZnIDE1bSB0b28gaGlnaFwiLFxuICAgICAgICBcImdyYWZhbmFfZm9sZGVyXCI6IFwiTm9kZSBhbGVydHNcIixcbiAgICAgICAgXCJpbnN0YW5jZVwiOiBcIjEwLjEwOC4wLjI6OTEwMFwiLFxuICAgICAgICBcImpvYlwiOiBcIm5vZGUtZXhwb3J0ZXJcIlxuICAgICAgfSxcbiAgICAgIFwiYW5ub3RhdGlvbnNcIjoge1xuICAgICAgICBcInN1bW1hcnlcIjogXCIxNW0gbG9hZCBhdmVyYWdlIHRvbyBoaWdoXCJcbiAgICAgIH0sXG4gICAgICBcInN0YXJ0c0F0XCI6IFwiMjAyNC0wMy0xNVQwMjoyODowMFpcIixcbiAgICAgIFwiZW5kc0F0XCI6IFwiMjAyNC0wMy0xNVQwMjo0MjowMFpcIixcbiAgICAgIFwiZ2VuZXJhdG9yVVJMXCI6IFwibG9jYWxob3N0OjMwMDAvYWxlcnRpbmcvZ3JhZmFuYS9OVzlvRHctNHovdmlld1wiLFxuICAgICAgXCJmaW5nZXJwcmludFwiOiBcImJlY2JmYjk0YmQ4MWVmNDhcIixcbiAgICAgIFwic2lsZW5jZVVSTFwiOiBcImxvY2FsaG9zdDozMDAwL2FsZXJ0aW5nL3NpbGVuY2UvbmV3P2FsZXJ0bWFuYWdlcj1ncmFmYW5hJm1hdGNoZXI9YWxlcnRuYW1lJTNETG9hZCthdmcrMTVtK3RvbytoaWdoJm1hdGNoZXI9Z3JhZmFuYV9mb2xkZXIlM0ROb2RlK2FsZXJ0cyZtYXRjaGVyPWluc3RhbmNlJTNEMTAuMTA4LjAuMiUzQTkxMDAmbWF0Y2hlcj1qb2IlM0Rub2RlLWV4cG9ydGVyXCIsXG4gICAgICBcImRhc2hib2FyZFVSTFwiOiBcIlwiLFxuICAgICAgXCJwYW5lbFVSTFwiOiBcIlwiLFxuICAgICAgXCJ2YWx1ZXNcIjoge1xuICAgICAgICBcIkJcIjogMTguOTgyMTEzMTQ0NzU4NzYsXG4gICAgICAgIFwiQ1wiOiAwXG4gICAgICB9LFxuICAgICAgXCJ2YWx1ZVN0cmluZ1wiOiBcIlsgdmFyPSdCJyBsYWJlbHM9e19fbmFtZV9fPW5vZGVfbG9hZDE1LCBpbnN0YW5jZT0xMC4xMDguMC4yOjkxMDAsIGpvYj1ub2RlLWV4cG9ydGVyfSB2YWx1ZT0xOC45ODIxMTMxNDQ3NTg3NiBdLCBbIHZhcj0nQycgbGFiZWxzPXtfX25hbWVfXz1ub2RlX2xvYWQxNSwgaW5zdGFuY2U9MTAuMTA4LjAuMjo5MTAwLCBqb2I9bm9kZS1leHBvcnRlcn0gdmFsdWU9MCBdXCJcbiAgICB9XG4gIF0sXG4gIFwiZ3JvdXBMYWJlbHNcIjoge1xuICAgIFwiYWxlcnRuYW1lXCI6IFwiTG9hZCBhdmcgMTVtIHRvbyBoaWdoXCIsXG4gICAgXCJncmFmYW5hX2ZvbGRlclwiOiBcIk5vZGUgYWxlcnRzXCJcbiAgfSxcbiAgXCJjb21tb25MYWJlbHNcIjoge1xuICAgIFwiYWxlcnRuYW1lXCI6IFwiTG9hZCBhdmcgMTVtIHRvbyBoaWdoXCIsXG4gICAgXCJncmFmYW5hX2ZvbGRlclwiOiBcIk5vZGUgYWxlcnRzXCIsXG4gICAgXCJpbnN0YW5jZVwiOiBcIjEwLjEwOC4wLjI6OTEwMFwiLFxuICAgIFwiam9iXCI6IFwibm9kZS1leHBvcnRlclwiXG4gIH0sXG4gIFwiY29tbW9uQW5ub3RhdGlvbnNcIjoge1xuICAgIFwic3VtbWFyeVwiOiBcIjE1bSBsb2FkIGF2ZXJhZ2UgdG9vIGhpZ2hcIlxuICB9LFxuICBcImV4dGVybmFsVVJMXCI6IFwibG9jYWxob3N0OjMwMDAvXCIsXG4gIFwidmVyc2lvblwiOiBcIjFcIixcbiAgXCJncm91cEtleVwiOiBcInt9OnthbGVydG5hbWU9XFxcIkxvYWQgYXZnIDE1bSB0b28gaGlnaFxcXCIsIGdyYWZhbmFfZm9sZGVyPVxcXCJOb2RlIGFsZXJ0c1xcXCJ9XCIsXG4gIFwidHJ1bmNhdGVkQWxlcnRzXCI6IDAsXG4gIFwib3JnSWRcIjogMSxcbiAgXCJ0aXRsZVwiOiBcIltSRVNPTFZFRF0gTG9hZCBhdmcgMTVtIHRvbyBoaWdoIE5vZGUgYWxlcnRzICgxMC4xMDguMC4yOjkxMDAgbm9kZS1leHBvcnRlcilcIixcbiAgXCJzdGF0ZVwiOiBcIm9rXCIsXG4gIFwibWVzc2FnZVwiOiBcIioqUmVzb2x2ZWQqKlxcblxcblZhbHVlOiBCPTE4Ljk4MjExMzE0NDc1ODc2LCBDPTBcXG5MYWJlbHM6XFxuIC0gYWxlcnRuYW1lID0gTG9hZCBhdmcgMTVtIHRvbyBoaWdoXFxuIC0gZ3JhZmFuYV9mb2xkZXIgPSBOb2RlIGFsZXJ0c1xcbiAtIGluc3RhbmNlID0gMTAuMTA4LjAuMjo5MTAwXFxuIC0gam9iID0gbm9kZS1leHBvcnRlclxcbkFubm90YXRpb25zOlxcbiAtIHN1bW1hcnkgPSAxNW0gbG9hZCBhdmVyYWdlIHRvbyBoaWdoXFxuU291cmNlOiBsb2NhbGhvc3Q6MzAwMC9hbGVydGluZy9ncmFmYW5hL05XOW9Edy00ei92aWV3XFxuU2lsZW5jZTogbG9jYWxob3N0OjMwMDAvYWxlcnRpbmcvc2lsZW5jZS9uZXc/YWxlcnRtYW5hZ2VyPWdyYWZhbmEmbWF0Y2hlcj1hbGVydG5hbWUlM0RMb2FkK2F2ZysxNW0rdG9vK2hpZ2gmbWF0Y2hlcj1ncmFmYW5hX2ZvbGRlciUzRE5vZGUrYWxlcnRzJm1hdGNoZXI9aW5zdGFuY2UlM0QxMC4xMDguMC4yJTNBOTEwMCZtYXRjaGVyPWpvYiUzRG5vZGUtZXhwb3J0ZXJcXG5cIlxufVxuIiwiY29uZmlnIjp7InRlbXBsYXRlIjoidGV4dCIsImZ1bGxTY3JlZW5IVE1MIjpmYWxzZSwiZnVuY3Rpb25zIjpbInNwcmlnIl0sIm9wdGlvbnMiOlsibGl2ZSJdLCJpbnB1dFR5cGUiOiJ5YW1sIn19)). +ntfy supports a subset of the Sprig template functions that are included in the **[Go Template Playground](https://repeatit.io)**. Please see +[Template Functions](sprig.md) for a list of supported template functions. + !!! info Please note that the Go templating language is quite terrible. My apologies for using it for this feature. It is the best option for Go-based programs like ntfy. Stay calm and don't harm yourself or others in despair. **You can do it. I believe in you!** diff --git a/docs/releases.md b/docs/releases.md index 0877527e..ed728fcb 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1440,6 +1440,7 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release * Full [IPv6 support](config.md#ipv6-support) for ntfy and the official ntfy.sh server ([#519](https://github.com/binwiederhier/ntfy/issues/519)/[#1380](https://github.com/binwiederhier/ntfy/pull/1380)/[ansible#4](https://github.com/binwiederhier/ntfy-ansible/pull/4)) * Support `X-Client-IP`, `X-Real-IP`, `Forwarded` headers for [rate limiting](config.md#ip-based-rate-limiting) via `proxy-forwarded-header` and `proxy-trusted-hosts` ([#1360](https://github.com/binwiederhier/ntfy/pull/1360)/[#1252](https://github.com/binwiederhier/ntfy/pull/1252), thanks to [@pixitha](https://github.com/pixitha)) * Add STDIN support for `ntfy publish` ([#1382](https://github.com/binwiederhier/ntfy/pull/1382), thanks to [@srevn](https://github.com/srevn)) +* You can now use [Slim-Sprig](https://github.com/go-task/slim-sprig) functions in message/title templates ([#1121](https://github.com/binwiederhier/ntfy/issues/1121), thanks to [@davidatkinsondoyle](https://github.com/davidatkinsondoyle) for reporting and to [@wunter8](https://github.com/wunter8) for implementing) **Languages** diff --git a/docs/sprig.md b/docs/sprig.md new file mode 100644 index 00000000..be4e6c9c --- /dev/null +++ b/docs/sprig.md @@ -0,0 +1,24 @@ +# Template Functions + +ntfy includes a (reduced) version of [Sprig](https://github.com/Masterminds/sprig) to add functions that can be used +when you are using the [message template](publish.md#message-templating) feature. + +Below are the functions that are available to use inside your message/title templates. + +* [String Functions](./sprig/strings.md): `trim`, `trunc`, `substr`, `plural`, etc. + * [String List Functions](./sprig/string_slice.md): `splitList`, `sortAlpha`, etc. +* [Integer Math Functions](./sprig/math.md): `add`, `max`, `mul`, etc. + * [Integer List Functions](./sprig/integer_slice.md): `until`, `untilStep` +* [Date Functions](./sprig/date.md): `now`, `date`, etc. +* [Defaults Functions](./sprig/defaults.md): `default`, `empty`, `coalesce`, `fromJSON`, `toJSON`, `toPrettyJSON`, `toRawJSON`, `ternary` +* [Encoding Functions](./sprig/encoding.md): `b64enc`, `b64dec`, etc. +* [Lists and List Functions](./sprig/lists.md): `list`, `first`, `uniq`, etc. +* [Dictionaries and Dict Functions](./sprig/dicts.md): `get`, `set`, `dict`, `hasKey`, `pluck`, `dig`, etc. +* [Type Conversion Functions](./sprig/conversion.md): `atoi`, `int64`, `toString`, etc. +* [Path and Filepath Functions](./sprig/paths.md): `base`, `dir`, `ext`, `clean`, `isAbs`, `osBase`, `osDir`, `osExt`, `osClean`, `osIsAbs` +* [Flow Control Functions](./sprig/flow_control.md): `fail` +* Advanced Functions + * [UUID Functions](./sprig/uuid.md): `uuidv4` + * [Reflection](./sprig/reflection.md): `typeOf`, `kindIs`, `typeIsLike`, etc. + * [Cryptographic and Security Functions](./sprig/crypto.md): `sha256sum`, etc. + * [URL](./sprig/url.md): `urlParse`, `urlJoin` diff --git a/docs/sprig/conversion.md b/docs/sprig/conversion.md new file mode 100644 index 00000000..af952682 --- /dev/null +++ b/docs/sprig/conversion.md @@ -0,0 +1,36 @@ +# Type Conversion Functions + +The following type conversion functions are provided by Sprig: + +- `atoi`: Convert a string to an integer. +- `float64`: Convert to a `float64`. +- `int`: Convert to an `int` at the system's width. +- `int64`: Convert to an `int64`. +- `toDecimal`: Convert a unix octal to a `int64`. +- `toString`: Convert to a string. +- `toStrings`: Convert a list, slice, or array to a list of strings. + +Only `atoi` requires that the input be a specific type. The others will attempt +to convert from any type to the destination type. For example, `int64` can convert +floats to ints, and it can also convert strings to ints. + +## toStrings + +Given a list-like collection, produce a slice of strings. + +``` +list 1 2 3 | toStrings +``` + +The above converts `1` to `"1"`, `2` to `"2"`, and so on, and then returns +them as a list. + +## toDecimal + +Given a unix octal permission, produce a decimal. + +``` +"0777" | toDecimal +``` + +The above converts `0777` to `511` and returns the value as an int64. diff --git a/docs/sprig/crypto.md b/docs/sprig/crypto.md new file mode 100644 index 00000000..c66a269d --- /dev/null +++ b/docs/sprig/crypto.md @@ -0,0 +1,41 @@ +# Cryptographic and Security Functions + +Sprig provides a couple of advanced cryptographic functions. + +## sha1sum + +The `sha1sum` function receives a string, and computes it's SHA1 digest. + +``` +sha1sum "Hello world!" +``` + +## sha256sum + +The `sha256sum` function receives a string, and computes it's SHA256 digest. + +``` +sha256sum "Hello world!" +``` + +The above will compute the SHA 256 sum in an "ASCII armored" format that is +safe to print. + +## sha512sum + +The `sha512sum` function receives a string, and computes it's SHA512 digest. + +``` +sha512sum "Hello world!" +``` + +The above will compute the SHA 512 sum in an "ASCII armored" format that is +safe to print. + +## adler32sum + +The `adler32sum` function receives a string, and computes its Adler-32 checksum. + +``` +adler32sum "Hello world!" +``` diff --git a/docs/sprig/date.md b/docs/sprig/date.md new file mode 100644 index 00000000..7410c08d --- /dev/null +++ b/docs/sprig/date.md @@ -0,0 +1,126 @@ +# Date Functions + +## now + +The current date/time. Use this in conjunction with other date functions. + +## ago + +The `ago` function returns duration from time.Now in seconds resolution. + +``` +ago .CreatedAt +``` + +returns in `time.Duration` String() format + +``` +2h34m7s +``` + +## date + +The `date` function formats a date. + +Format the date to YEAR-MONTH-DAY: + +``` +now | date "2006-01-02" +``` + +Date formatting in Go is a [little bit different](https://pauladamsmith.com/blog/2011/05/go_time.html). + +In short, take this as the base date: + +``` +Mon Jan 2 15:04:05 MST 2006 +``` + +Write it in the format you want. Above, `2006-01-02` is the same date, but +in the format we want. + +## dateInZone + +Same as `date`, but with a timezone. + +``` +dateInZone "2006-01-02" (now) "UTC" +``` + +## duration + +Formats a given amount of seconds as a `time.Duration`. + +This returns 1m35s + +``` +duration "95" +``` + +## durationRound + +Rounds a given duration to the most significant unit. Strings and `time.Duration` +gets parsed as a duration, while a `time.Time` is calculated as the duration since. + +This return 2h + +``` +durationRound "2h10m5s" +``` + +This returns 3mo + +``` +durationRound "2400h10m5s" +``` + +## unixEpoch + +Returns the seconds since the unix epoch for a `time.Time`. + +``` +now | unixEpoch +``` + +## dateModify, mustDateModify + +The `dateModify` takes a modification and a date and returns the timestamp. + +Subtract an hour and thirty minutes from the current time: + +``` +now | date_modify "-1.5h" +``` + +If the modification format is wrong `dateModify` will return the date unmodified. `mustDateModify` will return an error otherwise. + +## htmlDate + +The `htmlDate` function formats a date for inserting into an HTML date picker +input field. + +``` +now | htmlDate +``` + +## htmlDateInZone + +Same as htmlDate, but with a timezone. + +``` +htmlDateInZone (now) "UTC" +``` + +## toDate, mustToDate + +`toDate` converts a string to a date. The first argument is the date layout and +the second the date string. If the string can't be convert it returns the zero +value. +`mustToDate` will return an error in case the string cannot be converted. + +This is useful when you want to convert a string date to another format +(using pipe). The example below converts "2017-12-31" to "31/12/2017". + +``` +toDate "2006-01-02" "2017-12-31" | date "02/01/2006" +``` diff --git a/docs/sprig/defaults.md b/docs/sprig/defaults.md new file mode 100644 index 00000000..b8af1455 --- /dev/null +++ b/docs/sprig/defaults.md @@ -0,0 +1,169 @@ +# Default Functions + +Sprig provides tools for setting default values for templates. + +## default + +To set a simple default value, use `default`: + +``` +default "foo" .Bar +``` + +In the above, if `.Bar` evaluates to a non-empty value, it will be used. But if +it is empty, `foo` will be returned instead. + +The definition of "empty" depends on type: + +- Numeric: 0 +- String: "" +- Lists: `[]` +- Dicts: `{}` +- Boolean: `false` +- And always `nil` (aka null) + +For structs, there is no definition of empty, so a struct will never return the +default. + +## empty + +The `empty` function returns `true` if the given value is considered empty, and +`false` otherwise. The empty values are listed in the `default` section. + +``` +empty .Foo +``` + +Note that in Go template conditionals, emptiness is calculated for you. Thus, +you rarely need `if empty .Foo`. Instead, just use `if .Foo`. + +## coalesce + +The `coalesce` function takes a list of values and returns the first non-empty +one. + +``` +coalesce 0 1 2 +``` + +The above returns `1`. + +This function is useful for scanning through multiple variables or values: + +``` +coalesce .name .parent.name "Matt" +``` + +The above will first check to see if `.name` is empty. If it is not, it will return +that value. If it _is_ empty, `coalesce` will evaluate `.parent.name` for emptiness. +Finally, if both `.name` and `.parent.name` are empty, it will return `Matt`. + +## all + +The `all` function takes a list of values and returns true if all values are non-empty. + +``` +all 0 1 2 +``` + +The above returns `false`. + +This function is useful for evaluating multiple conditions of variables or values: + +``` +all (eq .Request.TLS.Version 0x0304) (.Request.ProtoAtLeast 2 0) (eq .Request.Method "POST") +``` + +The above will check http.Request is POST with tls 1.3 and http/2. + +## any + +The `any` function takes a list of values and returns true if any value is non-empty. + +``` +any 0 1 2 +``` + +The above returns `true`. + +This function is useful for evaluating multiple conditions of variables or values: + +``` +any (eq .Request.Method "GET") (eq .Request.Method "POST") (eq .Request.Method "OPTIONS") +``` + +The above will check http.Request method is one of GET/POST/OPTIONS. + +## fromJSON, mustFromJSON + +`fromJSON` decodes a JSON document into a structure. If the input cannot be decoded as JSON the function will return an empty string. +`mustFromJSON` will return an error in case the JSON is invalid. + +``` +fromJSON "{\"foo\": 55}" +``` + +## toJSON, mustToJSON + +The `toJSON` function encodes an item into a JSON string. If the item cannot be converted to JSON the function will return an empty string. +`mustToJSON` will return an error in case the item cannot be encoded in JSON. + +``` +toJSON .Item +``` + +The above returns JSON string representation of `.Item`. + +## toPrettyJSON, mustToPrettyJSON + +The `toPrettyJSON` function encodes an item into a pretty (indented) JSON string. + +``` +toPrettyJSON .Item +``` + +The above returns indented JSON string representation of `.Item`. + +## toRawJSON, mustToRawJSON + +The `toRawJSON` function encodes an item into JSON string with HTML characters unescaped. + +``` +toRawJSON .Item +``` + +The above returns unescaped JSON string representation of `.Item`. + +## ternary + +The `ternary` function takes two values, and a test value. If the test value is +true, the first value will be returned. If the test value is empty, the second +value will be returned. This is similar to the c ternary operator. + +### true test value + +``` +ternary "foo" "bar" true +``` + +or + +``` +true | ternary "foo" "bar" +``` + +The above returns `"foo"`. + +### false test value + +``` +ternary "foo" "bar" false +``` + +or + +``` +false | ternary "foo" "bar" +``` + +The above returns `"bar"`. diff --git a/docs/sprig/dicts.md b/docs/sprig/dicts.md new file mode 100644 index 00000000..5a4490d5 --- /dev/null +++ b/docs/sprig/dicts.md @@ -0,0 +1,172 @@ +# Dictionaries and Dict Functions + +Sprig provides a key/value storage type called a `dict` (short for "dictionary", +as in Python). A `dict` is an _unorder_ type. + +The key to a dictionary **must be a string**. However, the value can be any +type, even another `dict` or `list`. + +Unlike `list`s, `dict`s are not immutable. The `set` and `unset` functions will +modify the contents of a dictionary. + +## dict + +Creating dictionaries is done by calling the `dict` function and passing it a +list of pairs. + +The following creates a dictionary with three items: + +``` +$myDict := dict "name1" "value1" "name2" "value2" "name3" "value 3" +``` + +## get + +Given a map and a key, get the value from the map. + +``` +get $myDict "name1" +``` + +The above returns `"value1"` + +Note that if the key is not found, this operation will simply return `""`. No error +will be generated. + +## set + +Use `set` to add a new key/value pair to a dictionary. + +``` +$_ := set $myDict "name4" "value4" +``` + +Note that `set` _returns the dictionary_ (a requirement of Go template functions), +so you may need to trap the value as done above with the `$_` assignment. + +## unset + +Given a map and a key, delete the key from the map. + +``` +$_ := unset $myDict "name4" +``` + +As with `set`, this returns the dictionary. + +Note that if the key is not found, this operation will simply return. No error +will be generated. + +## hasKey + +The `hasKey` function returns `true` if the given dict contains the given key. + +``` +hasKey $myDict "name1" +``` + +If the key is not found, this returns `false`. + +## pluck + +The `pluck` function makes it possible to give one key and multiple maps, and +get a list of all of the matches: + +``` +pluck "name1" $myDict $myOtherDict +``` + +The above will return a `list` containing every found value (`[value1 otherValue1]`). + +If the give key is _not found_ in a map, that map will not have an item in the +list (and the length of the returned list will be less than the number of dicts +in the call to `pluck`. + +If the key is _found_ but the value is an empty value, that value will be +inserted. + +A common idiom in Sprig templates is to uses `pluck... | first` to get the first +matching key out of a collection of dictionaries. + +## dig + +The `dig` function traverses a nested set of dicts, selecting keys from a list +of values. It returns a default value if any of the keys are not found at the +associated dict. + +``` +dig "user" "role" "humanName" "guest" $dict +``` + +Given a dict structured like +``` +{ + user: { + role: { + humanName: "curator" + } + } +} +``` + +the above would return `"curator"`. If the dict lacked even a `user` field, +the result would be `"guest"`. + +Dig can be very useful in cases where you'd like to avoid guard clauses, +especially since Go's template package's `and` doesn't shortcut. For instance +`and a.maybeNil a.maybeNil.iNeedThis` will always evaluate +`a.maybeNil.iNeedThis`, and panic if `a` lacks a `maybeNil` field.) + +`dig` accepts its dict argument last in order to support pipelining. + +## keys + +The `keys` function will return a `list` of all of the keys in one or more `dict` +types. Since a dictionary is _unordered_, the keys will not be in a predictable order. +They can be sorted with `sortAlpha`. + +``` +keys $myDict | sortAlpha +``` + +When supplying multiple dictionaries, the keys will be concatenated. Use the `uniq` +function along with `sortAlpha` to get a unqiue, sorted list of keys. + +``` +keys $myDict $myOtherDict | uniq | sortAlpha +``` + +## pick + +The `pick` function selects just the given keys out of a dictionary, creating a +new `dict`. + +``` +$new := pick $myDict "name1" "name2" +``` + +The above returns `{name1: value1, name2: value2}` + +## omit + +The `omit` function is similar to `pick`, except it returns a new `dict` with all +the keys that _do not_ match the given keys. + +``` +$new := omit $myDict "name1" "name3" +``` + +The above returns `{name2: value2}` + +## values + +The `values` function is similar to `keys`, except it returns a new `list` with +all the values of the source `dict` (only one dictionary is supported). + +``` +$vals := values $myDict +``` + +The above returns `list["value1", "value2", "value 3"]`. Note that the `values` +function gives no guarantees about the result ordering- if you care about this, +then use `sortAlpha`. diff --git a/docs/sprig/encoding.md b/docs/sprig/encoding.md new file mode 100644 index 00000000..1c7a36f8 --- /dev/null +++ b/docs/sprig/encoding.md @@ -0,0 +1,6 @@ +# Encoding Functions + +Sprig has the following encoding and decoding functions: + +- `b64enc`/`b64dec`: Encode or decode with Base64 +- `b32enc`/`b32dec`: Encode or decode with Base32 diff --git a/docs/sprig/flow_control.md b/docs/sprig/flow_control.md new file mode 100644 index 00000000..6414640a --- /dev/null +++ b/docs/sprig/flow_control.md @@ -0,0 +1,11 @@ +# Flow Control Functions + +## fail + +Unconditionally returns an empty `string` and an `error` with the specified +text. This is useful in scenarios where other conditionals have determined that +template rendering should fail. + +``` +fail "Please accept the end user license agreement" +``` diff --git a/docs/sprig/integer_slice.md b/docs/sprig/integer_slice.md new file mode 100644 index 00000000..ab4bef6d --- /dev/null +++ b/docs/sprig/integer_slice.md @@ -0,0 +1,41 @@ +# Integer List Functions + +## until + +The `until` function builds a range of integers. + +``` +until 5 +``` + +The above generates the list `[0, 1, 2, 3, 4]`. + +This is useful for looping with `range $i, $e := until 5`. + +## untilStep + +Like `until`, `untilStep` generates a list of counting integers. But it allows +you to define a start, stop, and step: + +``` +untilStep 3 6 2 +``` + +The above will produce `[3 5]` by starting with 3, and adding 2 until it is equal +or greater than 6. This is similar to Python's `range` function. + +## seq + +Works like the bash `seq` command. +* 1 parameter (end) - will generate all counting integers between 1 and `end` inclusive. +* 2 parameters (start, end) - will generate all counting integers between `start` and `end` inclusive incrementing or decrementing by 1. +* 3 parameters (start, step, end) - will generate all counting integers between `start` and `end` inclusive incrementing or decrementing by `step`. + +``` +seq 5 => 1 2 3 4 5 +seq -3 => 1 0 -1 -2 -3 +seq 0 2 => 0 1 2 +seq 2 -2 => 2 1 0 -1 -2 +seq 0 2 10 => 0 2 4 6 8 10 +seq 0 -2 -5 => 0 -2 -4 +``` diff --git a/docs/sprig/lists.md b/docs/sprig/lists.md new file mode 100644 index 00000000..ed8c52b3 --- /dev/null +++ b/docs/sprig/lists.md @@ -0,0 +1,188 @@ +# Lists and List Functions + +Sprig provides a simple `list` type that can contain arbitrary sequential lists +of data. This is similar to arrays or slices, but lists are designed to be used +as immutable data types. + +Create a list of integers: + +``` +$myList := list 1 2 3 4 5 +``` + +The above creates a list of `[1 2 3 4 5]`. + +## first, mustFirst + +To get the head item on a list, use `first`. + +`first $myList` returns `1` + +`first` panics if there is a problem while `mustFirst` returns an error to the +template engine if there is a problem. + +## rest, mustRest + +To get the tail of the list (everything but the first item), use `rest`. + +`rest $myList` returns `[2 3 4 5]` + +`rest` panics if there is a problem while `mustRest` returns an error to the +template engine if there is a problem. + +## last, mustLast + +To get the last item on a list, use `last`: + +`last $myList` returns `5`. This is roughly analogous to reversing a list and +then calling `first`. + +`last` panics if there is a problem while `mustLast` returns an error to the +template engine if there is a problem. + +## initial, mustInitial + +This compliments `last` by returning all _but_ the last element. +`initial $myList` returns `[1 2 3 4]`. + +`initial` panics if there is a problem while `mustInitial` returns an error to the +template engine if there is a problem. + +## append, mustAppend + +Append a new item to an existing list, creating a new list. + +``` +$new = append $myList 6 +``` + +The above would set `$new` to `[1 2 3 4 5 6]`. `$myList` would remain unaltered. + +`append` panics if there is a problem while `mustAppend` returns an error to the +template engine if there is a problem. + +## prepend, mustPrepend + +Push an element onto the front of a list, creating a new list. + +``` +prepend $myList 0 +``` + +The above would produce `[0 1 2 3 4 5]`. `$myList` would remain unaltered. + +`prepend` panics if there is a problem while `mustPrepend` returns an error to the +template engine if there is a problem. + +## concat + +Concatenate arbitrary number of lists into one. + +``` +concat $myList ( list 6 7 ) ( list 8 ) +``` + +The above would produce `[1 2 3 4 5 6 7 8]`. `$myList` would remain unaltered. + +## reverse, mustReverse + +Produce a new list with the reversed elements of the given list. + +``` +reverse $myList +``` + +The above would generate the list `[5 4 3 2 1]`. + +`reverse` panics if there is a problem while `mustReverse` returns an error to the +template engine if there is a problem. + +## uniq, mustUniq + +Generate a list with all of the duplicates removed. + +``` +list 1 1 1 2 | uniq +``` + +The above would produce `[1 2]` + +`uniq` panics if there is a problem while `mustUniq` returns an error to the +template engine if there is a problem. + +## without, mustWithout + +The `without` function filters items out of a list. + +``` +without $myList 3 +``` + +The above would produce `[1 2 4 5]` + +Without can take more than one filter: + +``` +without $myList 1 3 5 +``` + +That would produce `[2 4]` + +`without` panics if there is a problem while `mustWithout` returns an error to the +template engine if there is a problem. + +## has, mustHas + +Test to see if a list has a particular element. + +``` +has 4 $myList +``` + +The above would return `true`, while `has "hello" $myList` would return false. + +`has` panics if there is a problem while `mustHas` returns an error to the +template engine if there is a problem. + +## compact, mustCompact + +Accepts a list and removes entries with empty values. + +``` +$list := list 1 "a" "foo" "" +$copy := compact $list +``` + +`compact` will return a new list with the empty (i.e., "") item removed. + +`compact` panics if there is a problem and `mustCompact` returns an error to the +template engine if there is a problem. + +## slice, mustSlice + +To get partial elements of a list, use `slice list [n] [m]`. It is +equivalent of `list[n:m]`. + +- `slice $myList` returns `[1 2 3 4 5]`. It is same as `myList[:]`. +- `slice $myList 3` returns `[4 5]`. It is same as `myList[3:]`. +- `slice $myList 1 3` returns `[2 3]`. It is same as `myList[1:3]`. +- `slice $myList 0 3` returns `[1 2 3]`. It is same as `myList[:3]`. + +`slice` panics if there is a problem while `mustSlice` returns an error to the +template engine if there is a problem. + +## chunk + +To split a list into chunks of given size, use `chunk size list`. This is useful for pagination. + +``` +chunk 3 (list 1 2 3 4 5 6 7 8) +``` + +This produces list of lists `[ [ 1 2 3 ] [ 4 5 6 ] [ 7 8 ] ]`. + +## A Note on List Internals + +A list is implemented in Go as a `[]interface{}`. For Go developers embedding +Sprig, you may pass `[]interface{}` items into your template context and be +able to use all of the `list` functions on those items. diff --git a/docs/sprig/math.md b/docs/sprig/math.md new file mode 100644 index 00000000..b08d0a2f --- /dev/null +++ b/docs/sprig/math.md @@ -0,0 +1,78 @@ +# Integer Math Functions + +The following math functions operate on `int64` values. + +## add + +Sum numbers with `add`. Accepts two or more inputs. + +``` +add 1 2 3 +``` + +## add1 + +To increment by 1, use `add1` + +## sub + +To subtract, use `sub` + +## div + +Perform integer division with `div` + +## mod + +Modulo with `mod` + +## mul + +Multiply with `mul`. Accepts two or more inputs. + +``` +mul 1 2 3 +``` + +## max + +Return the largest of a series of integers: + +This will return `3`: + +``` +max 1 2 3 +``` + +## min + +Return the smallest of a series of integers. + +`min 1 2 3` will return `1` + +## floor + +Returns the greatest float value less than or equal to input value + +`floor 123.9999` will return `123.0` + +## ceil + +Returns the greatest float value greater than or equal to input value + +`ceil 123.001` will return `124.0` + +## round + +Returns a float value with the remainder rounded to the given number to digits after the decimal point. + +`round 123.555555 3` will return `123.556` + +## randInt +Returns a random integer value from min (inclusive) to max (exclusive). + +``` +randInt 12 30 +``` + +The above will produce a random number in the range [12,30]. diff --git a/docs/sprig/os.md b/docs/sprig/os.md new file mode 100644 index 00000000..e6120c03 --- /dev/null +++ b/docs/sprig/os.md @@ -0,0 +1,24 @@ +# OS Functions + +_WARNING:_ These functions can lead to information leakage if not used +appropriately. + +_WARNING:_ Some notable implementations of Sprig (such as +[Kubernetes Helm](http://helm.sh)) _do not provide these functions for security +reasons_. + +## env + +The `env` function reads an environment variable: + +``` +env "HOME" +``` + +## expandenv + +To substitute environment variables in a string, use `expandenv`: + +``` +expandenv "Your path is set to $PATH" +``` diff --git a/docs/sprig/paths.md b/docs/sprig/paths.md new file mode 100644 index 00000000..f847e357 --- /dev/null +++ b/docs/sprig/paths.md @@ -0,0 +1,114 @@ +# Path and Filepath Functions + +While Sprig does not grant access to the filesystem, it does provide functions +for working with strings that follow file path conventions. + +## Paths + +Paths separated by the slash character (`/`), processed by the `path` package. + +Examples: + +* The [Linux](https://en.wikipedia.org/wiki/Linux) and + [MacOS](https://en.wikipedia.org/wiki/MacOS) + [filesystems](https://en.wikipedia.org/wiki/File_system): + `/home/user/file`, `/etc/config`; +* The path component of + [URIs](https://en.wikipedia.org/wiki/Uniform_Resource_Identifier): + `https://example.com/some/content/`, `ftp://example.com/file/`. + +### base + +Return the last element of a path. + +``` +base "foo/bar/baz" +``` + +The above prints "baz". + +### dir + +Return the directory, stripping the last part of the path. So `dir "foo/bar/baz"` +returns `foo/bar`. + +### clean + +Clean up a path. + +``` +clean "foo/bar/../baz" +``` + +The above resolves the `..` and returns `foo/baz`. + +### ext + +Return the file extension. + +``` +ext "foo.bar" +``` + +The above returns `.bar`. + +### isAbs + +To check whether a path is absolute, use `isAbs`. + +## Filepaths + +Paths separated by the `os.PathSeparator` variable, processed by the `path/filepath` package. + +These are the recommended functions to use when parsing paths of local filesystems, usually when dealing with local files, directories, etc. + +Examples: + +* Running on Linux or MacOS the filesystem path is separated by the slash character (`/`): + `/home/user/file`, `/etc/config`; +* Running on [Windows](https://en.wikipedia.org/wiki/Microsoft_Windows) + the filesystem path is separated by the backslash character (`\`): + `C:\Users\Username\`, `C:\Program Files\Application\`; + +### osBase + +Return the last element of a filepath. + +``` +osBase "/foo/bar/baz" +osBase "C:\\foo\\bar\\baz" +``` + +The above prints "baz" on Linux and Windows, respectively. + +### osDir + +Return the directory, stripping the last part of the path. So `osDir "/foo/bar/baz"` +returns `/foo/bar` on Linux, and `osDir "C:\\foo\\bar\\baz"` +returns `C:\\foo\\bar` on Windows. + +### osClean + +Clean up a path. + +``` +osClean "/foo/bar/../baz" +osClean "C:\\foo\\bar\\..\\baz" +``` + +The above resolves the `..` and returns `foo/baz` on Linux and `C:\\foo\\baz` on Windows. + +### osExt + +Return the file extension. + +``` +osExt "/foo.bar" +osExt "C:\\foo.bar" +``` + +The above returns `.bar` on Linux and Windows, respectively. + +### osIsAbs + +To check whether a file path is absolute, use `osIsAbs`. diff --git a/docs/sprig/reflection.md b/docs/sprig/reflection.md new file mode 100644 index 00000000..51e167aa --- /dev/null +++ b/docs/sprig/reflection.md @@ -0,0 +1,50 @@ +# Reflection Functions + +Sprig provides rudimentary reflection tools. These help advanced template +developers understand the underlying Go type information for a particular value. + +Go has several primitive _kinds_, like `string`, `slice`, `int64`, and `bool`. + +Go has an open _type_ system that allows developers to create their own types. + +Sprig provides a set of functions for each. + +## Kind Functions + +There are two Kind functions: `kindOf` returns the kind of an object. + +``` +kindOf "hello" +``` + +The above would return `string`. For simple tests (like in `if` blocks), the +`kindIs` function will let you verify that a value is a particular kind: + +``` +kindIs "int" 123 +``` + +The above will return `true` + +## Type Functions + +Types are slightly harder to work with, so there are three different functions: + +- `typeOf` returns the underlying type of a value: `typeOf $foo` +- `typeIs` is like `kindIs`, but for types: `typeIs "*io.Buffer" $myVal` +- `typeIsLike` works as `typeIs`, except that it also dereferences pointers. + +**Note:** None of these can test whether or not something implements a given +interface, since doing so would require compiling the interface in ahead of time. + +## deepEqual + +`deepEqual` returns true if two values are ["deeply equal"](https://golang.org/pkg/reflect/#DeepEqual) + +Works for non-primitive types as well (compared to the built-in `eq`). + +``` +deepEqual (list 1 2 3) (list 1 2 3) +``` + +The above will return `true` diff --git a/docs/sprig/semver.md b/docs/sprig/semver.md new file mode 100644 index 00000000..f049613d --- /dev/null +++ b/docs/sprig/semver.md @@ -0,0 +1,151 @@ +# Semantic Version Functions + +Some version schemes are easily parseable and comparable. Sprig provides functions +for working with [SemVer 2](http://semver.org) versions. + +## semver + +The `semver` function parses a string into a Semantic Version: + +``` +$version := semver "1.2.3-alpha.1+123" +``` + +_If the parser fails, it will cause template execution to halt with an error._ + +At this point, `$version` is a pointer to a `Version` object with the following +properties: + +- `$version.Major`: The major number (`1` above) +- `$version.Minor`: The minor number (`2` above) +- `$version.Patch`: The patch number (`3` above) +- `$version.Prerelease`: The prerelease (`alpha.1` above) +- `$version.Metadata`: The build metadata (`123` above) +- `$version.Original`: The original version as a string + +Additionally, you can compare a `Version` to another `version` using the `Compare` +function: + +``` +semver "1.4.3" | (semver "1.2.3").Compare +``` + +The above will return `-1`. + +The return values are: + +- `-1` if the given semver is greater than the semver whose `Compare` method was called +- `1` if the version who's `Compare` function was called is greater. +- `0` if they are the same version + +(Note that in SemVer, the `Metadata` field is not compared during version +comparison operations.) + +## semverCompare + +A more robust comparison function is provided as `semverCompare`. It returns `true` if +the constraint matches, or `false` if it does not match. This version supports version ranges: + +- `semverCompare "1.2.3" "1.2.3"` checks for an exact match +- `semverCompare "^1.2.0" "1.2.3"` checks that the major and minor versions match, and that the patch + number of the second version is _greater than or equal to_ the first parameter. + +The SemVer functions use the [Masterminds semver library](https://github.com/Masterminds/semver), +from the creators of Sprig. + +## Basic Comparisons + +There are two elements to the comparisons. First, a comparison string is a list +of space or comma separated AND comparisons. These are then separated by || (OR) +comparisons. For example, `">= 1.2 < 3.0.0 || >= 4.2.3"` is looking for a +comparison that's greater than or equal to 1.2 and less than 3.0.0 or is +greater than or equal to 4.2.3. + +The basic comparisons are: + +- `=`: equal (aliased to no operator) +- `!=`: not equal +- `>`: greater than +- `<`: less than +- `>=`: greater than or equal to +- `<=`: less than or equal to + +_Note, according to the Semantic Version specification pre-releases may not be +API compliant with their release counterpart. It says,_ + +## Working With Prerelease Versions + +Pre-releases, for those not familiar with them, are used for software releases +prior to stable or generally available releases. Examples of prereleases include +development, alpha, beta, and release candidate releases. A prerelease may be +a version such as `1.2.3-beta.1` while the stable release would be `1.2.3`. In the +order of precedence, prereleases come before their associated releases. In this +example `1.2.3-beta.1 < 1.2.3`. + +According to the Semantic Version specification prereleases may not be +API compliant with their release counterpart. It says, + +> A pre-release version indicates that the version is unstable and might not satisfy the intended compatibility requirements as denoted by its associated normal version. + +SemVer comparisons using constraints without a prerelease comparator will skip +prerelease versions. For example, `>=1.2.3` will skip prereleases when looking +at a list of releases while `>=1.2.3-0` will evaluate and find prereleases. + +The reason for the `0` as a pre-release version in the example comparison is +because pre-releases can only contain ASCII alphanumerics and hyphens (along with +`.` separators), per the spec. Sorting happens in ASCII sort order, again per the +spec. The lowest character is a `0` in ASCII sort order +(see an [ASCII Table](http://www.asciitable.com/)) + +Understanding ASCII sort ordering is important because A-Z comes before a-z. That +means `>=1.2.3-BETA` will return `1.2.3-alpha`. What you might expect from case +sensitivity doesn't apply here. This is due to ASCII sort ordering which is what +the spec specifies. + +## Hyphen Range Comparisons + +There are multiple methods to handle ranges and the first is hyphens ranges. +These look like: + +- `1.2 - 1.4.5` which is equivalent to `>= 1.2 <= 1.4.5` +- `2.3.4 - 4.5` which is equivalent to `>= 2.3.4 <= 4.5` + +## Wildcards In Comparisons + +The `x`, `X`, and `*` characters can be used as a wildcard character. This works +for all comparison operators. When used on the `=` operator it falls +back to the patch level comparison (see tilde below). For example, + +- `1.2.x` is equivalent to `>= 1.2.0, < 1.3.0` +- `>= 1.2.x` is equivalent to `>= 1.2.0` +- `<= 2.x` is equivalent to `< 3` +- `*` is equivalent to `>= 0.0.0` + +## Tilde Range Comparisons (Patch) + +The tilde (`~`) comparison operator is for patch level ranges when a minor +version is specified and major level changes when the minor number is missing. +For example, + +- `~1.2.3` is equivalent to `>= 1.2.3, < 1.3.0` +- `~1` is equivalent to `>= 1, < 2` +- `~2.3` is equivalent to `>= 2.3, < 2.4` +- `~1.2.x` is equivalent to `>= 1.2.0, < 1.3.0` +- `~1.x` is equivalent to `>= 1, < 2` + +## Caret Range Comparisons (Major) + +The caret (`^`) comparison operator is for major level changes once a stable +(1.0.0) release has occurred. Prior to a 1.0.0 release the minor versions acts +as the API stability level. This is useful when comparisons of API versions as a +major change is API breaking. For example, + +- `^1.2.3` is equivalent to `>= 1.2.3, < 2.0.0` +- `^1.2.x` is equivalent to `>= 1.2.0, < 2.0.0` +- `^2.3` is equivalent to `>= 2.3, < 3` +- `^2.x` is equivalent to `>= 2.0.0, < 3` +- `^0.2.3` is equivalent to `>=0.2.3 <0.3.0` +- `^0.2` is equivalent to `>=0.2.0 <0.3.0` +- `^0.0.3` is equivalent to `>=0.0.3 <0.0.4` +- `^0.0` is equivalent to `>=0.0.0 <0.1.0` +- `^0` is equivalent to `>=0.0.0 <1.0.0` diff --git a/docs/sprig/string_slice.md b/docs/sprig/string_slice.md new file mode 100644 index 00000000..96c0c83b --- /dev/null +++ b/docs/sprig/string_slice.md @@ -0,0 +1,72 @@ +# String List Functions + +These function operate on or generate slices of strings. In Go, a slice is a +growable array. In Sprig, it's a special case of a `list`. + +## join + +Join a list of strings into a single string, with the given separator. + +``` +list "hello" "world" | join "_" +``` + +The above will produce `hello_world` + +`join` will try to convert non-strings to a string value: + +``` +list 1 2 3 | join "+" +``` + +The above will produce `1+2+3` + +## splitList and split + +Split a string into a list of strings: + +``` +splitList "$" "foo$bar$baz" +``` + +The above will return `[foo bar baz]` + +The older `split` function splits a string into a `dict`. It is designed to make +it easy to use template dot notation for accessing members: + +``` +$a := split "$" "foo$bar$baz" +``` + +The above produces a map with index keys. `{_0: foo, _1: bar, _2: baz}` + +``` +$a._0 +``` + +The above produces `foo` + +## splitn + +`splitn` function splits a string into a `dict` with `n` keys. It is designed to make +it easy to use template dot notation for accessing members: + +``` +$a := splitn "$" 2 "foo$bar$baz" +``` + +The above produces a map with index keys. `{_0: foo, _1: bar$baz}` + +``` +$a._0 +``` + +The above produces `foo` + +## sortAlpha + +The `sortAlpha` function sorts a list of strings into alphabetical (lexicographical) +order. + +It does _not_ sort in place, but returns a sorted copy of the list, in keeping +with the immutability of lists. diff --git a/docs/sprig/strings.md b/docs/sprig/strings.md new file mode 100644 index 00000000..784392f1 --- /dev/null +++ b/docs/sprig/strings.md @@ -0,0 +1,309 @@ +# String Functions + +Sprig has a number of string manipulation functions. + +## trim + +The `trim` function removes space from either side of a string: + +``` +trim " hello " +``` + +The above produces `hello` + +## trimAll + +Remove given characters from the front or back of a string: + +``` +trimAll "$" "$5.00" +``` + +The above returns `5.00` (as a string). + +## trimSuffix + +Trim just the suffix from a string: + +``` +trimSuffix "-" "hello-" +``` + +The above returns `hello` + +## trimPrefix + +Trim just the prefix from a string: + +``` +trimPrefix "-" "-hello" +``` + +The above returns `hello` + +## upper + +Convert the entire string to uppercase: + +``` +upper "hello" +``` + +The above returns `HELLO` + +## lower + +Convert the entire string to lowercase: + +``` +lower "HELLO" +``` + +The above returns `hello` + +## title + +Convert to title case: + +``` +title "hello world" +``` + +The above returns `Hello World` + +## repeat + +Repeat a string multiple times: + +``` +repeat 3 "hello" +``` + +The above returns `hellohellohello` + +## substr + +Get a substring from a string. It takes three parameters: + +- start (int) +- end (int) +- string (string) + +``` +substr 0 5 "hello world" +``` + +The above returns `hello` + +## trunc + +Truncate a string (and add no suffix) + +``` +trunc 5 "hello world" +``` + +The above produces `hello`. + +``` +trunc -5 "hello world" +``` + +The above produces `world`. + +## contains + +Test to see if one string is contained inside of another: + +``` +contains "cat" "catch" +``` + +The above returns `true` because `catch` contains `cat`. + +## hasPrefix and hasSuffix + +The `hasPrefix` and `hasSuffix` functions test whether a string has a given +prefix or suffix: + +``` +hasPrefix "cat" "catch" +``` + +The above returns `true` because `catch` has the prefix `cat`. + +## quote and squote + +These functions wrap a string in double quotes (`quote`) or single quotes +(`squote`). + +## cat + +The `cat` function concatenates multiple strings together into one, separating +them with spaces: + +``` +cat "hello" "beautiful" "world" +``` + +The above produces `hello beautiful world` + +## indent + +The `indent` function indents every line in a given string to the specified +indent width. This is useful when aligning multi-line strings: + +``` +indent 4 $lots_of_text +``` + +The above will indent every line of text by 4 space characters. + +## nindent + +The `nindent` function is the same as the indent function, but prepends a new +line to the beginning of the string. + +``` +nindent 4 $lots_of_text +``` + +The above will indent every line of text by 4 space characters and add a new +line to the beginning. + +## replace + +Perform simple string replacement. + +It takes three arguments: + +- string to replace +- string to replace with +- source string + +``` +"I Am Henry VIII" | replace " " "-" +``` + +The above will produce `I-Am-Henry-VIII` + +## plural + +Pluralize a string. + +``` +len $fish | plural "one anchovy" "many anchovies" +``` + +In the above, if the length of the string is 1, the first argument will be +printed (`one anchovy`). Otherwise, the second argument will be printed +(`many anchovies`). + +The arguments are: + +- singular string +- plural string +- length integer + +NOTE: Sprig does not currently support languages with more complex pluralization +rules. And `0` is considered a plural because the English language treats it +as such (`zero anchovies`). The Sprig developers are working on a solution for +better internationalization. + +## regexMatch, mustRegexMatch + +Returns true if the input string contains any match of the regular expression. + +``` +regexMatch "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$" "test@acme.com" +``` + +The above produces `true` + +`regexMatch` panics if there is a problem and `mustRegexMatch` returns an error to the +template engine if there is a problem. + +## regexFindAll, mustRegexFindAll + +Returns a slice of all matches of the regular expression in the input string. +The last parameter n determines the number of substrings to return, where -1 means return all matches + +``` +regexFindAll "[2,4,6,8]" "123456789" -1 +``` + +The above produces `[2 4 6 8]` + +`regexFindAll` panics if there is a problem and `mustRegexFindAll` returns an error to the +template engine if there is a problem. + +## regexFind, mustRegexFind + +Return the first (left most) match of the regular expression in the input string + +``` +regexFind "[a-zA-Z][1-9]" "abcd1234" +``` + +The above produces `d1` + +`regexFind` panics if there is a problem and `mustRegexFind` returns an error to the +template engine if there is a problem. + +## regexReplaceAll, mustRegexReplaceAll + +Returns a copy of the input string, replacing matches of the Regexp with the replacement string replacement. +Inside string replacement, $ signs are interpreted as in Expand, so for instance $1 represents the text of the first submatch + +``` +regexReplaceAll "a(x*)b" "-ab-axxb-" "${1}W" +``` + +The above produces `-W-xxW-` + +`regexReplaceAll` panics if there is a problem and `mustRegexReplaceAll` returns an error to the +template engine if there is a problem. + +## regexReplaceAllLiteral, mustRegexReplaceAllLiteral + +Returns a copy of the input string, replacing matches of the Regexp with the replacement string replacement +The replacement string is substituted directly, without using Expand + +``` +regexReplaceAllLiteral "a(x*)b" "-ab-axxb-" "${1}" +``` + +The above produces `-${1}-${1}-` + +`regexReplaceAllLiteral` panics if there is a problem and `mustRegexReplaceAllLiteral` returns an error to the +template engine if there is a problem. + +## regexSplit, mustRegexSplit + +Slices the input string into substrings separated by the expression and returns a slice of the substrings between those expression matches. The last parameter `n` determines the number of substrings to return, where `-1` means return all matches + +``` +regexSplit "z+" "pizza" -1 +``` + +The above produces `[pi a]` + +`regexSplit` panics if there is a problem and `mustRegexSplit` returns an error to the +template engine if there is a problem. + +## regexQuoteMeta + +Returns a string that escapes all regular expression metacharacters inside the argument text; +the returned string is a regular expression matching the literal text. + +``` +regexQuoteMeta "1.2.3" +``` + +The above produces `1\.2\.3` + +## See Also... + +The [Conversion Functions](conversion.md) contain functions for converting strings. The [String List Functions](string_slice.md) contains +functions for working with an array of strings. diff --git a/docs/sprig/url.md b/docs/sprig/url.md new file mode 100644 index 00000000..21d54a29 --- /dev/null +++ b/docs/sprig/url.md @@ -0,0 +1,33 @@ +# URL Functions + +## urlParse +Parses string for URL and produces dict with URL parts + +``` +urlParse "http://admin:secret@server.com:8080/api?list=false#anchor" +``` + +The above returns a dict, containing URL object: +```yaml +scheme: 'http' +host: 'server.com:8080' +path: '/api' +query: 'list=false' +opaque: nil +fragment: 'anchor' +userinfo: 'admin:secret' +``` + +For more info, check https://golang.org/pkg/net/url/#URL + +## urlJoin +Joins map (produced by `urlParse`) to produce URL string + +``` +urlJoin (dict "fragment" "fragment" "host" "host:80" "path" "/path" "query" "query" "scheme" "http") +``` + +The above returns the following string: +``` +proto://host:80/path?query#fragment +``` diff --git a/docs/sprig/uuid.md b/docs/sprig/uuid.md new file mode 100644 index 00000000..1b57a330 --- /dev/null +++ b/docs/sprig/uuid.md @@ -0,0 +1,9 @@ +# UUID Functions + +Sprig can generate UUID v4 universally unique IDs. + +``` +uuidv4 +``` + +The above returns a new UUID of the v4 (randomly generated) type. diff --git a/mkdocs.yml b/mkdocs.yml index ef746518..8006eac4 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -94,6 +94,7 @@ nav: - "Integrations + projects": integrations.md - "Release notes": releases.md - "Emojis 🥳 🎉": emojis.md + - "Template Functions": sprig.md - "Troubleshooting": troubleshooting.md - "Known issues": known-issues.md - "Deprecation notices": deprecations.md diff --git a/server/server.go b/server/server.go index bfa7eb6b..94461fbb 100644 --- a/server/server.go +++ b/server/server.go @@ -34,6 +34,7 @@ import ( "heckel.io/ntfy/v2/log" "heckel.io/ntfy/v2/user" "heckel.io/ntfy/v2/util" + "heckel.io/ntfy/v2/util/sprig" ) // Server is the main server, providing the UI and API for ntfy @@ -1132,7 +1133,11 @@ func replaceTemplate(tpl string, source string) (string, error) { if err := json.Unmarshal([]byte(source), &data); err != nil { return "", errHTTPBadRequestTemplateMessageNotJSON } - t, err := template.New("").Parse(tpl) + sprigFuncs := sprig.FuncMap() + // remove unsafe functions + delete(sprigFuncs, "env") + delete(sprigFuncs, "expandenv") + t, err := template.New("").Funcs(sprigFuncs).Parse(tpl) if err != nil { return "", errHTTPBadRequestTemplateInvalid } diff --git a/server/server_test.go b/server/server_test.go index e09f67a2..4fa059b6 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -3024,6 +3024,51 @@ template ""}}`, } } +func TestServer_MessageTemplate_SprigFunctions(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) + bodies := []string{ + `{"foo":"bar","nested":{"title":"here"}}`, + `{"topic":"ntfy-test"}`, + `{"topic":"another-topic"}`, + } + templates := []string{ + `{{.foo | upper}} is {{.nested.title | repeat 3}}`, + `{{if hasPrefix "ntfy-" .topic}}Topic: {{trimPrefix "ntfy-" .topic}}{{ else }}Topic: {{.topic}}{{end}}`, + `{{if hasPrefix "ntfy-" .topic}}Topic: {{trimPrefix "ntfy-" .topic}}{{ else }}Topic: {{.topic}}{{end}}`, + } + targets := []string{ + `BAR is hereherehere`, + `Topic: test`, + `Topic: another-topic`, + } + for i, body := range bodies { + template := templates[i] + target := targets[i] + t.Run(template, func(t *testing.T) { + response := request(t, s, "PUT", `/mytopic`, body, map[string]string{ + "Template": "yes", + "Message": template, + }) + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.Equal(t, target, m.Message) + }) + } +} + +func TestServer_MessageTemplate_UnsafeSprigFunctions(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "POST", "/mytopic", `{}`, map[string]string{ + "X-Message": `{{ env "PATH" }}`, + "X-Template": "1", + }) + + require.Equal(t, 400, response.Code) + require.Equal(t, 40043, toHTTPError(t, response.Body.String()).Code) +} + func newTestConfig(t *testing.T) *Config { conf := NewConfig() conf.BaseURL = "http://127.0.0.1:12345" diff --git a/util/sprig/LICENSE.txt b/util/sprig/LICENSE.txt new file mode 100644 index 00000000..f311b1ea --- /dev/null +++ b/util/sprig/LICENSE.txt @@ -0,0 +1,19 @@ +Copyright (C) 2013-2020 Masterminds + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/util/sprig/crypto.go b/util/sprig/crypto.go new file mode 100644 index 00000000..4d027781 --- /dev/null +++ b/util/sprig/crypto.go @@ -0,0 +1,37 @@ +package sprig + +import ( + "crypto/sha1" + "crypto/sha256" + "crypto/sha512" + "encoding/hex" + "fmt" + "hash/adler32" + + "github.com/google/uuid" +) + +func sha512sum(input string) string { + hash := sha512.Sum512([]byte(input)) + return hex.EncodeToString(hash[:]) +} + +func sha256sum(input string) string { + hash := sha256.Sum256([]byte(input)) + return hex.EncodeToString(hash[:]) +} + +func sha1sum(input string) string { + hash := sha1.Sum([]byte(input)) + return hex.EncodeToString(hash[:]) +} + +func adler32sum(input string) string { + hash := adler32.Checksum([]byte(input)) + return fmt.Sprintf("%d", hash) +} + +// uuidv4 provides a safe and secure UUID v4 implementation +func uuidv4() string { + return uuid.New().String() +} diff --git a/util/sprig/crypto_test.go b/util/sprig/crypto_test.go new file mode 100644 index 00000000..bad809a5 --- /dev/null +++ b/util/sprig/crypto_test.go @@ -0,0 +1,54 @@ +package sprig + +import ( + "testing" +) + +func TestSha512Sum(t *testing.T) { + tpl := `{{"abc" | sha512sum}}` + if err := runt(tpl, "ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f"); err != nil { + t.Error(err) + } +} + +func TestSha256Sum(t *testing.T) { + tpl := `{{"abc" | sha256sum}}` + if err := runt(tpl, "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"); err != nil { + t.Error(err) + } +} + +func TestSha1Sum(t *testing.T) { + tpl := `{{"abc" | sha1sum}}` + if err := runt(tpl, "a9993e364706816aba3e25717850c26c9cd0d89d"); err != nil { + t.Error(err) + } +} + +func TestAdler32Sum(t *testing.T) { + tpl := `{{"abc" | adler32sum}}` + if err := runt(tpl, "38600999"); err != nil { + t.Error(err) + } +} + +func TestUUIDGeneration(t *testing.T) { + tpl := `{{uuidv4}}` + out, err := runRaw(tpl, nil) + if err != nil { + t.Error(err) + } + + if len(out) != 36 { + t.Error("Expected UUID of length 36") + } + + out2, err := runRaw(tpl, nil) + if err != nil { + t.Error(err) + } + + if out == out2 { + t.Error("Expected subsequent UUID generations to be different") + } +} diff --git a/util/sprig/date.go b/util/sprig/date.go new file mode 100644 index 00000000..ed022dda --- /dev/null +++ b/util/sprig/date.go @@ -0,0 +1,152 @@ +package sprig + +import ( + "strconv" + "time" +) + +// Given a format and a date, format the date string. +// +// Date can be a `time.Time` or an `int, int32, int64`. +// In the later case, it is treated as seconds since UNIX +// epoch. +func date(fmt string, date interface{}) string { + return dateInZone(fmt, date, "Local") +} + +func htmlDate(date interface{}) string { + return dateInZone("2006-01-02", date, "Local") +} + +func htmlDateInZone(date interface{}, zone string) string { + return dateInZone("2006-01-02", date, zone) +} + +func dateInZone(fmt string, date interface{}, zone string) string { + var t time.Time + switch date := date.(type) { + default: + t = time.Now() + case time.Time: + t = date + case *time.Time: + t = *date + case int64: + t = time.Unix(date, 0) + case int: + t = time.Unix(int64(date), 0) + case int32: + t = time.Unix(int64(date), 0) + } + + loc, err := time.LoadLocation(zone) + if err != nil { + loc, _ = time.LoadLocation("UTC") + } + + return t.In(loc).Format(fmt) +} + +func dateModify(fmt string, date time.Time) time.Time { + d, err := time.ParseDuration(fmt) + if err != nil { + return date + } + return date.Add(d) +} + +func mustDateModify(fmt string, date time.Time) (time.Time, error) { + d, err := time.ParseDuration(fmt) + if err != nil { + return time.Time{}, err + } + return date.Add(d), nil +} + +func dateAgo(date interface{}) string { + var t time.Time + + switch date := date.(type) { + default: + t = time.Now() + case time.Time: + t = date + case int64: + t = time.Unix(date, 0) + case int: + t = time.Unix(int64(date), 0) + } + // Drop resolution to seconds + duration := time.Since(t).Round(time.Second) + return duration.String() +} + +func duration(sec interface{}) string { + var n int64 + switch value := sec.(type) { + default: + n = 0 + case string: + n, _ = strconv.ParseInt(value, 10, 64) + case int64: + n = value + } + return (time.Duration(n) * time.Second).String() +} + +func durationRound(duration interface{}) string { + var d time.Duration + switch duration := duration.(type) { + default: + d = 0 + case string: + d, _ = time.ParseDuration(duration) + case int64: + d = time.Duration(duration) + case time.Time: + d = time.Since(duration) + } + + u := uint64(d) + neg := d < 0 + if neg { + u = -u + } + + var ( + year = uint64(time.Hour) * 24 * 365 + month = uint64(time.Hour) * 24 * 30 + day = uint64(time.Hour) * 24 + hour = uint64(time.Hour) + minute = uint64(time.Minute) + second = uint64(time.Second) + ) + switch { + case u > year: + return strconv.FormatUint(u/year, 10) + "y" + case u > month: + return strconv.FormatUint(u/month, 10) + "mo" + case u > day: + return strconv.FormatUint(u/day, 10) + "d" + case u > hour: + return strconv.FormatUint(u/hour, 10) + "h" + case u > minute: + return strconv.FormatUint(u/minute, 10) + "m" + case u > second: + return strconv.FormatUint(u/second, 10) + "s" + } + return "0s" +} + +func toDate(fmt, str string) time.Time { + t, _ := time.ParseInLocation(fmt, str, time.Local) + return t +} + +func mustToDate(fmt, str string) (time.Time, error) { + return time.ParseInLocation(fmt, str, time.Local) +} + +func unixEpoch(date time.Time) string { + return strconv.FormatInt(date.Unix(), 10) +} diff --git a/util/sprig/date_test.go b/util/sprig/date_test.go new file mode 100644 index 00000000..be7ec9d9 --- /dev/null +++ b/util/sprig/date_test.go @@ -0,0 +1,120 @@ +package sprig + +import ( + "testing" + "time" +) + +func TestHtmlDate(t *testing.T) { + t.Skip() + tpl := `{{ htmlDate 0}}` + if err := runt(tpl, "1970-01-01"); err != nil { + t.Error(err) + } +} + +func TestAgo(t *testing.T) { + tpl := "{{ ago .Time }}" + if err := runtv(tpl, "2m5s", map[string]interface{}{"Time": time.Now().Add(-125 * time.Second)}); err != nil { + t.Error(err) + } + + if err := runtv(tpl, "2h34m17s", map[string]interface{}{"Time": time.Now().Add(-(2*3600 + 34*60 + 17) * time.Second)}); err != nil { + t.Error(err) + } + + if err := runtv(tpl, "-5s", map[string]interface{}{"Time": time.Now().Add(5 * time.Second)}); err != nil { + t.Error(err) + } +} + +func TestToDate(t *testing.T) { + tpl := `{{toDate "2006-01-02" "2017-12-31" | date "02/01/2006"}}` + if err := runt(tpl, "31/12/2017"); err != nil { + t.Error(err) + } +} + +func TestUnixEpoch(t *testing.T) { + tm, err := time.Parse("02 Jan 06 15:04:05 MST", "13 Jun 19 20:39:39 GMT") + if err != nil { + t.Error(err) + } + tpl := `{{unixEpoch .Time}}` + + if err = runtv(tpl, "1560458379", map[string]interface{}{"Time": tm}); err != nil { + t.Error(err) + } +} + +func TestDateInZone(t *testing.T) { + tm, err := time.Parse("02 Jan 06 15:04:05 MST", "13 Jun 19 20:39:39 GMT") + if err != nil { + t.Error(err) + } + tpl := `{{ date_in_zone "02 Jan 06 15:04 -0700" .Time "UTC" }}` + + // Test time.Time input + if err = runtv(tpl, "13 Jun 19 20:39 +0000", map[string]interface{}{"Time": tm}); err != nil { + t.Error(err) + } + + // Test pointer to time.Time input + if err = runtv(tpl, "13 Jun 19 20:39 +0000", map[string]interface{}{"Time": &tm}); err != nil { + t.Error(err) + } + + // Test no time input. This should be close enough to time.Now() we can test + loc, _ := time.LoadLocation("UTC") + if err = runtv(tpl, time.Now().In(loc).Format("02 Jan 06 15:04 -0700"), map[string]interface{}{"Time": ""}); err != nil { + t.Error(err) + } + + // Test unix timestamp as int64 + if err = runtv(tpl, "13 Jun 19 20:39 +0000", map[string]interface{}{"Time": int64(1560458379)}); err != nil { + t.Error(err) + } + + // Test unix timestamp as int32 + if err = runtv(tpl, "13 Jun 19 20:39 +0000", map[string]interface{}{"Time": int32(1560458379)}); err != nil { + t.Error(err) + } + + // Test unix timestamp as int + if err = runtv(tpl, "13 Jun 19 20:39 +0000", map[string]interface{}{"Time": int(1560458379)}); err != nil { + t.Error(err) + } + + // Test case of invalid timezone + tpl = `{{ date_in_zone "02 Jan 06 15:04 -0700" .Time "foobar" }}` + if err = runtv(tpl, "13 Jun 19 20:39 +0000", map[string]interface{}{"Time": tm}); err != nil { + t.Error(err) + } +} + +func TestDuration(t *testing.T) { + tpl := "{{ duration .Secs }}" + if err := runtv(tpl, "1m1s", map[string]interface{}{"Secs": "61"}); err != nil { + t.Error(err) + } + if err := runtv(tpl, "1h0m0s", map[string]interface{}{"Secs": "3600"}); err != nil { + t.Error(err) + } + // 1d2h3m4s but go is opinionated + if err := runtv(tpl, "26h3m4s", map[string]interface{}{"Secs": "93784"}); err != nil { + t.Error(err) + } +} + +func TestDurationRound(t *testing.T) { + tpl := "{{ durationRound .Time }}" + if err := runtv(tpl, "2h", map[string]interface{}{"Time": "2h5s"}); err != nil { + t.Error(err) + } + if err := runtv(tpl, "1d", map[string]interface{}{"Time": "24h5s"}); err != nil { + t.Error(err) + } + if err := runtv(tpl, "3mo", map[string]interface{}{"Time": "2400h5s"}); err != nil { + t.Error(err) + } +} diff --git a/util/sprig/defaults.go b/util/sprig/defaults.go new file mode 100644 index 00000000..201b7e24 --- /dev/null +++ b/util/sprig/defaults.go @@ -0,0 +1,163 @@ +package sprig + +import ( + "bytes" + "encoding/json" + "math/rand" + "reflect" + "strings" + "time" +) + +func init() { + rand.Seed(time.Now().UnixNano()) +} + +// dfault checks whether `given` is set, and returns default if not set. +// +// This returns `d` if `given` appears not to be set, and `given` otherwise. +// +// For numeric types 0 is unset. +// For strings, maps, arrays, and slices, len() = 0 is considered unset. +// For bool, false is unset. +// Structs are never considered unset. +// +// For everything else, including pointers, a nil value is unset. +func dfault(d interface{}, given ...interface{}) interface{} { + + if empty(given) || empty(given[0]) { + return d + } + return given[0] +} + +// empty returns true if the given value has the zero value for its type. +func empty(given interface{}) bool { + g := reflect.ValueOf(given) + if !g.IsValid() { + return true + } + + // Basically adapted from text/template.isTrue + switch g.Kind() { + default: + return g.IsNil() + case reflect.Array, reflect.Slice, reflect.Map, reflect.String: + return g.Len() == 0 + case reflect.Bool: + return !g.Bool() + case reflect.Complex64, reflect.Complex128: + return g.Complex() == 0 + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return g.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return g.Uint() == 0 + case reflect.Float32, reflect.Float64: + return g.Float() == 0 + case reflect.Struct: + return false + } +} + +// coalesce returns the first non-empty value. +func coalesce(v ...interface{}) interface{} { + for _, val := range v { + if !empty(val) { + return val + } + } + return nil +} + +// all returns true if empty(x) is false for all values x in the list. +// If the list is empty, return true. +func all(v ...interface{}) bool { + for _, val := range v { + if empty(val) { + return false + } + } + return true +} + +// any returns true if empty(x) is false for any x in the list. +// If the list is empty, return false. +func any(v ...interface{}) bool { + for _, val := range v { + if !empty(val) { + return true + } + } + return false +} + +// fromJSON decodes JSON into a structured value, ignoring errors. +func fromJSON(v string) interface{} { + output, _ := mustFromJSON(v) + return output +} + +// mustFromJSON decodes JSON into a structured value, returning errors. +func mustFromJSON(v string) (interface{}, error) { + var output interface{} + err := json.Unmarshal([]byte(v), &output) + return output, err +} + +// toJSON encodes an item into a JSON string +func toJSON(v interface{}) string { + output, _ := json.Marshal(v) + return string(output) +} + +func mustToJSON(v interface{}) (string, error) { + output, err := json.Marshal(v) + if err != nil { + return "", err + } + return string(output), nil +} + +// toPrettyJSON encodes an item into a pretty (indented) JSON string +func toPrettyJSON(v interface{}) string { + output, _ := json.MarshalIndent(v, "", " ") + return string(output) +} + +func mustToPrettyJSON(v interface{}) (string, error) { + output, err := json.MarshalIndent(v, "", " ") + if err != nil { + return "", err + } + return string(output), nil +} + +// toRawJSON encodes an item into a JSON string with no escaping of HTML characters. +func toRawJSON(v interface{}) string { + output, err := mustToRawJSON(v) + if err != nil { + panic(err) + } + return string(output) +} + +// mustToRawJSON encodes an item into a JSON string with no escaping of HTML characters. +func mustToRawJSON(v interface{}) (string, error) { + buf := new(bytes.Buffer) + enc := json.NewEncoder(buf) + enc.SetEscapeHTML(false) + err := enc.Encode(&v) + if err != nil { + return "", err + } + return strings.TrimSuffix(buf.String(), "\n"), nil +} + +// ternary returns the first value if the last value is true, otherwise returns the second value. +func ternary(vt interface{}, vf interface{}, v bool) interface{} { + if v { + return vt + } + + return vf +} diff --git a/util/sprig/defaults_test.go b/util/sprig/defaults_test.go new file mode 100644 index 00000000..eb7e35b4 --- /dev/null +++ b/util/sprig/defaults_test.go @@ -0,0 +1,196 @@ +package sprig + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDefault(t *testing.T) { + tpl := `{{"" | default "foo"}}` + if err := runt(tpl, "foo"); err != nil { + t.Error(err) + } + tpl = `{{default "foo" 234}}` + if err := runt(tpl, "234"); err != nil { + t.Error(err) + } + tpl = `{{default "foo" 2.34}}` + if err := runt(tpl, "2.34"); err != nil { + t.Error(err) + } + + tpl = `{{ .Nothing | default "123" }}` + if err := runt(tpl, "123"); err != nil { + t.Error(err) + } + tpl = `{{ default "123" }}` + if err := runt(tpl, "123"); err != nil { + t.Error(err) + } +} + +func TestEmpty(t *testing.T) { + tpl := `{{if empty 1}}1{{else}}0{{end}}` + if err := runt(tpl, "0"); err != nil { + t.Error(err) + } + + tpl = `{{if empty 0}}1{{else}}0{{end}}` + if err := runt(tpl, "1"); err != nil { + t.Error(err) + } + tpl = `{{if empty ""}}1{{else}}0{{end}}` + if err := runt(tpl, "1"); err != nil { + t.Error(err) + } + tpl = `{{if empty 0.0}}1{{else}}0{{end}}` + if err := runt(tpl, "1"); err != nil { + t.Error(err) + } + tpl = `{{if empty false}}1{{else}}0{{end}}` + if err := runt(tpl, "1"); err != nil { + t.Error(err) + } + + dict := map[string]interface{}{"top": map[string]interface{}{}} + tpl = `{{if empty .top.NoSuchThing}}1{{else}}0{{end}}` + if err := runtv(tpl, "1", dict); err != nil { + t.Error(err) + } + tpl = `{{if empty .bottom.NoSuchThing}}1{{else}}0{{end}}` + if err := runtv(tpl, "1", dict); err != nil { + t.Error(err) + } +} + +func TestCoalesce(t *testing.T) { + tests := map[string]string{ + `{{ coalesce 1 }}`: "1", + `{{ coalesce "" 0 nil 2 }}`: "2", + `{{ $two := 2 }}{{ coalesce "" 0 nil $two }}`: "2", + `{{ $two := 2 }}{{ coalesce "" $two 0 0 0 }}`: "2", + `{{ $two := 2 }}{{ coalesce "" $two 3 4 5 }}`: "2", + `{{ coalesce }}`: "", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } + + dict := map[string]interface{}{"top": map[string]interface{}{}} + tpl := `{{ coalesce .top.NoSuchThing .bottom .bottom.dollar "airplane"}}` + if err := runtv(tpl, "airplane", dict); err != nil { + t.Error(err) + } +} + +func TestAll(t *testing.T) { + tests := map[string]string{ + `{{ all 1 }}`: "true", + `{{ all "" 0 nil 2 }}`: "false", + `{{ $two := 2 }}{{ all "" 0 nil $two }}`: "false", + `{{ $two := 2 }}{{ all "" $two 0 0 0 }}`: "false", + `{{ $two := 2 }}{{ all "" $two 3 4 5 }}`: "false", + `{{ all }}`: "true", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } + + dict := map[string]interface{}{"top": map[string]interface{}{}} + tpl := `{{ all .top.NoSuchThing .bottom .bottom.dollar "airplane"}}` + if err := runtv(tpl, "false", dict); err != nil { + t.Error(err) + } +} + +func TestAny(t *testing.T) { + tests := map[string]string{ + `{{ any 1 }}`: "true", + `{{ any "" 0 nil 2 }}`: "true", + `{{ $two := 2 }}{{ any "" 0 nil $two }}`: "true", + `{{ $two := 2 }}{{ any "" $two 3 4 5 }}`: "true", + `{{ $zero := 0 }}{{ any "" $zero 0 0 0 }}`: "false", + `{{ any }}`: "false", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } + + dict := map[string]interface{}{"top": map[string]interface{}{}} + tpl := `{{ any .top.NoSuchThing .bottom .bottom.dollar "airplane"}}` + if err := runtv(tpl, "true", dict); err != nil { + t.Error(err) + } +} + +func TestFromJSON(t *testing.T) { + dict := map[string]interface{}{"Input": `{"foo": 55}`} + + tpl := `{{.Input | fromJSON}}` + expected := `map[foo:55]` + if err := runtv(tpl, expected, dict); err != nil { + t.Error(err) + } + + tpl = `{{(.Input | fromJSON).foo}}` + expected = `55` + if err := runtv(tpl, expected, dict); err != nil { + t.Error(err) + } +} + +func TestToJSON(t *testing.T) { + dict := map[string]interface{}{"Top": map[string]interface{}{"bool": true, "string": "test", "number": 42}} + + tpl := `{{.Top | toJSON}}` + expected := `{"bool":true,"number":42,"string":"test"}` + if err := runtv(tpl, expected, dict); err != nil { + t.Error(err) + } +} + +func TestToPrettyJSON(t *testing.T) { + dict := map[string]interface{}{"Top": map[string]interface{}{"bool": true, "string": "test", "number": 42}} + tpl := `{{.Top | toPrettyJSON}}` + expected := `{ + "bool": true, + "number": 42, + "string": "test" +}` + if err := runtv(tpl, expected, dict); err != nil { + t.Error(err) + } +} + +func TestToRawJSON(t *testing.T) { + dict := map[string]interface{}{"Top": map[string]interface{}{"bool": true, "string": "test", "number": 42, "html": ""}} + tpl := `{{.Top | toRawJSON}}` + expected := `{"bool":true,"html":"","number":42,"string":"test"}` + + if err := runtv(tpl, expected, dict); err != nil { + t.Error(err) + } +} + +func TestTernary(t *testing.T) { + tpl := `{{true | ternary "foo" "bar"}}` + if err := runt(tpl, "foo"); err != nil { + t.Error(err) + } + + tpl = `{{ternary "foo" "bar" true}}` + if err := runt(tpl, "foo"); err != nil { + t.Error(err) + } + + tpl = `{{false | ternary "foo" "bar"}}` + if err := runt(tpl, "bar"); err != nil { + t.Error(err) + } + + tpl = `{{ternary "foo" "bar" false}}` + if err := runt(tpl, "bar"); err != nil { + t.Error(err) + } +} diff --git a/util/sprig/dict.go b/util/sprig/dict.go new file mode 100644 index 00000000..fd2dd711 --- /dev/null +++ b/util/sprig/dict.go @@ -0,0 +1,118 @@ +package sprig + +func get(d map[string]interface{}, key string) interface{} { + if val, ok := d[key]; ok { + return val + } + return "" +} + +func set(d map[string]interface{}, key string, value interface{}) map[string]interface{} { + d[key] = value + return d +} + +func unset(d map[string]interface{}, key string) map[string]interface{} { + delete(d, key) + return d +} + +func hasKey(d map[string]interface{}, key string) bool { + _, ok := d[key] + return ok +} + +func pluck(key string, d ...map[string]interface{}) []interface{} { + res := []interface{}{} + for _, dict := range d { + if val, ok := dict[key]; ok { + res = append(res, val) + } + } + return res +} + +func keys(dicts ...map[string]interface{}) []string { + k := []string{} + for _, dict := range dicts { + for key := range dict { + k = append(k, key) + } + } + return k +} + +func pick(dict map[string]interface{}, keys ...string) map[string]interface{} { + res := map[string]interface{}{} + for _, k := range keys { + if v, ok := dict[k]; ok { + res[k] = v + } + } + return res +} + +func omit(dict map[string]interface{}, keys ...string) map[string]interface{} { + res := map[string]interface{}{} + + omit := make(map[string]bool, len(keys)) + for _, k := range keys { + omit[k] = true + } + + for k, v := range dict { + if _, ok := omit[k]; !ok { + res[k] = v + } + } + return res +} + +func dict(v ...interface{}) map[string]interface{} { + dict := map[string]interface{}{} + lenv := len(v) + for i := 0; i < lenv; i += 2 { + key := strval(v[i]) + if i+1 >= lenv { + dict[key] = "" + continue + } + dict[key] = v[i+1] + } + return dict +} + +func values(dict map[string]interface{}) []interface{} { + values := []interface{}{} + for _, value := range dict { + values = append(values, value) + } + + return values +} + +func dig(ps ...interface{}) (interface{}, error) { + if len(ps) < 3 { + panic("dig needs at least three arguments") + } + dict := ps[len(ps)-1].(map[string]interface{}) + def := ps[len(ps)-2] + ks := make([]string, len(ps)-2) + for i := 0; i < len(ks); i++ { + ks[i] = ps[i].(string) + } + + return digFromDict(dict, def, ks) +} + +func digFromDict(dict map[string]interface{}, d interface{}, ks []string) (interface{}, error) { + k, ns := ks[0], ks[1:] + step, has := dict[k] + if !has { + return d, nil + } + if len(ns) == 0 { + return step, nil + } + return digFromDict(step.(map[string]interface{}), d, ns) +} diff --git a/util/sprig/dict_test.go b/util/sprig/dict_test.go new file mode 100644 index 00000000..0b293140 --- /dev/null +++ b/util/sprig/dict_test.go @@ -0,0 +1,166 @@ +package sprig + +import ( + "strings" + "testing" +) + +func TestDict(t *testing.T) { + tpl := `{{$d := dict 1 2 "three" "four" 5}}{{range $k, $v := $d}}{{$k}}{{$v}}{{end}}` + out, err := runRaw(tpl, nil) + if err != nil { + t.Error(err) + } + if len(out) != 12 { + t.Errorf("Expected length 12, got %d", len(out)) + } + // dict does not guarantee ordering because it is backed by a map. + if !strings.Contains(out, "12") { + t.Error("Expected grouping 12") + } + if !strings.Contains(out, "threefour") { + t.Error("Expected grouping threefour") + } + if !strings.Contains(out, "5") { + t.Error("Expected 5") + } + tpl = `{{$t := dict "I" "shot" "the" "albatross"}}{{$t.the}} {{$t.I}}` + if err := runt(tpl, "albatross shot"); err != nil { + t.Error(err) + } +} + +func TestUnset(t *testing.T) { + tpl := `{{- $d := dict "one" 1 "two" 222222 -}} + {{- $_ := unset $d "two" -}} + {{- range $k, $v := $d}}{{$k}}{{$v}}{{- end -}} + ` + + expect := "one1" + if err := runt(tpl, expect); err != nil { + t.Error(err) + } +} +func TestHasKey(t *testing.T) { + tpl := `{{- $d := dict "one" 1 "two" 222222 -}} + {{- if hasKey $d "one" -}}1{{- end -}} + ` + + expect := "1" + if err := runt(tpl, expect); err != nil { + t.Error(err) + } +} + +func TestPluck(t *testing.T) { + tpl := ` + {{- $d := dict "one" 1 "two" 222222 -}} + {{- $d2 := dict "one" 1 "two" 33333 -}} + {{- $d3 := dict "one" 1 -}} + {{- $d4 := dict "one" 1 "two" 4444 -}} + {{- pluck "two" $d $d2 $d3 $d4 -}} + ` + + expect := "[222222 33333 4444]" + if err := runt(tpl, expect); err != nil { + t.Error(err) + } +} + +func TestKeys(t *testing.T) { + tests := map[string]string{ + `{{ dict "foo" 1 "bar" 2 | keys | sortAlpha }}`: "[bar foo]", + `{{ dict | keys }}`: "[]", + `{{ keys (dict "foo" 1) (dict "bar" 2) (dict "bar" 3) | uniq | sortAlpha }}`: "[bar foo]", + } + for tpl, expect := range tests { + if err := runt(tpl, expect); err != nil { + t.Error(err) + } + } +} + +func TestPick(t *testing.T) { + tests := map[string]string{ + `{{- $d := dict "one" 1 "two" 222222 }}{{ pick $d "two" | len -}}`: "1", + `{{- $d := dict "one" 1 "two" 222222 }}{{ pick $d "two" -}}`: "map[two:222222]", + `{{- $d := dict "one" 1 "two" 222222 }}{{ pick $d "one" "two" | len -}}`: "2", + `{{- $d := dict "one" 1 "two" 222222 }}{{ pick $d "one" "two" "three" | len -}}`: "2", + `{{- $d := dict }}{{ pick $d "two" | len -}}`: "0", + } + for tpl, expect := range tests { + if err := runt(tpl, expect); err != nil { + t.Error(err) + } + } +} +func TestOmit(t *testing.T) { + tests := map[string]string{ + `{{- $d := dict "one" 1 "two" 222222 }}{{ omit $d "one" | len -}}`: "1", + `{{- $d := dict "one" 1 "two" 222222 }}{{ omit $d "one" -}}`: "map[two:222222]", + `{{- $d := dict "one" 1 "two" 222222 }}{{ omit $d "one" "two" | len -}}`: "0", + `{{- $d := dict "one" 1 "two" 222222 }}{{ omit $d "two" "three" | len -}}`: "1", + `{{- $d := dict }}{{ omit $d "two" | len -}}`: "0", + } + for tpl, expect := range tests { + if err := runt(tpl, expect); err != nil { + t.Error(err) + } + } +} + +func TestGet(t *testing.T) { + tests := map[string]string{ + `{{- $d := dict "one" 1 }}{{ get $d "one" -}}`: "1", + `{{- $d := dict "one" 1 "two" "2" }}{{ get $d "two" -}}`: "2", + `{{- $d := dict }}{{ get $d "two" -}}`: "", + } + for tpl, expect := range tests { + if err := runt(tpl, expect); err != nil { + t.Error(err) + } + } +} + +func TestSet(t *testing.T) { + tpl := `{{- $d := dict "one" 1 "two" 222222 -}} + {{- $_ := set $d "two" 2 -}} + {{- $_ := set $d "three" 3 -}} + {{- if hasKey $d "one" -}}{{$d.one}}{{- end -}} + {{- if hasKey $d "two" -}}{{$d.two}}{{- end -}} + {{- if hasKey $d "three" -}}{{$d.three}}{{- end -}} + ` + + expect := "123" + if err := runt(tpl, expect); err != nil { + t.Error(err) + } +} + +func TestValues(t *testing.T) { + tests := map[string]string{ + `{{- $d := dict "a" 1 "b" 2 }}{{ values $d | sortAlpha | join "," }}`: "1,2", + `{{- $d := dict "a" "first" "b" 2 }}{{ values $d | sortAlpha | join "," }}`: "2,first", + } + + for tpl, expect := range tests { + if err := runt(tpl, expect); err != nil { + t.Error(err) + } + } +} + +func TestDig(t *testing.T) { + tests := map[string]string{ + `{{- $d := dict "a" (dict "b" (dict "c" 1)) }}{{ dig "a" "b" "c" "" $d }}`: "1", + `{{- $d := dict "a" (dict "b" (dict "c" 1)) }}{{ dig "a" "b" "z" "2" $d }}`: "2", + `{{ dict "a" 1 | dig "a" "" }}`: "1", + `{{ dict "a" 1 | dig "z" "2" }}`: "2", + } + + for tpl, expect := range tests { + if err := runt(tpl, expect); err != nil { + t.Error(err) + } + } +} diff --git a/util/sprig/doc.go b/util/sprig/doc.go new file mode 100644 index 00000000..91031d6d --- /dev/null +++ b/util/sprig/doc.go @@ -0,0 +1,19 @@ +/* +Package sprig provides template functions for Go. + +This package contains a number of utility functions for working with data +inside of Go `html/template` and `text/template` files. + +To add these functions, use the `template.Funcs()` method: + + t := template.New("foo").Funcs(sprig.FuncMap()) + +Note that you should add the function map before you parse any template files. + + In several cases, Sprig reverses the order of arguments from the way they + appear in the standard library. This is to make it easier to pipe + arguments into functions. + +See http://masterminds.github.io/sprig/ for more detailed documentation on each of the available functions. +*/ +package sprig diff --git a/util/sprig/example_test.go b/util/sprig/example_test.go new file mode 100644 index 00000000..2d7696bf --- /dev/null +++ b/util/sprig/example_test.go @@ -0,0 +1,25 @@ +package sprig + +import ( + "fmt" + "os" + "text/template" +) + +func Example() { + // Set up variables and template. + vars := map[string]interface{}{"Name": " John Jacob Jingleheimer Schmidt "} + tpl := `Hello {{.Name | trim | lower}}` + + // Get the Sprig function map. + fmap := TxtFuncMap() + t := template.Must(template.New("test").Funcs(fmap).Parse(tpl)) + + err := t.Execute(os.Stdout, vars) + if err != nil { + fmt.Printf("Error during template execution: %s", err) + return + } + // Output: + // Hello john jacob jingleheimer schmidt +} diff --git a/util/sprig/flow_control_test.go b/util/sprig/flow_control_test.go new file mode 100644 index 00000000..d4e5ebf0 --- /dev/null +++ b/util/sprig/flow_control_test.go @@ -0,0 +1,16 @@ +package sprig + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFail(t *testing.T) { + const msg = "This is an error!" + tpl := fmt.Sprintf(`{{fail "%s"}}`, msg) + _, err := runRaw(tpl, nil) + assert.Error(t, err) + assert.Contains(t, err.Error(), msg) +} diff --git a/util/sprig/functions.go b/util/sprig/functions.go new file mode 100644 index 00000000..8549e99c --- /dev/null +++ b/util/sprig/functions.go @@ -0,0 +1,302 @@ +package sprig + +import ( + "errors" + "html/template" + "math/rand" + "path" + "path/filepath" + "reflect" + "strconv" + "strings" + ttemplate "text/template" + "time" +) + +// FuncMap produces the function map. +// +// Use this to pass the functions into the template engine: +// +// tpl := template.New("foo").Funcs(sprig.FuncMap())) +func FuncMap() template.FuncMap { + return HTMLFuncMap() +} + +// HermeticTxtFuncMap returns a 'text/template'.FuncMap with only repeatable functions. +func HermeticTxtFuncMap() ttemplate.FuncMap { + r := TxtFuncMap() + for _, name := range nonhermeticFunctions { + delete(r, name) + } + return r +} + +// HermeticHTMLFuncMap returns an 'html/template'.Funcmap with only repeatable functions. +func HermeticHTMLFuncMap() template.FuncMap { + r := HTMLFuncMap() + for _, name := range nonhermeticFunctions { + delete(r, name) + } + return r +} + +// TxtFuncMap returns a 'text/template'.FuncMap +func TxtFuncMap() ttemplate.FuncMap { + return ttemplate.FuncMap(GenericFuncMap()) +} + +// HTMLFuncMap returns an 'html/template'.Funcmap +func HTMLFuncMap() template.FuncMap { + return template.FuncMap(GenericFuncMap()) +} + +// GenericFuncMap returns a copy of the basic function map as a map[string]interface{}. +func GenericFuncMap() map[string]interface{} { + gfm := make(map[string]interface{}, len(genericMap)) + for k, v := range genericMap { + gfm[k] = v + } + return gfm +} + +// These functions are not guaranteed to evaluate to the same result for given input, because they +// refer to the environment or global state. +var nonhermeticFunctions = []string{ + // Date functions + "date", + "date_in_zone", + "date_modify", + "now", + "htmlDate", + "htmlDateInZone", + "dateInZone", + "dateModify", + + // Strings + "randAlphaNum", + "randAlpha", + "randAscii", + "randNumeric", + "randBytes", + "uuidv4", +} + +var genericMap = map[string]interface{}{ + "hello": func() string { return "Hello!" }, + + // Date functions + "ago": dateAgo, + "date": date, + "date_in_zone": dateInZone, + "date_modify": dateModify, + "dateInZone": dateInZone, + "dateModify": dateModify, + "duration": duration, + "durationRound": durationRound, + "htmlDate": htmlDate, + "htmlDateInZone": htmlDateInZone, + "must_date_modify": mustDateModify, + "mustDateModify": mustDateModify, + "mustToDate": mustToDate, + "now": time.Now, + "toDate": toDate, + "unixEpoch": unixEpoch, + + // Strings + "trunc": trunc, + "trim": strings.TrimSpace, + "upper": strings.ToUpper, + "lower": strings.ToLower, + "title": strings.Title, + "substr": substring, + // Switch order so that "foo" | repeat 5 + "repeat": func(count int, str string) string { return strings.Repeat(str, count) }, + // Deprecated: Use trimAll. + "trimall": func(a, b string) string { return strings.Trim(b, a) }, + // Switch order so that "$foo" | trimall "$" + "trimAll": func(a, b string) string { return strings.Trim(b, a) }, + "trimSuffix": func(a, b string) string { return strings.TrimSuffix(b, a) }, + "trimPrefix": func(a, b string) string { return strings.TrimPrefix(b, a) }, + // Switch order so that "foobar" | contains "foo" + "contains": func(substr string, str string) bool { return strings.Contains(str, substr) }, + "hasPrefix": func(substr string, str string) bool { return strings.HasPrefix(str, substr) }, + "hasSuffix": func(substr string, str string) bool { return strings.HasSuffix(str, substr) }, + "quote": quote, + "squote": squote, + "cat": cat, + "indent": indent, + "nindent": nindent, + "replace": replace, + "plural": plural, + "sha1sum": sha1sum, + "sha256sum": sha256sum, + "sha512sum": sha512sum, + "adler32sum": adler32sum, + "toString": strval, + + // Wrap Atoi to stop errors. + "atoi": func(a string) int { i, _ := strconv.Atoi(a); return i }, + "seq": seq, + "toDecimal": toDecimal, + + //"gt": func(a, b int) bool {return a > b}, + //"gte": func(a, b int) bool {return a >= b}, + //"lt": func(a, b int) bool {return a < b}, + //"lte": func(a, b int) bool {return a <= b}, + + // split "/" foo/bar returns map[int]string{0: foo, 1: bar} + "split": split, + "splitList": func(sep, orig string) []string { return strings.Split(orig, sep) }, + // splitn "/" foo/bar/fuu returns map[int]string{0: foo, 1: bar/fuu} + "splitn": splitn, + "toStrings": strslice, + + "until": until, + "untilStep": untilStep, + + // VERY basic arithmetic. + "add1": func(i interface{}) int64 { return toInt64(i) + 1 }, + "add": func(i ...interface{}) int64 { + var a int64 = 0 + for _, b := range i { + a += toInt64(b) + } + return a + }, + "sub": func(a, b interface{}) int64 { return toInt64(a) - toInt64(b) }, + "div": func(a, b interface{}) int64 { return toInt64(a) / toInt64(b) }, + "mod": func(a, b interface{}) int64 { return toInt64(a) % toInt64(b) }, + "mul": func(a interface{}, v ...interface{}) int64 { + val := toInt64(a) + for _, b := range v { + val = val * toInt64(b) + } + return val + }, + "randInt": func(min, max int) int { return rand.Intn(max-min) + min }, + "biggest": max, + "max": max, + "min": min, + "maxf": maxf, + "minf": minf, + "ceil": ceil, + "floor": floor, + "round": round, + + // string slices. Note that we reverse the order b/c that's better + // for template processing. + "join": join, + "sortAlpha": sortAlpha, + + // Defaults + "default": dfault, + "empty": empty, + "coalesce": coalesce, + "all": all, + "any": any, + "compact": compact, + "mustCompact": mustCompact, + "fromJSON": fromJSON, + "toJSON": toJSON, + "toPrettyJSON": toPrettyJSON, + "toRawJSON": toRawJSON, + "mustFromJSON": mustFromJSON, + "mustToJSON": mustToJSON, + "mustToPrettyJSON": mustToPrettyJSON, + "mustToRawJSON": mustToRawJSON, + "ternary": ternary, + + // Reflection + "typeOf": typeOf, + "typeIs": typeIs, + "typeIsLike": typeIsLike, + "kindOf": kindOf, + "kindIs": kindIs, + "deepEqual": reflect.DeepEqual, + + // Paths: + "base": path.Base, + "dir": path.Dir, + "clean": path.Clean, + "ext": path.Ext, + "isAbs": path.IsAbs, + + // Filepaths: + "osBase": filepath.Base, + "osClean": filepath.Clean, + "osDir": filepath.Dir, + "osExt": filepath.Ext, + "osIsAbs": filepath.IsAbs, + + // Encoding: + "b64enc": base64encode, + "b64dec": base64decode, + "b32enc": base32encode, + "b32dec": base32decode, + + // Data Structures: + "tuple": list, // FIXME: with the addition of append/prepend these are no longer immutable. + "list": list, + "dict": dict, + "get": get, + "set": set, + "unset": unset, + "hasKey": hasKey, + "pluck": pluck, + "keys": keys, + "pick": pick, + "omit": omit, + "values": values, + + "append": push, "push": push, + "mustAppend": mustPush, "mustPush": mustPush, + "prepend": prepend, + "mustPrepend": mustPrepend, + "first": first, + "mustFirst": mustFirst, + "rest": rest, + "mustRest": mustRest, + "last": last, + "mustLast": mustLast, + "initial": initial, + "mustInitial": mustInitial, + "reverse": reverse, + "mustReverse": mustReverse, + "uniq": uniq, + "mustUniq": mustUniq, + "without": without, + "mustWithout": mustWithout, + "has": has, + "mustHas": mustHas, + "slice": slice, + "mustSlice": mustSlice, + "concat": concat, + "dig": dig, + "chunk": chunk, + "mustChunk": mustChunk, + + // UUIDs: + "uuidv4": uuidv4, + + // Flow Control: + "fail": func(msg string) (string, error) { return "", errors.New(msg) }, + + // Regex + "regexMatch": regexMatch, + "mustRegexMatch": mustRegexMatch, + "regexFindAll": regexFindAll, + "mustRegexFindAll": mustRegexFindAll, + "regexFind": regexFind, + "mustRegexFind": mustRegexFind, + "regexReplaceAll": regexReplaceAll, + "mustRegexReplaceAll": mustRegexReplaceAll, + "regexReplaceAllLiteral": regexReplaceAllLiteral, + "mustRegexReplaceAllLiteral": mustRegexReplaceAllLiteral, + "regexSplit": regexSplit, + "mustRegexSplit": mustRegexSplit, + "regexQuoteMeta": regexQuoteMeta, + + // URLs: + "urlParse": urlParse, + "urlJoin": urlJoin, +} diff --git a/util/sprig/functions_linux_test.go b/util/sprig/functions_linux_test.go new file mode 100644 index 00000000..cfbf253a --- /dev/null +++ b/util/sprig/functions_linux_test.go @@ -0,0 +1,28 @@ +package sprig + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestOsBase(t *testing.T) { + assert.NoError(t, runt(`{{ osBase "foo/bar" }}`, "bar")) +} + +func TestOsDir(t *testing.T) { + assert.NoError(t, runt(`{{ osDir "foo/bar/baz" }}`, "foo/bar")) +} + +func TestOsIsAbs(t *testing.T) { + assert.NoError(t, runt(`{{ osIsAbs "/foo" }}`, "true")) + assert.NoError(t, runt(`{{ osIsAbs "foo" }}`, "false")) +} + +func TestOsClean(t *testing.T) { + assert.NoError(t, runt(`{{ osClean "/foo/../foo/../bar" }}`, "/bar")) +} + +func TestOsExt(t *testing.T) { + assert.NoError(t, runt(`{{ osExt "/foo/bar/baz.txt" }}`, ".txt")) +} diff --git a/util/sprig/functions_test.go b/util/sprig/functions_test.go new file mode 100644 index 00000000..b7bc01f4 --- /dev/null +++ b/util/sprig/functions_test.go @@ -0,0 +1,70 @@ +package sprig + +import ( + "bytes" + "fmt" + "testing" + "text/template" + + "github.com/stretchr/testify/assert" +) + +func TestBase(t *testing.T) { + assert.NoError(t, runt(`{{ base "foo/bar" }}`, "bar")) +} + +func TestDir(t *testing.T) { + assert.NoError(t, runt(`{{ dir "foo/bar/baz" }}`, "foo/bar")) +} + +func TestIsAbs(t *testing.T) { + assert.NoError(t, runt(`{{ isAbs "/foo" }}`, "true")) + assert.NoError(t, runt(`{{ isAbs "foo" }}`, "false")) +} + +func TestClean(t *testing.T) { + assert.NoError(t, runt(`{{ clean "/foo/../foo/../bar" }}`, "/bar")) +} + +func TestExt(t *testing.T) { + assert.NoError(t, runt(`{{ ext "/foo/bar/baz.txt" }}`, ".txt")) +} + +func TestRegex(t *testing.T) { + assert.NoError(t, runt(`{{ regexQuoteMeta "1.2.3" }}`, "1\\.2\\.3")) + assert.NoError(t, runt(`{{ regexQuoteMeta "pretzel" }}`, "pretzel")) +} + +// runt runs a template and checks that the output exactly matches the expected string. +func runt(tpl, expect string) error { + return runtv(tpl, expect, map[string]string{}) +} + +// runtv takes a template, and expected return, and values for substitution. +// +// It runs the template and verifies that the output is an exact match. +func runtv(tpl, expect string, vars interface{}) error { + fmap := TxtFuncMap() + t := template.Must(template.New("test").Funcs(fmap).Parse(tpl)) + var b bytes.Buffer + err := t.Execute(&b, vars) + if err != nil { + return err + } + if expect != b.String() { + return fmt.Errorf("Expected '%s', got '%s'", expect, b.String()) + } + return nil +} + +// runRaw runs a template with the given variables and returns the result. +func runRaw(tpl string, vars interface{}) (string, error) { + fmap := TxtFuncMap() + t := template.Must(template.New("test").Funcs(fmap).Parse(tpl)) + var b bytes.Buffer + err := t.Execute(&b, vars) + if err != nil { + return "", err + } + return b.String(), nil +} diff --git a/util/sprig/functions_windows_test.go b/util/sprig/functions_windows_test.go new file mode 100644 index 00000000..9d8bd0e5 --- /dev/null +++ b/util/sprig/functions_windows_test.go @@ -0,0 +1,28 @@ +package sprig + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestOsBase(t *testing.T) { + assert.NoError(t, runt(`{{ osBase "C:\\foo\\bar" }}`, "bar")) +} + +func TestOsDir(t *testing.T) { + assert.NoError(t, runt(`{{ osDir "C:\\foo\\bar\\baz" }}`, "C:\\foo\\bar")) +} + +func TestOsIsAbs(t *testing.T) { + assert.NoError(t, runt(`{{ osIsAbs "C:\\foo" }}`, "true")) + assert.NoError(t, runt(`{{ osIsAbs "foo" }}`, "false")) +} + +func TestOsClean(t *testing.T) { + assert.NoError(t, runt(`{{ osClean "C:\\foo\\..\\foo\\..\\bar" }}`, "C:\\bar")) +} + +func TestOsExt(t *testing.T) { + assert.NoError(t, runt(`{{ osExt "C:\\foo\\bar\\baz.txt" }}`, ".txt")) +} diff --git a/util/sprig/list.go b/util/sprig/list.go new file mode 100644 index 00000000..ca0fbb78 --- /dev/null +++ b/util/sprig/list.go @@ -0,0 +1,464 @@ +package sprig + +import ( + "fmt" + "math" + "reflect" + "sort" +) + +// Reflection is used in these functions so that slices and arrays of strings, +// ints, and other types not implementing []interface{} can be worked with. +// For example, this is useful if you need to work on the output of regexs. + +func list(v ...interface{}) []interface{} { + return v +} + +func push(list interface{}, v interface{}) []interface{} { + l, err := mustPush(list, v) + if err != nil { + panic(err) + } + + return l +} + +func mustPush(list interface{}, v interface{}) ([]interface{}, error) { + tp := reflect.TypeOf(list).Kind() + switch tp { + case reflect.Slice, reflect.Array: + l2 := reflect.ValueOf(list) + + l := l2.Len() + nl := make([]interface{}, l) + for i := 0; i < l; i++ { + nl[i] = l2.Index(i).Interface() + } + + return append(nl, v), nil + + default: + return nil, fmt.Errorf("Cannot push on type %s", tp) + } +} + +func prepend(list interface{}, v interface{}) []interface{} { + l, err := mustPrepend(list, v) + if err != nil { + panic(err) + } + + return l +} + +func mustPrepend(list interface{}, v interface{}) ([]interface{}, error) { + //return append([]interface{}{v}, list...) + + tp := reflect.TypeOf(list).Kind() + switch tp { + case reflect.Slice, reflect.Array: + l2 := reflect.ValueOf(list) + + l := l2.Len() + nl := make([]interface{}, l) + for i := 0; i < l; i++ { + nl[i] = l2.Index(i).Interface() + } + + return append([]interface{}{v}, nl...), nil + + default: + return nil, fmt.Errorf("Cannot prepend on type %s", tp) + } +} + +func chunk(size int, list interface{}) [][]interface{} { + l, err := mustChunk(size, list) + if err != nil { + panic(err) + } + + return l +} + +func mustChunk(size int, list interface{}) ([][]interface{}, error) { + tp := reflect.TypeOf(list).Kind() + switch tp { + case reflect.Slice, reflect.Array: + l2 := reflect.ValueOf(list) + + l := l2.Len() + + cs := int(math.Floor(float64(l-1)/float64(size)) + 1) + nl := make([][]interface{}, cs) + + for i := 0; i < cs; i++ { + clen := size + if i == cs-1 { + clen = int(math.Floor(math.Mod(float64(l), float64(size)))) + if clen == 0 { + clen = size + } + } + + nl[i] = make([]interface{}, clen) + + for j := 0; j < clen; j++ { + ix := i*size + j + nl[i][j] = l2.Index(ix).Interface() + } + } + + return nl, nil + + default: + return nil, fmt.Errorf("Cannot chunk type %s", tp) + } +} + +func last(list interface{}) interface{} { + l, err := mustLast(list) + if err != nil { + panic(err) + } + + return l +} + +func mustLast(list interface{}) (interface{}, error) { + tp := reflect.TypeOf(list).Kind() + switch tp { + case reflect.Slice, reflect.Array: + l2 := reflect.ValueOf(list) + + l := l2.Len() + if l == 0 { + return nil, nil + } + + return l2.Index(l - 1).Interface(), nil + default: + return nil, fmt.Errorf("Cannot find last on type %s", tp) + } +} + +func first(list interface{}) interface{} { + l, err := mustFirst(list) + if err != nil { + panic(err) + } + + return l +} + +func mustFirst(list interface{}) (interface{}, error) { + tp := reflect.TypeOf(list).Kind() + switch tp { + case reflect.Slice, reflect.Array: + l2 := reflect.ValueOf(list) + + l := l2.Len() + if l == 0 { + return nil, nil + } + + return l2.Index(0).Interface(), nil + default: + return nil, fmt.Errorf("Cannot find first on type %s", tp) + } +} + +func rest(list interface{}) []interface{} { + l, err := mustRest(list) + if err != nil { + panic(err) + } + + return l +} + +func mustRest(list interface{}) ([]interface{}, error) { + tp := reflect.TypeOf(list).Kind() + switch tp { + case reflect.Slice, reflect.Array: + l2 := reflect.ValueOf(list) + + l := l2.Len() + if l == 0 { + return nil, nil + } + + nl := make([]interface{}, l-1) + for i := 1; i < l; i++ { + nl[i-1] = l2.Index(i).Interface() + } + + return nl, nil + default: + return nil, fmt.Errorf("Cannot find rest on type %s", tp) + } +} + +func initial(list interface{}) []interface{} { + l, err := mustInitial(list) + if err != nil { + panic(err) + } + + return l +} + +func mustInitial(list interface{}) ([]interface{}, error) { + tp := reflect.TypeOf(list).Kind() + switch tp { + case reflect.Slice, reflect.Array: + l2 := reflect.ValueOf(list) + + l := l2.Len() + if l == 0 { + return nil, nil + } + + nl := make([]interface{}, l-1) + for i := 0; i < l-1; i++ { + nl[i] = l2.Index(i).Interface() + } + + return nl, nil + default: + return nil, fmt.Errorf("Cannot find initial on type %s", tp) + } +} + +func sortAlpha(list interface{}) []string { + k := reflect.Indirect(reflect.ValueOf(list)).Kind() + switch k { + case reflect.Slice, reflect.Array: + a := strslice(list) + s := sort.StringSlice(a) + s.Sort() + return s + } + return []string{strval(list)} +} + +func reverse(v interface{}) []interface{} { + l, err := mustReverse(v) + if err != nil { + panic(err) + } + + return l +} + +func mustReverse(v interface{}) ([]interface{}, error) { + tp := reflect.TypeOf(v).Kind() + switch tp { + case reflect.Slice, reflect.Array: + l2 := reflect.ValueOf(v) + + l := l2.Len() + // We do not sort in place because the incoming array should not be altered. + nl := make([]interface{}, l) + for i := 0; i < l; i++ { + nl[l-i-1] = l2.Index(i).Interface() + } + + return nl, nil + default: + return nil, fmt.Errorf("Cannot find reverse on type %s", tp) + } +} + +func compact(list interface{}) []interface{} { + l, err := mustCompact(list) + if err != nil { + panic(err) + } + + return l +} + +func mustCompact(list interface{}) ([]interface{}, error) { + tp := reflect.TypeOf(list).Kind() + switch tp { + case reflect.Slice, reflect.Array: + l2 := reflect.ValueOf(list) + + l := l2.Len() + nl := []interface{}{} + var item interface{} + for i := 0; i < l; i++ { + item = l2.Index(i).Interface() + if !empty(item) { + nl = append(nl, item) + } + } + + return nl, nil + default: + return nil, fmt.Errorf("Cannot compact on type %s", tp) + } +} + +func uniq(list interface{}) []interface{} { + l, err := mustUniq(list) + if err != nil { + panic(err) + } + + return l +} + +func mustUniq(list interface{}) ([]interface{}, error) { + tp := reflect.TypeOf(list).Kind() + switch tp { + case reflect.Slice, reflect.Array: + l2 := reflect.ValueOf(list) + + l := l2.Len() + dest := []interface{}{} + var item interface{} + for i := 0; i < l; i++ { + item = l2.Index(i).Interface() + if !inList(dest, item) { + dest = append(dest, item) + } + } + + return dest, nil + default: + return nil, fmt.Errorf("Cannot find uniq on type %s", tp) + } +} + +func inList(haystack []interface{}, needle interface{}) bool { + for _, h := range haystack { + if reflect.DeepEqual(needle, h) { + return true + } + } + return false +} + +func without(list interface{}, omit ...interface{}) []interface{} { + l, err := mustWithout(list, omit...) + if err != nil { + panic(err) + } + + return l +} + +func mustWithout(list interface{}, omit ...interface{}) ([]interface{}, error) { + tp := reflect.TypeOf(list).Kind() + switch tp { + case reflect.Slice, reflect.Array: + l2 := reflect.ValueOf(list) + + l := l2.Len() + res := []interface{}{} + var item interface{} + for i := 0; i < l; i++ { + item = l2.Index(i).Interface() + if !inList(omit, item) { + res = append(res, item) + } + } + + return res, nil + default: + return nil, fmt.Errorf("Cannot find without on type %s", tp) + } +} + +func has(needle interface{}, haystack interface{}) bool { + l, err := mustHas(needle, haystack) + if err != nil { + panic(err) + } + + return l +} + +func mustHas(needle interface{}, haystack interface{}) (bool, error) { + if haystack == nil { + return false, nil + } + tp := reflect.TypeOf(haystack).Kind() + switch tp { + case reflect.Slice, reflect.Array: + l2 := reflect.ValueOf(haystack) + var item interface{} + l := l2.Len() + for i := 0; i < l; i++ { + item = l2.Index(i).Interface() + if reflect.DeepEqual(needle, item) { + return true, nil + } + } + + return false, nil + default: + return false, fmt.Errorf("Cannot find has on type %s", tp) + } +} + +// $list := [1, 2, 3, 4, 5] +// slice $list -> list[0:5] = list[:] +// slice $list 0 3 -> list[0:3] = list[:3] +// slice $list 3 5 -> list[3:5] +// slice $list 3 -> list[3:5] = list[3:] +func slice(list interface{}, indices ...interface{}) interface{} { + l, err := mustSlice(list, indices...) + if err != nil { + panic(err) + } + + return l +} + +func mustSlice(list interface{}, indices ...interface{}) (interface{}, error) { + tp := reflect.TypeOf(list).Kind() + switch tp { + case reflect.Slice, reflect.Array: + l2 := reflect.ValueOf(list) + + l := l2.Len() + if l == 0 { + return nil, nil + } + + var start, end int + if len(indices) > 0 { + start = toInt(indices[0]) + } + if len(indices) < 2 { + end = l + } else { + end = toInt(indices[1]) + } + + return l2.Slice(start, end).Interface(), nil + default: + return nil, fmt.Errorf("list should be type of slice or array but %s", tp) + } +} + +func concat(lists ...interface{}) interface{} { + var res []interface{} + for _, list := range lists { + tp := reflect.TypeOf(list).Kind() + switch tp { + case reflect.Slice, reflect.Array: + l2 := reflect.ValueOf(list) + for i := 0; i < l2.Len(); i++ { + res = append(res, l2.Index(i).Interface()) + } + default: + panic(fmt.Sprintf("Cannot concat type %s as list", tp)) + } + } + return res +} diff --git a/util/sprig/list_test.go b/util/sprig/list_test.go new file mode 100644 index 00000000..ec4c4c14 --- /dev/null +++ b/util/sprig/list_test.go @@ -0,0 +1,364 @@ +package sprig + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestTuple(t *testing.T) { + tpl := `{{$t := tuple 1 "a" "foo"}}{{index $t 2}}{{index $t 0 }}{{index $t 1}}` + if err := runt(tpl, "foo1a"); err != nil { + t.Error(err) + } +} + +func TestList(t *testing.T) { + tpl := `{{$t := list 1 "a" "foo"}}{{index $t 2}}{{index $t 0 }}{{index $t 1}}` + if err := runt(tpl, "foo1a"); err != nil { + t.Error(err) + } +} + +func TestPush(t *testing.T) { + // Named `append` in the function map + tests := map[string]string{ + `{{ $t := tuple 1 2 3 }}{{ append $t 4 | len }}`: "4", + `{{ $t := tuple 1 2 3 4 }}{{ append $t 5 | join "-" }}`: "1-2-3-4-5", + `{{ $t := regexSplit "/" "foo/bar/baz" -1 }}{{ append $t "qux" | join "-" }}`: "foo-bar-baz-qux", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} + +func TestMustPush(t *testing.T) { + // Named `append` in the function map + tests := map[string]string{ + `{{ $t := tuple 1 2 3 }}{{ mustAppend $t 4 | len }}`: "4", + `{{ $t := tuple 1 2 3 4 }}{{ mustAppend $t 5 | join "-" }}`: "1-2-3-4-5", + `{{ $t := regexSplit "/" "foo/bar/baz" -1 }}{{ mustPush $t "qux" | join "-" }}`: "foo-bar-baz-qux", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} + +func TestChunk(t *testing.T) { + tests := map[string]string{ + `{{ tuple 1 2 3 4 5 6 7 | chunk 3 | len }}`: "3", + `{{ tuple | chunk 3 | len }}`: "0", + `{{ range ( tuple 1 2 3 4 5 6 7 8 9 | chunk 3 ) }}{{. | join "-"}}|{{end}}`: "1-2-3|4-5-6|7-8-9|", + `{{ range ( tuple 1 2 3 4 5 6 7 8 | chunk 3 ) }}{{. | join "-"}}|{{end}}`: "1-2-3|4-5-6|7-8|", + `{{ range ( tuple 1 2 | chunk 3 ) }}{{. | join "-"}}|{{end}}`: "1-2|", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} + +func TestMustChunk(t *testing.T) { + tests := map[string]string{ + `{{ tuple 1 2 3 4 5 6 7 | mustChunk 3 | len }}`: "3", + `{{ tuple | mustChunk 3 | len }}`: "0", + `{{ range ( tuple 1 2 3 4 5 6 7 8 9 | mustChunk 3 ) }}{{. | join "-"}}|{{end}}`: "1-2-3|4-5-6|7-8-9|", + `{{ range ( tuple 1 2 3 4 5 6 7 8 | mustChunk 3 ) }}{{. | join "-"}}|{{end}}`: "1-2-3|4-5-6|7-8|", + `{{ range ( tuple 1 2 | mustChunk 3 ) }}{{. | join "-"}}|{{end}}`: "1-2|", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} + +func TestPrepend(t *testing.T) { + tests := map[string]string{ + `{{ $t := tuple 1 2 3 }}{{ prepend $t 0 | len }}`: "4", + `{{ $t := tuple 1 2 3 4 }}{{ prepend $t 0 | join "-" }}`: "0-1-2-3-4", + `{{ $t := regexSplit "/" "foo/bar/baz" -1 }}{{ prepend $t "qux" | join "-" }}`: "qux-foo-bar-baz", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} + +func TestMustPrepend(t *testing.T) { + tests := map[string]string{ + `{{ $t := tuple 1 2 3 }}{{ mustPrepend $t 0 | len }}`: "4", + `{{ $t := tuple 1 2 3 4 }}{{ mustPrepend $t 0 | join "-" }}`: "0-1-2-3-4", + `{{ $t := regexSplit "/" "foo/bar/baz" -1 }}{{ mustPrepend $t "qux" | join "-" }}`: "qux-foo-bar-baz", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} + +func TestFirst(t *testing.T) { + tests := map[string]string{ + `{{ list 1 2 3 | first }}`: "1", + `{{ list | first }}`: "", + `{{ regexSplit "/src/" "foo/src/bar" -1 | first }}`: "foo", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} + +func TestMustFirst(t *testing.T) { + tests := map[string]string{ + `{{ list 1 2 3 | mustFirst }}`: "1", + `{{ list | mustFirst }}`: "", + `{{ regexSplit "/src/" "foo/src/bar" -1 | mustFirst }}`: "foo", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} + +func TestLast(t *testing.T) { + tests := map[string]string{ + `{{ list 1 2 3 | last }}`: "3", + `{{ list | last }}`: "", + `{{ regexSplit "/src/" "foo/src/bar" -1 | last }}`: "bar", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} + +func TestMustLast(t *testing.T) { + tests := map[string]string{ + `{{ list 1 2 3 | mustLast }}`: "3", + `{{ list | mustLast }}`: "", + `{{ regexSplit "/src/" "foo/src/bar" -1 | mustLast }}`: "bar", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} + +func TestInitial(t *testing.T) { + tests := map[string]string{ + `{{ list 1 2 3 | initial | len }}`: "2", + `{{ list 1 2 3 | initial | last }}`: "2", + `{{ list 1 2 3 | initial | first }}`: "1", + `{{ list | initial }}`: "[]", + `{{ regexSplit "/" "foo/bar/baz" -1 | initial }}`: "[foo bar]", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} + +func TestMustInitial(t *testing.T) { + tests := map[string]string{ + `{{ list 1 2 3 | mustInitial | len }}`: "2", + `{{ list 1 2 3 | mustInitial | last }}`: "2", + `{{ list 1 2 3 | mustInitial | first }}`: "1", + `{{ list | mustInitial }}`: "[]", + `{{ regexSplit "/" "foo/bar/baz" -1 | mustInitial }}`: "[foo bar]", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} + +func TestRest(t *testing.T) { + tests := map[string]string{ + `{{ list 1 2 3 | rest | len }}`: "2", + `{{ list 1 2 3 | rest | last }}`: "3", + `{{ list 1 2 3 | rest | first }}`: "2", + `{{ list | rest }}`: "[]", + `{{ regexSplit "/" "foo/bar/baz" -1 | rest }}`: "[bar baz]", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} + +func TestMustRest(t *testing.T) { + tests := map[string]string{ + `{{ list 1 2 3 | mustRest | len }}`: "2", + `{{ list 1 2 3 | mustRest | last }}`: "3", + `{{ list 1 2 3 | mustRest | first }}`: "2", + `{{ list | mustRest }}`: "[]", + `{{ regexSplit "/" "foo/bar/baz" -1 | mustRest }}`: "[bar baz]", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} + +func TestReverse(t *testing.T) { + tests := map[string]string{ + `{{ list 1 2 3 | reverse | first }}`: "3", + `{{ list 1 2 3 | reverse | rest | first }}`: "2", + `{{ list 1 2 3 | reverse | last }}`: "1", + `{{ list 1 2 3 4 | reverse }}`: "[4 3 2 1]", + `{{ list 1 | reverse }}`: "[1]", + `{{ list | reverse }}`: "[]", + `{{ regexSplit "/" "foo/bar/baz" -1 | reverse }}`: "[baz bar foo]", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} + +func TestMustReverse(t *testing.T) { + tests := map[string]string{ + `{{ list 1 2 3 | mustReverse | first }}`: "3", + `{{ list 1 2 3 | mustReverse | rest | first }}`: "2", + `{{ list 1 2 3 | mustReverse | last }}`: "1", + `{{ list 1 2 3 4 | mustReverse }}`: "[4 3 2 1]", + `{{ list 1 | mustReverse }}`: "[1]", + `{{ list | mustReverse }}`: "[]", + `{{ regexSplit "/" "foo/bar/baz" -1 | mustReverse }}`: "[baz bar foo]", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} + +func TestCompact(t *testing.T) { + tests := map[string]string{ + `{{ list 1 0 "" "hello" | compact }}`: `[1 hello]`, + `{{ list "" "" | compact }}`: `[]`, + `{{ list | compact }}`: `[]`, + `{{ regexSplit "/" "foo//bar" -1 | compact }}`: "[foo bar]", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} + +func TestMustCompact(t *testing.T) { + tests := map[string]string{ + `{{ list 1 0 "" "hello" | mustCompact }}`: `[1 hello]`, + `{{ list "" "" | mustCompact }}`: `[]`, + `{{ list | mustCompact }}`: `[]`, + `{{ regexSplit "/" "foo//bar" -1 | mustCompact }}`: "[foo bar]", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} + +func TestUniq(t *testing.T) { + tests := map[string]string{ + `{{ list 1 2 3 4 | uniq }}`: `[1 2 3 4]`, + `{{ list "a" "b" "c" "d" | uniq }}`: `[a b c d]`, + `{{ list 1 1 1 1 2 2 2 2 | uniq }}`: `[1 2]`, + `{{ list "foo" 1 1 1 1 "foo" "foo" | uniq }}`: `[foo 1]`, + `{{ list | uniq }}`: `[]`, + `{{ regexSplit "/" "foo/foo/bar" -1 | uniq }}`: "[foo bar]", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} + +func TestMustUniq(t *testing.T) { + tests := map[string]string{ + `{{ list 1 2 3 4 | mustUniq }}`: `[1 2 3 4]`, + `{{ list "a" "b" "c" "d" | mustUniq }}`: `[a b c d]`, + `{{ list 1 1 1 1 2 2 2 2 | mustUniq }}`: `[1 2]`, + `{{ list "foo" 1 1 1 1 "foo" "foo" | mustUniq }}`: `[foo 1]`, + `{{ list | mustUniq }}`: `[]`, + `{{ regexSplit "/" "foo/foo/bar" -1 | mustUniq }}`: "[foo bar]", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} + +func TestWithout(t *testing.T) { + tests := map[string]string{ + `{{ without (list 1 2 3 4) 1 }}`: `[2 3 4]`, + `{{ without (list "a" "b" "c" "d") "a" }}`: `[b c d]`, + `{{ without (list 1 1 1 1 2) 1 }}`: `[2]`, + `{{ without (list) 1 }}`: `[]`, + `{{ without (list 1 2 3) }}`: `[1 2 3]`, + `{{ without list }}`: `[]`, + `{{ without (regexSplit "/" "foo/bar/baz" -1 ) "foo" }}`: "[bar baz]", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} + +func TestMustWithout(t *testing.T) { + tests := map[string]string{ + `{{ mustWithout (list 1 2 3 4) 1 }}`: `[2 3 4]`, + `{{ mustWithout (list "a" "b" "c" "d") "a" }}`: `[b c d]`, + `{{ mustWithout (list 1 1 1 1 2) 1 }}`: `[2]`, + `{{ mustWithout (list) 1 }}`: `[]`, + `{{ mustWithout (list 1 2 3) }}`: `[1 2 3]`, + `{{ mustWithout list }}`: `[]`, + `{{ mustWithout (regexSplit "/" "foo/bar/baz" -1 ) "foo" }}`: "[bar baz]", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} + +func TestHas(t *testing.T) { + tests := map[string]string{ + `{{ list 1 2 3 | has 1 }}`: `true`, + `{{ list 1 2 3 | has 4 }}`: `false`, + `{{ regexSplit "/" "foo/bar/baz" -1 | has "bar" }}`: `true`, + `{{ has "bar" nil }}`: `false`, + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} + +func TestMustHas(t *testing.T) { + tests := map[string]string{ + `{{ list 1 2 3 | mustHas 1 }}`: `true`, + `{{ list 1 2 3 | mustHas 4 }}`: `false`, + `{{ regexSplit "/" "foo/bar/baz" -1 | mustHas "bar" }}`: `true`, + `{{ mustHas "bar" nil }}`: `false`, + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} + +func TestSlice(t *testing.T) { + tests := map[string]string{ + `{{ slice (list 1 2 3) }}`: "[1 2 3]", + `{{ slice (list 1 2 3) 0 1 }}`: "[1]", + `{{ slice (list 1 2 3) 1 3 }}`: "[2 3]", + `{{ slice (list 1 2 3) 1 }}`: "[2 3]", + `{{ slice (regexSplit "/" "foo/bar/baz" -1) 1 2 }}`: "[bar]", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} + +func TestMustSlice(t *testing.T) { + tests := map[string]string{ + `{{ mustSlice (list 1 2 3) }}`: "[1 2 3]", + `{{ mustSlice (list 1 2 3) 0 1 }}`: "[1]", + `{{ mustSlice (list 1 2 3) 1 3 }}`: "[2 3]", + `{{ mustSlice (list 1 2 3) 1 }}`: "[2 3]", + `{{ mustSlice (regexSplit "/" "foo/bar/baz" -1) 1 2 }}`: "[bar]", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} + +func TestConcat(t *testing.T) { + tests := map[string]string{ + `{{ concat (list 1 2 3) }}`: "[1 2 3]", + `{{ concat (list 1 2 3) (list 4 5) }}`: "[1 2 3 4 5]", + `{{ concat (list 1 2 3) (list 4 5) (list) }}`: "[1 2 3 4 5]", + `{{ concat (list 1 2 3) (list 4 5) (list nil) }}`: "[1 2 3 4 5 ]", + `{{ concat (list 1 2 3) (list 4 5) (list ( list "foo" ) ) }}`: "[1 2 3 4 5 [foo]]", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} diff --git a/util/sprig/numeric.go b/util/sprig/numeric.go new file mode 100644 index 00000000..0b23cd21 --- /dev/null +++ b/util/sprig/numeric.go @@ -0,0 +1,228 @@ +package sprig + +import ( + "fmt" + "math" + "reflect" + "strconv" + "strings" +) + +// toFloat64 converts 64-bit floats +func toFloat64(v interface{}) float64 { + if str, ok := v.(string); ok { + iv, err := strconv.ParseFloat(str, 64) + if err != nil { + return 0 + } + return iv + } + + val := reflect.Indirect(reflect.ValueOf(v)) + switch val.Kind() { + case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: + return float64(val.Int()) + case reflect.Uint8, reflect.Uint16, reflect.Uint32: + return float64(val.Uint()) + case reflect.Uint, reflect.Uint64: + return float64(val.Uint()) + case reflect.Float32, reflect.Float64: + return val.Float() + case reflect.Bool: + if val.Bool() { + return 1 + } + return 0 + default: + return 0 + } +} + +func toInt(v interface{}) int { + // It's not optimal. But I don't want duplicate toInt64 code. + return int(toInt64(v)) +} + +// toInt64 converts integer types to 64-bit integers +func toInt64(v interface{}) int64 { + if str, ok := v.(string); ok { + iv, err := strconv.ParseInt(str, 10, 64) + if err != nil { + return 0 + } + return iv + } + + val := reflect.Indirect(reflect.ValueOf(v)) + switch val.Kind() { + case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: + return val.Int() + case reflect.Uint8, reflect.Uint16, reflect.Uint32: + return int64(val.Uint()) + case reflect.Uint, reflect.Uint64: + tv := val.Uint() + if tv <= math.MaxInt64 { + return int64(tv) + } + // TODO: What is the sensible thing to do here? + return math.MaxInt64 + case reflect.Float32, reflect.Float64: + return int64(val.Float()) + case reflect.Bool: + if val.Bool() { + return 1 + } + return 0 + default: + return 0 + } +} + +func max(a interface{}, i ...interface{}) int64 { + aa := toInt64(a) + for _, b := range i { + bb := toInt64(b) + if bb > aa { + aa = bb + } + } + return aa +} + +func maxf(a interface{}, i ...interface{}) float64 { + aa := toFloat64(a) + for _, b := range i { + bb := toFloat64(b) + aa = math.Max(aa, bb) + } + return aa +} + +func min(a interface{}, i ...interface{}) int64 { + aa := toInt64(a) + for _, b := range i { + bb := toInt64(b) + if bb < aa { + aa = bb + } + } + return aa +} + +func minf(a interface{}, i ...interface{}) float64 { + aa := toFloat64(a) + for _, b := range i { + bb := toFloat64(b) + aa = math.Min(aa, bb) + } + return aa +} + +func until(count int) []int { + step := 1 + if count < 0 { + step = -1 + } + return untilStep(0, count, step) +} + +func untilStep(start, stop, step int) []int { + v := []int{} + + if stop < start { + if step >= 0 { + return v + } + for i := start; i > stop; i += step { + v = append(v, i) + } + return v + } + + if step <= 0 { + return v + } + for i := start; i < stop; i += step { + v = append(v, i) + } + return v +} + +func floor(a interface{}) float64 { + aa := toFloat64(a) + return math.Floor(aa) +} + +func ceil(a interface{}) float64 { + aa := toFloat64(a) + return math.Ceil(aa) +} + +func round(a interface{}, p int, rOpt ...float64) float64 { + roundOn := .5 + if len(rOpt) > 0 { + roundOn = rOpt[0] + } + val := toFloat64(a) + places := toFloat64(p) + + var round float64 + pow := math.Pow(10, places) + digit := pow * val + _, div := math.Modf(digit) + if div >= roundOn { + round = math.Ceil(digit) + } else { + round = math.Floor(digit) + } + return round / pow +} + +// converts unix octal to decimal +func toDecimal(v interface{}) int64 { + result, err := strconv.ParseInt(fmt.Sprint(v), 8, 64) + if err != nil { + return 0 + } + return result +} + +func seq(params ...int) string { + increment := 1 + switch len(params) { + case 0: + return "" + case 1: + start := 1 + end := params[0] + if end < start { + increment = -1 + } + return intArrayToString(untilStep(start, end+increment, increment), " ") + case 3: + start := params[0] + end := params[2] + step := params[1] + if end < start { + increment = -1 + if step > 0 { + return "" + } + } + return intArrayToString(untilStep(start, end+increment, step), " ") + case 2: + start := params[0] + end := params[1] + step := 1 + if end < start { + step = -1 + } + return intArrayToString(untilStep(start, end+step, step), " ") + default: + return "" + } +} + +func intArrayToString(slice []int, delimeter string) string { + return strings.Trim(strings.Join(strings.Fields(fmt.Sprint(slice)), delimeter), "[]") +} diff --git a/util/sprig/numeric_test.go b/util/sprig/numeric_test.go new file mode 100644 index 00000000..94e8a6d4 --- /dev/null +++ b/util/sprig/numeric_test.go @@ -0,0 +1,307 @@ +package sprig + +import ( + "fmt" + "github.com/stretchr/testify/assert" + "strconv" + "testing" +) + +func TestUntil(t *testing.T) { + tests := map[string]string{ + `{{range $i, $e := until 5}}{{$i}}{{$e}}{{end}}`: "0011223344", + `{{range $i, $e := until -5}}{{$i}}{{$e}} {{end}}`: "00 1-1 2-2 3-3 4-4 ", + } + for tpl, expect := range tests { + if err := runt(tpl, expect); err != nil { + t.Error(err) + } + } +} +func TestUntilStep(t *testing.T) { + tests := map[string]string{ + `{{range $i, $e := untilStep 0 5 1}}{{$i}}{{$e}}{{end}}`: "0011223344", + `{{range $i, $e := untilStep 3 6 1}}{{$i}}{{$e}}{{end}}`: "031425", + `{{range $i, $e := untilStep 0 -10 -2}}{{$i}}{{$e}} {{end}}`: "00 1-2 2-4 3-6 4-8 ", + `{{range $i, $e := untilStep 3 0 1}}{{$i}}{{$e}}{{end}}`: "", + `{{range $i, $e := untilStep 3 99 0}}{{$i}}{{$e}}{{end}}`: "", + `{{range $i, $e := untilStep 3 99 -1}}{{$i}}{{$e}}{{end}}`: "", + `{{range $i, $e := untilStep 3 0 0}}{{$i}}{{$e}}{{end}}`: "", + } + for tpl, expect := range tests { + if err := runt(tpl, expect); err != nil { + t.Error(err) + } + } + +} +func TestBiggest(t *testing.T) { + tpl := `{{ biggest 1 2 3 345 5 6 7}}` + if err := runt(tpl, `345`); err != nil { + t.Error(err) + } + + tpl = `{{ max 345}}` + if err := runt(tpl, `345`); err != nil { + t.Error(err) + } +} +func TestMaxf(t *testing.T) { + tpl := `{{ maxf 1 2 3 345.7 5 6 7}}` + if err := runt(tpl, `345.7`); err != nil { + t.Error(err) + } + + tpl = `{{ max 345 }}` + if err := runt(tpl, `345`); err != nil { + t.Error(err) + } +} +func TestMin(t *testing.T) { + tpl := `{{ min 1 2 3 345 5 6 7}}` + if err := runt(tpl, `1`); err != nil { + t.Error(err) + } + + tpl = `{{ min 345}}` + if err := runt(tpl, `345`); err != nil { + t.Error(err) + } +} + +func TestMinf(t *testing.T) { + tpl := `{{ minf 1.4 2 3 345.6 5 6 7}}` + if err := runt(tpl, `1.4`); err != nil { + t.Error(err) + } + + tpl = `{{ minf 345 }}` + if err := runt(tpl, `345`); err != nil { + t.Error(err) + } +} + +func TestToFloat64(t *testing.T) { + target := float64(102) + if target != toFloat64(int8(102)) { + t.Errorf("Expected 102") + } + if target != toFloat64(int(102)) { + t.Errorf("Expected 102") + } + if target != toFloat64(int32(102)) { + t.Errorf("Expected 102") + } + if target != toFloat64(int16(102)) { + t.Errorf("Expected 102") + } + if target != toFloat64(int64(102)) { + t.Errorf("Expected 102") + } + if target != toFloat64("102") { + t.Errorf("Expected 102") + } + if 0 != toFloat64("frankie") { + t.Errorf("Expected 0") + } + if target != toFloat64(uint16(102)) { + t.Errorf("Expected 102") + } + if target != toFloat64(uint64(102)) { + t.Errorf("Expected 102") + } + if 102.1234 != toFloat64(float64(102.1234)) { + t.Errorf("Expected 102.1234") + } + if 1 != toFloat64(true) { + t.Errorf("Expected 102") + } +} +func TestToInt64(t *testing.T) { + target := int64(102) + if target != toInt64(int8(102)) { + t.Errorf("Expected 102") + } + if target != toInt64(int(102)) { + t.Errorf("Expected 102") + } + if target != toInt64(int32(102)) { + t.Errorf("Expected 102") + } + if target != toInt64(int16(102)) { + t.Errorf("Expected 102") + } + if target != toInt64(int64(102)) { + t.Errorf("Expected 102") + } + if target != toInt64("102") { + t.Errorf("Expected 102") + } + if 0 != toInt64("frankie") { + t.Errorf("Expected 0") + } + if target != toInt64(uint16(102)) { + t.Errorf("Expected 102") + } + if target != toInt64(uint64(102)) { + t.Errorf("Expected 102") + } + if target != toInt64(float64(102.1234)) { + t.Errorf("Expected 102") + } + if 1 != toInt64(true) { + t.Errorf("Expected 102") + } +} + +func TestToInt(t *testing.T) { + target := int(102) + if target != toInt(int8(102)) { + t.Errorf("Expected 102") + } + if target != toInt(int(102)) { + t.Errorf("Expected 102") + } + if target != toInt(int32(102)) { + t.Errorf("Expected 102") + } + if target != toInt(int16(102)) { + t.Errorf("Expected 102") + } + if target != toInt(int64(102)) { + t.Errorf("Expected 102") + } + if target != toInt("102") { + t.Errorf("Expected 102") + } + if 0 != toInt("frankie") { + t.Errorf("Expected 0") + } + if target != toInt(uint16(102)) { + t.Errorf("Expected 102") + } + if target != toInt(uint64(102)) { + t.Errorf("Expected 102") + } + if target != toInt(float64(102.1234)) { + t.Errorf("Expected 102") + } + if 1 != toInt(true) { + t.Errorf("Expected 102") + } +} + +func TestToDecimal(t *testing.T) { + tests := map[interface{}]int64{ + "777": 511, + 777: 511, + 770: 504, + 755: 493, + } + + for input, expectedResult := range tests { + result := toDecimal(input) + if result != expectedResult { + t.Errorf("Expected %v but got %v", expectedResult, result) + } + } +} + +func TestAdd1(t *testing.T) { + tpl := `{{ 3 | add1 }}` + if err := runt(tpl, `4`); err != nil { + t.Error(err) + } +} + +func TestAdd(t *testing.T) { + tpl := `{{ 3 | add 1 2}}` + if err := runt(tpl, `6`); err != nil { + t.Error(err) + } +} + +func TestDiv(t *testing.T) { + tpl := `{{ 4 | div 5 }}` + if err := runt(tpl, `1`); err != nil { + t.Error(err) + } +} + +func TestMul(t *testing.T) { + tpl := `{{ 1 | mul "2" 3 "4"}}` + if err := runt(tpl, `24`); err != nil { + t.Error(err) + } +} + +func TestSub(t *testing.T) { + tpl := `{{ 3 | sub 14 }}` + if err := runt(tpl, `11`); err != nil { + t.Error(err) + } +} + +func TestCeil(t *testing.T) { + assert.Equal(t, 123.0, ceil(123)) + assert.Equal(t, 123.0, ceil("123")) + assert.Equal(t, 124.0, ceil(123.01)) + assert.Equal(t, 124.0, ceil("123.01")) +} + +func TestFloor(t *testing.T) { + assert.Equal(t, 123.0, floor(123)) + assert.Equal(t, 123.0, floor("123")) + assert.Equal(t, 123.0, floor(123.9999)) + assert.Equal(t, 123.0, floor("123.9999")) +} + +func TestRound(t *testing.T) { + assert.Equal(t, 123.556, round(123.5555, 3)) + assert.Equal(t, 123.556, round("123.55555", 3)) + assert.Equal(t, 124.0, round(123.500001, 0)) + assert.Equal(t, 123.0, round(123.49999999, 0)) + assert.Equal(t, 123.23, round(123.2329999, 2, .3)) + assert.Equal(t, 123.24, round(123.233, 2, .3)) +} + +func TestRandomInt(t *testing.T) { + var tests = []struct { + min int + max int + }{ + {10, 11}, + {10, 13}, + {0, 1}, + {5, 50}, + } + for _, v := range tests { + x, _ := runRaw(fmt.Sprintf(`{{ randInt %d %d }}`, v.min, v.max), nil) + r, err := strconv.Atoi(x) + assert.NoError(t, err) + assert.True(t, func(min, max, r int) bool { + return r >= v.min && r < v.max + }(v.min, v.max, r)) + } +} + +func TestSeq(t *testing.T) { + tests := map[string]string{ + `{{seq 0 1 3}}`: "0 1 2 3", + `{{seq 0 3 10}}`: "0 3 6 9", + `{{seq 3 3 2}}`: "", + `{{seq 3 -3 2}}`: "3", + `{{seq}}`: "", + `{{seq 0 4}}`: "0 1 2 3 4", + `{{seq 5}}`: "1 2 3 4 5", + `{{seq -5}}`: "1 0 -1 -2 -3 -4 -5", + `{{seq 0}}`: "1 0", + `{{seq 0 1 2 3}}`: "", + `{{seq 0 -4}}`: "0 -1 -2 -3 -4", + } + for tpl, expect := range tests { + if err := runt(tpl, expect); err != nil { + t.Error(err) + } + } +} diff --git a/util/sprig/reflect.go b/util/sprig/reflect.go new file mode 100644 index 00000000..8a65c132 --- /dev/null +++ b/util/sprig/reflect.go @@ -0,0 +1,28 @@ +package sprig + +import ( + "fmt" + "reflect" +) + +// typeIs returns true if the src is the type named in target. +func typeIs(target string, src interface{}) bool { + return target == typeOf(src) +} + +func typeIsLike(target string, src interface{}) bool { + t := typeOf(src) + return target == t || "*"+target == t +} + +func typeOf(src interface{}) string { + return fmt.Sprintf("%T", src) +} + +func kindIs(target string, src interface{}) bool { + return target == kindOf(src) +} + +func kindOf(src interface{}) string { + return reflect.ValueOf(src).Kind().String() +} diff --git a/util/sprig/reflect_test.go b/util/sprig/reflect_test.go new file mode 100644 index 00000000..f102907e --- /dev/null +++ b/util/sprig/reflect_test.go @@ -0,0 +1,73 @@ +package sprig + +import ( + "testing" +) + +type fixtureTO struct { + Name, Value string +} + +func TestTypeOf(t *testing.T) { + f := &fixtureTO{"hello", "world"} + tpl := `{{typeOf .}}` + if err := runtv(tpl, "*sprig.fixtureTO", f); err != nil { + t.Error(err) + } +} + +func TestKindOf(t *testing.T) { + tpl := `{{kindOf .}}` + + f := fixtureTO{"hello", "world"} + if err := runtv(tpl, "struct", f); err != nil { + t.Error(err) + } + + f2 := []string{"hello"} + if err := runtv(tpl, "slice", f2); err != nil { + t.Error(err) + } + + var f3 *fixtureTO + if err := runtv(tpl, "ptr", f3); err != nil { + t.Error(err) + } +} + +func TestTypeIs(t *testing.T) { + f := &fixtureTO{"hello", "world"} + tpl := `{{if typeIs "*sprig.fixtureTO" .}}t{{else}}f{{end}}` + if err := runtv(tpl, "t", f); err != nil { + t.Error(err) + } + + f2 := "hello" + if err := runtv(tpl, "f", f2); err != nil { + t.Error(err) + } +} +func TestTypeIsLike(t *testing.T) { + f := "foo" + tpl := `{{if typeIsLike "string" .}}t{{else}}f{{end}}` + if err := runtv(tpl, "t", f); err != nil { + t.Error(err) + } + + // Now make a pointer. Should still match. + f2 := &f + if err := runtv(tpl, "t", f2); err != nil { + t.Error(err) + } +} +func TestKindIs(t *testing.T) { + f := &fixtureTO{"hello", "world"} + tpl := `{{if kindIs "ptr" .}}t{{else}}f{{end}}` + if err := runtv(tpl, "t", f); err != nil { + t.Error(err) + } + f2 := "hello" + if err := runtv(tpl, "f", f2); err != nil { + t.Error(err) + } +} diff --git a/util/sprig/regex.go b/util/sprig/regex.go new file mode 100644 index 00000000..fab55101 --- /dev/null +++ b/util/sprig/regex.go @@ -0,0 +1,83 @@ +package sprig + +import ( + "regexp" +) + +func regexMatch(regex string, s string) bool { + match, _ := regexp.MatchString(regex, s) + return match +} + +func mustRegexMatch(regex string, s string) (bool, error) { + return regexp.MatchString(regex, s) +} + +func regexFindAll(regex string, s string, n int) []string { + r := regexp.MustCompile(regex) + return r.FindAllString(s, n) +} + +func mustRegexFindAll(regex string, s string, n int) ([]string, error) { + r, err := regexp.Compile(regex) + if err != nil { + return []string{}, err + } + return r.FindAllString(s, n), nil +} + +func regexFind(regex string, s string) string { + r := regexp.MustCompile(regex) + return r.FindString(s) +} + +func mustRegexFind(regex string, s string) (string, error) { + r, err := regexp.Compile(regex) + if err != nil { + return "", err + } + return r.FindString(s), nil +} + +func regexReplaceAll(regex string, s string, repl string) string { + r := regexp.MustCompile(regex) + return r.ReplaceAllString(s, repl) +} + +func mustRegexReplaceAll(regex string, s string, repl string) (string, error) { + r, err := regexp.Compile(regex) + if err != nil { + return "", err + } + return r.ReplaceAllString(s, repl), nil +} + +func regexReplaceAllLiteral(regex string, s string, repl string) string { + r := regexp.MustCompile(regex) + return r.ReplaceAllLiteralString(s, repl) +} + +func mustRegexReplaceAllLiteral(regex string, s string, repl string) (string, error) { + r, err := regexp.Compile(regex) + if err != nil { + return "", err + } + return r.ReplaceAllLiteralString(s, repl), nil +} + +func regexSplit(regex string, s string, n int) []string { + r := regexp.MustCompile(regex) + return r.Split(s, n) +} + +func mustRegexSplit(regex string, s string, n int) ([]string, error) { + r, err := regexp.Compile(regex) + if err != nil { + return []string{}, err + } + return r.Split(s, n), nil +} + +func regexQuoteMeta(s string) string { + return regexp.QuoteMeta(s) +} diff --git a/util/sprig/regex_test.go b/util/sprig/regex_test.go new file mode 100644 index 00000000..60aafc29 --- /dev/null +++ b/util/sprig/regex_test.go @@ -0,0 +1,203 @@ +package sprig + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRegexMatch(t *testing.T) { + regex := "[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}" + + assert.True(t, regexMatch(regex, "test@acme.com")) + assert.True(t, regexMatch(regex, "Test@Acme.Com")) + assert.False(t, regexMatch(regex, "test")) + assert.False(t, regexMatch(regex, "test.com")) + assert.False(t, regexMatch(regex, "test@acme")) +} + +func TestMustRegexMatch(t *testing.T) { + regex := "[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}" + + o, err := mustRegexMatch(regex, "test@acme.com") + assert.True(t, o) + assert.Nil(t, err) + + o, err = mustRegexMatch(regex, "Test@Acme.Com") + assert.True(t, o) + assert.Nil(t, err) + + o, err = mustRegexMatch(regex, "test") + assert.False(t, o) + assert.Nil(t, err) + + o, err = mustRegexMatch(regex, "test.com") + assert.False(t, o) + assert.Nil(t, err) + + o, err = mustRegexMatch(regex, "test@acme") + assert.False(t, o) + assert.Nil(t, err) +} + +func TestRegexFindAll(t *testing.T) { + regex := "a{2}" + assert.Equal(t, 1, len(regexFindAll(regex, "aa", -1))) + assert.Equal(t, 1, len(regexFindAll(regex, "aaaaaaaa", 1))) + assert.Equal(t, 2, len(regexFindAll(regex, "aaaa", -1))) + assert.Equal(t, 0, len(regexFindAll(regex, "none", -1))) +} + +func TestMustRegexFindAll(t *testing.T) { + type args struct { + regex, s string + n int + } + cases := []struct { + expected int + args args + }{ + {1, args{"a{2}", "aa", -1}}, + {1, args{"a{2}", "aaaaaaaa", 1}}, + {2, args{"a{2}", "aaaa", -1}}, + {0, args{"a{2}", "none", -1}}, + } + + for _, c := range cases { + res, err := mustRegexFindAll(c.args.regex, c.args.s, c.args.n) + if err != nil { + t.Errorf("regexFindAll test case %v failed with err %s", c, err) + } + assert.Equal(t, c.expected, len(res), "case %#v", c.args) + } +} + +func TestRegexFindl(t *testing.T) { + regex := "fo.?" + assert.Equal(t, "foo", regexFind(regex, "foorbar")) + assert.Equal(t, "foo", regexFind(regex, "foo foe fome")) + assert.Equal(t, "", regexFind(regex, "none")) +} + +func TestMustRegexFindl(t *testing.T) { + type args struct{ regex, s string } + cases := []struct { + expected string + args args + }{ + {"foo", args{"fo.?", "foorbar"}}, + {"foo", args{"fo.?", "foo foe fome"}}, + {"", args{"fo.?", "none"}}, + } + + for _, c := range cases { + res, err := mustRegexFind(c.args.regex, c.args.s) + if err != nil { + t.Errorf("regexFind test case %v failed with err %s", c, err) + } + assert.Equal(t, c.expected, res, "case %#v", c.args) + } +} + +func TestRegexReplaceAll(t *testing.T) { + regex := "a(x*)b" + assert.Equal(t, "-T-T-", regexReplaceAll(regex, "-ab-axxb-", "T")) + assert.Equal(t, "--xx-", regexReplaceAll(regex, "-ab-axxb-", "$1")) + assert.Equal(t, "---", regexReplaceAll(regex, "-ab-axxb-", "$1W")) + assert.Equal(t, "-W-xxW-", regexReplaceAll(regex, "-ab-axxb-", "${1}W")) +} + +func TestMustRegexReplaceAll(t *testing.T) { + type args struct{ regex, s, repl string } + cases := []struct { + expected string + args args + }{ + {"-T-T-", args{"a(x*)b", "-ab-axxb-", "T"}}, + {"--xx-", args{"a(x*)b", "-ab-axxb-", "$1"}}, + {"---", args{"a(x*)b", "-ab-axxb-", "$1W"}}, + {"-W-xxW-", args{"a(x*)b", "-ab-axxb-", "${1}W"}}, + } + + for _, c := range cases { + res, err := mustRegexReplaceAll(c.args.regex, c.args.s, c.args.repl) + if err != nil { + t.Errorf("regexReplaceAll test case %v failed with err %s", c, err) + } + assert.Equal(t, c.expected, res, "case %#v", c.args) + } +} + +func TestRegexReplaceAllLiteral(t *testing.T) { + regex := "a(x*)b" + assert.Equal(t, "-T-T-", regexReplaceAllLiteral(regex, "-ab-axxb-", "T")) + assert.Equal(t, "-$1-$1-", regexReplaceAllLiteral(regex, "-ab-axxb-", "$1")) + assert.Equal(t, "-${1}-${1}-", regexReplaceAllLiteral(regex, "-ab-axxb-", "${1}")) +} + +func TestMustRegexReplaceAllLiteral(t *testing.T) { + type args struct{ regex, s, repl string } + cases := []struct { + expected string + args args + }{ + {"-T-T-", args{"a(x*)b", "-ab-axxb-", "T"}}, + {"-$1-$1-", args{"a(x*)b", "-ab-axxb-", "$1"}}, + {"-${1}-${1}-", args{"a(x*)b", "-ab-axxb-", "${1}"}}, + } + + for _, c := range cases { + res, err := mustRegexReplaceAllLiteral(c.args.regex, c.args.s, c.args.repl) + if err != nil { + t.Errorf("regexReplaceAllLiteral test case %v failed with err %s", c, err) + } + assert.Equal(t, c.expected, res, "case %#v", c.args) + } +} + +func TestRegexSplit(t *testing.T) { + regex := "a" + assert.Equal(t, 4, len(regexSplit(regex, "banana", -1))) + assert.Equal(t, 0, len(regexSplit(regex, "banana", 0))) + assert.Equal(t, 1, len(regexSplit(regex, "banana", 1))) + assert.Equal(t, 2, len(regexSplit(regex, "banana", 2))) + + regex = "z+" + assert.Equal(t, 2, len(regexSplit(regex, "pizza", -1))) + assert.Equal(t, 0, len(regexSplit(regex, "pizza", 0))) + assert.Equal(t, 1, len(regexSplit(regex, "pizza", 1))) + assert.Equal(t, 2, len(regexSplit(regex, "pizza", 2))) +} + +func TestMustRegexSplit(t *testing.T) { + type args struct { + regex, s string + n int + } + cases := []struct { + expected int + args args + }{ + {4, args{"a", "banana", -1}}, + {0, args{"a", "banana", 0}}, + {1, args{"a", "banana", 1}}, + {2, args{"a", "banana", 2}}, + {2, args{"z+", "pizza", -1}}, + {0, args{"z+", "pizza", 0}}, + {1, args{"z+", "pizza", 1}}, + {2, args{"z+", "pizza", 2}}, + } + + for _, c := range cases { + res, err := mustRegexSplit(c.args.regex, c.args.s, c.args.n) + if err != nil { + t.Errorf("regexSplit test case %v failed with err %s", c, err) + } + assert.Equal(t, c.expected, len(res), "case %#v", c.args) + } +} + +func TestRegexQuoteMeta(t *testing.T) { + assert.Equal(t, "1\\.2\\.3", regexQuoteMeta("1.2.3")) + assert.Equal(t, "pretzel", regexQuoteMeta("pretzel")) +} diff --git a/util/sprig/strings.go b/util/sprig/strings.go new file mode 100644 index 00000000..3c62d6b6 --- /dev/null +++ b/util/sprig/strings.go @@ -0,0 +1,189 @@ +package sprig + +import ( + "encoding/base32" + "encoding/base64" + "fmt" + "reflect" + "strconv" + "strings" +) + +func base64encode(v string) string { + return base64.StdEncoding.EncodeToString([]byte(v)) +} + +func base64decode(v string) string { + data, err := base64.StdEncoding.DecodeString(v) + if err != nil { + return err.Error() + } + return string(data) +} + +func base32encode(v string) string { + return base32.StdEncoding.EncodeToString([]byte(v)) +} + +func base32decode(v string) string { + data, err := base32.StdEncoding.DecodeString(v) + if err != nil { + return err.Error() + } + return string(data) +} + +func quote(str ...interface{}) string { + out := make([]string, 0, len(str)) + for _, s := range str { + if s != nil { + out = append(out, fmt.Sprintf("%q", strval(s))) + } + } + return strings.Join(out, " ") +} + +func squote(str ...interface{}) string { + out := make([]string, 0, len(str)) + for _, s := range str { + if s != nil { + out = append(out, fmt.Sprintf("'%v'", s)) + } + } + return strings.Join(out, " ") +} + +func cat(v ...interface{}) string { + v = removeNilElements(v) + r := strings.TrimSpace(strings.Repeat("%v ", len(v))) + return fmt.Sprintf(r, v...) +} + +func indent(spaces int, v string) string { + pad := strings.Repeat(" ", spaces) + return pad + strings.Replace(v, "\n", "\n"+pad, -1) +} + +func nindent(spaces int, v string) string { + return "\n" + indent(spaces, v) +} + +func replace(old, new, src string) string { + return strings.Replace(src, old, new, -1) +} + +func plural(one, many string, count int) string { + if count == 1 { + return one + } + return many +} + +func strslice(v interface{}) []string { + switch v := v.(type) { + case []string: + return v + case []interface{}: + b := make([]string, 0, len(v)) + for _, s := range v { + if s != nil { + b = append(b, strval(s)) + } + } + return b + default: + val := reflect.ValueOf(v) + switch val.Kind() { + case reflect.Array, reflect.Slice: + l := val.Len() + b := make([]string, 0, l) + for i := 0; i < l; i++ { + value := val.Index(i).Interface() + if value != nil { + b = append(b, strval(value)) + } + } + return b + default: + if v == nil { + return []string{} + } + + return []string{strval(v)} + } + } +} + +func removeNilElements(v []interface{}) []interface{} { + newSlice := make([]interface{}, 0, len(v)) + for _, i := range v { + if i != nil { + newSlice = append(newSlice, i) + } + } + return newSlice +} + +func strval(v interface{}) string { + switch v := v.(type) { + case string: + return v + case []byte: + return string(v) + case error: + return v.Error() + case fmt.Stringer: + return v.String() + default: + return fmt.Sprintf("%v", v) + } +} + +func trunc(c int, s string) string { + if c < 0 && len(s)+c > 0 { + return s[len(s)+c:] + } + if c >= 0 && len(s) > c { + return s[:c] + } + return s +} + +func join(sep string, v interface{}) string { + return strings.Join(strslice(v), sep) +} + +func split(sep, orig string) map[string]string { + parts := strings.Split(orig, sep) + res := make(map[string]string, len(parts)) + for i, v := range parts { + res["_"+strconv.Itoa(i)] = v + } + return res +} + +func splitn(sep string, n int, orig string) map[string]string { + parts := strings.SplitN(orig, sep, n) + res := make(map[string]string, len(parts)) + for i, v := range parts { + res["_"+strconv.Itoa(i)] = v + } + return res +} + +// substring creates a substring of the given string. +// +// If start is < 0, this calls string[:end]. +// +// If start is >= 0 and end < 0 or end bigger than s length, this calls string[start:] +// +// Otherwise, this calls string[start, end]. +func substring(start, end int, s string) string { + if start < 0 { + return s[:end] + } + if end < 0 || end > len(s) { + return s[start:] + } + return s[start:end] +} diff --git a/util/sprig/strings_test.go b/util/sprig/strings_test.go new file mode 100644 index 00000000..38c96c4e --- /dev/null +++ b/util/sprig/strings_test.go @@ -0,0 +1,233 @@ +package sprig + +import ( + "encoding/base32" + "encoding/base64" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSubstr(t *testing.T) { + tpl := `{{"fooo" | substr 0 3 }}` + if err := runt(tpl, "foo"); err != nil { + t.Error(err) + } +} + +func TestSubstr_shorterString(t *testing.T) { + tpl := `{{"foo" | substr 0 10 }}` + if err := runt(tpl, "foo"); err != nil { + t.Error(err) + } +} + +func TestTrunc(t *testing.T) { + tpl := `{{ "foooooo" | trunc 3 }}` + if err := runt(tpl, "foo"); err != nil { + t.Error(err) + } + tpl = `{{ "baaaaaar" | trunc -3 }}` + if err := runt(tpl, "aar"); err != nil { + t.Error(err) + } + tpl = `{{ "baaaaaar" | trunc -999 }}` + if err := runt(tpl, "baaaaaar"); err != nil { + t.Error(err) + } + tpl = `{{ "baaaaaz" | trunc 0 }}` + if err := runt(tpl, ""); err != nil { + t.Error(err) + } +} + +func TestQuote(t *testing.T) { + tpl := `{{quote "a" "b" "c"}}` + if err := runt(tpl, `"a" "b" "c"`); err != nil { + t.Error(err) + } + tpl = `{{quote "\"a\"" "b" "c"}}` + if err := runt(tpl, `"\"a\"" "b" "c"`); err != nil { + t.Error(err) + } + tpl = `{{quote 1 2 3 }}` + if err := runt(tpl, `"1" "2" "3"`); err != nil { + t.Error(err) + } + tpl = `{{ .value | quote }}` + values := map[string]interface{}{"value": nil} + if err := runtv(tpl, ``, values); err != nil { + t.Error(err) + } +} +func TestSquote(t *testing.T) { + tpl := `{{squote "a" "b" "c"}}` + if err := runt(tpl, `'a' 'b' 'c'`); err != nil { + t.Error(err) + } + tpl = `{{squote 1 2 3 }}` + if err := runt(tpl, `'1' '2' '3'`); err != nil { + t.Error(err) + } + tpl = `{{ .value | squote }}` + values := map[string]interface{}{"value": nil} + if err := runtv(tpl, ``, values); err != nil { + t.Error(err) + } +} + +func TestContains(t *testing.T) { + // Mainly, we're just verifying the paramater order swap. + tests := []string{ + `{{if contains "cat" "fair catch"}}1{{end}}`, + `{{if hasPrefix "cat" "catch"}}1{{end}}`, + `{{if hasSuffix "cat" "ducat"}}1{{end}}`, + } + for _, tt := range tests { + if err := runt(tt, "1"); err != nil { + t.Error(err) + } + } +} + +func TestTrim(t *testing.T) { + tests := []string{ + `{{trim " 5.00 "}}`, + `{{trimAll "$" "$5.00$"}}`, + `{{trimPrefix "$" "$5.00"}}`, + `{{trimSuffix "$" "5.00$"}}`, + } + for _, tt := range tests { + if err := runt(tt, "5.00"); err != nil { + t.Error(err) + } + } +} + +func TestSplit(t *testing.T) { + tpl := `{{$v := "foo$bar$baz" | split "$"}}{{$v._0}}` + if err := runt(tpl, "foo"); err != nil { + t.Error(err) + } +} + +func TestSplitn(t *testing.T) { + tpl := `{{$v := "foo$bar$baz" | splitn "$" 2}}{{$v._0}}` + if err := runt(tpl, "foo"); err != nil { + t.Error(err) + } +} + +func TestToString(t *testing.T) { + tpl := `{{ toString 1 | kindOf }}` + assert.NoError(t, runt(tpl, "string")) +} + +func TestToStrings(t *testing.T) { + tpl := `{{ $s := list 1 2 3 | toStrings }}{{ index $s 1 | kindOf }}` + assert.NoError(t, runt(tpl, "string")) + tpl = `{{ list 1 .value 2 | toStrings }}` + values := map[string]interface{}{"value": nil} + if err := runtv(tpl, `[1 2]`, values); err != nil { + t.Error(err) + } +} + +func TestJoin(t *testing.T) { + assert.NoError(t, runt(`{{ tuple "a" "b" "c" | join "-" }}`, "a-b-c")) + assert.NoError(t, runt(`{{ tuple 1 2 3 | join "-" }}`, "1-2-3")) + assert.NoError(t, runtv(`{{ join "-" .V }}`, "a-b-c", map[string]interface{}{"V": []string{"a", "b", "c"}})) + assert.NoError(t, runtv(`{{ join "-" .V }}`, "abc", map[string]interface{}{"V": "abc"})) + assert.NoError(t, runtv(`{{ join "-" .V }}`, "1-2-3", map[string]interface{}{"V": []int{1, 2, 3}})) + assert.NoError(t, runtv(`{{ join "-" .value }}`, "1-2", map[string]interface{}{"value": []interface{}{"1", nil, "2"}})) +} + +func TestSortAlpha(t *testing.T) { + // Named `append` in the function map + tests := map[string]string{ + `{{ list "c" "a" "b" | sortAlpha | join "" }}`: "abc", + `{{ list 2 1 4 3 | sortAlpha | join "" }}`: "1234", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} +func TestBase64EncodeDecode(t *testing.T) { + magicWord := "coffee" + expect := base64.StdEncoding.EncodeToString([]byte(magicWord)) + + if expect == magicWord { + t.Fatal("Encoder doesn't work.") + } + + tpl := `{{b64enc "coffee"}}` + if err := runt(tpl, expect); err != nil { + t.Error(err) + } + tpl = fmt.Sprintf("{{b64dec %q}}", expect) + if err := runt(tpl, magicWord); err != nil { + t.Error(err) + } +} +func TestBase32EncodeDecode(t *testing.T) { + magicWord := "coffee" + expect := base32.StdEncoding.EncodeToString([]byte(magicWord)) + + if expect == magicWord { + t.Fatal("Encoder doesn't work.") + } + + tpl := `{{b32enc "coffee"}}` + if err := runt(tpl, expect); err != nil { + t.Error(err) + } + tpl = fmt.Sprintf("{{b32dec %q}}", expect) + if err := runt(tpl, magicWord); err != nil { + t.Error(err) + } +} + +func TestCat(t *testing.T) { + tpl := `{{$b := "b"}}{{"c" | cat "a" $b}}` + if err := runt(tpl, "a b c"); err != nil { + t.Error(err) + } + tpl = `{{ .value | cat "a" "b"}}` + values := map[string]interface{}{"value": nil} + if err := runtv(tpl, "a b", values); err != nil { + t.Error(err) + } +} + +func TestIndent(t *testing.T) { + tpl := `{{indent 4 "a\nb\nc"}}` + if err := runt(tpl, " a\n b\n c"); err != nil { + t.Error(err) + } +} + +func TestNindent(t *testing.T) { + tpl := `{{nindent 4 "a\nb\nc"}}` + if err := runt(tpl, "\n a\n b\n c"); err != nil { + t.Error(err) + } +} + +func TestReplace(t *testing.T) { + tpl := `{{"I Am Henry VIII" | replace " " "-"}}` + if err := runt(tpl, "I-Am-Henry-VIII"); err != nil { + t.Error(err) + } +} + +func TestPlural(t *testing.T) { + tpl := `{{$num := len "two"}}{{$num}} {{$num | plural "1 char" "chars"}}` + if err := runt(tpl, "3 chars"); err != nil { + t.Error(err) + } + tpl = `{{len "t" | plural "cheese" "%d chars"}}` + if err := runt(tpl, "cheese"); err != nil { + t.Error(err) + } +} diff --git a/util/sprig/url.go b/util/sprig/url.go new file mode 100644 index 00000000..b8e120e1 --- /dev/null +++ b/util/sprig/url.go @@ -0,0 +1,66 @@ +package sprig + +import ( + "fmt" + "net/url" + "reflect" +) + +func dictGetOrEmpty(dict map[string]interface{}, key string) string { + value, ok := dict[key] + if !ok { + return "" + } + tp := reflect.TypeOf(value).Kind() + if tp != reflect.String { + panic(fmt.Sprintf("unable to parse %s key, must be of type string, but %s found", key, tp.String())) + } + return reflect.ValueOf(value).String() +} + +// parses given URL to return dict object +func urlParse(v string) map[string]interface{} { + dict := map[string]interface{}{} + parsedURL, err := url.Parse(v) + if err != nil { + panic(fmt.Sprintf("unable to parse url: %s", err)) + } + dict["scheme"] = parsedURL.Scheme + dict["host"] = parsedURL.Host + dict["hostname"] = parsedURL.Hostname() + dict["path"] = parsedURL.Path + dict["query"] = parsedURL.RawQuery + dict["opaque"] = parsedURL.Opaque + dict["fragment"] = parsedURL.Fragment + if parsedURL.User != nil { + dict["userinfo"] = parsedURL.User.String() + } else { + dict["userinfo"] = "" + } + + return dict +} + +// join given dict to URL string +func urlJoin(d map[string]interface{}) string { + resURL := url.URL{ + Scheme: dictGetOrEmpty(d, "scheme"), + Host: dictGetOrEmpty(d, "host"), + Path: dictGetOrEmpty(d, "path"), + RawQuery: dictGetOrEmpty(d, "query"), + Opaque: dictGetOrEmpty(d, "opaque"), + Fragment: dictGetOrEmpty(d, "fragment"), + } + userinfo := dictGetOrEmpty(d, "userinfo") + var user *url.Userinfo + if userinfo != "" { + tempURL, err := url.Parse(fmt.Sprintf("proto://%s@host", userinfo)) + if err != nil { + panic(fmt.Sprintf("unable to parse userinfo in dict: %s", err)) + } + user = tempURL.User + } + + resURL.User = user + return resURL.String() +} diff --git a/util/sprig/url_test.go b/util/sprig/url_test.go new file mode 100644 index 00000000..f9c00b17 --- /dev/null +++ b/util/sprig/url_test.go @@ -0,0 +1,87 @@ +package sprig + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +var urlTests = map[string]map[string]interface{}{ + "proto://auth@host:80/path?query#fragment": { + "fragment": "fragment", + "host": "host:80", + "hostname": "host", + "opaque": "", + "path": "/path", + "query": "query", + "scheme": "proto", + "userinfo": "auth", + }, + "proto://host:80/path": { + "fragment": "", + "host": "host:80", + "hostname": "host", + "opaque": "", + "path": "/path", + "query": "", + "scheme": "proto", + "userinfo": "", + }, + "something": { + "fragment": "", + "host": "", + "hostname": "", + "opaque": "", + "path": "something", + "query": "", + "scheme": "", + "userinfo": "", + }, + "proto://user:passwor%20d@host:80/path": { + "fragment": "", + "host": "host:80", + "hostname": "host", + "opaque": "", + "path": "/path", + "query": "", + "scheme": "proto", + "userinfo": "user:passwor%20d", + }, + "proto://host:80/pa%20th?key=val%20ue": { + "fragment": "", + "host": "host:80", + "hostname": "host", + "opaque": "", + "path": "/pa th", + "query": "key=val%20ue", + "scheme": "proto", + "userinfo": "", + }, +} + +func TestUrlParse(t *testing.T) { + // testing that function is exported and working properly + assert.NoError(t, runt( + `{{ index ( urlParse "proto://auth@host:80/path?query#fragment" ) "host" }}`, + "host:80")) + + // testing scenarios + for url, expected := range urlTests { + assert.EqualValues(t, expected, urlParse(url)) + } +} + +func TestUrlJoin(t *testing.T) { + tests := map[string]string{ + `{{ urlJoin (dict "fragment" "fragment" "host" "host:80" "path" "/path" "query" "query" "scheme" "proto") }}`: "proto://host:80/path?query#fragment", + `{{ urlJoin (dict "fragment" "fragment" "host" "host:80" "path" "/path" "scheme" "proto" "userinfo" "ASDJKJSD") }}`: "proto://ASDJKJSD@host:80/path#fragment", + } + for tpl, expected := range tests { + assert.NoError(t, runt(tpl, expected)) + } + + for expected, urlMap := range urlTests { + assert.EqualValues(t, expected, urlJoin(urlMap)) + } + +}