copy subset of Sprig template functions

This commit is contained in:
Hunter Kehoe
2025-07-07 22:23:32 -06:00
parent 3c8ac4a1e1
commit 1f2c76e63d
53 changed files with 5550 additions and 2 deletions

19
util/sprig/LICENSE.txt Normal file
View File

@@ -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.

37
util/sprig/crypto.go Normal file
View File

@@ -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()
}

54
util/sprig/crypto_test.go Normal file
View File

@@ -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")
}
}

152
util/sprig/date.go Normal file
View File

@@ -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)
}

120
util/sprig/date_test.go Normal file
View File

@@ -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)
}
}

163
util/sprig/defaults.go Normal file
View File

@@ -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
}

196
util/sprig/defaults_test.go Normal file
View File

@@ -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 }}`: "<no value>",
}
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": "<HEAD>"}}
tpl := `{{.Top | toRawJSON}}`
expected := `{"bool":true,"html":"<HEAD>","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)
}
}

118
util/sprig/dict.go Normal file
View File

@@ -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)
}

166
util/sprig/dict_test.go Normal file
View File

@@ -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)
}
}
}

19
util/sprig/doc.go Normal file
View File

@@ -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

View File

@@ -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
}

View File

@@ -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)
}

302
util/sprig/functions.go Normal file
View File

@@ -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,
}

View File

@@ -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"))
}

View File

@@ -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
}

View File

@@ -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"))
}

464
util/sprig/list.go Normal file
View File

@@ -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
}

364
util/sprig/list_test.go Normal file
View File

@@ -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 }}`: "<no value>",
`{{ 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 }}`: "<no value>",
`{{ 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 }}`: "<no value>",
`{{ 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 }}`: "<no value>",
`{{ 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 <nil>]",
`{{ 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))
}
}

228
util/sprig/numeric.go Normal file
View File

@@ -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), "[]")
}

307
util/sprig/numeric_test.go Normal file
View File

@@ -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)
}
}
}

28
util/sprig/reflect.go Normal file
View File

@@ -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()
}

View File

@@ -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)
}
}

83
util/sprig/regex.go Normal file
View File

@@ -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)
}

203
util/sprig/regex_test.go Normal file
View File

@@ -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"))
}

189
util/sprig/strings.go Normal file
View File

@@ -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]
}

233
util/sprig/strings_test.go Normal file
View File

@@ -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)
}
}

66
util/sprig/url.go Normal file
View File

@@ -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()
}

87
util/sprig/url_test.go Normal file
View File

@@ -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))
}
}