From cf9dc27b7c5ccf6ef6ee78c949f42edb73575acc Mon Sep 17 00:00:00 2001 From: Viacheslav Isaev Date: Wed, 4 Oct 2023 17:36:20 +0200 Subject: [PATCH] Adding select-promter. --- Makefile | 3 +- README.md | 39 +++++++++++++++++++++--- inputs/cli_test.go | 21 +++++++++++++ inputs/input-spec.go | 7 +++-- inputs/input-spec_test.go | 15 +++++++--- inputs/prompter.go | 2 ++ inputs/prompter_test.go | 1 + inputs/select-promter.go | 56 +++++++++++++++++++++++++++++++++++ inputs/select-promter_test.go | 17 +++++++++++ main.go | 12 ++++++++ transformations.yml | 19 ++++++++++++ 11 files changed, 180 insertions(+), 12 deletions(-) create mode 100644 inputs/select-promter.go create mode 100644 inputs/select-promter_test.go diff --git a/Makefile b/Makefile index a2b6511..ec3c005 100644 --- a/Makefile +++ b/Makefile @@ -32,7 +32,8 @@ test-template: cleanup-test-dir -- \ --ProjectName my-go-project \ --IncludeReadme no \ - --ProjectDescription "bla bla" + --ProjectDescription "bla bla" \ + --ExampleType simple cd $(TMP_DIR) &&\ make diff --git a/README.md b/README.md index ce40729..3f00945 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,10 @@ See the file transformations.yml in this very project as an example. It is common to request user inputs in order to apply a set of transformations. For example you might want to request for the project name, description, whether to include this or that functionality. -There are two type of inputs: `text` and `yesno`. Text provide simple, single-line text inputs. While yesno provides for a boolean [y/N] question. +There are two type of inputs: `text`, `yesno` and `select`. +- `text` provides simple, single-line text inputs. +- `yesno` originates a boolean [y/N] question. +- `select` allows to select from a list of predefined options. Example: @@ -72,6 +75,12 @@ inputs: - id: IncludeReadme text: Would you like to include the readme file? type: yesno + - id: ExampleType + text: Select example type + type: select + options: + - simple + - advanced ``` The `id` must be unique and is later also used for performing the transformations (see below). @@ -91,11 +100,12 @@ go-archetype transform --transformations=transformations.yml \ --source=. \ --destination=.tmp/go/my-go-project \ -- \ - --ProjectName my-go-project - --IncludeReadme yes + --ProjectName my-go-project \ + --IncludeReadme yes \ + --ExampleType simple ``` -In this example there are two inputs: `ProjectName` and `IncludeReadme`. +In this example there are following inputs: `ProjectName`, `IncludeReadme` and `ExampleType`. To seperate program args from user input we use `--`. After the `--` the list of user inputs is provided. @@ -361,6 +371,27 @@ replacement: "{{ .name | snakecase | camelcase | swapcase | title | swapcase }}" Yeah it works... +#### Inclusion By Selection +Select allows to select one of the options and include section depends on selected choice. + +```yml + - name: simple + type: include + region_marker: __SIMPLE__ + condition: "eq .ExampleType \"simple\"" + files: ["main.go"] + - name: medium + type: include + region_marker: __MEDIUM__ + condition: "eq .ExampleType \"medium\"" + files: ["main.go"] + - name: advanced + type: include + region_marker: __ADVANCED__ + condition: "eq .ExampleType \"advanced\"" + files: ["main.go"] +``` + ## Order of execution Transformatinos are executed by the order they appear inside the transformations.yml file. The output of the first transformation is then piped into the input of the second transformation and so forth. diff --git a/inputs/cli_test.go b/inputs/cli_test.go index fb36063..6f1b87d 100644 --- a/inputs/cli_test.go +++ b/inputs/cli_test.go @@ -53,3 +53,24 @@ func TestParseCLIArgsInputs(t *testing.T) { assert.True(p.Answered) assert.Equal("y", p.Answer) } + +func TestParseSelectCLIArgsInputs(t *testing.T) { + assert := assert.New(t) + p := newSimpleTextPrompter(InputSpec{ + Type: "select", + ID: "s", + Options: []string{"simple", "advanced"}, + }) + + err := ParseCLIArgsInputs(&mockInputsCollector{prompters: []Prompter{p}}, []string{}) + require.NoError(t, err) + assert.False(p.Answered) + + err = ParseCLIArgsInputs(&mockInputsCollector{prompters: []Prompter{p}}, []string{"--s", "simple"}) + require.NoError(t, err) + + err = ParseCLIArgsInputs(&mockInputsCollector{prompters: []Prompter{p}}, []string{"--s", "simple"}) + require.NoError(t, err) + assert.True(p.Answered) + assert.Equal("simple", p.Answer) +} diff --git a/inputs/input-spec.go b/inputs/input-spec.go index 0dc49a3..5958065 100644 --- a/inputs/input-spec.go +++ b/inputs/input-spec.go @@ -1,9 +1,10 @@ package inputs type InputSpec struct { - ID string `yaml:"id"` - Text string `yaml:"text"` - Type string `yaml:"type"` + ID string `yaml:"id"` + Text string `yaml:"text"` + Type string `yaml:"type"` + Options []string `yaml:"options,omitempty"` } func FromSpec(specs []InputSpec) []Prompter { diff --git a/inputs/input-spec_test.go b/inputs/input-spec_test.go index efc58c6..bd27b4e 100644 --- a/inputs/input-spec_test.go +++ b/inputs/input-spec_test.go @@ -11,18 +11,25 @@ func TestFromSpec(t *testing.T) { prompters := FromSpec([]InputSpec{ { - ID: "1", + ID: "0", Type: "text", Text: "what's your name?", }, { - ID: "2", - Type: "yesno", + ID: "1", Text: "are you sure?", + Type: "yesno", + }, + { + ID: "2", + Type: "select", + Text: "Choose value:", + Options: []string{"one", "two", "three"}, }, }) - assert.Len(prompters, 2) + assert.Len(prompters, 3) assert.IsType(&simpleTextPrompter{}, prompters[0]) assert.IsType(&yesNoPrompter{}, prompters[1]) + assert.IsType(&selectPrompter{}, prompters[2]) } diff --git a/inputs/prompter.go b/inputs/prompter.go index 0a13e32..bbdda89 100644 --- a/inputs/prompter.go +++ b/inputs/prompter.go @@ -23,6 +23,8 @@ func NewPrompt(spec InputSpec) Prompter { return newSimpleTextPrompter(spec) case "yesno": return newYesNoPrompter(spec) + case "select": + return newSelectPrompter(spec) default: panic("Unknown user input type") } diff --git a/inputs/prompter_test.go b/inputs/prompter_test.go index 00d71c3..0c526ed 100644 --- a/inputs/prompter_test.go +++ b/inputs/prompter_test.go @@ -11,4 +11,5 @@ func TestNewPrompt(t *testing.T) { assert.IsType(&yesNoPrompter{}, NewPrompt(InputSpec{Type: "yesno"})) assert.IsType(&simpleTextPrompter{}, NewPrompt(InputSpec{Type: "text"})) + assert.IsType(&selectPrompter{}, NewPrompt(InputSpec{Type: "select"})) } diff --git a/inputs/select-promter.go b/inputs/select-promter.go new file mode 100644 index 0000000..f11ed6e --- /dev/null +++ b/inputs/select-promter.go @@ -0,0 +1,56 @@ +package inputs + +import ( + "fmt" + "github.com/AlecAivazis/survey/v2" +) + +type selectPrompter struct { + PromptResponse +} + +func newSelectPrompter(spec InputSpec) *selectPrompter { + return &selectPrompter{PromptResponse: PromptResponse{InputSpec: spec}} +} + +func (p *selectPrompter) GetID() string { + return p.ID +} + +func (p *selectPrompter) Prompt() (PromptResponse, error) { + if p.Answered { + return p.PromptResponse, nil + } + + var answer string + prompt := &survey.Select{ + Message: p.Text, + Options: p.Options, + Default: p.Options[0], + } + err := survey.AskOne(prompt, &answer) + if err != nil { + return PromptResponse{}, fmt.Errorf("prompt error: %w", err) + } + return p.SetStringResponse(answer) +} + +func (p *selectPrompter) SetStringResponse(answer string) (PromptResponse, error) { + if err := p.validateAnswer(answer); err != nil { + return PromptResponse{}, err + } + p.Answer = answer + p.Answered = true + return p.PromptResponse, nil +} + +func (p *selectPrompter) validateAnswer(answer string) error { + for _, b := range p.Options { + if b == answer { + return nil + } + } + return fmt.Errorf("answer %s is not in a list %s", answer, p.Options) +} + +var _ Prompter = &selectPrompter{} diff --git a/inputs/select-promter_test.go b/inputs/select-promter_test.go new file mode 100644 index 0000000..a7c014b --- /dev/null +++ b/inputs/select-promter_test.go @@ -0,0 +1,17 @@ +package inputs + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestValidateAnswer(t *testing.T) { + assert := assert.New(t) + + p := newSelectPrompter(InputSpec{Options: []string{"simple", "advanced"}}) + + assert.Equal(nil, p.validateAnswer("simple")) + assert.Equal(nil, p.validateAnswer("advanced")) + assert.NotEqual(nil, p.validateAnswer("empty")) +} diff --git a/main.go b/main.go index e2e10f2..f6d9bf4 100644 --- a/main.go +++ b/main.go @@ -8,6 +8,18 @@ import ( // See the README.md file for specification // END __INCLUDE_README__ +// BEGIN __SIMPLE__ +// Simple section +// END __SIMPLE__ + +// BEGIN __MEDIUM__ +// Medium section +// END __MEDIUM__ + +// BEGIN __ADVANCED__ +// Advanced section +// END __ADVANCED__ + func main() { cmd.Execute() } diff --git a/transformations.yml b/transformations.yml index 61f7f1d..e85fb77 100644 --- a/transformations.yml +++ b/transformations.yml @@ -9,6 +9,10 @@ inputs: - id: ProjectDescription text: Please provide a long project description type: text + - id: ExampleType + text: Select example type + type: select + options: [ "simple", "medium", "advanced" ] before: operations: - sh: @@ -39,6 +43,21 @@ transformations: pattern: go-archetype replacement: "{{ .ProjectName }}" # Reference to an input ID; go templates syntax files: ["*.go", "**/*.go"] + - name: simple + type: include + region_marker: __SIMPLE__ + condition: eq .ExampleType "simple" + files: ["main.go"] + - name: simple + type: include + region_marker: __MEDIUM__ + condition: eq .ExampleType "medium" + files: ["main.go"] + - name: advanced + type: include + region_marker: __ADVANCED__ + condition: eq .ExampleType "advanced" + files: ["main.go"] after: operations: - sh: