Skip to content

Commit

Permalink
Merge pull request #46 from choria-io/45
Browse files Browse the repository at this point in the history
(#45) Support user supplied validators for flags and args
  • Loading branch information
ripienaar authored Sep 12, 2023
2 parents 6e188f7 + 5e7250b commit 5c565c1
Show file tree
Hide file tree
Showing 8 changed files with 142 additions and 4 deletions.
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Some historical points in time are kept:
* Extended parsing for durations that include weeks (`w`, `W`), months (`M`), years (`y`, `Y`) and days (`d`, `D`) units (`v0.1.3` or newer)
* More contextually useful help when using `app.MustParseWithUsage(os.Args[1:])` (`v0.1.4` or newer)
* Default usage template is `CompactMainUsageTemplate` since `v0.3.0`
* Support per Flag and Argument validation since `v0.6.0`

### UnNegatableBool

Expand Down Expand Up @@ -92,6 +93,36 @@ Available Cheats:
You can save your cheats to a directory of your choice with `nats cheat --save /some/dir`, the directory
must not already exist.

## Flag and Argument Validations

To support a rich validation capability without the core fisk library having to implement it all we support passing
in validators that operate on the string value given by the user

Here is a Regular expression validator:

```go
func RegexValidator(pattern string) OptionValidator {
return func(value string) error {
ok, err := regexp.MatchString(pattern, value)
if err != nil {
return fmt.Errorf("invalid validation pattern %q: %w", pattern, err)
}

if !ok {
return fmt.Errorf("does not validate using %q", pattern)
}

return nil
}
}
```

Use this on a Flag or Argument:

```go
app.Flag("name", "A object name consisting of alphanumeric characters").Validator(RegexValidator("^[a-zA-Z]+$")).StringVar(&val)
```

## External Plugins

Often one wants to make a CLI tool that can be extended using plugins. Think for example the `nats` CLI that is built
Expand Down
52 changes: 52 additions & 0 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,12 @@ var (
envarTransformRegexp = regexp.MustCompile(`[^a-zA-Z0-9_]+`)
)

// ApplicationValidator can be used to validate entire application during parsing
type ApplicationValidator func(*Application) error

// OptionValidator can be used to validate individual flags or arguments during parsing
type OptionValidator func(string) error

// An Application contains the definitions of flags, arguments and commands
// for an application.
type Application struct {
Expand Down Expand Up @@ -238,6 +242,10 @@ func (a *Application) Parse(args []string) (command string, err error) {
return "", err
}

if err := a.validateFlagsAndArgs(context); err != nil {
return "", err
}

selected, setValuesErr = a.setValues(context)

if err = a.applyPreActions(context, !a.completion); err != nil {
Expand Down Expand Up @@ -609,6 +617,50 @@ func (a *Application) execute(context *ParseContext, selected []string) (string,
return command, err
}

func (a *Application) validateFlagsAndArgs(context *ParseContext) error {
flagElements := map[string]*ParseElement{}
for _, element := range context.Elements {
if flag, ok := element.Clause.(*FlagClause); ok {
if flag.validator == nil {
return nil
}
flagElements[flag.name] = element
}
}

argElements := map[string]*ParseElement{}
for _, element := range context.Elements {
if arg, ok := element.Clause.(*ArgClause); ok {
if arg.validator == nil {
return nil
}
argElements[arg.name] = element
}
}

for _, flag := range flagElements {
clause := flag.Clause.(*FlagClause)
if clause.validator != nil {
err := clause.validator(*flag.Value)
if err != nil {
return fmt.Errorf("%s: %w", clause.name, err)
}
}
}

for _, arg := range argElements {
clause := arg.Clause.(*ArgClause)
if clause.validator != nil {
err := clause.validator(*arg.Value)
if err != nil {
return fmt.Errorf("%s: %w", clause.name, err)
}
}
}

return nil
}

func (a *Application) setDefaults(context *ParseContext) error {
flagElements := map[string]*ParseElement{}
for _, element := range context.Elements {
Expand Down
6 changes: 6 additions & 0 deletions args.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ type ArgClause struct {
placeholder string
hidden bool
required bool
validator OptionValidator
}

func newArg(name, help string) *ArgClause {
Expand Down Expand Up @@ -128,6 +129,11 @@ func (a *ArgClause) Hidden() *ArgClause {
return a
}

func (a *ArgClause) Validator(validator OptionValidator) *ArgClause {
a.validator = validator
return a
}

// PlaceHolder sets the place-holder string used for arg values in the help. The
// default behavior is to use the arg name between < > brackets.
func (a *ArgClause) PlaceHolder(value string) *ArgClause {
Expand Down
6 changes: 6 additions & 0 deletions flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ type FlagClause struct {
placeholder string
hidden bool
setByUser *bool
validator OptionValidator
}

func newFlag(name, help string) *FlagClause {
Expand Down Expand Up @@ -219,6 +220,11 @@ func (f *FlagClause) init() error {
return nil
}

func (f *FlagClause) Validator(validator OptionValidator) *FlagClause {
f.validator = validator
return f
}

// Dispatch to the given function after the flag is parsed and validated.
func (f *FlagClause) Action(action Action) *FlagClause {
f.addAction(action)
Expand Down
43 changes: 43 additions & 0 deletions flags_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package fisk

import (
"bytes"
"fmt"
"io"
"os"
"regexp"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -391,6 +393,47 @@ func TestIsSetByUser(t *testing.T) {
assert.False(t, isSet2)
}

func TestValidator(t *testing.T) {
regexpValidator := func(r string) OptionValidator {
return func(v string) error {
ok, err := regexp.MatchString(r, v)
if err != nil {
return err
}

if !ok {
return fmt.Errorf("does not validate using %q", r)
}

return nil
}
}

app := newTestApp()

arg := app.Arg("arg", "A arg").Default("a").Validator(regexpValidator("^[abc]$")).String()
flag := app.Flag("flag", "A flag").Validator(regexpValidator("^[xyz]$")).String()

_, err := app.Parse([]string{"--flag", "x"})
assert.NoError(t, err)
assert.Equal(t, *flag, "x")
assert.Equal(t, *arg, "a")

*arg = ""
*flag = ""
_, err = app.Parse([]string{"b", "--flag", "x"})
assert.NoError(t, err)
assert.Equal(t, *flag, "x")
assert.Equal(t, *arg, "b")

*arg = ""
*flag = ""
_, err = app.Parse([]string{"z", "--flag", "x"})
assert.Error(t, err, `does not validate using "^[abc]$"`)
assert.Equal(t, *flag, "")
assert.Equal(t, *arg, "")
}

func TestNegatableBool(t *testing.T) {
app := newTestApp()
boolFlag := app.Flag("neg", "").Bool()
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ go 1.19

require (
github.com/stretchr/testify v1.8.4
golang.org/x/text v0.12.0
golang.org/x/text v0.13.0
)

require (
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
Expand Down
2 changes: 1 addition & 1 deletion parsers.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ func (p *parserMixin) Enums(options ...string) (target *[]string) {
return
}

// EnumVar allows a value from a set of options.
// EnumsVar allows a value from a set of options.
func (p *parserMixin) EnumsVar(target *[]string, options ...string) {
p.SetValue(newEnumsFlag(target, options...))
}
Expand Down

0 comments on commit 5c565c1

Please sign in to comment.