Skip to content
Permalink
Browse files

Adding sub-variables (contionals)

  • Loading branch information...
Depado committed Oct 23, 2018
1 parent a2b1fb5 commit 2ec76b6a9024e4b32b3f681d6814f5dd1e525921
Showing with 223 additions and 46 deletions.
  1. +33 −1 README.md
  2. +80 −0 _example/drone/.drone.yml
  3. +37 −0 _example/drone/.projectmpl.yml
  4. +5 −28 conf/conf.go
  5. +3 −12 conf/file.go
  6. +61 −3 conf/variables.go
  7. +4 −2 renderer/analyze.go
@@ -36,6 +36,7 @@ projectmpl
- [Boolean/Confirmation](#booleanconfirmation)
- [Other options and help](#other-options-and-help)
- [Validation](#validation)
- [Sub Variables](#sub-variables)
- [Standard `.projectmpl.yml` files](#standard-projectmplyml-files)
- [Per-file configuration](#per-file-configuration)
- [Conditional Rendering/Copy](#conditional-renderingcopy)
@@ -71,9 +72,12 @@ new project.
Need a different behavior or additional variables in a specific directory?
Just add another `.projectmpl.yml` file in there. You can even overwrite
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**
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…
- **After render commands**
Projectmpl allows you to define commands to be run once the boilerplate has
@@ -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
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

If you place a `.projectmpl.yml` file in a sub-directory of your template, this
@@ -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 ]]
@@ -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?"

@@ -4,41 +4,18 @@ import (
"io/ioutil"
"os"
"path/filepath"
"sort"

yaml "gopkg.in/yaml.v2"
)

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

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

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

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

import (
"fmt"
"sort"

"github.com/Depado/projectmpl/utils"
"github.com/fatih/color"
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
type Variable struct {
// Default value to display to the user for input prompts
@@ -29,12 +78,18 @@ type Variable struct {

// Confirm is used both for default variable and to store the result.
// 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
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
func (v *Variable) Prompt() {
var prompt survey.Prompt
@@ -77,4 +132,7 @@ func (v *Variable) Prompt() {
if err := survey.AskOne(prompt, out, validator); err != nil {
utils.FatalPrintln("Couldn't get an answer:", err)
}
if v.True() && v.Variables != nil {
v.Variables.Prompt()
}
}
@@ -41,7 +41,7 @@ func HandleRootConfig(dir string) *conf.Root {
if root.Description != "" {
utils.OkPrintln(color.CyanString(root.Description))
}
root.PromptVariables()
root.Variables.Prompt()
return root
}

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

0 comments on commit 2ec76b6

Please sign in to comment.
You can’t perform that action at this time.