diff --git a/cmd/docs/templates/schema.tmpl b/cmd/docs/templates/schema.tmpl index 008f400b..a715d41e 100644 --- a/cmd/docs/templates/schema.tmpl +++ b/cmd/docs/templates/schema.tmpl @@ -8,6 +8,7 @@ | `version` | `string` | | Version of the recipe. Must be valid [semver](https://semver.org/). | | `description` | `string` | | Description of what the recipe does | | `sources` | `[]string` | | A list of URLs to source code for this recipe. | +| `templateExtension` | `string` | | Extension of files in `sources` that are considered templates and are processed with the template engine. Must start with a period if not empty. | | `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. | | `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. | | `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 predefined with `--set` flag. | diff --git a/internal/cli/execute.go b/internal/cli/execute.go index 3525c6a0..b87bc82b 100644 --- a/internal/cli/execute.go +++ b/internal/cli/execute.go @@ -9,6 +9,7 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/futurice/jalapeno/internal/cli/option" + "github.com/futurice/jalapeno/pkg/engine" "github.com/futurice/jalapeno/pkg/oci" "github.com/futurice/jalapeno/pkg/recipe" "github.com/futurice/jalapeno/pkg/recipeutil" @@ -156,7 +157,7 @@ func runExecute(cmd *cobra.Command, opts executeOptions) error { values = recipeutil.MergeValues(values, promptedValues) } - sauce, err := re.Execute(values, uuid.Must(uuid.NewV4())) + sauce, err := re.Execute(engine.Engine{}, values, uuid.Must(uuid.NewV4())) if err != nil { return err } diff --git a/internal/cli/test.go b/internal/cli/test.go index 448b0b61..8eb070ab 100644 --- a/internal/cli/test.go +++ b/internal/cli/test.go @@ -5,6 +5,7 @@ import ( "path/filepath" "github.com/futurice/jalapeno/internal/cli/option" + "github.com/futurice/jalapeno/pkg/engine" "github.com/futurice/jalapeno/pkg/recipe" "github.com/futurice/jalapeno/pkg/recipeutil" "github.com/spf13/cobra" @@ -83,7 +84,7 @@ func runTest(cmd *cobra.Command, opts testOptions) error { if opts.UpdateSnapshots { for i := range re.Tests { test := &re.Tests[i] - sauce, err := re.Execute(test.Values, recipe.TestID) + sauce, err := re.Execute(engine.Engine{}, test.Values, recipe.TestID) if err != nil { return fmt.Errorf("failed to render templates: %w", err) } diff --git a/internal/cli/upgrade.go b/internal/cli/upgrade.go index 142c75b4..80faa4f6 100644 --- a/internal/cli/upgrade.go +++ b/internal/cli/upgrade.go @@ -10,6 +10,7 @@ import ( "strings" "github.com/futurice/jalapeno/internal/cli/option" + "github.com/futurice/jalapeno/pkg/engine" "github.com/futurice/jalapeno/pkg/oci" "github.com/futurice/jalapeno/pkg/recipe" "github.com/futurice/jalapeno/pkg/recipeutil" @@ -191,7 +192,7 @@ func runUpgrade(cmd *cobra.Command, opts upgradeOptions) error { values = recipeutil.MergeValues(values, promptedValues) } - newSauce, err := re.Execute(values, oldSauce.ID) + newSauce, err := re.Execute(engine.Engine{}, values, oldSauce.ID) if err != nil { return err } diff --git a/pkg/recipe/execute.go b/pkg/recipe/execute.go index 515e7ee8..0e4855e8 100644 --- a/pkg/recipe/execute.go +++ b/pkg/recipe/execute.go @@ -4,17 +4,14 @@ import ( "crypto/sha256" "errors" "fmt" + "maps" "strings" "github.com/gofrs/uuid" ) // Execute executes the recipe and returns a sauce -func (re *Recipe) Execute(values VariableValues, id uuid.UUID) (*Sauce, error) { - if re.engine == nil { - return nil, errors.New("render engine has not been set") - } - +func (re *Recipe) Execute(engine RenderEngine, values VariableValues, id uuid.UUID) (*Sauce, error) { if id.IsNil() { return nil, errors.New("ID was nil") } @@ -35,12 +32,27 @@ func (re *Recipe) Execute(values VariableValues, id uuid.UUID) (*Sauce, error) { "Variables": values, } - files, err := re.engine.Render(re.Templates, context) + // Filter out templates we might not want to render + templates := make(map[string][]byte) + plainFiles := make(map[string][]byte) + for filename, content := range re.Templates { + if strings.HasSuffix(filename, re.TemplateExtension) { + templates[filename] = content + } else { + plainFiles[filename] = content + } + } + + files, err := engine.Render(templates, context) if err != nil { return nil, err } - sauce.Files = make(map[string]File, len(files)) + // Add the plain files + maps.Copy(files, plainFiles) + + sauce.Files = make(map[string]File, len(re.Templates)) + idx := 0 for filename, content := range files { // Skip empty files @@ -53,6 +65,8 @@ func (re *Recipe) Execute(values VariableValues, id uuid.UUID) (*Sauce, error) { continue } + filename = strings.TrimSuffix(filename, re.TemplateExtension) + sum := sha256.Sum256(content) sauce.Files[filename] = File{Content: content, Checksum: fmt.Sprintf("sha256:%x", sum)} idx += 1 diff --git a/pkg/recipe/execute_test.go b/pkg/recipe/execute_test.go index 1a7f7677..4c85b4f0 100644 --- a/pkg/recipe/execute_test.go +++ b/pkg/recipe/execute_test.go @@ -42,9 +42,7 @@ func TestRecipeRenderChecksums(t *testing.T) { "README.md": []byte("{{ .Variables.foo }}"), } - recipe.SetEngine(TestRenderEngine{}) - - sauce, err := recipe.Execute(VariableValues{"foo": "bar"}, uuid.Must(uuid.NewV4())) + sauce, err := recipe.Execute(TestRenderEngine{}, VariableValues{"foo": "bar"}, uuid.Must(uuid.NewV4())) if err != nil { t.Fatalf("Failed to render recipe: %s", err) } @@ -61,9 +59,7 @@ func TestRecipeRenderID(t *testing.T) { recipe.Metadata.Name = "test" recipe.Metadata.Version = "v1.0.0" - recipe.SetEngine(TestRenderEngine{}) - - sauce, err := recipe.Execute(nil, uuid.Must(uuid.NewV4())) + sauce, err := recipe.Execute(TestRenderEngine{}, nil, uuid.Must(uuid.NewV4())) if err != nil { t.Fatalf("Failed to render recipe: %s", err) } @@ -78,14 +74,12 @@ func TestRecipeRenderIDReuse(t *testing.T) { recipe.Metadata.Name = "test" recipe.Metadata.Version = "v1.0.0" - recipe.SetEngine(TestRenderEngine{}) - - sauce1, err := recipe.Execute(nil, TestID) + sauce1, err := recipe.Execute(TestRenderEngine{}, nil, TestID) if err != nil { t.Fatalf("Failed to render first recipe: %s", err) } - sauce2, err := recipe.Execute(nil, TestID) + sauce2, err := recipe.Execute(TestRenderEngine{}, nil, TestID) if err != nil { t.Fatalf("Failed to render second recipe: %s", err) } @@ -110,9 +104,7 @@ func TestRecipeRenderEmptyFiles(t *testing.T) { "file-with-empty-variable": []byte(" {{ .Variables.foo }} "), } - recipe.SetEngine(TestRenderEngine{}) - - sauce, err := recipe.Execute(VariableValues{"foo": ""}, uuid.Must(uuid.NewV4())) + sauce, err := recipe.Execute(TestRenderEngine{}, VariableValues{"foo": ""}, uuid.Must(uuid.NewV4())) if err != nil { t.Fatalf("Failed to render recipe: %s", err) } @@ -128,3 +120,32 @@ func TestRecipeRenderEmptyFiles(t *testing.T) { t.Fatalf("Rendered templates contains empty files, which exist on the output. Failing files: %s", failingFiles) } } + +func TestRecipeRenderWithTemplateExtension(t *testing.T) { + recipe := NewRecipe() + recipe.Metadata.Name = "test" + recipe.Metadata.Version = "v1.0.0" + recipe.Variables = []Variable{{Name: "foo"}} + recipe.Templates = map[string][]byte{ + "subdirectory/file.md.tmpl": []byte("{{ .Variables.foo }}"), + "file": []byte("{{ .Variables.foo }}"), + } + recipe.Metadata.TemplateExtension = ".tmpl" + + sauce, err := recipe.Execute(TestRenderEngine{}, VariableValues{"foo": "bar"}, uuid.Must(uuid.NewV4())) + if err != nil { + t.Fatalf("Failed to render recipe: %s", err) + } + + if len(sauce.Files) != 2 { + t.Fatalf("Expected 2 files, got %d", len(sauce.Files)) + } + + if string(sauce.Files["subdirectory/file.md"].Content) != "bar" { + t.Fatalf("Expected file content to be \"bar\", got \"%s\"", sauce.Files["subdirectory/file.md"].Content) + } + + if string(sauce.Files["file"].Content) != "{{ .Variables.foo }}" { + t.Fatalf("Expected file content to be \"{{ .Variables.foo }}\", got \"%s\"", sauce.Files["file"].Content) + } +} diff --git a/pkg/recipe/metadata.go b/pkg/recipe/metadata.go index 41d8b85e..b7beab3c 100644 --- a/pkg/recipe/metadata.go +++ b/pkg/recipe/metadata.go @@ -4,6 +4,7 @@ import ( "fmt" "net/url" "regexp" + "strings" "golang.org/x/mod/semver" ) @@ -34,6 +35,11 @@ type Metadata struct { // files will not be regenerated even if their templates change in future versions // of the recipe. IgnorePatterns []string `yaml:"ignorePatterns,omitempty"` + + // File extension for which files in Sources should be templated. Files not matched by + // this extension will be copied as-is. If empty (the default) all files will be + // templated. + TemplateExtension string `yaml:"templateExtension,omitempty"` } func (m *Metadata) Validate() error { @@ -54,6 +60,10 @@ func (m *Metadata) Validate() error { return fmt.Errorf("version \"%s\" is not a valid semver", m.Version) } + if m.TemplateExtension != "" && !strings.HasPrefix(m.TemplateExtension, ".") { + return fmt.Errorf("template extension must start with a dot") + } + for _, sourceURL := range m.Sources { if _, err := url.ParseRequestURI(sourceURL); err != nil { return fmt.Errorf("source url is invalid: %w", err) diff --git a/pkg/recipe/recipe.go b/pkg/recipe/recipe.go index 83cf1622..35768cdd 100644 --- a/pkg/recipe/recipe.go +++ b/pkg/recipe/recipe.go @@ -2,8 +2,6 @@ package recipe import ( "fmt" - - "github.com/futurice/jalapeno/pkg/engine" ) type Recipe struct { @@ -11,7 +9,6 @@ type Recipe struct { Variables []Variable `yaml:"vars,omitempty"` Templates map[string][]byte `yaml:"-"` Tests []Test `yaml:"-"` - engine RenderEngine } type RenderEngine interface { @@ -23,7 +20,6 @@ func NewRecipe() *Recipe { Metadata: Metadata{ APIVersion: "v1", }, - engine: engine.Engine{}, } } @@ -51,7 +47,3 @@ func (re *Recipe) Validate() error { return nil } - -func (re *Recipe) SetEngine(e RenderEngine) { - re.engine = e -} diff --git a/pkg/recipe/saver_test.go b/pkg/recipe/saver_test.go index d8a89a0b..e169ec2d 100644 --- a/pkg/recipe/saver_test.go +++ b/pkg/recipe/saver_test.go @@ -5,6 +5,7 @@ import ( "path/filepath" "testing" + "github.com/futurice/jalapeno/pkg/engine" "github.com/gofrs/uuid" ) @@ -92,7 +93,7 @@ func TestSaveSauce(t *testing.T) { t.Fatalf("test recipe was not valid: %s", err) } - sauce, err := re.Execute(nil, uuid.Must(uuid.NewV4())) + sauce, err := re.Execute(engine.Engine{}, nil, uuid.Must(uuid.NewV4())) if err != nil { t.Fatalf("recipe execution failed: %s", err) } diff --git a/pkg/recipe/test.go b/pkg/recipe/test.go index 98d52ced..88180919 100644 --- a/pkg/recipe/test.go +++ b/pkg/recipe/test.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" + "github.com/futurice/jalapeno/pkg/engine" "github.com/gofrs/uuid" ) @@ -43,7 +44,7 @@ func (t *Test) Validate() error { func (re *Recipe) RunTests() []error { errors := make([]error, len(re.Tests)) for i, t := range re.Tests { - sauce, err := re.Execute(t.Values, TestID) + sauce, err := re.Execute(engine.Engine{}, t.Values, TestID) if err != nil { errors[i] = fmt.Errorf("%w", err) continue