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

Add test.Required function #392

Merged
merged 1 commit into from
Sep 8, 2018
Merged
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
39 changes: 39 additions & 0 deletions docs-src/content/functions/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,42 @@ funcs:
template: <arg>:1:3: executing "<arg>" at <fail>: error calling fail: template generation failed
$ gomplate -i '{{ test.Fail "something is wrong!" }}'
template: <arg>:1:7: executing "<arg>" at <test.Fail>: error calling Fail: template generation failed: something is wrong!
- name: test.Required
alias: required
description: |
Passes through the given value, if it's non-empty, and non-`nil`. Otherwise,
exits and prints a given error message so the user can adjust as necessary.

This is particularly useful for cases where templates require user-provided
data (such as datasources or environment variables), and rendering can not
continue correctly.

This was inspired by [Helm's `required` function](https://github.com/kubernetes/helm/blob/master/docs/charts_tips_and_tricks.md#know-your-template-functions),
but has slightly different behaviour. Notably, gomplate will always fail in
cases where a referenced _key_ is missing, and this function will have no
effect.
pipeline: true
arguments:
- name: message
required: false
description: The optional message to provide when the required value is not provided
- name: value
required: true
description: The required value
examples:
- |
$ FOO=foobar gomplate -i '{{ getenv "FOO" | required "Missing FOO environment variable!" }}'
foobar
$ FOO= gomplate -i '{{ getenv "FOO" | required "Missing FOO environment variable!" }}'
error: Missing FOO environment variable!
- |
$ cat <<EOF> config.yaml
defined: a value
empty: ""
EOF
$ gomplate -d config=config.yaml -i '{{ (ds "config").defined | required "The `config` datasource must have a value defined for `defined`" }}'
a value
$ gomplate -d config=config.yaml -i '{{ (ds "config").empty | required "The `config` datasource must have a value defined for `empty`" }}'
template: <arg>:1:25: executing "<arg>" at <required "The `confi...>: error calling required: The `config` datasource must have a value defined for `empty`
$ gomplate -d config=config.yaml -i '{{ (ds "config").bogus | required "The `config` datasource must have a value defined for `bogus`" }}'
template: <arg>:1:7: executing "<arg>" at <"config">: map has no entry for key "bogus"
53 changes: 53 additions & 0 deletions docs/content/functions/test.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,56 @@ template: <arg>:1:3: executing "<arg>" at <fail>: error calling fail: template g
$ gomplate -i '{{ test.Fail "something is wrong!" }}'
template: <arg>:1:7: executing "<arg>" at <test.Fail>: error calling Fail: template generation failed: something is wrong!
```

## `test.Required`

**Alias:** `required`

Passes through the given value, if it's non-empty, and non-`nil`. Otherwise,
exits and prints a given error message so the user can adjust as necessary.

This is particularly useful for cases where templates require user-provided
data (such as datasources or environment variables), and rendering can not
continue correctly.

This was inspired by [Helm's `required` function](https://github.com/kubernetes/helm/blob/master/docs/charts_tips_and_tricks.md#know-your-template-functions),
but has slightly different behaviour. Notably, gomplate will always fail in
cases where a referenced _key_ is missing, and this function will have no
effect.

### Usage
```go
test.Required [message] value
```

```go
value | test.Required [message]
```

### Arguments

| name | description |
|------|-------------|
| `message` | _(optional)_ The optional message to provide when the required value is not provided |
| `value` | _(required)_ The required value |

### Examples

```console
$ FOO=foobar gomplate -i '{{ getenv "FOO" | required "Missing FOO environment variable!" }}'
foobar
$ FOO= gomplate -i '{{ getenv "FOO" | required "Missing FOO environment variable!" }}'
error: Missing FOO environment variable!
```
```console
$ cat <<EOF> config.yaml
defined: a value
empty: ""
EOF
$ gomplate -d config=config.yaml -i '{{ (ds "config").defined | required "The `config` datasource must have a value defined for `defined`" }}'
a value
$ gomplate -d config=config.yaml -i '{{ (ds "config").empty | required "The `config` datasource must have a value defined for `empty`" }}'
template: <arg>:1:25: executing "<arg>" at <required "The `confi...>: error calling required: The `config` datasource must have a value defined for `empty`
hairyhenderson marked this conversation as resolved.
Show resolved Hide resolved
$ gomplate -d config=config.yaml -i '{{ (ds "config").bogus | required "The `config` datasource must have a value defined for `bogus`" }}'
template: <arg>:1:7: executing "<arg>" at <"config">: map has no entry for key "bogus"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way we could do something like:

{{ (ds "config") | get "bogus" | required "The `config` datasource must have a value defined for `bogus`"

where get works like getenv and does not error?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

haven't tried...

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, that might work. There is the index built-in, but it doesn't pipeline so well:

$  bin/gomplate -i '{{ $j := (json `{"foo":"bar"}`)}}{{ index $j "baz" | required "nope!" }}'
template: <arg>:1:53: executing "<arg>" at <required "nope!">: error calling required: nope!

if only index took arguments the other way around, it'd be a simple case of {{ $j | index "baz" | required "nope!" }}.

I could write an alternate for index that takes arguments in the opposite order for easier pipelining, but it may not be that necessary...

```
17 changes: 17 additions & 0 deletions funcs/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ func AddTestFuncs(f map[string]interface{}) {

f["assert"] = TestNS().Assert
f["fail"] = TestNS().Fail
f["required"] = TestNS().Required
}

// TestFuncs -
Expand Down Expand Up @@ -59,3 +60,19 @@ func (f *TestFuncs) Fail(args ...interface{}) (string, error) {
return "", errors.Errorf("wrong number of args: want 0 or 1, got %d", len(args))
}
}

// Required -
func (f *TestFuncs) Required(args ...interface{}) (interface{}, error) {
switch len(args) {
case 1:
return test.Required("", args[0])
case 2:
message, ok := args[0].(string)
if !ok {
return nil, errors.Errorf("at <1>: expected string; found %T", args[0])
}
return test.Required(message, args[1])
default:
return nil, errors.Errorf("wrong number of args: want 1 or 2, got %d", len(args))
}
}
65 changes: 65 additions & 0 deletions funcs/test_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package funcs

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestAssert(t *testing.T) {
f := TestNS()
_, err := f.Assert(false)
assert.Error(t, err)

_, err = f.Assert(true)
assert.NoError(t, err)

_, err = f.Assert("foo", true)
assert.NoError(t, err)

_, err = f.Assert("foo", "false")
assert.EqualError(t, err, "assertion failed: foo")
}

func TestRequired(t *testing.T) {
f := TestNS()
errMsg := "can not render template: a required value was not set"
v, err := f.Required("")
assert.Error(t, err)
assert.EqualError(t, err, errMsg)
assert.Nil(t, v)

v, err = f.Required(nil)
assert.Error(t, err)
assert.EqualError(t, err, errMsg)
assert.Nil(t, v)

errMsg = "hello world"
v, err = f.Required(errMsg, nil)
assert.Error(t, err)
assert.EqualError(t, err, errMsg)
assert.Nil(t, v)

v, err = f.Required(42, nil)
assert.Error(t, err)
assert.EqualError(t, err, "at <1>: expected string; found int")
assert.Nil(t, v)

v, err = f.Required()
assert.Error(t, err)
assert.EqualError(t, err, "wrong number of args: want 1 or 2, got 0")
assert.Nil(t, v)

v, err = f.Required("", 2, 3)
assert.Error(t, err)
assert.EqualError(t, err, "wrong number of args: want 1 or 2, got 3")
assert.Nil(t, v)

v, err = f.Required(0)
assert.NoError(t, err)
assert.Equal(t, v, 0)

v, err = f.Required("foo")
assert.NoError(t, err)
assert.Equal(t, v, "foo")
}
14 changes: 14 additions & 0 deletions test/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package test

import (
"github.com/pkg/errors"
// "reflect"
)

// Assert -
Expand All @@ -22,3 +23,16 @@ func Fail(message string) error {
}
return errors.New("template generation failed")
}

// Required -
func Required(message string, value interface{}) (interface{}, error) {
if message == "" {
message = "can not render template: a required value was not set"
}

if s, ok := value.(string); value == nil || (ok && s == "") {
return nil, errors.New(message)
}

return value, nil
}
27 changes: 27 additions & 0 deletions test/test_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,30 @@ func TestFail(t *testing.T) {
err = Fail("msg")
assert.EqualError(t, err, "template generation failed: msg")
}

func TestRequired(t *testing.T) {
v, err := Required("", nil)
assert.Error(t, err)
assert.Nil(t, v)

v, err = Required("", "")
assert.Error(t, err)
assert.Nil(t, v)

v, err = Required("foo", "")
assert.Error(t, err)
assert.EqualError(t, err, "foo")
assert.Nil(t, v)

v, err = Required("", 0)
assert.NoError(t, err)
assert.Equal(t, v, 0)

v, err = Required("", false)
assert.NoError(t, err)
assert.Equal(t, v, false)

v, err = Required("", map[string]string{})
assert.NoError(t, err)
assert.NotNil(t, v)
}
3 changes: 0 additions & 3 deletions tests/integration/basic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ package integration

import (
"bytes"
"fmt"
"io/ioutil"
"os"

Expand Down Expand Up @@ -121,7 +120,6 @@ func (s *BasicSuite) TestRoutesInputsToProperOutputsWithChmod(c *C) {
cmd.Stdin = bytes.NewBufferString("hello world")
})
result.Assert(c, icmd.Success)
fmt.Println(result.Combined())

testdata := []struct {
path string
Expand Down Expand Up @@ -150,7 +148,6 @@ func (s *BasicSuite) TestOverridesOutputModeWithChmod(c *C) {
cmd.Stdin = bytes.NewBufferString("hello world")
})
result.Assert(c, icmd.Success)
fmt.Println(result.Combined())

testdata := []struct {
path string
Expand Down
105 changes: 105 additions & 0 deletions tests/integration/test_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
//+build integration
//+build !windows

package integration

import (
"bytes"

. "gopkg.in/check.v1"

"github.com/gotestyourself/gotestyourself/icmd"
)

type TestSuite struct {
}

var _ = Suite(&TestSuite{})

func (s *TestSuite) SetUpTest(c *C) {
}

func (s *TestSuite) TearDownTest(c *C) {
}

func (s *TestSuite) TestFail(c *C) {
result := icmd.RunCommand(GomplateBin, "-i", "{{ fail }}")
result.Assert(c, icmd.Expected{ExitCode: 1, Err: `template generation failed`})

result = icmd.RunCommand(GomplateBin, "-i", "{{ fail `some message` }}")
result.Assert(c, icmd.Expected{ExitCode: 1, Err: `some message`})
}

func (s *TestSuite) TestRequired(c *C) {
result := icmd.RunCmd(icmd.Command(GomplateBin,
"-i", `{{getenv "FOO" | required "FOO missing" }}`))
result.Assert(c, icmd.Expected{
ExitCode: 1,
Err: "FOO missing",
})

result = icmd.RunCmd(icmd.Command(GomplateBin,
"-i", `{{getenv "FOO" | required "FOO missing" }}`),
func(c *icmd.Cmd) {
c.Env = []string{"FOO=bar"}
})
result.Assert(c, icmd.Expected{
ExitCode: 0,
Out: "bar",
})

result = icmd.RunCmd(icmd.Command(GomplateBin,
"-d", "in=stdin:///?type=application/yaml",
"-i", `{{ (ds "in").foo | required "foo should not be null" }}`),
func(c *icmd.Cmd) {
c.Stdin = bytes.NewBufferString(`foo: null`)
})
result.Assert(c, icmd.Expected{
ExitCode: 1,
Err: "foo should not be null",
})

result = icmd.RunCmd(icmd.Command(GomplateBin,
"-d", "in=stdin:///?type=application/yaml",
"-i", `{{ (ds "in").foo | required }}`),
func(c *icmd.Cmd) {
c.Stdin = bytes.NewBufferString(`foo: []`)
})
result.Assert(c, icmd.Expected{
ExitCode: 0,
Out: "[]",
})

result = icmd.RunCmd(icmd.Command(GomplateBin,
"-d", "in=stdin:///?type=application/yaml",
"-i", `{{ (ds "in").foo | required }}`),
func(c *icmd.Cmd) {
c.Stdin = bytes.NewBufferString(`foo: {}`)
})
result.Assert(c, icmd.Expected{
ExitCode: 0,
Out: "map[]",
})

result = icmd.RunCmd(icmd.Command(GomplateBin,
"-d", "in=stdin:///?type=application/yaml",
"-i", `{{ (ds "in").foo | required }}`),
func(c *icmd.Cmd) {
c.Stdin = bytes.NewBufferString(`foo: 0`)
})
result.Assert(c, icmd.Expected{
ExitCode: 0,
Out: "0",
})

result = icmd.RunCmd(icmd.Command(GomplateBin,
"-d", "in=stdin:///?type=application/yaml",
"-i", `{{ (ds "in").foo | required }}`),
func(c *icmd.Cmd) {
c.Stdin = bytes.NewBufferString(`foo: false`)
})
result.Assert(c, icmd.Expected{
ExitCode: 0,
Out: "false",
})
}