From 2ec76b6a9024e4b32b3f681d6814f5dd1e525921 Mon Sep 17 00:00:00 2001 From: Paul Lhussiez Date: Tue, 23 Oct 2018 13:37:04 +0200 Subject: [PATCH] Adding sub-variables (contionals) --- README.md | 34 ++++++++++++++- _example/drone/.drone.yml | 80 ++++++++++++++++++++++++++++++++++ _example/drone/.projectmpl.yml | 37 ++++++++++++++++ conf/conf.go | 33 +++----------- conf/file.go | 15 ++----- conf/variables.go | 64 +++++++++++++++++++++++++-- renderer/analyze.go | 6 ++- 7 files changed, 223 insertions(+), 46 deletions(-) create mode 100644 _example/drone/.drone.yml create mode 100644 _example/drone/.projectmpl.yml diff --git a/README.md b/README.md index 381b3f7..1178997 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/_example/drone/.drone.yml b/_example/drone/.drone.yml new file mode 100644 index 0000000..8a17d76 --- /dev/null +++ b/_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 ]] \ No newline at end of file diff --git a/_example/drone/.projectmpl.yml b/_example/drone/.projectmpl.yml new file mode 100644 index 0000000..f140cf4 --- /dev/null +++ b/_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?" + diff --git a/conf/conf.go b/conf/conf.go index 09df365..1cc9fd2 100644 --- a/conf/conf.go +++ b/conf/conf.go @@ -4,7 +4,6 @@ import ( "io/ioutil" "os" "path/filepath" - "sort" yaml "gopkg.in/yaml.v2" ) @@ -12,33 +11,11 @@ import ( // 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 diff --git a/conf/file.go b/conf/file.go index 35a8255..c92d378 100644 --- a/conf/file.go +++ b/conf/file.go @@ -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") diff --git a/conf/variables.go b/conf/variables.go index 8601c37..c6f1be1 100644 --- a/conf/variables.go +++ b/conf/variables.go @@ -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() + } } diff --git a/renderer/analyze.go b/renderer/analyze.go index 9416476..821fceb 100644 --- a/renderer/analyze.go +++ b/renderer/analyze.go @@ -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 })