Skip to content

Commit

Permalink
Add support for custom placeholders
Browse files Browse the repository at this point in the history
Signed-off-by: Mathieu Frenette <[email protected]>
  • Loading branch information
silphid committed Mar 8, 2021
1 parent 4b28c02 commit 2dcca34
Show file tree
Hide file tree
Showing 10 changed files with 71 additions and 34 deletions.
4 changes: 4 additions & 0 deletions .prettierrc.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
overrides:
- files: "*.yaml"
options:
bracketSpacing: false
20 changes: 10 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -448,11 +448,18 @@ As the conditional for `if` steps is always a template expression, _do not_ encl

## Special placeholders

Because the PROJECT variable is typically used pervasively throughout templates in the form of `{{.PROJECT}}` and `{{.PROJECT | upper}}`, we have introduced the special placeholders `projekt` and `PROJEKT`, which can be used anywhere in file/dir names and templates without any adornments.
Placeholders are a lightweight alternative to go template expressions, which can be used as plain text anywhere in file/dir names and template files. Because placeholders are processed using plain search-and-replace, ensure they have improbable names that don't risk conflicting with anything else (ie: "projekt").

For example, the text "MY PROJEKT FILE.TXT" is equivalent to "MY {{.PROJECT | upper}} FILE.TXT".
For example, you can define the following placeholders in your template spec:

Currently, those two placeholders are hardcoded and are the only ones supported, but we plan to add support for defining your own in the template spec.
```yaml
placeholders:
projekt: "{{ .PROJECT | lower }}"
Projekt: "{{ .PROJECT | title }}"
PROJEKT: "{{ .PROJECT | upper }}"
```

You can then use these placeholders anywhere without any adornments. For example, the text "MY PROJEKT FILE.TXT" is equivalent to "MY {{.PROJECT | upper}} FILE.TXT".

This feature was inspired by the way we were previously creating new projects by duplicating an existing project and doing a search-and-replace for the project name in different case variants. That strategy was very simple and effective, as long as the project name was a very distinct string that did not appear in any other undesired contexts, hence our choice of `projekt` as something that you are (hopefully!) very
unlikely to encounter in your project for any other reason than those placeholders!
Expand Down Expand Up @@ -486,10 +493,3 @@ To associate a template with an existing project that was not initially generate
- Add more example templates, for go, node...
- Fix `choice` step to pre-select current value, if any.
- Allow special `.tmpl` and `.notmpl` extensions to be placed before actual extension (ie: `file.tmpl.txt`), to allow file editor to recognize them better during template editing.
- Allow to customize placeholders in spec file:

```
placeholders:
projekt: {{.PROJECT | lower}}
PROJEKT: {{.PROJECT | upper}},
```
4 changes: 4 additions & 0 deletions examples/.prettierrc.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
overrides:
- files: "*.yaml"
options:
bracketSpacing: false
9 changes: 9 additions & 0 deletions examples/templates/hello-world/spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ version: 0.2.0
# Description displayed to user during template selection
description: The customary Hello World example

# Placeholders are a lightweight alternative to go template expressions, which can be used as
# plain text anywhere in file/dir names and template files. Because placeholders are processed
# using plain search-and-replace, ensure they have improbable names that don't risk conflicting
# with anything else (ie: "projekt").
placeholders:
projekt: "{{.PROJECT | lower}}"
Projekt: "{{.PROJECT | title}}"
PROJEKT: "{{.PROJECT | upper}}"

# Actions are sets of steps that can be invoked by user by their name
actions:
# By convention, the "create" action is in charge of scaffolding the project initially
Expand Down
3 changes: 2 additions & 1 deletion examples/templates/hello-world/src/README.md.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@

The PROJEKT project is managed by the {{.TEAM}} team.

`PROJEKT` is a special placeholder equivalent to {{.PROJECT | upper}}
The `PROJEKT` placeholder is defined in spec as equivalent to {{.PROJECT | upper}}

Project name: {{.PROJECT}}
Project name in uppercase: {{.PROJECT | upper}}
Placeholder lowercase: projekt
Placeholder titlecase: Projekt
Placeholder uppercase: PROJEKT

It was created with:
Expand Down
15 changes: 3 additions & 12 deletions src/cmd/internal/internal.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,19 +97,10 @@ func (c context) IsVarOverriden(name string) bool {
}

// GetPlaceholders returns a map of special placeholders that can be used instead
// of go template expression, for lighter weight templating, especially for the
// project's name, which appears everywhere. For now, the only supported placeholder
// is PROJECT, but we will eventually make placeholders configurable in spec file.
// of go template expressions, for more lightweight templating, especially for the
// project's name, which appears everywhere.
func (c context) GetPlaceholders() map[string]string {
value, _ := c.project.Vars["PROJECT"]
str, ok := value.(string)
if !ok {
return nil
}
return map[string]string{
"projekt": strings.ToLower(str),
"PROJEKT": strings.ToUpper(str),
}
return c.spec.Placeholders
}

// GetEvalVars returns a dictionary of the project's variable names mapped to
Expand Down
6 changes: 3 additions & 3 deletions src/internal/evaluation/evaluation.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ type Context interface {
GetEvalVars() map[string]interface{}

// GetPlaceholders returns a map of special placeholders that can be used instead
// of go template expression, for lighter weight templating, especially for the
// of go template expressions, for more lightweight templating, especially for the
// project's name, which appears everywhere.
GetPlaceholders() map[string]string

Expand Down Expand Up @@ -64,8 +64,8 @@ func EvalTemplate(context Context, text string) (string, error) {
text = strings.ReplaceAll(text, tripleClose, doubleOpen+"`"+doubleClose+"`"+doubleClose)

// Perform replacement of placeholders
for search, replace := range context.GetPlaceholders() {
text = strings.ReplaceAll(text, search, replace)
for placeholderName, placeholderValue := range context.GetPlaceholders() {
text = strings.ReplaceAll(text, placeholderName, placeholderValue)
}

// Render go template
Expand Down
2 changes: 1 addition & 1 deletion src/internal/exec/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ type Context interface {
IsVarOverriden(name string) bool

// GetPlaceholders returns a map of special placeholders that can be used instead
// of go template expression, for lighter weight templating, especially for the
// of go template expressions, for more lightweight templating, especially for the
// project's name, which appears everywhere.
GetPlaceholders() map[string]string

Expand Down
30 changes: 26 additions & 4 deletions src/internal/spec/spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@ import (

// Spec represents a template specification file found in the root of the template's dir
type Spec struct {
Name string
Version string
Description string
Actions map[string]Action
Name string
Version string
Description string
Placeholders map[string]string
Actions map[string]Action
}

// Load loads spec object from a template directory
Expand Down Expand Up @@ -58,6 +59,15 @@ func loadFromMap(_map yaml.Map, templateDir string) (*Spec, error) {
return nil, err
}

// Load placeholders
placeholders, ok, err := getOptionalMap(_map, "placeholders")
if err != nil {
return nil, err
}
if ok {
spec.Placeholders, err = loadPlaceholders(placeholders)
}

// Load actions
actions, err := getRequiredMap(_map, "actions")
if err != nil {
Expand All @@ -71,6 +81,18 @@ func loadFromMap(_map yaml.Map, templateDir string) (*Spec, error) {
return spec, nil
}

func loadPlaceholders(_map yaml.Map) (map[string]string, error) {
placeholders := make(map[string]string, len(_map))
for key, node := range _map {
value, ok := getString(node)
if !ok {
return nil, fmt.Errorf("value for placeholder %q must be a string", key)
}
placeholders[key] = value
}
return placeholders, nil
}

func loadActions(node yaml.Map, templateDir string) (ActionMap, error) {
var actions []Action
for name, value := range node {
Expand Down
12 changes: 9 additions & 3 deletions src/internal/spec/spec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -364,9 +364,10 @@ func TestLoadSpec(t *testing.T) {
Buffer: `
version: 0.2.0
description: Description
import:
common: common
go: go/common
placeholders:
projekt: {{.PROJECT | lower}}
Projekt: {{.PROJECT | title}}
PROJEKT: {{.PROJECT | upper}}
actions:
action1:
- if: Condition 1
Expand All @@ -382,6 +383,11 @@ actions:
Name: "template_name",
Version: "0.2.0",
Description: "Description",
Placeholders: map[string]string{
"projekt": "{{.PROJECT | lower}}",
"Projekt": "{{.PROJECT | title}}",
"PROJEKT": "{{.PROJECT | upper}}",
},
Actions: ActionMap{
"action1": Action{
Name: "action1",
Expand Down

0 comments on commit 2dcca34

Please sign in to comment.