Skip to content

Commit

Permalink
Adding sub-variables (contionals)
Browse files Browse the repository at this point in the history
  • Loading branch information
Paul Lhussiez committed Oct 23, 2018
1 parent a2b1fb5 commit 2ec76b6
Show file tree
Hide file tree
Showing 7 changed files with 223 additions and 46 deletions.
34 changes: 33 additions & 1 deletion README.md
Expand Up @@ -36,6 +36,7 @@ projectmpl
- [Boolean/Confirmation](#booleanconfirmation) - [Boolean/Confirmation](#booleanconfirmation)
- [Other options and help](#other-options-and-help) - [Other options and help](#other-options-and-help)
- [Validation](#validation) - [Validation](#validation)
- [Sub Variables](#sub-variables)
- [Standard `.projectmpl.yml` files](#standard-projectmplyml-files) - [Standard `.projectmpl.yml` files](#standard-projectmplyml-files)
- [Per-file configuration](#per-file-configuration) - [Per-file configuration](#per-file-configuration)
- [Conditional Rendering/Copy](#conditional-renderingcopy) - [Conditional Rendering/Copy](#conditional-renderingcopy)
Expand Down Expand Up @@ -71,9 +72,12 @@ new project.
Need a different behavior or additional variables in a specific directory? Need a different behavior or additional variables in a specific directory?
Just add another `.projectmpl.yml` file in there. You can even overwrite Just add another `.projectmpl.yml` file in there. You can even overwrite
variables. variables.
- **Conditional prompts (sub-variables)**
Each variable can have its own subset of variables which will only be
prompted to the user if the parent variable is filled or set to true.
- **Customizable templates** - **Customizable templates**
Projectmpl allows fine-grained control over what needs to be done when Projectmpl allows fine-grained control over what needs to be done when
rendering the template. Just copy the file, ignore it, add conditonals based rendering the template. Just copy the file, ignore it, add conditionals based
on what the user answered, change the template delimiters… on what the user answered, change the template delimiters…
- **After render commands** - **After render commands**
Projectmpl allows you to define commands to be run once the boilerplate has Projectmpl allows you to define commands to be run once the boilerplate has
Expand Down Expand Up @@ -240,6 +244,34 @@ This will prevent the user from rendering your template with missing variables.
Note that if you specified a default value for an input, it becomes impossible Note that if you specified a default value for an input, it becomes impossible
to not fill in that value. So the validator becomes obsolete. to not fill in that value. So the validator becomes obsolete.


### Sub Variables

It's not uncommon to ask for additional information when the user answered yes
or filled in a variable. Thus, each variable can have its own variables:

```yaml
variables:
slack:
confirm: true
prompt: "Add Slack integration?"
variables:
channel:
required: true
prompt: "In which Slack channel should the result be posted?"
webhook:
required: true
help: "See https://api.slack.com/incoming-webhooks for more information"
prompt: "Provide the Slack webhook URL:"
```

In the example above we ask the user if he wants a Slack integration. If he
answers yes to that, then we'll ask him about the Slack channel and the webhook
URL. Otherwise we won't bother him with these details since they won't be used
in our template rendering.

The sub variables can be accessed in your templates with the form `.parent_sub`.
In this case, `.slack_channel` and `.slack_webhook`.

## Standard `.projectmpl.yml` files ## Standard `.projectmpl.yml` files


If you place a `.projectmpl.yml` file in a sub-directory of your template, this If you place a `.projectmpl.yml` file in a sub-directory of your template, this
Expand Down
80 changes: 80 additions & 0 deletions _example/drone/.drone.yml
@@ -0,0 +1,80 @@
workspace:
base: /go
path: src/[[ .repo ]]

[[ if .tags -]]
clone:
git:
image: plugins/git
tags: true
[[- end ]]

pipeline:
[[ if not .gomodules -]]
prerequisites:
image: "golang:[[ .goversion ]]"
commands:
- go version
- go get -u github.com/golang/dep/cmd/dep
- dep ensure -vendor-only
environment:
- GO111MODULE=off
[[- end ]]
[[ if .linter -]]
linter:
image: "golang:[[ .goversion ]]"
commands:
- go get -u github.com/golangci/golangci-lint/cmd/golangci-lint
- golangci-lint run
[[ if not .gomodules -]]
environment:
- GO111MODULE=off
[[- end ]]
[[- end ]]
test:
image: "golang:[[ .goversion ]]"
commands:
- go test -cover -failfast ./...
[[ if not .gomodules -]]
environment:
- GO111MODULE=off
[[- end ]]
[[ if .slack -]]
slack:
image: plugins/slack
channel: [[ .slack_channel ]]
webhook: [[ .slack_webhook ]]
username: Notification
template: >
{{#success build.status}}
<{{build.link}}|Build {{build.number}}> by {{build.author}} succeeded in {{since build.started}}
Version {{#if build.tag}}`{{build.tag}}`{{else}}`latest`{{/if}} deployed.
`chatbot-backend:{{build.commit}}`
{{else}}
<{{build.link}}|Build {{build.number}}> by {{build.author}} failed in {{since build.started}}
{{/success}}
when:
status: [ success, failure ]
event: [ tag, push ]
branch: master
local: false

slack:
image: plugins/slack
channel: [[ .slack_channel ]]
webhook: [[ .slack_webhook ]]
username: Notification
template: >
{{#success build.status}}
<{{build.link}}|Build {{build.number}}> on branch `{{build.branch}}` by {{build.author}} succeeded in {{since build.started}}
{{else}}
<{{build.link}}|Build {{build.number}}> on branch `{{build.branch}}` by {{build.author}} failed in {{since build.started}}
{{/success}}
when:
status: [ success, failure ]
branch:
exclude: master
local: false
[[- end ]]
37 changes: 37 additions & 0 deletions _example/drone/.projectmpl.yml
@@ -0,0 +1,37 @@
name: "Drone Template"
version: "0.1.0"
description: "Add a .drone.yml file to your project"
delimiters: ["[[", "]]"]
variables:
goversion:
default: "latest"
prompt: "Which go version should be used?"
repo:
required: true
prompt: "What's the full path of your project?"
help: "Full path of your repo or go package, for example 'github.com/Depado/projectmpl'"
gomodules:
confirm: false
prompt: "Use gomodules instead of dep?"
linter:
confirm: true
prompt: "Use golangci-lint as the main linter?"
slack:
confirm: true
prompt: "Add Slack integration?"
variables:
channel:
required: true
help: "Channel in which the build result should be posted. Should start with a # to work properly"
prompt: "Slack channel:"
webhook:
required: true
help: "See https://api.slack.com/incoming-webhooks for more information"
prompt: "Provide the Slack webhook URL:"
username:
default: "Build Notification"
prompt: "Username the integration will use to post in the channel:"
tags:
confirm: true
prompt: "Clone with tags?"

33 changes: 5 additions & 28 deletions conf/conf.go
Expand Up @@ -4,41 +4,18 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"sort"


yaml "gopkg.in/yaml.v2" yaml "gopkg.in/yaml.v2"
) )


// Config is a configuration that can be applied to a single file (inline conf) // Config is a configuration that can be applied to a single file (inline conf)
// or to an entire directory // or to an entire directory
type Config struct { type Config struct {
Delimiters []string `yaml:"delimiters"` Delimiters []string `yaml:"delimiters"`
Copy *bool `yaml:"copy"` Copy *bool `yaml:"copy"`
Ignore *bool `yaml:"ignore"` Ignore *bool `yaml:"ignore"`
Variables map[string]*Variable `yaml:"variables"` Variables Variables `yaml:"variables"`
If string `yaml:"if"` If string `yaml:"if"`
}

// PromptVariables will prompt the user for the different variables in the file
func (c *Config) PromptVariables() {
// Order the variables alphabetically to keep the same order
var ordered []*Variable
// ordered := make([]*Variable, len(c.Variables))
for k, v := range c.Variables {
if v == nil { // Unconfigured values do have a key but no value
v = &Variable{Name: k}
} else {
v.Name = k
}
ordered = append(ordered, v)
}
sort.Slice(ordered, func(i, j int) bool {
return ordered[i].Name < ordered[j].Name
})

for _, variable := range ordered {
variable.Prompt()
}
} }


// ConfigFile is the combination of File and Config // ConfigFile is the combination of File and Config
Expand Down
15 changes: 3 additions & 12 deletions conf/file.go
Expand Up @@ -83,7 +83,7 @@ func (f *File) ParseFrontMatter() error {
f.Metadata = &r f.Metadata = &r
if f.Metadata.Variables != nil && len(f.Metadata.Variables) > 0 { if f.Metadata.Variables != nil && len(f.Metadata.Variables) > 0 {
utils.OkPrintln("Variables for single file", color.YellowString(f.Path)) utils.OkPrintln("Variables for single file", color.YellowString(f.Path))
f.Metadata.PromptVariables() f.Metadata.Variables.Prompt()
} }
return nil return nil
} }
Expand Down Expand Up @@ -173,10 +173,9 @@ func (f *File) Render() error {
var condition string var condition string
var copy bool var copy bool
var ignore bool var ignore bool
var ctx map[string]interface{}


delims := []string{"{{", "}}"} delims := []string{"{{", "}}"}
ctx := make(map[string]interface{})

for i := len(f.Renderers) - 1; i >= 0; i-- { for i := len(f.Renderers) - 1; i >= 0; i-- {
r := f.Renderers[i] r := f.Renderers[i]
if r.Copy != nil { if r.Copy != nil {
Expand All @@ -185,15 +184,7 @@ func (f *File) Render() error {
if r.Ignore != nil { if r.Ignore != nil {
ignore = *r.Ignore ignore = *r.Ignore
} }
for k, v := range r.Variables { ctx = r.Variables.Ctx()
if v != nil {
if v.Confirm != nil {
ctx[k] = *v.Confirm
} else {
ctx[k] = v.Result
}
}
}
if r.Delimiters != nil { if r.Delimiters != nil {
if len(r.Delimiters) != 2 { if len(r.Delimiters) != 2 {
return fmt.Errorf("Delimiters should be an array of two string") return fmt.Errorf("Delimiters should be an array of two string")
Expand Down
64 changes: 61 additions & 3 deletions conf/variables.go
Expand Up @@ -2,13 +2,62 @@ package conf


import ( import (
"fmt" "fmt"
"sort"


"github.com/Depado/projectmpl/utils"
"github.com/fatih/color" "github.com/fatih/color"
survey "gopkg.in/AlecAivazis/survey.v1" survey "gopkg.in/AlecAivazis/survey.v1"

"github.com/Depado/projectmpl/utils"
) )


// Variables represents a map of variable
type Variables map[string]*Variable

// Prompt will prompt the variables
func (vv Variables) Prompt() {
// Order the variables alphabetically to keep the same order
var ordered []*Variable
for k, v := range vv {
if v == nil { // Unconfigured values do have a key but no value
v = &Variable{Name: k}
} else {
v.Name = k
}
ordered = append(ordered, v)
}
sort.Slice(ordered, func(i, j int) bool {
return ordered[i].Name < ordered[j].Name
})

for _, variable := range ordered {
variable.Prompt()
}
}

// Ctx generates the context from the variables
func (vv Variables) Ctx() map[string]interface{} {
ctx := make(map[string]interface{})
for k, v := range vv {
if v != nil {
if v.Confirm != nil {
ctx[k] = *v.Confirm
} else {
ctx[k] = v.Result
}
}
if v.Variables != nil {
v.Variables.AddToCtx(k, ctx)
}
}
return ctx
}

// AddToCtx will add the variable results to a sub-key
func (vv Variables) AddToCtx(key string, ctx map[string]interface{}) {
for k, v := range vv.Ctx() {
ctx[key+"_"+k] = v
}
}

// Variable represents a single variable // Variable represents a single variable
type Variable struct { type Variable struct {
// Default value to display to the user for input prompts // Default value to display to the user for input prompts
Expand All @@ -29,12 +78,18 @@ type Variable struct {


// Confirm is used both for default variable and to store the result. // Confirm is used both for default variable and to store the result.
// If this field isn't nil, then a confirmation survey is used. // If this field isn't nil, then a confirmation survey is used.
Confirm *bool `yaml:"confirm,omitempty"` Confirm *bool `yaml:"confirm,omitempty"`
Variables Variables `yaml:"variables,omitempty"`


Result string Result string
Name string Name string
} }


// True returns if the variable has been filled
func (v *Variable) True() bool {
return v.Result != "" || v.Confirm != nil && *v.Confirm
}

// Prompt prompts for the variable // Prompt prompts for the variable
func (v *Variable) Prompt() { func (v *Variable) Prompt() {
var prompt survey.Prompt var prompt survey.Prompt
Expand Down Expand Up @@ -77,4 +132,7 @@ func (v *Variable) Prompt() {
if err := survey.AskOne(prompt, out, validator); err != nil { if err := survey.AskOne(prompt, out, validator); err != nil {
utils.FatalPrintln("Couldn't get an answer:", err) utils.FatalPrintln("Couldn't get an answer:", err)
} }
if v.True() && v.Variables != nil {
v.Variables.Prompt()
}
} }
6 changes: 4 additions & 2 deletions renderer/analyze.go
Expand Up @@ -41,7 +41,7 @@ func HandleRootConfig(dir string) *conf.Root {
if root.Description != "" { if root.Description != "" {
utils.OkPrintln(color.CyanString(root.Description)) utils.OkPrintln(color.CyanString(root.Description))
} }
root.PromptVariables() root.Variables.Prompt()
return root return root
} }


Expand All @@ -66,7 +66,9 @@ func Analyze(dir string) {
if err := cf.Parse(); err != nil { if err := cf.Parse(); err != nil {
utils.FatalPrintln("Couldn't parse configuration:", err) utils.FatalPrintln("Couldn't parse configuration:", err)
} }
cf.PromptVariables() if cf.Variables != nil {
cf.Variables.Prompt()
}
} }
return nil return nil
}) })
Expand Down

0 comments on commit 2ec76b6

Please sign in to comment.