Skip to content

Commit

Permalink
- Breaking: Only allowing Select options (v1) to be specified once.
Browse files Browse the repository at this point in the history
- New: Select options v2.
  • Loading branch information
kristofferahl committed Apr 23, 2023
1 parent 5cc7524 commit 1a20b8f
Show file tree
Hide file tree
Showing 14 changed files with 330 additions and 71 deletions.
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"mode": "auto",
"program": "${workspaceFolder}/cmd/centry/",
"cwd": "${workspaceFolder}",
"args": ["--centry-file", "./examples/centry/centry.yaml", "--prod", "get", "env"]
"args": ["--centry-file", "./examples/centry/centry.yaml", "--dev", "get", "env"]
}
]
}
14 changes: 14 additions & 0 deletions cmd/centry/flag.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package main

import (
"github.com/kristofferahl/go-centry/internal/pkg/cmd"
"github.com/urfave/cli/v2"
)

type SelectOptionFlag struct {
cli.BoolFlag

GroupName string
GroupRequired bool
Values []cmd.OptionValue
}
95 changes: 87 additions & 8 deletions cmd/centry/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"strings"

"github.com/kristofferahl/go-centry/internal/pkg/cmd"
"github.com/kristofferahl/go-centry/internal/pkg/config"
"github.com/kristofferahl/go-centry/internal/pkg/shell"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
Expand Down Expand Up @@ -61,6 +62,7 @@ func createGlobalOptions(runtime *Runtime) *cmd.OptionsSet {
Short: o.Short,
Description: o.Description,
EnvName: o.EnvName,
Values: mapOptionValuesToCmdOptionValues(o),
Default: o.Default,
Required: o.Required,
Hidden: o.Hidden,
Expand Down Expand Up @@ -100,6 +102,34 @@ func optionsSetToFlags(options *cmd.OptionsSet) []cli.Flag {
Required: false,
Hidden: o.Hidden,
})
case cmd.SelectOptionV2:
def := false
if o.Default != nil {
def = o.Default.(bool)
}
for _, v := range o.Values {
short := []string{v.Short}
if v.Short == "" {
short = nil
}
value := v.Value
if value == "" {
value = v.Name
}
flags = append(flags, &SelectOptionFlag{
BoolFlag: cli.BoolFlag{
Name: v.Name,
Aliases: short,
Usage: fmt.Sprintf("%s (%s=%s)", o.Description, o.Name, value),
Value: def,
Required: false,
Hidden: o.Hidden,
},
GroupName: o.Name,
GroupRequired: o.Required,
Values: o.Values,
})
}
case cmd.IntegerOption:
def := 0
if o.Default != nil {
Expand Down Expand Up @@ -192,6 +222,26 @@ func optionsSetToEnvVars(c *cli.Context, set *cmd.OptionsSet, prefix string) []s
Type: shell.EnvironmentVariableTypeString,
})
}
case cmd.SelectOptionV2:
value := ""
for _, v := range o.Values {
ov := c.String(v.Name)
if ov == "true" {
value = v.Value
if value == "" {
value = v.Name
}
break
}
}

if value != "" {
envVars = append(envVars, shell.EnvironmentVariable{
Name: envName,
Value: value,
Type: shell.EnvironmentVariableTypeString,
})
}
default:
panic(fmt.Sprintf("option type \"%s\" not implemented", o.Type))
}
Expand All @@ -200,41 +250,70 @@ func optionsSetToEnvVars(c *cli.Context, set *cmd.OptionsSet, prefix string) []s
return shell.SortEnvironmentVariables(envVars)
}

func mapOptionValuesToCmdOptionValues(o config.Option) []cmd.OptionValue {
values := []cmd.OptionValue{}
for _, v := range o.Values {
values = append(values, cmd.OptionValue{
Name: v.Name,
Short: v.Short,
Value: v.Value,
})
}
return values
}

func validateOptionsSet(c *cli.Context, set *cmd.OptionsSet, cmdName string, level string, log *logrus.Entry) error {
selectOptions := make(map[string][]string)
selectOptionRequired := make(map[string]bool)
selectOptionSelected := make(map[string]string)
selectOptionSelectedValues := make(map[string][]string)

for _, o := range set.Sorted() {
o := o

switch o.Type {
case cmd.SelectOption:
group := o.EnvName
selectOptions[o.EnvName] = append(selectOptions[group], o.Name)
selectOptions[group] = append(selectOptions[group], o.Name)
if o.Required {
selectOptionRequired[group] = true
}
v := c.String(o.Name)
log.Debugf("found select option %s (group=%s value=%v required=%v)\n", o.Name, group, v, o.Required)
if v == "true" {
selectOptionSelected[group] = o.Name
selectOptionSelectedValues[group] = append(selectOptionSelectedValues[group], o.Name)
}
case cmd.SelectOptionV2:
group := o.Name
if o.Required {
selectOptionRequired[group] = true
}
for _, ov := range o.Values {
selectOptions[group] = append(selectOptions[group], ov.Name)
v := c.String(ov.Name)
log.Debugf("found select option %s (group=%s value=%v required=%v)\n", ov.Name, group, v, o.Required)
if v == "true" {
selectOptionSelectedValues[group] = append(selectOptionSelectedValues[group], ov.Name)
}
}
break
}
}

for group, _ := range selectOptions {
for group := range selectOptions {
if selectOptionRequired[group] {
if option, ok := selectOptionSelected[group]; ok && option != "" {
log.Debugf("select option group %s was set by option %s", group, option)
optionValues, ok := selectOptionSelectedValues[group]
if ok && optionValues[0] != "" {
log.Debugf("select option group %s was set by option %s", group, optionValues[0])
} else {
cli.ShowCommandHelp(c, cmdName)
return fmt.Errorf("Required %s flag missing for select option group %s (one of \" %s \" must be provided)\n", level, group, strings.Join(selectOptions[group], " | "))
return fmt.Errorf("Required %s flag missing for select option group \"%s\" (one of \" %s \" must be provided)\n", level, group, strings.Join(selectOptions[group], " | "))
}
} else {
log.Debugf("select option group %s does not require a value", group)
}

if optionValues, ok := selectOptionSelectedValues[group]; ok && len(optionValues) > 1 {
return fmt.Errorf("%s flag specified multiple times for select option group \"%s\" (one of \" %s \" must be provided)\n", level, group, strings.Join(selectOptions[group], " | "))
}
}
return nil
}
55 changes: 34 additions & 21 deletions cmd/centry/runtime_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,20 +156,20 @@ func TestMain(t *testing.T) {
g.Describe("invoking command with options", func() {
g.It("should have arguments passed", func() {
expected := "command args (foo bar baz)"
out := execQuiet("commandtest options args --cmdstringopt=hello --cmdboolopt --cmdsel1 --cmdsel2 foo bar baz")
out := execQuiet("commandtest options args --cmdstringopt=hello --cmdboolopt --cmdsel2 foo bar baz")
test.AssertStringContains(g, out.Stdout, expected)
})

g.It("should have environment variables set", func() {
out := execQuiet("commandtest options printenv --cmdstringopt=world --cmdboolopt --cmdsel1 --cmdsel2 --dashed-opt dashed-val")
out := execQuiet("commandtest options printenv --cmdstringopt=world --cmdboolopt --cmdsel2 --dashed-opt dashed-val")
test.AssertStringHasKeyValue(g, out.Stdout, "CMDSTRINGOPT", "world")
test.AssertStringHasKeyValue(g, out.Stdout, "CMDBOOLOPT", "true")
test.AssertStringHasKeyValue(g, out.Stdout, "CMDSELECTOPT", "cmdsel2")
test.AssertStringHasKeyValue(g, out.Stdout, "DASHED_OPT", "dashed-val")
})

g.It("should hav prefixed environment variables set", func() {
out := execCentry("commandtest options printenv --cmdstringopt=world --cmdboolopt --cmdsel1 --cmdsel2", true, "test/data/runtime_test_environment_prefix.yaml")
out := execCentry("commandtest options printenv --cmdstringopt=world --cmdboolopt --cmdsel2", true, "test/data/runtime_test_environment_prefix.yaml")
test.AssertStringHasKeyValue(g, out.Stdout, "ENV_PREFIX_CMDSTRINGOPT", "world")
test.AssertStringHasKeyValue(g, out.Stdout, "ENV_PREFIX_CMDBOOLOPT", "true")
test.AssertStringHasKeyValue(g, out.Stdout, "ENV_PREFIX_CMDSELECTOPT", "cmdsel2")
Expand Down Expand Up @@ -206,20 +206,14 @@ func TestMain(t *testing.T) {
test.AssertStringContains(g, out.Stdout, expected)
})

g.It("should have environment set for select option", func() {
g.It("should have environment set for select option (v1)", func() {
out := execQuiet("--selectopt1 optiontest printenv")
test.AssertStringHasKeyValue(g, out.Stdout, "SELECTOPT", "selectopt1")
})

g.It("should have environment set to last select option with same env_name (selectopt2)", func() {
out := execQuiet("--selectopt1 --selectopt2 optiontest printenv")
test.AssertStringHasKeyValue(g, out.Stdout, "SELECTOPT", "selectopt2")
})

// TODO: Do we really need =false??
g.It("should have environment set to last select option with same env_name (selectopt1)", func() {
out := execQuiet("--selectopt2=false --selectopt1 optiontest printenv")
test.AssertStringHasKeyValue(g, out.Stdout, "SELECTOPT", "selectopt1")
g.It("should have environment set for select option (v2)", func() {
out := execQuiet("--opt1 optiontest printenv")
test.AssertStringHasKeyValue(g, out.Stdout, "SELECTOPTV2", "value1")
})
})

Expand All @@ -240,48 +234,65 @@ func TestMain(t *testing.T) {
})

g.Describe("invoke with invalid option", func() {
g.It("should print error message", func() {
g.It("should fail with error message", func() {
out := execQuiet("--invalidoption optiontest args")
test.AssertStringContains(g, out.Stdout, "Incorrect Usage. flag provided but not defined: -invalidoption")
test.AssertStringContains(g, out.Stderr, "flag provided but not defined: -invalidoption")
test.AssertNoError(g, out.Error)
})

g.It("should fail with error when specifying multiple options for the same select option group (v1)", func() {
out := execCentry("--selectopt1 --selectopt2 optiontest args", false, "test/data/runtime_test.yaml")
test.AssertStringContains(g, out.Stderr, "level=error msg=\"global flag specified multiple times for select option group \\\"SELECTOPT\\\" (one of \\\" selectopt1 | selectopt2 \\\" must be provided)")
})

g.It("should fail with error when specifying multiple options for the same select option group (v2)", func() {
out := execCentry("--opt1 --opt2 optiontest args", false, "test/data/runtime_test.yaml")
test.AssertStringContains(g, out.Stderr, "level=error msg=\"global flag specified multiple times for select option group \\\"selectoptv2\\\" (one of \\\" opt1 | opt2 \\\" must be provided)")
})
})

g.Describe("invoke without required option", func() {
g.Describe("of type string", func() {
g.It("should fail with error message", func() {
out := execCentry("optiontest required --boolopt --intopt=999 --selectopt1", false, "test/data/runtime_test.yaml")
out := execCentry("optiontest required --boolopt --intopt=999 --selectopt1 --selectopt_v2_1", false, "test/data/runtime_test.yaml")
test.AssertStringContains(g, out.Stderr, "level=error msg=\"Required flag \\\"stringopt\\\" not set\"")
})
})
g.Describe("of type bool", func() {
g.It("should fail with error message", func() {
out := execCentry("optiontest required --stringopt=foo --intopt=999 --selectopt1", false, "test/data/runtime_test.yaml")
out := execCentry("optiontest required --stringopt=foo --intopt=999 --selectopt1 --selectopt_v2_1", false, "test/data/runtime_test.yaml")
test.AssertStringContains(g, out.Stderr, "level=error msg=\"Required flag \\\"boolopt\\\" not set\"")
})
})
g.Describe("of type integer", func() {
g.It("should fail with error message", func() {
out := execCentry("optiontest required --stringopt=foo --boolopt --selectopt1", false, "test/data/runtime_test.yaml")
out := execCentry("optiontest required --stringopt=foo --boolopt --selectopt1 --selectopt_v2_1", false, "test/data/runtime_test.yaml")
test.AssertStringContains(g, out.Stderr, "level=error msg=\"Required flag \\\"intopt\\\" not set\"")
})
})
g.Describe("of type select", func() {
g.It("should fail with error message", func() {
out := execCentry("optiontest required --stringopt=foo --boolopt --intopt=999", false, "test/data/runtime_test.yaml")
test.AssertStringContains(g, out.Stderr, "level=error msg=\"Required command flag missing for select option group SELECT (one of \\\" selectopt1 | selectopt2 \\\" must be provided)")
out := execCentry("optiontest required --stringopt=foo --boolopt --intopt=999 --selectopt_v2_1", false, "test/data/runtime_test.yaml")
test.AssertStringContains(g, out.Stderr, "level=error msg=\"Required command flag missing for select option group \\\"SELECTOPTV1\\\" (one of \\\" selectopt1 | selectopt2 \\\" must be provided)")
})
})
g.Describe("of type select/v2", func() {
g.It("should fail with error message", func() {
out := execCentry("optiontest required --stringopt=foo --boolopt --intopt=999 --selectopt1", false, "test/data/runtime_test.yaml")
test.AssertStringContains(g, out.Stderr, "level=error msg=\"Required command flag missing for select option group \\\"selectoptv2\\\" (one of \\\" selectopt_v2_1 | selectopt_v2_2 \\\" must be provided)")
})
})
})

g.Describe("invoke with required option", func() {
g.It("should pass", func() {
out := execCentry("optiontest required --stringopt=foo --boolopt --intopt=111 --selectopt1", false, "test/data/runtime_test.yaml")
out := execCentry("optiontest required --stringopt=foo --boolopt --intopt=111 --selectopt1 --selectopt_v2_1", false, "test/data/runtime_test.yaml")
test.AssertStringHasKeyValue(g, out.Stdout, "STRINGOPT", "foo")
test.AssertStringHasKeyValue(g, out.Stdout, "BOOLOPT", "true")
test.AssertStringHasKeyValue(g, out.Stdout, "INTOPT", "111")
test.AssertStringHasKeyValue(g, out.Stdout, "SELECTOPT", "selectopt1")
test.AssertStringHasKeyValue(g, out.Stdout, "SELECTOPTV1", "selectopt1")
test.AssertStringHasKeyValue(g, out.Stdout, "SELECTOPTV2", "selectopt_v2_1")
})
})
})
Expand Down Expand Up @@ -395,6 +406,8 @@ func TestMain(t *testing.T) {
--intopt value, -I value A custom option (default: 0)
--selectopt1 Sets the selection to option 1 (default: false)
--selectopt2 Sets the selection to option 2 (default: false)
--opt1, --o1 Sets the selection (selectoptv2=value1) (default: false)
--opt2, --o2 Sets the selection (selectoptv2=value2) (default: false)
--stringopt value, -S value A custom option (default: "foobar")
--help, -h Show help (default: false)
--version, -v Print the version (default: false)`
Expand Down
41 changes: 16 additions & 25 deletions examples/centry/centry.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,33 +38,24 @@ options:
type: bool
description: Run commands without logo

- name: ops
type: select
- name: context
type: select/v2
env_name: CONTEXT
required: true
description: Set the context to ops (operations)
annotations:
centry.api/serve: "true"

- name: dev
type: select
env_name: CONTEXT
required: true
description: Set the context to dev (development)
annotations:
centry.api/serve: "false"

- name: qa
type: select
env_name: CONTEXT
required: true
description: Set the context to qa (quality assurance)

- name: prod
type: select
required: true
env_name: CONTEXT
description: Set the context to prod (production)
description: Set the context for execution
values:
- name: operations
short: ops
value: ops
- name: development
short: dev
value: dev
- name: qualityassurance
short: qa
value: qa
- name: production
short: prod
value: prod

- name: asc
type: select
Expand Down
13 changes: 12 additions & 1 deletion examples/centry/commands/get.sh
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,21 @@ get:required() {
# centry.cmd[get:selected].option[def]/envName=SELECTED
# centry.cmd[get:selected].option[def]/required=true
get:selected() {
echo "The selected value was ${SELECTED:?}"
echo "The selected value was ${SELECTED:?} (select v1)"
}

# centry.cmd[get:selectedv2].option[selected]/type=select/v2
# centry.cmd[get:selectedv2].option[selected]/envName=SELECTED
# centry.cmd[get:selectedv2].option[selected]/required=true
# centry.cmd[get:selectedv2].option[selected]/values=[{"name":"abc","short":"a","value":"val1"},{"name":"def","short":"d","value":"val2"}]
get:selectedv2() {
echo "The selected value was ${SELECTED:?} (select v2)"
}

# centry.cmd[get:url]/description=Call a url
# centry.cmd[get:url].option[url]/description=URL to call
# centry.cmd[get:url].option[url]/required=true
# centry.cmd[get:url].option[max-retries]/description=Maximum number of retries
# centry.cmd[get:url].option[max-retries]/type=integer
# centry.cmd[get:url].option[max-retries]/default=3
get:url() {
Expand Down
Loading

0 comments on commit 1a20b8f

Please sign in to comment.