Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: Add functions safeDefault, safeCoalesce, and nonNil #385

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ func dfault(d interface{}, given ...interface{}) interface{} {
return given[0]
}

// Similar to dfault but only considers nil as unset. 0, false, "" are considered set
govindbalaji-s marked this conversation as resolved.
Show resolved Hide resolved
func safeDefault(d interface{}, given ...interface{}) interface{} {
if given == nil || len(given) == 0 || given[0] == nil {
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)
Expand Down Expand Up @@ -59,6 +67,10 @@ func empty(given interface{}) bool {
}
}

func nonNil(given interface{}) bool {
return given != nil
}

// coalesce returns the first non-empty value.
func coalesce(v ...interface{}) interface{} {
for _, val := range v {
Expand All @@ -69,6 +81,16 @@ func coalesce(v ...interface{}) interface{} {
return nil
}

// safeCoalesce returns the first non-nil value
func safeCoalesce(v ...interface{}) interface{} {
for _, val := range v {
if val != nil {
return val
}
}
return nil
}

// all returns true if empty(x) is false for all values x in the list.
// If the list is empty, return true.
func all(v ...interface{}) bool {
Expand Down
104 changes: 104 additions & 0 deletions defaults_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,55 @@ func TestDefault(t *testing.T) {
}
}

func TestSafeDefault(t *testing.T) {
tpl := `{{"" | safeDefault "foo"}}`
if err := runt(tpl, ""); err != nil {
t.Error(err)
}
tpl = `{{safeDefault "foo" 234}}`
if err := runt(tpl, "234"); err != nil {
t.Error(err)
}
tpl = `{{safeDefault "foo" 2.34}}`
if err := runt(tpl, "2.34"); err != nil {
t.Error(err)
}

tpl = `{{ .Nothing | safeDefault "123" }}`
if err := runt(tpl, "123"); err != nil {
t.Error(err)
}
tpl = `{{ safeDefault "123" }}`
if err := runt(tpl, "123"); err != nil {
t.Error(err)
}

tpl = `{{ .Nothing | safeDefault true }}`
if err := runt(tpl, "true"); err != nil {
t.Error(err)
}
tpl = `{{ false | safeDefault true }}`
if err := runt(tpl, "false"); err != nil {
t.Error(err)
}
tpl = `{{ true | safeDefault false }}`
if err := runt(tpl, "true"); err != nil {
t.Error(err)
}
tpl = `{{ .Nothing | safeDefault 100 }}`
if err := runt(tpl, "100"); err != nil {
t.Error(err)
}
tpl = `{{ 0 | safeDefault 100 }}`
if err := runt(tpl, "0"); err != nil {
t.Error(err)
}
tpl = `{{ 55 | safeDefault 0 }}`
if err := runt(tpl, "55"); 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 {
Expand Down Expand Up @@ -64,6 +113,40 @@ func TestEmpty(t *testing.T) {
}
}

func TestNonNil(t *testing.T) {
tpl := `{{if nonNil 1}}1{{else}}0{{end}}`
if err := runt(tpl, "1"); err != nil {
t.Error(err)
}

tpl = `{{if nonNil 0}}1{{else}}0{{end}}`
if err := runt(tpl, "1"); err != nil {
t.Error(err)
}
tpl = `{{if nonNil ""}}1{{else}}0{{end}}`
if err := runt(tpl, "1"); err != nil {
t.Error(err)
}
tpl = `{{if nonNil 0.0}}1{{else}}0{{end}}`
if err := runt(tpl, "1"); err != nil {
t.Error(err)
}
tpl = `{{if nonNil 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 nonNil .top.NoSuchThing}}1{{else}}0{{end}}`
if err := runtv(tpl, "0", dict); err != nil {
t.Error(err)
}
tpl = `{{if nonNil .bottom.NoSuchThing}}1{{else}}0{{end}}`
if err := runtv(tpl, "0", dict); err != nil {
t.Error(err)
}
}

func TestCoalesce(t *testing.T) {
tests := map[string]string{
`{{ coalesce 1 }}`: "1",
Expand All @@ -84,6 +167,27 @@ func TestCoalesce(t *testing.T) {
}
}

func TestSafeCoalesce(t *testing.T) {
tests := map[string]string{
`{{ safeCoalesce 1 }}`: "1",
`{{ safeCoalesce "" 0 nil 2 }}`: "",
`{{ safeCoalesce nil 0 "" 2 }}`: "0",
`{{ $two := 2 }}{{ safeCoalesce nil $two "" 0 }}`: "2",
`{{ $two := 2 }}{{ safeCoalesce "" $two 0 0 0 }}`: "",
`{{ $two := 2 }}{{ safeCoalesce "" $two 3 4 5 }}`: "",
`{{ safeCoalesce }}`: "<no value>",
}
for tpl, expect := range tests {
assert.NoError(t, runt(tpl, expect))
}

dict := map[string]interface{}{"top": map[string]interface{}{}}
tpl := `{{ coalesce .top.NoSuchThing .bottom .bottom.dollar "airplane"}}`
if err := runtv(tpl, "airplane", dict); err != nil {
t.Error(err)
}
}

func TestAll(t *testing.T) {
tests := map[string]string{
`{{ all 1 }}`: "true",
Expand Down
25 changes: 25 additions & 0 deletions docs/defaults.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,26 @@ The definition of "empty" depends on type:
For structs, there is no definition of empty, so a struct will never return the
default.

## safeDefault

Similar to `default` but only considers `nil` as unset value. `0`, `false`,
`""` are not considered unset, unlike `default`.

```
safeDefault true .Enabled
```

In the above, if `.Enabled` evaluates to `false`, same will be returned.
However, `default` would return `true` here.

## empty

The `empty` function returns `true` if the given value is considered empty, and
`false` otherwise. The empty values are listed in the `default` section.

## nonNil
The `nonNil` function returns `true` if the given value is not `nil`.

```
empty .Foo
```
Expand Down Expand Up @@ -58,6 +73,16 @@ The above will first check to see if `.name` is empty. If it is not, it will ret
that value. If it _is_ empty, `coalesce` will evaluate `.parent.name` for emptiness.
Finally, if both `.name` and `.parent.name` are empty, it will return `Matt`.

## safeCoalesce

Similar to `coalesce` but only considers `nil` as unset value.

```
safeCoalesce nil 0 1 2
```

The above evaluates to `0`.

## all

The `all` function takes a list of values and returns true if all values are non-empty.
Expand Down
32 changes: 17 additions & 15 deletions functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@ import (
//
// Use this to pass the functions into the template engine:
//
// tpl := template.New("foo").Funcs(sprig.FuncMap()))
//
// tpl := template.New("foo").Funcs(sprig.FuncMap()))
func FuncMap() template.FuncMap {
return HtmlFuncMap()
}
Expand Down Expand Up @@ -237,8 +236,11 @@ var genericMap = map[string]interface{}{

// Defaults
"default": dfault,
"safeDefault": safeDefault,
"empty": empty,
"nonNil": nonNil,
"coalesce": coalesce,
"safeCoalesce": safeCoalesce,
"all": all,
"any": any,
"compact": compact,
Expand Down Expand Up @@ -336,20 +338,20 @@ var genericMap = map[string]interface{}{
"mustChunk": mustChunk,

// Crypto:
"bcrypt": bcrypt,
"htpasswd": htpasswd,
"genPrivateKey": generatePrivateKey,
"derivePassword": derivePassword,
"buildCustomCert": buildCustomCertificate,
"genCA": generateCertificateAuthority,
"genCAWithKey": generateCertificateAuthorityWithPEMKey,
"genSelfSignedCert": generateSelfSignedCertificate,
"bcrypt": bcrypt,
"htpasswd": htpasswd,
"genPrivateKey": generatePrivateKey,
"derivePassword": derivePassword,
"buildCustomCert": buildCustomCertificate,
"genCA": generateCertificateAuthority,
"genCAWithKey": generateCertificateAuthorityWithPEMKey,
"genSelfSignedCert": generateSelfSignedCertificate,
"genSelfSignedCertWithKey": generateSelfSignedCertificateWithPEMKey,
"genSignedCert": generateSignedCertificate,
"genSignedCertWithKey": generateSignedCertificateWithPEMKey,
"encryptAES": encryptAES,
"decryptAES": decryptAES,
"randBytes": randBytes,
"genSignedCert": generateSignedCertificate,
"genSignedCertWithKey": generateSignedCertificateWithPEMKey,
"encryptAES": encryptAES,
"decryptAES": decryptAES,
"randBytes": randBytes,

// UUIDs:
"uuidv4": uuidv4,
Expand Down