Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Integer options #2

Merged
merged 3 commits into from
Sep 24, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 32 additions & 15 deletions cmd/centry/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,28 +55,22 @@ func createGlobalOptions(runtime *Runtime) *cmd.OptionsSet {
continue
}

var def interface{}

switch o.Type {
case cmd.SelectOption:
def = false
case cmd.BoolOption:
def = false
default:
def = o.Default
}

options.Add(&cmd.Option{
err := options.Add(&cmd.Option{
Type: o.Type,
Name: o.Name,
Short: o.Short,
Description: o.Description,
EnvName: o.EnvName,
Default: def,
Default: o.Default,
Required: o.Required,
Hidden: o.Hidden,
})

if err != nil {
runtime.events = append(runtime.events, fmt.Sprintf("failed to register global option \"%s\", error: %v", o.Name, err))
continue
}

runtime.events = append(runtime.events, fmt.Sprintf("registered global option \"%s\"", o.Name))
}

Expand Down Expand Up @@ -106,6 +100,19 @@ func optionsSetToFlags(options *cmd.OptionsSet) []cli.Flag {
Required: false,
Hidden: o.Hidden,
})
case cmd.IntegerOption:
def := 0
if o.Default != nil {
def = o.Default.(int)
}
flags = append(flags, &cli.IntFlag{
Name: o.Name,
Aliases: short,
Usage: o.Description,
Value: def,
Required: o.Required,
Hidden: o.Hidden,
})
case cmd.BoolOption:
def := false
if o.Default != nil {
Expand All @@ -132,6 +139,8 @@ func optionsSetToFlags(options *cmd.OptionsSet) []cli.Flag {
Required: o.Required,
Hidden: o.Hidden,
})
default:
panic(fmt.Sprintf("option type \"%s\" not implemented", o.Type))
}
}

Expand All @@ -157,17 +166,23 @@ func optionsSetToEnvVars(c *cli.Context, set *cmd.OptionsSet, prefix string) []s
value := c.String(o.Name)

switch o.Type {
case cmd.StringOption:
envVars = append(envVars, shell.EnvironmentVariable{
Name: envName,
Value: value,
Type: shell.EnvironmentVariableTypeString,
})
case cmd.BoolOption:
envVars = append(envVars, shell.EnvironmentVariable{
Name: envName,
Value: value,
Type: shell.EnvironmentVariableTypeBool,
})
case cmd.StringOption:
case cmd.IntegerOption:
envVars = append(envVars, shell.EnvironmentVariable{
Name: envName,
Value: value,
Type: shell.EnvironmentVariableTypeString,
Type: shell.EnvironmentVariableTypeInteger,
})
case cmd.SelectOption:
if value == "true" {
Expand All @@ -177,6 +192,8 @@ func optionsSetToEnvVars(c *cli.Context, set *cmd.OptionsSet, prefix string) []s
Type: shell.EnvironmentVariableTypeString,
})
}
default:
panic(fmt.Sprintf("option type \"%s\" not implemented", o.Type))
}
}

Expand Down
16 changes: 12 additions & 4 deletions cmd/centry/runtime_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -251,29 +251,36 @@ func TestMain(t *testing.T) {
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 --selectopt1", false, "test/data/runtime_test.yaml")
out := execCentry("optiontest required --boolopt --intopt=999 --selectopt1", 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 --selectopt1", false, "test/data/runtime_test.yaml")
out := execCentry("optiontest required --stringopt=foo --intopt=999 --selectopt1", 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")
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", false, "test/data/runtime_test.yaml")
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)")
})
})
})

g.Describe("invoke with required option", func() {
g.It("should pass", func() {
out := execCentry("optiontest required --stringopt=foo --boolopt --selectopt1", false, "test/data/runtime_test.yaml")
out := execCentry("optiontest required --stringopt=foo --boolopt --intopt=111 --selectopt1", 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")
})
})
Expand Down Expand Up @@ -385,6 +392,7 @@ func TestMain(t *testing.T) {
g.It("should display global options", func() {
expected := `OPTIONS:
--boolopt, -B A custom option (default: false)
--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)
--stringopt value, -S value A custom option (default: "foobar")
Expand Down
27 changes: 27 additions & 0 deletions docs/development/adding-new-flag-types.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Adding new flag types

## internal/pkg/cmd/

1. Create constant for new type in option.go (IntegerOption)
2. Add StringToOptionType test for new type and make it pass

## schema & test data

1. Add value type string to enum in schemas/manifest.json
1. Add option example to manifest_test_valid.yaml in test/data
1. Add option example to runtime_test.yaml in test/data

## optionsSetToFlags

1. Add to switch case for handling the type conversion to an cli.Flag in optionsSetToFlags()

## optionsSetToEnvVars

1. Add EnvironmentVariableType representing the type to environment.go
1. Add to switch case for handling the type conversion to an environment variable in optionsSetToEnvVars()

## required options

1. Add test case for required options in runtime_test.go ("invoke without required option")
1. Define option for "optiontest:required" in option_test.sh
1. Run tests and make it pass
37 changes: 37 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,43 @@ get:data() {
}
```

#### Integer option

Integer options can be used to pass numbers to your commands. Things like `--max-retries=5` and `--cluster-size=3` are great examples where you might want to use an integer option. Integer options have a default value of `0` but may be set to any integer value. Passing an integer option will override the default value to the value provided.

**Example**

_`// file: get.sh`_

```bash
#!/usr/bin/env bash

# centry.cmd[get:url].option[url]/required=true
# centry.cmd[get:url].option[max-retries]/type=integer
# centry.cmd[get:url].option[max-retries]/default=3
get:url() {
echo "Calling ${URL:?} a maximum of ${MAX_RETRIES:?} time(s)"
echo

local success=false
local attempts=0
until [[ ${success:?} == true ]]; do
((attempts++))

if ! curl "${URL:?}"; then
if [[ ${attempts:?} -ge ${MAX_RETRIES:?} ]]; then
echo
echo "Max retries reached... exiting!"
return 1
fi
sleep 1
else
success=true
fi
done
}
```

**Usage**: `--<option_name>` or `--<option_name>=<value>`

#### Select option
Expand Down
5 changes: 5 additions & 0 deletions examples/centry/centry.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@ options:
env_name: SORTED
description: Set's sort order to descending

- name: max-retries
type: integer
default: "3"
description: The default number of times to retry an action before failing

config:
name: centry
description: A tool for building declarative CLI's over bash scripts, written in go
Expand Down
25 changes: 25 additions & 0 deletions examples/centry/commands/get.sh
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,28 @@ get:required() {
get:selected() {
echo "The selected value was ${SELECTED:?}"
}

# centry.cmd[get:url].option[url]/required=true
# centry.cmd[get:url].option[max-retries]/type=integer
# centry.cmd[get:url].option[max-retries]/default=3
get:url() {
echo "Calling ${URL:?} a maximum of ${MAX_RETRIES:?} time(s)"
echo

local success=false
local attempts=0
until [[ ${success:?} == true ]]; do
((attempts++))

if ! curl "${URL:?}"; then
if [[ ${attempts:?} -ge ${MAX_RETRIES:?} ]]; then
echo
echo "Max retries reached... exiting!"
return 1
fi
sleep 1
else
success=true
fi
done
}
56 changes: 42 additions & 14 deletions internal/pkg/cmd/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ const StringOption OptionType = "string"
// BoolOption defines a boolean value option
const BoolOption OptionType = "bool"

// SelectOption defines a boolean select value option
// IntegerOption defines a boolean select value option
const IntegerOption OptionType = "integer"

// IntOption defines a boolean select value option
const SelectOption OptionType = "select"

// StringToOptionType returns the OptionType matching the provided string
Expand All @@ -33,6 +36,8 @@ func StringToOptionType(s string) OptionType {
return StringOption
case "bool":
return BoolOption
case "integer":
return IntegerOption
case "select":
return SelectOption
default:
Expand All @@ -51,7 +56,6 @@ type Option struct {
Hidden bool
Internal bool
Default interface{}
value valuePointer
}

// Validate returns true if the option is concidered valid
Expand All @@ -67,18 +71,6 @@ func (o *Option) Validate() error {
return nil
}

type boolValue bool

func (b *boolValue) string() string { return strconv.FormatBool(bool(*b)) }

type stringValue string

func (s *stringValue) string() string { return string(*s) }

type valuePointer interface {
string() string
}

// NewOptionsSet creates a new set of options
func NewOptionsSet(name string) *OptionsSet {
return &OptionsSet{
Expand All @@ -98,6 +90,11 @@ func (s *OptionsSet) Add(option *Option) error {
return err
}

err = convertDefaultValueToCorrectType(option)
if err != nil {
return err
}

if _, ok := s.items[option.Name]; ok {
return fmt.Errorf("an option with the name \"%s\" has already been added", option.Name)
}
Expand All @@ -123,3 +120,34 @@ func (s *OptionsSet) Sorted() []*Option {

return options
}

func convertDefaultValueToCorrectType(option *Option) error {
var def interface{}

switch option.Type {
case SelectOption:
def = false
case IntegerOption:
def = 0
switch option.Default.(type) {
case string:
if option.Default != "" {
val, err := strconv.Atoi(option.Default.(string))
if err != nil {
return err
}
def = val
}
}
case BoolOption:
def = false
case StringOption:
def = option.Default
default:
return fmt.Errorf("default value conversion not registered for type \"%s\"", option.Type)
}

option.Default = def

return nil
}
6 changes: 6 additions & 0 deletions internal/pkg/cmd/option_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,12 @@ func TestMain(t *testing.T) {
g.Assert(StringToOptionType("BOOL")).Equal(BoolOption)
})

g.It("should return IntegerOption", func() {
g.Assert(StringToOptionType("integer")).Equal(IntegerOption)
g.Assert(StringToOptionType("Integer")).Equal(IntegerOption)
g.Assert(StringToOptionType("INTEGER")).Equal(IntegerOption)
})

g.It("should return SelectOption", func() {
g.Assert(StringToOptionType("select")).Equal(SelectOption)
g.Assert(StringToOptionType("Select")).Equal(SelectOption)
Expand Down
8 changes: 4 additions & 4 deletions internal/pkg/config/schema.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading