diff --git a/docs-src/content/functions/test.yml b/docs-src/content/functions/test.yml index f2e210997..9f638c881 100644 --- a/docs-src/content/functions/test.yml +++ b/docs-src/content/functions/test.yml @@ -37,3 +37,42 @@ funcs: template: :1:3: executing "" at : error calling fail: template generation failed $ gomplate -i '{{ test.Fail "something is wrong!" }}' template: :1:7: executing "" at : 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 < 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: :1:25: executing "" at : 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: :1:7: executing "" at <"config">: map has no entry for key "bogus" diff --git a/docs/content/functions/test.md b/docs/content/functions/test.md index 6cccbdc52..c6d24008b 100644 --- a/docs/content/functions/test.md +++ b/docs/content/functions/test.md @@ -69,3 +69,56 @@ template: :1:3: executing "" at : error calling fail: template g $ gomplate -i '{{ test.Fail "something is wrong!" }}' template: :1:7: executing "" at : 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 < 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: :1:25: executing "" at : 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: :1:7: executing "" at <"config">: map has no entry for key "bogus" +``` diff --git a/funcs/test.go b/funcs/test.go index b9d6376bd..4def26f1a 100644 --- a/funcs/test.go +++ b/funcs/test.go @@ -26,6 +26,7 @@ func AddTestFuncs(f map[string]interface{}) { f["assert"] = TestNS().Assert f["fail"] = TestNS().Fail + f["required"] = TestNS().Required } // TestFuncs - @@ -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)) + } +} diff --git a/funcs/test_test.go b/funcs/test_test.go new file mode 100644 index 000000000..7b7adfca7 --- /dev/null +++ b/funcs/test_test.go @@ -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") +} diff --git a/test/test.go b/test/test.go index c3c2a62c4..f615b013c 100644 --- a/test/test.go +++ b/test/test.go @@ -2,6 +2,7 @@ package test import ( "github.com/pkg/errors" + // "reflect" ) // Assert - @@ -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 +} diff --git a/test/test_test.go b/test/test_test.go index cba36440f..f048a9615 100644 --- a/test/test_test.go +++ b/test/test_test.go @@ -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) +} diff --git a/tests/integration/basic_test.go b/tests/integration/basic_test.go index 349efe04f..1dc52b857 100644 --- a/tests/integration/basic_test.go +++ b/tests/integration/basic_test.go @@ -5,7 +5,6 @@ package integration import ( "bytes" - "fmt" "io/ioutil" "os" @@ -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 @@ -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 diff --git a/tests/integration/test_test.go b/tests/integration/test_test.go new file mode 100644 index 000000000..24376dde2 --- /dev/null +++ b/tests/integration/test_test.go @@ -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", + }) +}