Skip to content

Commit

Permalink
Merge pull request #35 from evilmarty/nested-templates
Browse files Browse the repository at this point in the history
feat(templates): Add support for nested templates
  • Loading branch information
evilmarty authored Jul 1, 2024
2 parents c4add5f + fb17af8 commit 796471e
Show file tree
Hide file tree
Showing 7 changed files with 196 additions and 83 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,9 @@ after inputs are collected but before script execution. Along with inputs,
templates can access environment variables that are present and regardless
whether `pure` is enabled or not.

Nested commands can include the run scripts from their parent commands.
ie. `{{template "<command_name>"}}`

### .Input.<input_name>

The expression to reference an input value. ie. '{{ .Input.my_input }}'
Expand Down
28 changes: 22 additions & 6 deletions command_set.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"os"
"os/exec"
"strings"
"text/template"
)

var (
Expand Down Expand Up @@ -95,15 +96,30 @@ func (cs CommandSet) RenderEnv(data TemplateData) ([]string, error) {
}

func (cs CommandSet) RenderScript(data TemplateData) (string, error) {
for i := len(cs.Commands) - 1; i >= 0; {
command := cs.Commands[i]
if script, err := RenderTemplate(command.Run, data); err != nil {
return script, fmt.Errorf("template error for command: '%s' - %v", command.Name, err)
var tmpl *template.Template
for _, command := range cs.Commands {
var err error
if command.Run == "" {
continue
}
if tmpl == nil {
tmpl = template.New(command.Name)
} else {
return script, nil
tmpl = tmpl.New(command.Name)
}
tmpl, err = tmpl.Parse(command.Run)
if err != nil {
return "", fmt.Errorf("template error: %v", err)
}
}
if tmpl == nil {
return "", fmt.Errorf("no script present")
}
if script, err := RenderTemplate(tmpl, data); err != nil {
return script, fmt.Errorf("script error: %v", err)
} else {
return script, err
}
return "", fmt.Errorf("no script present")
}

func (cs CommandSet) RenderScriptToTemp(data TemplateData) (string, error) {
Expand Down
177 changes: 126 additions & 51 deletions command_set_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,60 +197,135 @@ func TestCommandSetRenderEnv_TemplateError(t *testing.T) {
assertEqual(t, expected, actual, "CommandSet.RenderEnv() returned unexpected error")
}

func TestCommandSetRenderScript_Empty(t *testing.T) {
data := TemplateData{}
cs := CommandSet{}
_, err := cs.RenderScript(data)
expected := "no script present"
actual := fmt.Sprintf("%s", err)
assertEqual(t, expected, actual, "CommandSet.RenderScript() returned unexpected error")
}

func TestCommandSetRenderScript_TemplateError(t *testing.T) {
data := TemplateData{
Input: map[string]any{
"A": "a",
},
}
cs := CommandSet{
Commands: []ConfigCommand{
{
Name: "foobar",
Run: "{{.Input.A}",
func TestCommandSetRenderScript(t *testing.T) {
t.Run("empty command set", func(t *testing.T) {
data := TemplateData{}
cs := CommandSet{}
_, err := cs.RenderScript(data)
expected := "no script present"
actual := fmt.Sprintf("%s", err)
assertEqual(t, expected, actual, "CommandSet.RenderScript() returned unexpected error")
})
t.Run("template error", func(t *testing.T) {
data := TemplateData{
Input: map[string]any{
"A": "a",
},
},
}
_, err := cs.RenderScript(data)
expected := "template error for command: 'foobar' - template: :1: bad character U+007D '}'"
actual := fmt.Sprintf("%s", err)
assertEqual(t, expected, actual, "CommandSet.RenderScript() returned unexpected error")
}

func TestCommandSetRenderScript_NonError(t *testing.T) {
data := TemplateData{
Input: map[string]any{
"A": "a",
"B": "b",
},
}
cs := CommandSet{
Commands: []ConfigCommand{
{
Name: "foobaz",
Run: "echo {{.Input.B}}",
}
cs := CommandSet{
Commands: []ConfigCommand{
{
Name: "foobar",
Run: "{{.Input.A}",
},
},
{
Name: "foobar",
Run: "echo {{.Input.A}}",
}
_, err := cs.RenderScript(data)
expected := "template error: template: foobar:1: bad character U+007D '}'"
actual := fmt.Sprintf("%s", err)
assertEqual(t, expected, actual, "CommandSet.RenderScript() returned unexpected error")
})
t.Run("script error", func(t *testing.T) {
data := TemplateData{
Input: map[string]any{
"A": "a",
},
},
}
expected := "echo a"
actual, err := cs.RenderScript(data)
if err != nil {
t.Fatalf("CommandSet.RenderScript() returned an unexpected error: %v", err)
}
assertEqual(t, expected, actual, "CommandSet.RenderScript() returned unexpected result")
}
cs := CommandSet{
Commands: []ConfigCommand{
{
Name: "foobar",
Run: "{{template \"foobaz\"}}",
},
},
}
_, err := cs.RenderScript(data)
expected := "script error: template: foobar:1:11: executing \"foobar\" at <{{template \"foobaz\"}}>: template \"foobaz\" not defined"
actual := fmt.Sprintf("%s", err)
assertEqual(t, expected, actual, "CommandSet.RenderScript() returned unexpected error")
})
t.Run("render single template", func(t *testing.T) {
data := TemplateData{
Input: map[string]any{
"A": "a",
"B": "b",
},
}
cs := CommandSet{
Commands: []ConfigCommand{
{
Name: "foobaz",
Run: "echo {{.Input.B}}",
},
{
Name: "foobar",
Run: "echo {{.Input.A}}",
},
},
}
expected := "echo a"
actual, err := cs.RenderScript(data)
if err != nil {
t.Fatalf("CommandSet.RenderScript() returned an unexpected error: %v", err)
}
assertEqual(t, expected, actual, "CommandSet.RenderScript() returned unexpected result")
})
t.Run("render multiple templates", func(t *testing.T) {
data := TemplateData{
Input: map[string]any{
"A": "a",
"B": "b",
},
}
cs := CommandSet{
Commands: []ConfigCommand{
{
Name: "foobaz",
Run: "echo {{.Input.B}}",
},
{
Name: "foobar",
Run: "echo {{.Input.A}} {{template \"foobaz\" .}}",
},
},
}
expected := "echo a echo b"
actual, err := cs.RenderScript(data)
if err != nil {
t.Fatalf("CommandSet.RenderScript() returned an unexpected error: %v", err)
}
assertEqual(t, expected, actual, "CommandSet.RenderScript() returned unexpected result")
})
t.Run("latest command overrides existing template", func(t *testing.T) {
data := TemplateData{
Input: map[string]any{
"A": "a",
"B": "b",
},
}
cs := CommandSet{
Commands: []ConfigCommand{
{
Name: "foobar",
Run: "echo {{.Input.A}}",
},
{
Name: "foobaz",
Run: "echo {{.Input.B}}",
},
{
Name: "foobar",
Run: "echo {{.Input.A}} {{template \"foobaz\" .}}",
},
},
}
expected := "echo a echo b"
actual, err := cs.RenderScript(data)
if err != nil {
t.Fatalf("CommandSet.RenderScript() returned an unexpected error: %v", err)
}
assertEqual(t, expected, actual, "CommandSet.RenderScript() returned unexpected result")
})
}

func TestCommandSetRenderScriptToTemp(t *testing.T) {
Expand Down
2 changes: 0 additions & 2 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,8 +184,6 @@ func (x *ConfigCommands) UnmarshalYAML(value *yaml.Node) error {
}
if numCommands := len(command.Commands); command.Run == "" && numCommands == 0 {
return fmt.Errorf("line %d: command '%s' must have either 'run' or 'commands' attribute", keyNode.Line, keyNode.Value)
} else if command.Run != "" && numCommands > 0 {
return fmt.Errorf("line %d: command '%s' must only have 'run' or 'commands' attribute", keyNode.Line, keyNode.Value)
}
command.Name = keyNode.Value
}
Expand Down
14 changes: 0 additions & 14 deletions config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,20 +106,6 @@ commands:
assertEqual(t, expected, actual, "ParseConfig() returned unexpected error")
}

func TestParseConfig_CommandWithRunAndSubcommands(t *testing.T) {
content := `
commands:
invalidCommand:
run: ooops
commands:
shouldNotExist: bugger
`
expected := "line 3: command 'invalidCommand' must only have 'run' or 'commands' attribute"
_, err := ParseConfig([]byte(content))
actual := fmt.Sprintf("%s", err)
assertEqual(t, expected, actual, "ParseConfig() returned unexpected error")
}

func TestParseConfig_InputOptionsIsMap(t *testing.T) {
content := `
run: ok
Expand Down
20 changes: 16 additions & 4 deletions utils.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"fmt"
"strings"
"text/template"
)
Expand Down Expand Up @@ -37,11 +38,22 @@ func EnvMap(env []string) map[string]string {
return m
}

func RenderTemplate(text string, data TemplateData) (string, error) {
func RenderTemplate(text interface{}, data TemplateData) (string, error) {
var tmpl *template.Template
var err error
b := strings.Builder{}
if tmpl, err := renderTemplate.Parse(text); err != nil {
return "", err
} else if tmpl.Execute(&b, data); err != nil {
switch t := text.(type) {
case *template.Template:
tmpl = t
case string:
tmpl, err = renderTemplate.Parse(t)
if err != nil {
return "", err
}
default:
return "", fmt.Errorf("unsupported type: %v", t)
}
if err = tmpl.Execute(&b, data); err != nil {
return "", err
} else {
return b.String(), nil
Expand Down
35 changes: 29 additions & 6 deletions utils_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package main

import (
"fmt"
"testing"
"text/template"
)

func TestEnvMap(t *testing.T) {
Expand Down Expand Up @@ -36,18 +38,39 @@ func TestNewTemplateData(t *testing.T) {
}

func TestRenderTemplate(t *testing.T) {
template := "Input: {{ .Input.foobar }}, Input: {{ .Input.foobaz }}, Env: {{ .Env.FOOBAR }}, Env: {{ .Env.FOOBAZ }}"
text := "Input: {{ .Input.foobar }}, Input: {{ .Input.foobaz }}, Env: {{ .Env.FOOBAR }}, Env: {{ .Env.FOOBAZ }}"
data := TemplateData{
Input: map[string]any{"foobar": "a"},
Env: map[string]string{"FOOBAR": "b"},
}
expected := "Input: a, Input: <no value>, Env: b, Env: <no value>"
actual, err := RenderTemplate(template, data)
if err != nil {
t.Fatalf("RenderTemplate() returned unexpected error: %v", err)
}
t.Run("given a string", func(t *testing.T) {
actual, err := RenderTemplate(text, data)
if err != nil {
t.Fatalf("RenderTemplate() returned unexpected error: %v", err)
}

assertEqual(t, expected, actual, "RenderTemplate() returned unexpected results")
})

t.Run("given a template object", func(t *testing.T) {
tmpl := template.New("")
if _, err := tmpl.Parse(text); err != nil {
t.Fatalf("Could not render template: %v", err)
}
actual, err := RenderTemplate(tmpl, data)
if err != nil {
t.Fatalf("RenderTemplate() returned unexpected error: %v", err)
}

assertEqual(t, expected, actual, "RenderTemplate() returned unexpected results")
})

assertEqual(t, expected, actual, "RenderTemplate() returned unexpected results")
t.Run("given other", func(t *testing.T) {
_, err := RenderTemplate(nil, data)
actual := fmt.Sprintf("%s", err)
assertEqual(t, "unsupported type: <nil>", actual, "RenderTemplate() returned unexpected error")
})
}

func TestDiffStrings(t *testing.T) {
Expand Down

0 comments on commit 796471e

Please sign in to comment.