Skip to content

Commit 5c565c1

Browse files
authored
Merge pull request #46 from choria-io/45
(#45) Support user supplied validators for flags and args
2 parents 6e188f7 + 5e7250b commit 5c565c1

File tree

8 files changed

+142
-4
lines changed

8 files changed

+142
-4
lines changed

Diff for: README.md

+31
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ Some historical points in time are kept:
3131
* Extended parsing for durations that include weeks (`w`, `W`), months (`M`), years (`y`, `Y`) and days (`d`, `D`) units (`v0.1.3` or newer)
3232
* More contextually useful help when using `app.MustParseWithUsage(os.Args[1:])` (`v0.1.4` or newer)
3333
* Default usage template is `CompactMainUsageTemplate` since `v0.3.0`
34+
* Support per Flag and Argument validation since `v0.6.0`
3435

3536
### UnNegatableBool
3637

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

96+
## Flag and Argument Validations
97+
98+
To support a rich validation capability without the core fisk library having to implement it all we support passing
99+
in validators that operate on the string value given by the user
100+
101+
Here is a Regular expression validator:
102+
103+
```go
104+
func RegexValidator(pattern string) OptionValidator {
105+
return func(value string) error {
106+
ok, err := regexp.MatchString(pattern, value)
107+
if err != nil {
108+
return fmt.Errorf("invalid validation pattern %q: %w", pattern, err)
109+
}
110+
111+
if !ok {
112+
return fmt.Errorf("does not validate using %q", pattern)
113+
}
114+
115+
return nil
116+
}
117+
}
118+
```
119+
120+
Use this on a Flag or Argument:
121+
122+
```go
123+
app.Flag("name", "A object name consisting of alphanumeric characters").Validator(RegexValidator("^[a-zA-Z]+$")).StringVar(&val)
124+
```
125+
95126
## External Plugins
96127

97128
Often one wants to make a CLI tool that can be extended using plugins. Think for example the `nats` CLI that is built

Diff for: app.go

+52
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,12 @@ var (
1717
envarTransformRegexp = regexp.MustCompile(`[^a-zA-Z0-9_]+`)
1818
)
1919

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

23+
// OptionValidator can be used to validate individual flags or arguments during parsing
24+
type OptionValidator func(string) error
25+
2226
// An Application contains the definitions of flags, arguments and commands
2327
// for an application.
2428
type Application struct {
@@ -238,6 +242,10 @@ func (a *Application) Parse(args []string) (command string, err error) {
238242
return "", err
239243
}
240244

245+
if err := a.validateFlagsAndArgs(context); err != nil {
246+
return "", err
247+
}
248+
241249
selected, setValuesErr = a.setValues(context)
242250

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

620+
func (a *Application) validateFlagsAndArgs(context *ParseContext) error {
621+
flagElements := map[string]*ParseElement{}
622+
for _, element := range context.Elements {
623+
if flag, ok := element.Clause.(*FlagClause); ok {
624+
if flag.validator == nil {
625+
return nil
626+
}
627+
flagElements[flag.name] = element
628+
}
629+
}
630+
631+
argElements := map[string]*ParseElement{}
632+
for _, element := range context.Elements {
633+
if arg, ok := element.Clause.(*ArgClause); ok {
634+
if arg.validator == nil {
635+
return nil
636+
}
637+
argElements[arg.name] = element
638+
}
639+
}
640+
641+
for _, flag := range flagElements {
642+
clause := flag.Clause.(*FlagClause)
643+
if clause.validator != nil {
644+
err := clause.validator(*flag.Value)
645+
if err != nil {
646+
return fmt.Errorf("%s: %w", clause.name, err)
647+
}
648+
}
649+
}
650+
651+
for _, arg := range argElements {
652+
clause := arg.Clause.(*ArgClause)
653+
if clause.validator != nil {
654+
err := clause.validator(*arg.Value)
655+
if err != nil {
656+
return fmt.Errorf("%s: %w", clause.name, err)
657+
}
658+
}
659+
}
660+
661+
return nil
662+
}
663+
612664
func (a *Application) setDefaults(context *ParseContext) error {
613665
flagElements := map[string]*ParseElement{}
614666
for _, element := range context.Elements {

Diff for: args.go

+6
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ type ArgClause struct {
7474
placeholder string
7575
hidden bool
7676
required bool
77+
validator OptionValidator
7778
}
7879

7980
func newArg(name, help string) *ArgClause {
@@ -128,6 +129,11 @@ func (a *ArgClause) Hidden() *ArgClause {
128129
return a
129130
}
130131

132+
func (a *ArgClause) Validator(validator OptionValidator) *ArgClause {
133+
a.validator = validator
134+
return a
135+
}
136+
131137
// PlaceHolder sets the place-holder string used for arg values in the help. The
132138
// default behavior is to use the arg name between < > brackets.
133139
func (a *ArgClause) PlaceHolder(value string) *ArgClause {

Diff for: flags.go

+6
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ type FlagClause struct {
158158
placeholder string
159159
hidden bool
160160
setByUser *bool
161+
validator OptionValidator
161162
}
162163

163164
func newFlag(name, help string) *FlagClause {
@@ -219,6 +220,11 @@ func (f *FlagClause) init() error {
219220
return nil
220221
}
221222

223+
func (f *FlagClause) Validator(validator OptionValidator) *FlagClause {
224+
f.validator = validator
225+
return f
226+
}
227+
222228
// Dispatch to the given function after the flag is parsed and validated.
223229
func (f *FlagClause) Action(action Action) *FlagClause {
224230
f.addAction(action)

Diff for: flags_test.go

+43
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ package fisk
22

33
import (
44
"bytes"
5+
"fmt"
56
"io"
67
"os"
8+
"regexp"
79
"testing"
810

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

396+
func TestValidator(t *testing.T) {
397+
regexpValidator := func(r string) OptionValidator {
398+
return func(v string) error {
399+
ok, err := regexp.MatchString(r, v)
400+
if err != nil {
401+
return err
402+
}
403+
404+
if !ok {
405+
return fmt.Errorf("does not validate using %q", r)
406+
}
407+
408+
return nil
409+
}
410+
}
411+
412+
app := newTestApp()
413+
414+
arg := app.Arg("arg", "A arg").Default("a").Validator(regexpValidator("^[abc]$")).String()
415+
flag := app.Flag("flag", "A flag").Validator(regexpValidator("^[xyz]$")).String()
416+
417+
_, err := app.Parse([]string{"--flag", "x"})
418+
assert.NoError(t, err)
419+
assert.Equal(t, *flag, "x")
420+
assert.Equal(t, *arg, "a")
421+
422+
*arg = ""
423+
*flag = ""
424+
_, err = app.Parse([]string{"b", "--flag", "x"})
425+
assert.NoError(t, err)
426+
assert.Equal(t, *flag, "x")
427+
assert.Equal(t, *arg, "b")
428+
429+
*arg = ""
430+
*flag = ""
431+
_, err = app.Parse([]string{"z", "--flag", "x"})
432+
assert.Error(t, err, `does not validate using "^[abc]$"`)
433+
assert.Equal(t, *flag, "")
434+
assert.Equal(t, *arg, "")
435+
}
436+
394437
func TestNegatableBool(t *testing.T) {
395438
app := newTestApp()
396439
boolFlag := app.Flag("neg", "").Bool()

Diff for: go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ go 1.19
44

55
require (
66
github.com/stretchr/testify v1.8.4
7-
golang.org/x/text v0.12.0
7+
golang.org/x/text v0.13.0
88
)
99

1010
require (

Diff for: go.sum

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
44
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
55
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
66
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
7-
golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
8-
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
7+
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
8+
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
99
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
1010
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
1111
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

Diff for: parsers.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ func (p *parserMixin) Enums(options ...string) (target *[]string) {
199199
return
200200
}
201201

202-
// EnumVar allows a value from a set of options.
202+
// EnumsVar allows a value from a set of options.
203203
func (p *parserMixin) EnumsVar(target *[]string, options ...string) {
204204
p.SetValue(newEnumsFlag(target, options...))
205205
}

0 commit comments

Comments
 (0)