Skip to content

Commit 75da7fa

Browse files
committed
feat: support templates in initHelp
1 parent 2c74475 commit 75da7fa

File tree

8 files changed

+147
-25
lines changed

8 files changed

+147
-25
lines changed

cmd/docs/templates/_schema.tmpl

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
| `description` | `string` | | Description of what the recipe does |
1212
| `source` | `string` | | URL to source code for this recipe. |
1313
| `templateExtension` | `string` | | File extension of files in "templates" directory which should be templated. Files not matched by this extension will be copied as-is. If left empty (the default), all files will be templated. |
14-
| `initHelp` | `string` | | A message which will be showed to an user after a succesful recipe execution. Can be used to guide the user what should be done next in the project directory. |
14+
| `initHelp` | `string` | | A message which will be showed to an user after a succesful recipe execution. Can be used to guide the user what should be done next in the project directory. Supports templating |
1515
| `ignorePatterns` | `[]string` | | Glob patterns for ignoring generated files from future recipe upgrades. Ignored files will not be regenerated even if their templates change in future versions of the recipe. |
1616
| `vars` | [`[]Variable`](#variable) | | An array of variables which can be used in templates. The user will be prompted to provide the value for the variable if not set with `--set` flag. |
1717

go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ require (
3535
github.com/cucumber/messages/go/v21 v21.0.1 // indirect
3636
github.com/docker/docker v24.0.6+incompatible // indirect
3737
github.com/docker/docker-credential-helpers v0.8.0 // indirect
38+
github.com/fatih/structs v1.1.0 // indirect
3839
github.com/gogo/protobuf v1.3.2 // indirect
3940
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
4041
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect

go.sum

+2
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4
5959
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
6060
github.com/expr-lang/expr v1.16.0 h1:BQabx+PbjsL2PEQwkJ4GIn3CcuUh8flduHhJ0lHjWwE=
6161
github.com/expr-lang/expr v1.16.0/go.mod h1:uCkhfG+x7fcZ5A5sXHKuQ07jGZRl6J0FCAaf2k4PtVQ=
62+
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
63+
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
6264
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
6365
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
6466
github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=

internal/cli/execute.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,11 @@ func runExecute(cmd *cobra.Command, opts executeOptions) error {
208208
cmd.Printf("The following files were created:\n\n%s", tree)
209209

210210
if re.InitHelp != "" {
211-
cmd.Printf("\nNext up: %s\n", re.InitHelp)
211+
help, err := sauce.RenderInitHelp()
212+
if err != nil {
213+
return err
214+
}
215+
cmd.Printf("\nNext up: %s\n", help)
212216
}
213217

214218
return nil

pkg/recipe/execute.go

+16-23
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,18 @@ import (
99
"github.com/gofrs/uuid"
1010
)
1111

12+
// TemplateContext defines the context that is passed to the template engine
13+
type TemplateContext struct {
14+
ID string
15+
Recipe struct {
16+
APIVersion string
17+
Name string
18+
Version string
19+
Source string
20+
}
21+
Variables VariableValues
22+
}
23+
1224
type RenderEngine interface {
1325
Render(templates map[string][]byte, values map[string]interface{}) (map[string][]byte, error)
1426
}
@@ -23,28 +35,11 @@ func (re *Recipe) Execute(engine RenderEngine, values VariableValues, id uuid.UU
2335
sauce.Recipe = *re
2436
sauce.Values = values
2537
sauce.ID = id
38+
sauce.Files = make(map[string]File, len(re.Templates))
2639

27-
mappedValues := make(VariableValues)
28-
for name, value := range values {
29-
switch value := value.(type) {
30-
// Map table to more convenient format
31-
case TableValue:
32-
mappedValues[name] = value.ToMapSlice()
33-
default:
34-
mappedValues[name] = value
35-
}
36-
}
37-
38-
// Define the context which is available on templates
39-
context := map[string]interface{}{
40-
"ID": sauce.ID.String(),
41-
"Recipe": struct{ APIVersion, Name, Version, Source string }{
42-
re.APIVersion,
43-
re.Name,
44-
re.Version,
45-
re.Source,
46-
},
47-
"Variables": mappedValues,
40+
context, err := sauce.CreateTemplateContext()
41+
if err != nil {
42+
return nil, err
4843
}
4944

5045
// Filter out templates we might not want to render
@@ -66,8 +61,6 @@ func (re *Recipe) Execute(engine RenderEngine, values VariableValues, id uuid.UU
6661
// Add the plain files
6762
maps.Copy(files, plainFiles)
6863

69-
sauce.Files = make(map[string]File, len(re.Templates))
70-
7164
idx := 0
7265
for filename, content := range files {
7366
// Skip empty files

pkg/recipe/loader_test.go

+3
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ func TestLoadMultipleSauces(t *testing.T) {
4444

4545
sauces := `
4646
apiVersion: v1
47+
id: 12345678-1234-5678-1234-567812345678
4748
recipe:
4849
apiVersion: v1
4950
name: foo
@@ -54,6 +55,7 @@ files:
5455
checksum: sha256:a04042ce4a5e66443c5a26ef2d4432aa535421286c062ea7bf55cba5bae15ef4
5556
---
5657
apiVersion: v1
58+
id: 12345678-1234-5678-1234-567812345679
5759
recipe:
5860
apiVersion: v1
5961
name: bar
@@ -97,6 +99,7 @@ func TestLoadSauceWithMissingFile(t *testing.T) {
9799

98100
sauces := `
99101
apiVersion: v1
102+
id: 12345678-1234-5678-1234-567812345678
100103
recipe:
101104
apiVersion: v1
102105
name: foo

pkg/recipe/sauce.go

+58
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ package recipe
22

33
import (
44
"fmt"
5+
"strings"
6+
"text/template"
57

8+
"github.com/fatih/structs"
69
"github.com/gofrs/uuid"
710
)
811

@@ -54,6 +57,14 @@ func (s *Sauce) Validate() error {
5457
return fmt.Errorf("unreconized sauce API version \"%s\"", s.APIVersion)
5558
}
5659

60+
if s.ID.IsNil() {
61+
return fmt.Errorf("sauce ID was empty")
62+
}
63+
64+
if s.CheckFrom != "" && !strings.HasPrefix(s.CheckFrom, "oci://") {
65+
return fmt.Errorf("currently recipe updates can only be checked from OCI repositories, got: %s", s.CheckFrom)
66+
}
67+
5768
if err := s.Recipe.Validate(); err != nil {
5869
return fmt.Errorf("sauce recipe was invalid: %w", err)
5970
}
@@ -82,3 +93,50 @@ func (s *Sauce) Conflicts(other *Sauce) []RecipeConflict {
8293
}
8394
return conflicts
8495
}
96+
97+
func (s *Sauce) CreateTemplateContext() (map[string]interface{}, error) {
98+
if err := s.Validate(); err != nil {
99+
return nil, err
100+
}
101+
102+
mappedValues := make(VariableValues)
103+
for name, value := range s.Values {
104+
switch value := value.(type) {
105+
// Map table to more convenient format
106+
case TableValue:
107+
mappedValues[name] = value.ToMapSlice()
108+
default:
109+
mappedValues[name] = value
110+
}
111+
}
112+
113+
return structs.Map(TemplateContext{
114+
ID: s.ID.String(),
115+
Recipe: struct{ APIVersion, Name, Version, Source string }{
116+
s.Recipe.APIVersion,
117+
s.Recipe.Name,
118+
s.Recipe.Version,
119+
s.Recipe.Source,
120+
},
121+
Variables: mappedValues,
122+
}), nil
123+
}
124+
125+
func (s *Sauce) RenderInitHelp() (string, error) {
126+
context, err := s.CreateTemplateContext()
127+
if err != nil {
128+
return "", err
129+
}
130+
131+
t, err := template.New("initHelp").Parse(s.Recipe.InitHelp)
132+
if err != nil {
133+
return "", fmt.Errorf("failed to parse initHelp template: %w", err)
134+
}
135+
136+
var buf strings.Builder
137+
if err := t.Execute(&buf, context); err != nil {
138+
return "", fmt.Errorf("failed to render initHelp template: %w", err)
139+
}
140+
141+
return buf.String(), nil
142+
}

pkg/recipe/sauce_test.go

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package recipe
2+
3+
import (
4+
"testing"
5+
6+
"github.com/gofrs/uuid"
7+
)
8+
9+
func TestRenderInitHelp(t *testing.T) {
10+
scenarios := []struct {
11+
name string
12+
help string
13+
values VariableValues
14+
expectedOutput string
15+
expectingErr bool
16+
}{
17+
{
18+
"conditional text",
19+
"{{ if .Variables.FOO }}Foo is true{{ else }}Foo is false{{ end }}",
20+
VariableValues{
21+
"FOO": true,
22+
},
23+
"Foo is true",
24+
false,
25+
},
26+
{
27+
"invalid template",
28+
"{{ if .Variables.NOT_FOUND }}",
29+
VariableValues{
30+
"FOO": true,
31+
},
32+
"",
33+
true,
34+
},
35+
}
36+
37+
for _, scenario := range scenarios {
38+
t.Run(scenario.name, func(t *testing.T) {
39+
re := NewRecipe()
40+
re.Name = "test"
41+
re.Version = "v0.0.0"
42+
re.InitHelp = scenario.help
43+
44+
sauce := NewSauce()
45+
sauce.Recipe = *re
46+
sauce.ID = uuid.Must(uuid.NewV4())
47+
sauce.Values = scenario.values
48+
49+
if help, err := sauce.RenderInitHelp(); err != nil {
50+
if !scenario.expectingErr {
51+
t.Fatalf("Got error when not expected: %s", err)
52+
}
53+
} else if scenario.expectingErr {
54+
t.Fatal("Was expecting error, did not get any")
55+
56+
} else if help != scenario.expectedOutput {
57+
t.Fatalf("Expected output '%s', got '%s'", scenario.expectedOutput, help)
58+
}
59+
})
60+
}
61+
}

0 commit comments

Comments
 (0)