From 9e5d6d81df8c6208941133daaba1134065a477e8 Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Mon, 13 Mar 2017 14:42:10 -0600 Subject: [PATCH] Refactor into multiple files. --- Makefile | 4 + crypto.go | 148 ++++++++ crypto_test.go | 110 ++++++ date.go | 53 +++ date_test.go | 13 + defaults.go | 62 +++ defaults_test.go | 84 +++++ dict.go | 74 ++++ dict_test.go | 137 +++++++ doc.go | 213 +++++++++++ functions.go | 942 ---------------------------------------------- functions_test.go | 885 +------------------------------------------ list.go | 80 ++++ list_test.go | 98 +++++ numeric.go | 129 +++++++ numeric_test.go | 180 +++++++++ reflect.go | 28 ++ reflect_test.go | 73 ++++ strings.go | 197 ++++++++++ strings_test.go | 220 +++++++++++ 20 files changed, 1910 insertions(+), 1820 deletions(-) create mode 100644 Makefile create mode 100644 crypto.go create mode 100644 crypto_test.go create mode 100644 date.go create mode 100644 date_test.go create mode 100644 defaults.go create mode 100644 defaults_test.go create mode 100644 dict.go create mode 100644 dict_test.go create mode 100644 doc.go create mode 100644 list.go create mode 100644 list_test.go create mode 100644 numeric.go create mode 100644 numeric_test.go create mode 100644 reflect.go create mode 100644 reflect_test.go create mode 100644 strings.go create mode 100644 strings_test.go diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..017683da --- /dev/null +++ b/Makefile @@ -0,0 +1,4 @@ + +.PHONY: test +test: + go test -v . diff --git a/crypto.go b/crypto.go new file mode 100644 index 00000000..a935b6c1 --- /dev/null +++ b/crypto.go @@ -0,0 +1,148 @@ +package sprig + +import ( + "bytes" + "crypto/dsa" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/hmac" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "encoding/asn1" + "encoding/binary" + "encoding/hex" + "encoding/pem" + "fmt" + "math/big" + + uuid "github.com/satori/go.uuid" + "golang.org/x/crypto/scrypt" +) + +func sha256sum(input string) string { + hash := sha256.Sum256([]byte(input)) + return hex.EncodeToString(hash[:]) +} + +// uuidv4 provides a safe and secure UUID v4 implementation +func uuidv4() string { + return fmt.Sprintf("%s", uuid.NewV4()) +} + +var master_password_seed = "com.lyndir.masterpassword" + +var password_type_templates = map[string][][]byte{ + "maximum": {[]byte("anoxxxxxxxxxxxxxxxxx"), []byte("axxxxxxxxxxxxxxxxxno")}, + "long": {[]byte("CvcvnoCvcvCvcv"), []byte("CvcvCvcvnoCvcv"), []byte("CvcvCvcvCvcvno"), []byte("CvccnoCvcvCvcv"), []byte("CvccCvcvnoCvcv"), + []byte("CvccCvcvCvcvno"), []byte("CvcvnoCvccCvcv"), []byte("CvcvCvccnoCvcv"), []byte("CvcvCvccCvcvno"), []byte("CvcvnoCvcvCvcc"), + []byte("CvcvCvcvnoCvcc"), []byte("CvcvCvcvCvccno"), []byte("CvccnoCvccCvcv"), []byte("CvccCvccnoCvcv"), []byte("CvccCvccCvcvno"), + []byte("CvcvnoCvccCvcc"), []byte("CvcvCvccnoCvcc"), []byte("CvcvCvccCvccno"), []byte("CvccnoCvcvCvcc"), []byte("CvccCvcvnoCvcc"), + []byte("CvccCvcvCvccno")}, + "medium": {[]byte("CvcnoCvc"), []byte("CvcCvcno")}, + "short": {[]byte("Cvcn")}, + "basic": {[]byte("aaanaaan"), []byte("aannaaan"), []byte("aaannaaa")}, + "pin": {[]byte("nnnn")}, +} + +var template_characters = map[byte]string{ + 'V': "AEIOU", + 'C': "BCDFGHJKLMNPQRSTVWXYZ", + 'v': "aeiou", + 'c': "bcdfghjklmnpqrstvwxyz", + 'A': "AEIOUBCDFGHJKLMNPQRSTVWXYZ", + 'a': "AEIOUaeiouBCDFGHJKLMNPQRSTVWXYZbcdfghjklmnpqrstvwxyz", + 'n': "0123456789", + 'o': "@&%?,=[]_:-+*$#!'^~;()/.", + 'x': "AEIOUaeiouBCDFGHJKLMNPQRSTVWXYZbcdfghjklmnpqrstvwxyz0123456789!@#$%^&*()", +} + +func derivePassword(counter uint32, password_type, password, user, site string) string { + var templates = password_type_templates[password_type] + if templates == nil { + return fmt.Sprintf("cannot find password template %s", password_type) + } + + var buffer bytes.Buffer + buffer.WriteString(master_password_seed) + binary.Write(&buffer, binary.BigEndian, uint32(len(user))) + buffer.WriteString(user) + + salt := buffer.Bytes() + key, err := scrypt.Key([]byte(password), salt, 32768, 8, 2, 64) + if err != nil { + return fmt.Sprintf("failed to derive password: %s", err) + } + + buffer.Truncate(len(master_password_seed)) + binary.Write(&buffer, binary.BigEndian, uint32(len(site))) + buffer.WriteString(site) + binary.Write(&buffer, binary.BigEndian, counter) + + var hmacv = hmac.New(sha256.New, key) + hmacv.Write(buffer.Bytes()) + var seed = hmacv.Sum(nil) + var temp = templates[int(seed[0])%len(templates)] + + buffer.Truncate(0) + for i, element := range temp { + pass_chars := template_characters[element] + pass_char := pass_chars[int(seed[i+1])%len(pass_chars)] + buffer.WriteByte(pass_char) + } + + return buffer.String() +} + +func generatePrivateKey(typ string) string { + var priv interface{} + var err error + switch typ { + case "", "rsa": + // good enough for government work + priv, err = rsa.GenerateKey(rand.Reader, 4096) + case "dsa": + key := new(dsa.PrivateKey) + // again, good enough for government work + if err = dsa.GenerateParameters(&key.Parameters, rand.Reader, dsa.L2048N256); err != nil { + return fmt.Sprintf("failed to generate dsa params: %s", err) + } + err = dsa.GenerateKey(key, rand.Reader) + priv = key + case "ecdsa": + // again, good enough for government work + priv, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + default: + return "Unknown type " + typ + } + if err != nil { + return fmt.Sprintf("failed to generate private key: %s", err) + } + + return string(pem.EncodeToMemory(pemBlockForKey(priv))) +} + +type DSAKeyFormat struct { + Version int + P, Q, G, Y, X *big.Int +} + +func pemBlockForKey(priv interface{}) *pem.Block { + switch k := priv.(type) { + case *rsa.PrivateKey: + return &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(k)} + case *dsa.PrivateKey: + val := DSAKeyFormat{ + P: k.P, Q: k.Q, G: k.G, + Y: k.Y, X: k.X, + } + bytes, _ := asn1.Marshal(val) + return &pem.Block{Type: "DSA PRIVATE KEY", Bytes: bytes} + case *ecdsa.PrivateKey: + b, _ := x509.MarshalECPrivateKey(k) + return &pem.Block{Type: "EC PRIVATE KEY", Bytes: b} + default: + return nil + } +} diff --git a/crypto_test.go b/crypto_test.go new file mode 100644 index 00000000..01b3bfcc --- /dev/null +++ b/crypto_test.go @@ -0,0 +1,110 @@ +package sprig + +import ( + "strings" + "testing" +) + +func TestSha256Sum(t *testing.T) { + tpl := `{{"abc" | sha256sum}}` + if err := runt(tpl, "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"); err != nil { + t.Error(err) + } +} + +func TestDerivePassword(t *testing.T) { + expectations := map[string]string{ + `{{derivePassword 1 "long" "password" "user" "example.com"}}`: "ZedaFaxcZaso9*", + `{{derivePassword 2 "long" "password" "user" "example.com"}}`: "Fovi2@JifpTupx", + `{{derivePassword 1 "maximum" "password" "user" "example.com"}}`: "pf4zS1LjCg&LjhsZ7T2~", + `{{derivePassword 1 "medium" "password" "user" "example.com"}}`: "ZedJuz8$", + `{{derivePassword 1 "basic" "password" "user" "example.com"}}`: "pIS54PLs", + `{{derivePassword 1 "short" "password" "user" "example.com"}}`: "Zed5", + `{{derivePassword 1 "pin" "password" "user" "example.com"}}`: "6685", + } + + for tpl, result := range expectations { + out, err := runRaw(tpl, nil) + if err != nil { + t.Error(err) + } + if 0 != strings.Compare(out, result) { + t.Error("Generated password does not match for", tpl) + } + } +} + +// NOTE(bacongobbler): this test is really _slow_ because of how long it takes to compute +// and generate a new crypto key. +func TestGenPrivateKey(t *testing.T) { + // test that calling by default generates an RSA private key + tpl := `{{genPrivateKey ""}}` + out, err := runRaw(tpl, nil) + if err != nil { + t.Error(err) + } + if !strings.Contains(out, "RSA PRIVATE KEY") { + t.Error("Expected RSA PRIVATE KEY") + } + // test all acceptable arguments + tpl = `{{genPrivateKey "rsa"}}` + out, err = runRaw(tpl, nil) + if err != nil { + t.Error(err) + } + if !strings.Contains(out, "RSA PRIVATE KEY") { + t.Error("Expected RSA PRIVATE KEY") + } + tpl = `{{genPrivateKey "dsa"}}` + out, err = runRaw(tpl, nil) + if err != nil { + t.Error(err) + } + if !strings.Contains(out, "DSA PRIVATE KEY") { + t.Error("Expected DSA PRIVATE KEY") + } + tpl = `{{genPrivateKey "ecdsa"}}` + out, err = runRaw(tpl, nil) + if err != nil { + t.Error(err) + } + if !strings.Contains(out, "EC PRIVATE KEY") { + t.Error("Expected EC PRIVATE KEY") + } + // test bad + tpl = `{{genPrivateKey "bad"}}` + out, err = runRaw(tpl, nil) + if err != nil { + t.Error(err) + } + if out != "Unknown type bad" { + t.Error("Expected type 'bad' to be an unknown crypto algorithm") + } + // ensure that we can base64 encode the string + tpl = `{{genPrivateKey "rsa" | b64enc}}` + out, err = runRaw(tpl, nil) + if 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/date.go b/date.go new file mode 100644 index 00000000..dc5263f2 --- /dev/null +++ b/date.go @@ -0,0 +1,53 @@ +package sprig + +import ( + "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 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) +} diff --git a/date_test.go b/date_test.go new file mode 100644 index 00000000..8e98b8c9 --- /dev/null +++ b/date_test.go @@ -0,0 +1,13 @@ +package sprig + +import ( + "testing" +) + +func TestHtmlDate(t *testing.T) { + t.Skip() + tpl := `{{ htmlDate 0}}` + if err := runt(tpl, "1970-01-01"); err != nil { + t.Error(err) + } +} diff --git a/defaults.go b/defaults.go new file mode 100644 index 00000000..9892f07e --- /dev/null +++ b/defaults.go @@ -0,0 +1,62 @@ +package sprig + +import ( + "reflect" +) + +// 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() == false + 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 + } + return true +} + +// coalesce returns the first non-empty value. +func coalesce(v ...interface{}) interface{} { + for _, val := range v { + if !empty(val) { + return val + } + } + return nil +} diff --git a/defaults_test.go b/defaults_test.go new file mode 100644 index 00000000..6bb48f6a --- /dev/null +++ b/defaults_test.go @@ -0,0 +1,84 @@ +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) + } +} diff --git a/dict.go b/dict.go new file mode 100644 index 00000000..d50e11c5 --- /dev/null +++ b/dict.go @@ -0,0 +1,74 @@ +package sprig + +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(dict map[string]interface{}) []string { + k := []string{} + 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 +} diff --git a/dict_test.go b/dict_test.go new file mode 100644 index 00000000..2a786472 --- /dev/null +++ b/dict_test.go @@ -0,0 +1,137 @@ +package sprig + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +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 }}`: "[]", + } + 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 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 TestCompact(t *testing.T) { + tests := map[string]string{ + `{{ list 1 0 "" "hello" | compact }}`: `[1 hello]`, + `{{ list "" "" | compact }}`: `[]`, + `{{ list | compact }}`: `[]`, + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} diff --git a/doc.go b/doc.go new file mode 100644 index 00000000..309ce614 --- /dev/null +++ b/doc.go @@ -0,0 +1,213 @@ +/* +Sprig: 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 := templates.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. + +Date Functions + + - date FORMAT TIME: Format a date, where a date is an integer type or a time.Time type, and + format is a time.Format formatting string. + - dateModify: Given a date, modify it with a duration: `date_modify "-1.5h" now`. If the duration doesn't + parse, it returns the time unaltered. See `time.ParseDuration` for info on duration strings. + - now: Current time.Time, for feeding into date-related functions. + - htmlDate TIME: Format a date for use in the value field of an HTML "date" form element. + - dateInZone FORMAT TIME TZ: Like date, but takes three arguments: format, timestamp, + timezone. + - htmlDateInZone TIME TZ: Like htmlDate, but takes two arguments: timestamp, + timezone. + +String Functions + + - abbrev: Truncate a string with ellipses. `abbrev 5 "hello world"` yields "he..." + - abbrevboth: Abbreviate from both sides, yielding "...lo wo..." + - trunc: Truncate a string (no suffix). `trunc 5 "Hello World"` yields "hello". + - trim: strings.TrimSpace + - trimAll: strings.Trim, but with the argument order reversed `trimAll "$" "$5.00"` or `"$5.00 | trimAll "$"` + - trimSuffix: strings.TrimSuffix, but with the argument order reversed: `trimSuffix "-" "ends-with-"` + - trimPrefix: strings.TrimPrefix, but with the argument order reversed `trimPrefix "$" "$5"` + - upper: strings.ToUpper + - lower: strings.ToLower + - nospace: Remove all space characters from a string. `nospace "h e l l o"` becomes "hello" + - title: strings.Title + - untitle: Remove title casing + - repeat: strings.Repeat, but with the arguments switched: `repeat count str`. (This simplifies common pipelines) + - substr: Given string, start, and length, return a substr. + - initials: Given a multi-word string, return the initials. `initials "Matt Butcher"` returns "MB" + - randAlphaNum: Given a length, generate a random alphanumeric sequence + - randAlpha: Given a length, generate an alphabetic string + - randAscii: Given a length, generate a random ASCII string (symbols included) + - randNumeric: Given a length, generate a string of digits. + - wrap: Force a line wrap at the given width. `wrap 80 "imagine a longer string"` + - wrapWith: Wrap a line at the given length, but using 'sep' instead of a newline. `wrapWith 50, "
", $html` + - contains: strings.Contains, but with the arguments switched: `contains substr str`. (This simplifies common pipelines) + - hasPrefix: strings.hasPrefix, but with the arguments switched + - hasSuffix: strings.hasSuffix, but with the arguments switched + - quote: Wrap string(s) in double quotation marks, escape the contents by adding '\' before '"'. + - squote: Wrap string(s) in double quotation marks, does not escape content. + - cat: Concatenate strings, separating them by spaces. `cat $a $b $c`. + - indent: Indent a string using space characters. `indent 4 "foo\nbar"` produces " foo\n bar" + - replace: Replace an old with a new in a string: `$name | replace " " "-"` + - plural: Choose singular or plural based on length: `len $fish | plural "one anchovy" "many anchovies"` + - sha256sum: Generate a hex encoded sha256 hash of the input + - toString: Convert something to a string + +String Slice Functions: + + - join: strings.Join, but as `join SEP SLICE` + - split: strings.Split, but as `split SEP STRING`. The results are returned + as a map with the indexes set to _N, where N is an integer starting from 0. + Use it like this: `{{$v := "foo/bar/baz" | split "/"}}{{$v._0}}` (Prints `foo`) + - splitList: strings.Split, but as `split SEP STRING`. The results are returned + as an array. + - toStrings: convert a list to a list of strings. 'list 1 2 3 | toStrings' produces '["1" "2" "3"]' + - sortAlpha: sort a list lexicographically. + +Integer Slice Functions: + + - until: Given an integer, returns a slice of counting integers from 0 to one + less than the given integer: `range $i, $e := until 5` + - untilStep: Given start, stop, and step, return an integer slice starting at + 'start', stopping at `stop`, and incrementing by 'step. This is the same + as Python's long-form of 'range'. + +Conversions: + + - atoi: Convert a string to an integer. 0 if the integer could not be parsed. + - in64: Convert a string or another numeric type to an int64. + - int: Convert a string or another numeric type to an int. + - float64: Convert a string or another numeric type to a float64. + +Defaults: + + - default: Give a default value. Used like this: trim " "| default "empty". + Since trim produces an empty string, the default value is returned. For + things with a length (strings, slices, maps), len(0) will trigger the default. + For numbers, the value 0 will trigger the default. For booleans, false will + trigger the default. For structs, the default is never returned (there is + no clear empty condition). For everything else, nil value triggers a default. + - empty: Return true if the given value is the zero value for its type. + Caveats: structs are always non-empty. This should match the behavior of + {{if pipeline}}, but can be used inside of a pipeline. + - coalesce: Given a list of items, return the first non-empty one. + This follows the same rules as 'empty'. '{{ coalesce .someVal 0 "hello" }}` + will return `.someVal` if set, or else return "hello". The 0 is skipped + because it is an empty value. + - compact: Return a copy of a list with all of the empty values removed. + 'list 0 1 2 "" | compact' will return '[1 2]' + +OS: + - env: Resolve an environment variable + - expandenv: Expand a string through the environment + +File Paths: + - base: Return the last element of a path. https://golang.org/pkg/path#Base + - dir: Remove the last element of a path. https://golang.org/pkg/path#Dir + - clean: Clean a path to the shortest equivalent name. (e.g. remove "foo/.." + from "foo/../bar.html") https://golang.org/pkg/path#Clean + - ext: https://golang.org/pkg/path#Ext + - isAbs: https://golang.org/pkg/path#IsAbs + +Encoding: + - b64enc: Base 64 encode a string. + - b64dec: Base 64 decode a string. + +Reflection: + + - typeOf: Takes an interface and returns a string representation of the type. + For pointers, this will return a type prefixed with an asterisk(`*`). So + a pointer to type `Foo` will be `*Foo`. + - typeIs: Compares an interface with a string name, and returns true if they match. + Note that a pointer will not match a reference. For example `*Foo` will not + match `Foo`. + - typeIsLike: Compares an interface with a string name and returns true if + the interface is that `name` or that `*name`. In other words, if the given + value matches the given type or is a pointer to the given type, this returns + true. + - kindOf: Takes an interface and returns a string representation of its kind. + - kindIs: Returns true if the given string matches the kind of the given interface. + + 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. + +Data Structures: + + - tuple: Takes an arbitrary list of items and returns a slice of items. Its + tuple-ish properties are mainly gained through the template idiom, and not + through an API provided here. WARNING: The implementation of tuple will + change in the future. + - list: An arbitrary ordered list of items. (This is prefered over tuple.) + - dict: Takes a list of name/values and returns a map[string]interface{}. + The first parameter is converted to a string and stored as a key, the + second parameter is treated as the value. And so on, with odds as keys and + evens as values. If the function call ends with an odd, the last key will + be assigned the empty string. Non-string keys are converted to strings as + follows: []byte are converted, fmt.Stringers will have String() called. + errors will have Error() called. All others will be passed through + fmt.Sprtinf("%v"). + +Lists Functions: + +These are used to manipulate lists: '{{ list 1 2 3 | reverse | first }}' + + - first: Get the first item in a 'list'. 'list 1 2 3 | first' prints '1' + - last: Get the last item in a 'list': 'list 1 2 3 | last ' prints '3' + - rest: Get all but the first item in a list: 'list 1 2 3 | rest' returns '[2 3]' + - initial: Get all but the last item in a list: 'list 1 2 3 | initial' returns '[1 2]' + - append: Add an item to the end of a list: 'append $list 4' adds '4' to the end of '$list' + - prepend: Add an item to the beginning of a list: 'prepend $list 4' puts 4 at the beginning of the list. + +Dict Functions: + +These are used to manipulate dicts. + + - set: Takes a dict, a key, and a value, and sets that key/value pair in + the dict. `set $dict $key $value`. For convenience, it returns the dict, + even though the dict was modified in place. + - unset: Takes a dict and a key, and deletes that key/value pair from the + dict. `unset $dict $key`. This returns the dict for convenience. + - hasKey: Takes a dict and a key, and returns boolean true if the key is in + the dict. + - pluck: Given a key and one or more maps, get all of the values for that key. + - keys: Get an array of all of the keys in a dict. + - pick: Select just the given keys out of the dict, and return a new dict. + - omit: Return a dict without the given keys. + +Math Functions: + +Integer functions will convert integers of any width to `int64`. If a +string is passed in, functions will attempt to convert with +`strconv.ParseInt(s, 1064)`. If this fails, the value will be treated as 0. + + - add1: Increment an integer by 1 + - add: Sum an arbitrary number of integers + - sub: Subtract the second integer from the first + - div: Divide the first integer by the second + - mod: Module of first integer divided by second + - mul: Multiply integers + - max: Return the biggest of a series of one or more integers + - min: Return the smallest of a series of one or more integers + - biggest: DEPRECATED. Return the biggest of a series of one or more integers + +Crypto Functions: + + - genPrivateKey: Generate a private key for the given cryptosystem. If no + argument is supplied, by default it will generate a private key using + the RSA algorithm. Accepted values are `rsa`, `dsa`, and `ecdsa`. + - derivePassword: Derive a password from the given parameters according to the ["Master Password" algorithm](http://masterpasswordapp.com/algorithm.html) + Given parameters (in order) are: + `counter` (starting with 1), `password_type` (maximum, long, medium, short, basic, or pin), `password`, + `user`, and `site` +*/ +package sprig diff --git a/functions.go b/functions.go index 9b994ca2..0636eb84 100644 --- a/functions.go +++ b/functions.go @@ -1,250 +1,15 @@ -/* -Sprig: 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 := templates.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. - -Date Functions - - - date FORMAT TIME: Format a date, where a date is an integer type or a time.Time type, and - format is a time.Format formatting string. - - dateModify: Given a date, modify it with a duration: `date_modify "-1.5h" now`. If the duration doesn't - parse, it returns the time unaltered. See `time.ParseDuration` for info on duration strings. - - now: Current time.Time, for feeding into date-related functions. - - htmlDate TIME: Format a date for use in the value field of an HTML "date" form element. - - dateInZone FORMAT TIME TZ: Like date, but takes three arguments: format, timestamp, - timezone. - - htmlDateInZone TIME TZ: Like htmlDate, but takes two arguments: timestamp, - timezone. - -String Functions - - - abbrev: Truncate a string with ellipses. `abbrev 5 "hello world"` yields "he..." - - abbrevboth: Abbreviate from both sides, yielding "...lo wo..." - - trunc: Truncate a string (no suffix). `trunc 5 "Hello World"` yields "hello". - - trim: strings.TrimSpace - - trimAll: strings.Trim, but with the argument order reversed `trimAll "$" "$5.00"` or `"$5.00 | trimAll "$"` - - trimSuffix: strings.TrimSuffix, but with the argument order reversed: `trimSuffix "-" "ends-with-"` - - trimPrefix: strings.TrimPrefix, but with the argument order reversed `trimPrefix "$" "$5"` - - upper: strings.ToUpper - - lower: strings.ToLower - - nospace: Remove all space characters from a string. `nospace "h e l l o"` becomes "hello" - - title: strings.Title - - untitle: Remove title casing - - repeat: strings.Repeat, but with the arguments switched: `repeat count str`. (This simplifies common pipelines) - - substr: Given string, start, and length, return a substr. - - initials: Given a multi-word string, return the initials. `initials "Matt Butcher"` returns "MB" - - randAlphaNum: Given a length, generate a random alphanumeric sequence - - randAlpha: Given a length, generate an alphabetic string - - randAscii: Given a length, generate a random ASCII string (symbols included) - - randNumeric: Given a length, generate a string of digits. - - wrap: Force a line wrap at the given width. `wrap 80 "imagine a longer string"` - - wrapWith: Wrap a line at the given length, but using 'sep' instead of a newline. `wrapWith 50, "
", $html` - - contains: strings.Contains, but with the arguments switched: `contains substr str`. (This simplifies common pipelines) - - hasPrefix: strings.hasPrefix, but with the arguments switched - - hasSuffix: strings.hasSuffix, but with the arguments switched - - quote: Wrap string(s) in double quotation marks, escape the contents by adding '\' before '"'. - - squote: Wrap string(s) in double quotation marks, does not escape content. - - cat: Concatenate strings, separating them by spaces. `cat $a $b $c`. - - indent: Indent a string using space characters. `indent 4 "foo\nbar"` produces " foo\n bar" - - replace: Replace an old with a new in a string: `$name | replace " " "-"` - - plural: Choose singular or plural based on length: `len $fish | plural "one anchovy" "many anchovies"` - - sha256sum: Generate a hex encoded sha256 hash of the input - - toString: Convert something to a string - -String Slice Functions: - - - join: strings.Join, but as `join SEP SLICE` - - split: strings.Split, but as `split SEP STRING`. The results are returned - as a map with the indexes set to _N, where N is an integer starting from 0. - Use it like this: `{{$v := "foo/bar/baz" | split "/"}}{{$v._0}}` (Prints `foo`) - - splitList: strings.Split, but as `split SEP STRING`. The results are returned - as an array. - - toStrings: convert a list to a list of strings. 'list 1 2 3 | toStrings' produces '["1" "2" "3"]' - - sortAlpha: sort a list lexicographically. - -Integer Slice Functions: - - - until: Given an integer, returns a slice of counting integers from 0 to one - less than the given integer: `range $i, $e := until 5` - - untilStep: Given start, stop, and step, return an integer slice starting at - 'start', stopping at `stop`, and incrementing by 'step. This is the same - as Python's long-form of 'range'. - -Conversions: - - - atoi: Convert a string to an integer. 0 if the integer could not be parsed. - - in64: Convert a string or another numeric type to an int64. - - int: Convert a string or another numeric type to an int. - - float64: Convert a string or another numeric type to a float64. - -Defaults: - - - default: Give a default value. Used like this: trim " "| default "empty". - Since trim produces an empty string, the default value is returned. For - things with a length (strings, slices, maps), len(0) will trigger the default. - For numbers, the value 0 will trigger the default. For booleans, false will - trigger the default. For structs, the default is never returned (there is - no clear empty condition). For everything else, nil value triggers a default. - - empty: Return true if the given value is the zero value for its type. - Caveats: structs are always non-empty. This should match the behavior of - {{if pipeline}}, but can be used inside of a pipeline. - - coalesce: Given a list of items, return the first non-empty one. - This follows the same rules as 'empty'. '{{ coalesce .someVal 0 "hello" }}` - will return `.someVal` if set, or else return "hello". The 0 is skipped - because it is an empty value. - - compact: Return a copy of a list with all of the empty values removed. - 'list 0 1 2 "" | compact' will return '[1 2]' - -OS: - - env: Resolve an environment variable - - expandenv: Expand a string through the environment - -File Paths: - - base: Return the last element of a path. https://golang.org/pkg/path#Base - - dir: Remove the last element of a path. https://golang.org/pkg/path#Dir - - clean: Clean a path to the shortest equivalent name. (e.g. remove "foo/.." - from "foo/../bar.html") https://golang.org/pkg/path#Clean - - ext: https://golang.org/pkg/path#Ext - - isAbs: https://golang.org/pkg/path#IsAbs - -Encoding: - - b64enc: Base 64 encode a string. - - b64dec: Base 64 decode a string. - -Reflection: - - - typeOf: Takes an interface and returns a string representation of the type. - For pointers, this will return a type prefixed with an asterisk(`*`). So - a pointer to type `Foo` will be `*Foo`. - - typeIs: Compares an interface with a string name, and returns true if they match. - Note that a pointer will not match a reference. For example `*Foo` will not - match `Foo`. - - typeIsLike: Compares an interface with a string name and returns true if - the interface is that `name` or that `*name`. In other words, if the given - value matches the given type or is a pointer to the given type, this returns - true. - - kindOf: Takes an interface and returns a string representation of its kind. - - kindIs: Returns true if the given string matches the kind of the given interface. - - 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. - -Data Structures: - - - tuple: Takes an arbitrary list of items and returns a slice of items. Its - tuple-ish properties are mainly gained through the template idiom, and not - through an API provided here. WARNING: The implementation of tuple will - change in the future. - - list: An arbitrary ordered list of items. (This is prefered over tuple.) - - dict: Takes a list of name/values and returns a map[string]interface{}. - The first parameter is converted to a string and stored as a key, the - second parameter is treated as the value. And so on, with odds as keys and - evens as values. If the function call ends with an odd, the last key will - be assigned the empty string. Non-string keys are converted to strings as - follows: []byte are converted, fmt.Stringers will have String() called. - errors will have Error() called. All others will be passed through - fmt.Sprtinf("%v"). - -Lists Functions: - -These are used to manipulate lists: '{{ list 1 2 3 | reverse | first }}' - - - first: Get the first item in a 'list'. 'list 1 2 3 | first' prints '1' - - last: Get the last item in a 'list': 'list 1 2 3 | last ' prints '3' - - rest: Get all but the first item in a list: 'list 1 2 3 | rest' returns '[2 3]' - - initial: Get all but the last item in a list: 'list 1 2 3 | initial' returns '[1 2]' - - append: Add an item to the end of a list: 'append $list 4' adds '4' to the end of '$list' - - prepend: Add an item to the beginning of a list: 'prepend $list 4' puts 4 at the beginning of the list. - -Dict Functions: - -These are used to manipulate dicts. - - - set: Takes a dict, a key, and a value, and sets that key/value pair in - the dict. `set $dict $key $value`. For convenience, it returns the dict, - even though the dict was modified in place. - - unset: Takes a dict and a key, and deletes that key/value pair from the - dict. `unset $dict $key`. This returns the dict for convenience. - - hasKey: Takes a dict and a key, and returns boolean true if the key is in - the dict. - - pluck: Given a key and one or more maps, get all of the values for that key. - - keys: Get an array of all of the keys in a dict. - - pick: Select just the given keys out of the dict, and return a new dict. - - omit: Return a dict without the given keys. - -Math Functions: - -Integer functions will convert integers of any width to `int64`. If a -string is passed in, functions will attempt to convert with -`strconv.ParseInt(s, 1064)`. If this fails, the value will be treated as 0. - - - add1: Increment an integer by 1 - - add: Sum an arbitrary number of integers - - sub: Subtract the second integer from the first - - div: Divide the first integer by the second - - mod: Module of first integer divided by second - - mul: Multiply integers - - max: Return the biggest of a series of one or more integers - - min: Return the smallest of a series of one or more integers - - biggest: DEPRECATED. Return the biggest of a series of one or more integers - -Crypto Functions: - - - genPrivateKey: Generate a private key for the given cryptosystem. If no - argument is supplied, by default it will generate a private key using - the RSA algorithm. Accepted values are `rsa`, `dsa`, and `ecdsa`. - - derivePassword: Derive a password from the given parameters according to the ["Master Password" algorithm](http://masterpasswordapp.com/algorithm.html) - Given parameters (in order) are: - `counter` (starting with 1), `password_type` (maximum, long, medium, short, basic, or pin), `password`, - `user`, and `site` -*/ package sprig import ( - "bytes" - "crypto/dsa" - "crypto/ecdsa" - "crypto/elliptic" - "crypto/hmac" - "crypto/rand" - "crypto/rsa" - "crypto/sha256" - "crypto/x509" - "encoding/asn1" - "encoding/base32" - "encoding/base64" - "encoding/binary" - "encoding/hex" - "encoding/pem" - "fmt" "html/template" - "math" - "math/big" "os" "path" - "reflect" - "sort" "strconv" "strings" ttemplate "text/template" "time" util "github.com/aokoli/goutils" - uuid "github.com/satori/go.uuid" - - "golang.org/x/crypto/scrypt" ) // Produce the function map. @@ -467,710 +232,3 @@ var genericMap = map[string]interface{}{ // UUIDs: "uuidv4": uuidv4, } - -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 -} - -// substring creates a substring of the given string. -// -// If start is < 0, this calls string[:length]. -// -// If start is >= 0 and length < 0, this calls string[start:] -// -// Otherwise, this calls string[start, length]. -func substring(start, length int, s string) string { - if start < 0 { - return s[:length] - } - if length < 0 { - return s[start:] - } - return s[start:length] -} - -// 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 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 max(a interface{}, i ...interface{}) int64 { - aa := toInt64(a) - for _, b := range i { - bb := toInt64(b) - if bb > aa { - 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 -} - -// 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() == false - 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 - } - return true -} - -// coalesce returns the first non-empty value. -func coalesce(v ...interface{}) interface{} { - for _, val := range v { - if !empty(val) { - return val - } - } - return nil -} - -func compact(list []interface{}) []interface{} { - res := []interface{}{} - for _, item := range list { - if !empty(item) { - res = append(res, item) - } - } - return res -} - -// 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() -} - -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 abbrev(width int, s string) string { - if width < 4 { - return s - } - r, _ := util.Abbreviate(s, width) - return r -} - -func abbrevboth(left, right int, s string) string { - if right < 4 || left > 0 && right < 7 { - return s - } - r, _ := util.AbbreviateFull(s, left, right) - return r -} -func initials(s string) string { - // Wrap this just to eliminate the var args, which templates don't do well. - return util.Initials(s) -} - -func randAlphaNumeric(count int) string { - // It is not possible, it appears, to actually generate an error here. - r, _ := util.RandomAlphaNumeric(count) - return r -} - -func randAlpha(count int) string { - r, _ := util.RandomAlphabetic(count) - return r -} - -func randAscii(count int) string { - r, _ := util.RandomAscii(count) - return r -} - -func randNumeric(count int) string { - r, _ := util.RandomNumeric(count) - return r -} - -func untitle(str string) string { - return util.Uncapitalize(str) -} - -func quote(str ...interface{}) string { - out := make([]string, len(str)) - for i, s := range str { - out[i] = fmt.Sprintf("%q", strval(s)) - } - return strings.Join(out, " ") -} - -func squote(str ...interface{}) string { - out := make([]string, len(str)) - for i, s := range str { - out[i] = fmt.Sprintf("'%v'", s) - } - return strings.Join(out, " ") -} - -func list(v ...interface{}) []interface{} { - return v -} - -func push(list []interface{}, v interface{}) []interface{} { - return append(list, v) -} - -func prepend(list []interface{}, v interface{}) []interface{} { - return append([]interface{}{v}, list...) -} - -func last(list []interface{}) interface{} { - l := len(list) - if l == 0 { - return nil - } - return list[l-1] -} - -func first(list []interface{}) interface{} { - if len(list) == 0 { - return nil - } - return list[0] -} - -func rest(list []interface{}) []interface{} { - if len(list) == 0 { - return list - } - return list[1:] -} - -func initial(list []interface{}) []interface{} { - l := len(list) - if l == 0 { - return list - } - return list[:l-1] -} - -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(dict map[string]interface{}) []string { - k := []string{} - 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 join(sep string, v interface{}) string { - return strings.Join(strslice(v), sep) -} - -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 strslice(v interface{}) []string { - switch v := v.(type) { - case []string: - return v - case []interface{}: - l := len(v) - b := make([]string, l) - for i := 0; i < l; i++ { - b[i] = strval(v[i]) - } - return b - default: - val := reflect.ValueOf(v) - switch val.Kind() { - case reflect.Array, reflect.Slice: - l := val.Len() - b := make([]string, l) - for i := 0; i < l; i++ { - b[i] = strval(val.Index(i).Interface()) - } - return b - default: - return []string{strval(v)} - } - } -} - -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) - } -} - -// 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() == true { - return 1 - } - return 0 - default: - return 0 - } -} - -func toInt(v interface{}) int { - //It's not optimal. Bud 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() == true { - return 1 - } - return 0 - default: - return 0 - } -} - -var master_password_seed = "com.lyndir.masterpassword" - -var password_type_templates = map[string][][]byte{ - "maximum": {[]byte("anoxxxxxxxxxxxxxxxxx"), []byte("axxxxxxxxxxxxxxxxxno")}, - "long": {[]byte("CvcvnoCvcvCvcv"), []byte("CvcvCvcvnoCvcv"), []byte("CvcvCvcvCvcvno"), []byte("CvccnoCvcvCvcv"), []byte("CvccCvcvnoCvcv"), - []byte("CvccCvcvCvcvno"), []byte("CvcvnoCvccCvcv"), []byte("CvcvCvccnoCvcv"), []byte("CvcvCvccCvcvno"), []byte("CvcvnoCvcvCvcc"), - []byte("CvcvCvcvnoCvcc"), []byte("CvcvCvcvCvccno"), []byte("CvccnoCvccCvcv"), []byte("CvccCvccnoCvcv"), []byte("CvccCvccCvcvno"), - []byte("CvcvnoCvccCvcc"), []byte("CvcvCvccnoCvcc"), []byte("CvcvCvccCvccno"), []byte("CvccnoCvcvCvcc"), []byte("CvccCvcvnoCvcc"), - []byte("CvccCvcvCvccno")}, - "medium": {[]byte("CvcnoCvc"), []byte("CvcCvcno")}, - "short": {[]byte("Cvcn")}, - "basic": {[]byte("aaanaaan"), []byte("aannaaan"), []byte("aaannaaa")}, - "pin": {[]byte("nnnn")}, -} - -var template_characters = map[byte]string{ - 'V': "AEIOU", - 'C': "BCDFGHJKLMNPQRSTVWXYZ", - 'v': "aeiou", - 'c': "bcdfghjklmnpqrstvwxyz", - 'A': "AEIOUBCDFGHJKLMNPQRSTVWXYZ", - 'a': "AEIOUaeiouBCDFGHJKLMNPQRSTVWXYZbcdfghjklmnpqrstvwxyz", - 'n': "0123456789", - 'o': "@&%?,=[]_:-+*$#!'^~;()/.", - 'x': "AEIOUaeiouBCDFGHJKLMNPQRSTVWXYZbcdfghjklmnpqrstvwxyz0123456789!@#$%^&*()", -} - -func derivePassword(counter uint32, password_type, password, user, site string) string { - var templates = password_type_templates[password_type] - if templates == nil { - return fmt.Sprintf("cannot find password template %s", password_type) - } - - var buffer bytes.Buffer - buffer.WriteString(master_password_seed) - binary.Write(&buffer, binary.BigEndian, uint32(len(user))) - buffer.WriteString(user) - - salt := buffer.Bytes() - key, err := scrypt.Key([]byte(password), salt, 32768, 8, 2, 64) - if err != nil { - return fmt.Sprintf("failed to derive password: %s", err) - } - - buffer.Truncate(len(master_password_seed)) - binary.Write(&buffer, binary.BigEndian, uint32(len(site))) - buffer.WriteString(site) - binary.Write(&buffer, binary.BigEndian, counter) - - var hmacv = hmac.New(sha256.New, key) - hmacv.Write(buffer.Bytes()) - var seed = hmacv.Sum(nil) - var temp = templates[int(seed[0])%len(templates)] - - buffer.Truncate(0) - for i, element := range temp { - pass_chars := template_characters[element] - pass_char := pass_chars[int(seed[i+1])%len(pass_chars)] - buffer.WriteByte(pass_char) - } - - return buffer.String() -} - -func generatePrivateKey(typ string) string { - var priv interface{} - var err error - switch typ { - case "", "rsa": - // good enough for government work - priv, err = rsa.GenerateKey(rand.Reader, 4096) - case "dsa": - key := new(dsa.PrivateKey) - // again, good enough for government work - if err = dsa.GenerateParameters(&key.Parameters, rand.Reader, dsa.L2048N256); err != nil { - return fmt.Sprintf("failed to generate dsa params: %s", err) - } - err = dsa.GenerateKey(key, rand.Reader) - priv = key - case "ecdsa": - // again, good enough for government work - priv, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - default: - return "Unknown type " + typ - } - if err != nil { - return fmt.Sprintf("failed to generate private key: %s", err) - } - - return string(pem.EncodeToMemory(pemBlockForKey(priv))) -} - -type DSAKeyFormat struct { - Version int - P, Q, G, Y, X *big.Int -} - -func pemBlockForKey(priv interface{}) *pem.Block { - switch k := priv.(type) { - case *rsa.PrivateKey: - return &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(k)} - case *dsa.PrivateKey: - val := DSAKeyFormat{ - P: k.P, Q: k.Q, G: k.G, - Y: k.Y, X: k.X, - } - bytes, _ := asn1.Marshal(val) - return &pem.Block{Type: "DSA PRIVATE KEY", Bytes: bytes} - case *ecdsa.PrivateKey: - b, _ := x509.MarshalECPrivateKey(k) - return &pem.Block{Type: "EC PRIVATE KEY", Bytes: b} - default: - return nil - } -} - -func trunc(c int, s string) string { - if len(s) <= c { - return s - } - return s[0:c] -} - -func reverse(v []interface{}) []interface{} { - // We do not sort in place because the incomming array should not be altered. - l := len(v) - c := make([]interface{}, l) - for i := 0; i < l; i++ { - c[l-i-1] = v[i] - } - return c -} - -func cat(v ...interface{}) string { - 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 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 sha256sum(input string) string { - hash := sha256.Sum256([]byte(input)) - return hex.EncodeToString(hash[:]) -} - -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 -} - -// uuidv4 provides a safe and secure UUID v4 implementation -func uuidv4() string { - return fmt.Sprintf("%s", uuid.NewV4()) -} diff --git a/functions_test.go b/functions_test.go index 6233694e..499d4883 100644 --- a/functions_test.go +++ b/functions_test.go @@ -2,417 +2,14 @@ package sprig import ( "bytes" - "encoding/base32" - "encoding/base64" "fmt" - "math/rand" "os" - "strings" "testing" "text/template" - "github.com/aokoli/goutils" "github.com/stretchr/testify/assert" ) -// This is woefully incomplete. Please help. - -func TestSubstr(t *testing.T) { - tpl := `{{"fooo" | substr 0 3 }}` - 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) - } -} - -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) - } -} -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) - } -} - -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 TestAdd(t *testing.T) { - tpl := `{{ 3 | add 1 2}}` - if err := runt(tpl, `6`); 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 TestHtmlDate(t *testing.T) { - t.Skip() - tpl := `{{ htmlDate 0}}` - if err := runt(tpl, "1970-01-01"); 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 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 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 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 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 TestCompact(t *testing.T) { - tests := map[string]string{ - `{{ list 1 0 "" "hello" | compact }}`: `[1 hello]`, - `{{ list "" "" | compact }}`: `[]`, - `{{ list | compact }}`: `[]`, - } - for tpl, expect := range tests { - assert.NoError(t, runt(tpl, expect)) - } -} - -func TestSplit(t *testing.T) { - tpl := `{{$v := "foo$bar$baz" | split "$"}}{{$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")) -} - -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 = nil - 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) - } -} - func TestEnv(t *testing.T) { os.Setenv("FOO", "bar") tpl := `{{env "FOO"}}` @@ -429,357 +26,6 @@ func TestExpandEnv(t *testing.T) { } } -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 TestGoutils(t *testing.T) { - tests := map[string]string{ - `{{abbrev 5 "hello world"}}`: "he...", - `{{abbrevboth 5 10 "1234 5678 9123"}}`: "...5678...", - `{{nospace "h e l l o "}}`: "hello", - `{{untitle "First Try"}}`: "first try", //https://youtu.be/44-RsrF_V_w - `{{initials "First Try"}}`: "FT", - `{{wrap 5 "Hello World"}}`: "Hello\nWorld", - `{{wrapWith 5 "\t" "Hello World"}}`: "Hello\tWorld", - } - for k, v := range tests { - t.Log(k) - if err := runt(k, v); err != nil { - t.Errorf("Error on tpl %s: %s", err) - } - } -} - -func TestRandom(t *testing.T) { - // One of the things I love about Go: - goutils.RANDOM = rand.New(rand.NewSource(1)) - - // Because we're using a random number generator, we need these to go in - // a predictable sequence: - if err := runt(`{{randAlphaNum 5}}`, "9bzRv"); err != nil { - t.Errorf("Error on tpl %s: %s", err) - } - if err := runt(`{{randAlpha 5}}`, "VjwGe"); err != nil { - t.Errorf("Error on tpl %s: %s", err) - } - if err := runt(`{{randAscii 5}}`, "1KA5p"); err != nil { - t.Errorf("Error on tpl %s: %s", err) - } - if err := runt(`{{randNumeric 5}}`, "26018"); err != nil { - t.Errorf("Error on tpl %s: %s", 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) - } -} - -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 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) - } -} - -func TestSha256Sum(t *testing.T) { - tpl := `{{"abc" | sha256sum}}` - if err := runt(tpl, "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"); err != nil { - t.Error(err) - } -} - -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", - } - 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", - } - 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 }}`: "", - } - 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 }}`: "", - } - 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 }}`: "[]", - } - 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 }}`: "[]", - } - 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 }}`: "[]", - } - for tpl, expect := range tests { - assert.NoError(t, runt(tpl, expect)) - } -} - -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 }}`: "[]", - } - 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 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 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 TestBase(t *testing.T) { assert.NoError(t, runt(`{{ base "foo/bar" }}`, "bar")) } @@ -801,133 +47,14 @@ func TestExt(t *testing.T) { assert.NoError(t, runt(`{{ ext "/foo/bar/baz.txt" }}`, ".txt")) } -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}})) -} - -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 TestDelete(t *testing.T) { - fmap := TxtFuncMap() - delete(fmap, "split") - if _, ok := fmap["split"]; ok { - t.Error("Failed to delete split from map") - } -} - -func TestDerivePassword(t *testing.T) { - expectations := map[string]string{ - `{{derivePassword 1 "long" "password" "user" "example.com"}}`: "ZedaFaxcZaso9*", - `{{derivePassword 2 "long" "password" "user" "example.com"}}`: "Fovi2@JifpTupx", - `{{derivePassword 1 "maximum" "password" "user" "example.com"}}`: "pf4zS1LjCg&LjhsZ7T2~", - `{{derivePassword 1 "medium" "password" "user" "example.com"}}`: "ZedJuz8$", - `{{derivePassword 1 "basic" "password" "user" "example.com"}}`: "pIS54PLs", - `{{derivePassword 1 "short" "password" "user" "example.com"}}`: "Zed5", - `{{derivePassword 1 "pin" "password" "user" "example.com"}}`: "6685", - } - - for tpl, result := range expectations { - out, err := runRaw(tpl, nil) - if err != nil { - t.Error(err) - } - if 0 != strings.Compare(out, result) { - t.Error("Generated password does not match for", tpl) - } - } -} - -// NOTE(bacongobbler): this test is really _slow_ because of how long it takes to compute -// and generate a new crypto key. -func TestGenPrivateKey(t *testing.T) { - // test that calling by default generates an RSA private key - tpl := `{{genPrivateKey ""}}` - out, err := runRaw(tpl, nil) - if err != nil { - t.Error(err) - } - if !strings.Contains(out, "RSA PRIVATE KEY") { - t.Error("Expected RSA PRIVATE KEY") - } - // test all acceptable arguments - tpl = `{{genPrivateKey "rsa"}}` - out, err = runRaw(tpl, nil) - if err != nil { - t.Error(err) - } - if !strings.Contains(out, "RSA PRIVATE KEY") { - t.Error("Expected RSA PRIVATE KEY") - } - tpl = `{{genPrivateKey "dsa"}}` - out, err = runRaw(tpl, nil) - if err != nil { - t.Error(err) - } - if !strings.Contains(out, "DSA PRIVATE KEY") { - t.Error("Expected DSA PRIVATE KEY") - } - tpl = `{{genPrivateKey "ecdsa"}}` - out, err = runRaw(tpl, nil) - if err != nil { - t.Error(err) - } - if !strings.Contains(out, "EC PRIVATE KEY") { - t.Error("Expected EC PRIVATE KEY") - } - // test bad - tpl = `{{genPrivateKey "bad"}}` - out, err = runRaw(tpl, nil) - if err != nil { - t.Error(err) - } - if out != "Unknown type bad" { - t.Error("Expected type 'bad' to be an unknown crypto algorithm") - } - // ensure that we can base64 encode the string - tpl = `{{genPrivateKey "rsa" | b64enc}}` - out, err = runRaw(tpl, nil) - if 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") - } -} - +// 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)) @@ -941,6 +68,8 @@ func runtv(tpl, expect string, vars interface{}) error { } 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)) diff --git a/list.go b/list.go new file mode 100644 index 00000000..8c5f847a --- /dev/null +++ b/list.go @@ -0,0 +1,80 @@ +package sprig + +import ( + "reflect" + "sort" +) + +func list(v ...interface{}) []interface{} { + return v +} + +func push(list []interface{}, v interface{}) []interface{} { + return append(list, v) +} + +func prepend(list []interface{}, v interface{}) []interface{} { + return append([]interface{}{v}, list...) +} + +func last(list []interface{}) interface{} { + l := len(list) + if l == 0 { + return nil + } + return list[l-1] +} + +func first(list []interface{}) interface{} { + if len(list) == 0 { + return nil + } + return list[0] +} + +func rest(list []interface{}) []interface{} { + if len(list) == 0 { + return list + } + return list[1:] +} + +func initial(list []interface{}) []interface{} { + l := len(list) + if l == 0 { + return list + } + return list[:l-1] +} + +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{} { + // We do not sort in place because the incomming array should not be altered. + l := len(v) + c := make([]interface{}, l) + for i := 0; i < l; i++ { + c[l-i-1] = v[i] + } + return c +} + +func compact(list []interface{}) []interface{} { + res := []interface{}{} + for _, item := range list { + if !empty(item) { + res = append(res, item) + } + } + return res +} diff --git a/list_test.go b/list_test.go new file mode 100644 index 00000000..091512c2 --- /dev/null +++ b/list_test.go @@ -0,0 +1,98 @@ +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", + } + 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", + } + 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 }}`: "", + } + 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 }}`: "", + } + 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 }}`: "[]", + } + 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 }}`: "[]", + } + 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 }}`: "[]", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} diff --git a/numeric.go b/numeric.go new file mode 100644 index 00000000..191e3b97 --- /dev/null +++ b/numeric.go @@ -0,0 +1,129 @@ +package sprig + +import ( + "math" + "reflect" + "strconv" +) + +// 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() == true { + return 1 + } + return 0 + default: + return 0 + } +} + +func toInt(v interface{}) int { + //It's not optimal. Bud 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() == true { + 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 min(a interface{}, i ...interface{}) int64 { + aa := toInt64(a) + for _, b := range i { + bb := toInt64(b) + if bb < aa { + 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 +} diff --git a/numeric_test.go b/numeric_test.go new file mode 100644 index 00000000..97888f49 --- /dev/null +++ b/numeric_test.go @@ -0,0 +1,180 @@ +package sprig + +import ( + "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 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 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 TestAdd(t *testing.T) { + tpl := `{{ 3 | add 1 2}}` + if err := runt(tpl, `6`); 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) + } +} diff --git a/reflect.go b/reflect.go new file mode 100644 index 00000000..8a65c132 --- /dev/null +++ b/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/reflect_test.go b/reflect_test.go new file mode 100644 index 00000000..515fae9c --- /dev/null +++ b/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 = nil + 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/strings.go b/strings.go new file mode 100644 index 00000000..69bcd985 --- /dev/null +++ b/strings.go @@ -0,0 +1,197 @@ +package sprig + +import ( + "encoding/base32" + "encoding/base64" + "fmt" + "reflect" + "strconv" + "strings" + + util "github.com/aokoli/goutils" +) + +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 abbrev(width int, s string) string { + if width < 4 { + return s + } + r, _ := util.Abbreviate(s, width) + return r +} + +func abbrevboth(left, right int, s string) string { + if right < 4 || left > 0 && right < 7 { + return s + } + r, _ := util.AbbreviateFull(s, left, right) + return r +} +func initials(s string) string { + // Wrap this just to eliminate the var args, which templates don't do well. + return util.Initials(s) +} + +func randAlphaNumeric(count int) string { + // It is not possible, it appears, to actually generate an error here. + r, _ := util.RandomAlphaNumeric(count) + return r +} + +func randAlpha(count int) string { + r, _ := util.RandomAlphabetic(count) + return r +} + +func randAscii(count int) string { + r, _ := util.RandomAscii(count) + return r +} + +func randNumeric(count int) string { + r, _ := util.RandomNumeric(count) + return r +} + +func untitle(str string) string { + return util.Uncapitalize(str) +} + +func quote(str ...interface{}) string { + out := make([]string, len(str)) + for i, s := range str { + out[i] = fmt.Sprintf("%q", strval(s)) + } + return strings.Join(out, " ") +} + +func squote(str ...interface{}) string { + out := make([]string, len(str)) + for i, s := range str { + out[i] = fmt.Sprintf("'%v'", s) + } + return strings.Join(out, " ") +} + +func cat(v ...interface{}) string { + 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 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{}: + l := len(v) + b := make([]string, l) + for i := 0; i < l; i++ { + b[i] = strval(v[i]) + } + return b + default: + val := reflect.ValueOf(v) + switch val.Kind() { + case reflect.Array, reflect.Slice: + l := val.Len() + b := make([]string, l) + for i := 0; i < l; i++ { + b[i] = strval(val.Index(i).Interface()) + } + return b + default: + return []string{strval(v)} + } + } +} + +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 len(s) <= c { + return s + } + return s[0:c] +} + +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 +} + +// substring creates a substring of the given string. +// +// If start is < 0, this calls string[:length]. +// +// If start is >= 0 and length < 0, this calls string[start:] +// +// Otherwise, this calls string[start, length]. +func substring(start, length int, s string) string { + if start < 0 { + return s[:length] + } + if length < 0 { + return s[start:] + } + return s[start:length] +} diff --git a/strings_test.go b/strings_test.go new file mode 100644 index 00000000..742e6d74 --- /dev/null +++ b/strings_test.go @@ -0,0 +1,220 @@ +package sprig + +import ( + "encoding/base32" + "encoding/base64" + "fmt" + "math/rand" + "testing" + + "github.com/aokoli/goutils" + "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 TestTrunc(t *testing.T) { + tpl := `{{ "foooooo" | trunc 3 }}` + if err := runt(tpl, "foo"); 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) + } +} +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) + } +} + +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 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")) +} + +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}})) +} + +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 TestGoutils(t *testing.T) { + tests := map[string]string{ + `{{abbrev 5 "hello world"}}`: "he...", + `{{abbrevboth 5 10 "1234 5678 9123"}}`: "...5678...", + `{{nospace "h e l l o "}}`: "hello", + `{{untitle "First Try"}}`: "first try", //https://youtu.be/44-RsrF_V_w + `{{initials "First Try"}}`: "FT", + `{{wrap 5 "Hello World"}}`: "Hello\nWorld", + `{{wrapWith 5 "\t" "Hello World"}}`: "Hello\tWorld", + } + for k, v := range tests { + t.Log(k) + if err := runt(k, v); err != nil { + t.Errorf("Error on tpl %s: %s", err) + } + } +} + +func TestRandom(t *testing.T) { + // One of the things I love about Go: + goutils.RANDOM = rand.New(rand.NewSource(1)) + + // Because we're using a random number generator, we need these to go in + // a predictable sequence: + if err := runt(`{{randAlphaNum 5}}`, "9bzRv"); err != nil { + t.Errorf("Error on tpl %s: %s", err) + } + if err := runt(`{{randAlpha 5}}`, "VjwGe"); err != nil { + t.Errorf("Error on tpl %s: %s", err) + } + if err := runt(`{{randAscii 5}}`, "1KA5p"); err != nil { + t.Errorf("Error on tpl %s: %s", err) + } + if err := runt(`{{randNumeric 5}}`, "26018"); err != nil { + t.Errorf("Error on tpl %s: %s", 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) + } +} + +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 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) + } +}