From e2c52562d9442ed379ae7f9949b0e2fb9fc8889a Mon Sep 17 00:00:00 2001 From: Volker Schukai <volker.schukai@schukai.com> Date: Wed, 5 Oct 2022 16:25:34 +0200 Subject: [PATCH] feat: assign flags to structure --- README.md | 6 +++--- api.go | 23 ++++++++++++++++++++- command.go | 49 +++++++++++++++++++++----------------------- error.go | 1 + execute_test.go | 5 +++-- flag.go | 53 ++++++++++++++++++++---------------------------- parse.go | 30 +++++++++++++++++++++++++++ pathfind.go | 2 +- pathfind_test.go | 26 ++++++++++++------------ setting.go | 2 ++ setting_test.go | 2 +- shadow.go | 20 ++++++++++++++++++ shadow_test.go | 43 +++++++++++++++++++++++++++++++++++++++ tags.go | 10 +++++++++ 14 files changed, 194 insertions(+), 78 deletions(-) create mode 100644 parse.go create mode 100644 shadow.go create mode 100644 shadow_test.go diff --git a/README.md b/README.md index f5213b8..0384678 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ## X-Flags -## What do this library? +## What does this library? This library provides a simple way to use flags in your application. It extends the standard library to be able to define and use a structure with flags. @@ -25,8 +25,8 @@ go get gitlab.schukai.com/oss/libraries/go/network/xflags ### Initialize -A new flag set is created using the `xflags.New()` function. The passed -structure is used type for the flags. +A new flag set is created using the `xflags.New()` function. +The structure passed is used as the type for the flags. ```go package main diff --git a/api.go b/api.go index 0bc87fe..50dbaf0 100644 --- a/api.go +++ b/api.go @@ -17,7 +17,7 @@ func New[C any](name string, definitions C) *setting[C] { errorHandling: flag.ContinueOnError, }, } - + if reflect.TypeOf(definitions).Kind() != reflect.Struct { s.errors = append(s.errors, newUnsupportedReflectKindError(reflect.TypeOf(definitions))) return s @@ -40,3 +40,24 @@ func (s *setting[C]) FlagOutput() string { func (s *setting[C]) Args() []string { return s.args } + +//type ConfigAny[C any] struct { +// path string +// ConfigurationAdapter[C] +//} +// +//type ConfigurationAdapter[RT any] interface { +// InitFromFlagSet(flagSet *flag.FlagSet) RT +//} + +//func (s *setting[C]) CopyValuesToStruct(path string, adapter any) *setting[C] { +// +// if s.command == nil { +// s.errors = append(s.errors, MissingCommandError) +// return s +// } +// +// s.command.copyValuesToStruct(path, adapter) +// +// return s +//} diff --git a/command.go b/command.go index 09f8705..278c038 100644 --- a/command.go +++ b/command.go @@ -5,23 +5,15 @@ import ( "reflect" ) -const ( - tagIgnore = "ignore" - tagCall = "call" - tagCommand = "command" - tagShort = "short" - tagLong = "long" - tagDescription = "description" -) - type cmd[C any] struct { - name string - flagSet *flag.FlagSet - mapping map[string]string - commands []*cmd[C] - settings *setting[C] - valuePath []string - functionName string + name string + flagSet *flag.FlagSet + tagMapping map[string]string + shadowMapping map[string]string + commands []*cmd[C] + settings *setting[C] + valuePath []string + functionName string } func (c *cmd[C]) parse(args []string) { @@ -61,13 +53,14 @@ func (c *cmd[C]) parse(args []string) { func buildCommandStruct[C any](s *setting[C], name, fkt string, errorHandling flag.ErrorHandling, path []string) *cmd[C] { cc := &cmd[C]{ - name: name, - flagSet: flag.NewFlagSet(name, errorHandling), - commands: []*cmd[C]{}, - settings: s, - mapping: map[string]string{}, - valuePath: path, - functionName: fkt, + name: name, + flagSet: flag.NewFlagSet(name, errorHandling), + commands: []*cmd[C]{}, + settings: s, + tagMapping: map[string]string{}, + shadowMapping: map[string]string{}, + valuePath: path, + functionName: fkt, } cc.flagSet.SetOutput(s.flagOutput) @@ -155,18 +148,22 @@ func (c *cmd[C]) parseStruct(dta any) { } if m[tagShort] != "" { - c.mapping[m[tagShort]] = v.Type().Field(i).Name + c.tagMapping[m[tagShort]] = v.Type().Field(i).Name } if m[tagLong] != "" { - c.mapping[m[tagLong]] = v.Type().Field(i).Name + c.tagMapping[m[tagLong]] = v.Type().Field(i).Name + } + + if m[tagShadow] != "" { + c.shadowMapping[v.Type().Field(i).Name] = m[tagShadow] } c.initFlags(x, m) } else if m[tagCommand] != "" { //c.valuePath = append(c.valuePath, ) - c.mapping[m[tagCommand]] = v.Type().Field(i).Name + c.tagMapping[m[tagCommand]] = v.Type().Field(i).Name c.initCommands(x, m, v.Type().Field(i).Name) } else if m[tagIgnore] != "" { diff --git a/error.go b/error.go index 9803ea7..a31a283 100644 --- a/error.go +++ b/error.go @@ -25,6 +25,7 @@ func (s *setting[C]) Errors() []error { var WatchListNotInitializedError = errors.New("watch list not initialized") var MissingCommandError = errors.New("missing command") var NotParsedError = errors.New("flag set not parsed") +var ShadowMustBePointerError = errors.New("shadow must be a pointer to a struct") // At the reflect level, some types are not supported type UnsupportedReflectKindError error diff --git a/execute_test.go b/execute_test.go index 398cb61..a6ecbb4 100644 --- a/execute_test.go +++ b/execute_test.go @@ -9,11 +9,12 @@ import ( type testExecutionStruct struct { callbackCounter int `ignore:"true"` - Global1 bool `short:"a" long:"global1" description:"Global 1"` + // for tag shadow see TestFlagCopyToShadow + Global1 bool `short:"a" long:"global1" description:"Global 1" shadow:"ValGlobal1"` Global2 bool `short:"b" long:"global2" description:"Global 2"` Command1 struct { Command1Flag1 bool `short:"c" long:"command1flag1" description:"Command 1 Flag 1"` - Command1Flag2 bool `short:"d" long:"command1flag2" description:"Command 1 Flag 2"` + Command1Flag2 bool `short:"d" long:"command1flag2" description:"Command 1 Flag 2" shadow:"ValCommand1Flag2"` Command2 struct { Command2Flag1 bool `short:"e" long:"command2flag1" description:"Command 2 Flag 1"` Command2Flag2 bool `short:"f" long:"command2flag2" description:"Command 2 Flag 2"` diff --git a/flag.go b/flag.go index 3a0a17e..eb9f08c 100644 --- a/flag.go +++ b/flag.go @@ -5,35 +5,6 @@ import ( "strings" ) -// Parse parses the command line arguments and assigns the values to the settings. -func (s *setting[C]) Parse(args []string) *setting[C] { - if len(s.errors) > 0 { - return s - } - - if s.command == nil { - s.errors = append(s.errors, MissingCommandError) - return s - } - - err := s.command.flagSet.Parse(args[1:]) - if err != nil { - s.errors = append(s.errors, err) - return s - } - - s.assignValues(*s.command) - - r := s.command.flagSet.Args() - if len(r) == 0 { - return s - } - - s.command.parse(r) - - return s -} - func (s *setting[C]) assignValues(c cmd[C]) { flgs := c.flagSet flgs.Visit(func(f *flag.Flag) { @@ -41,7 +12,7 @@ func (s *setting[C]) assignValues(c cmd[C]) { name := f.Name value := f.Value.String() - k, ok := c.mapping[name] + k, ok := c.tagMapping[name] if !ok { s.errors = append(s.errors, newUnknownFlagError(name)) return @@ -50,7 +21,12 @@ func (s *setting[C]) assignValues(c cmd[C]) { pa := append(c.valuePath, k) p := strings.Join(pa, ".") - err := setTheValueOverPath(&s.definitions, p, value) + err := setValueUsingPath(&s.definitions, p, value) + if err != nil { + s.errors = append(s.errors, err) + } + + err = c.setShadowValue(s.shadow, k, value) if err != nil { s.errors = append(s.errors, err) } @@ -60,3 +36,18 @@ func (s *setting[C]) assignValues(c cmd[C]) { }) } + +func (c cmd[C]) setShadowValue(obj any, k string, value string) error { + + if obj == nil { + return nil + } + + // set shadow + n, ok := c.shadowMapping[k] + if !ok { + return nil + } + + return setValueUsingPath(obj, n, value) +} diff --git a/parse.go b/parse.go new file mode 100644 index 0000000..17ed664 --- /dev/null +++ b/parse.go @@ -0,0 +1,30 @@ +package xflags + +// Parse parses the command line arguments and assigns the values to the settings. +func (s *setting[C]) Parse(args []string) *setting[C] { + if len(s.errors) > 0 { + return s + } + + if s.command == nil { + s.errors = append(s.errors, MissingCommandError) + return s + } + + err := s.command.flagSet.Parse(args[1:]) + if err != nil { + s.errors = append(s.errors, err) + return s + } + + s.assignValues(*s.command) + + r := s.command.flagSet.Args() + if len(r) == 0 { + return s + } + + s.command.parse(r) + + return s +} diff --git a/pathfind.go b/pathfind.go index 8c9bdcb..643cdf7 100644 --- a/pathfind.go +++ b/pathfind.go @@ -42,7 +42,7 @@ func getValueFrom[D any](obj D, keyWithDots string) (interface{}, error) { } // This function sets the value of a field in a struct, given a path to the field. -func setTheValueOverPath[D any](obj D, keyWithDots string, newValue string) error { +func setValueUsingPath[D any](obj D, keyWithDots string, newValue string) error { keySlice := strings.Split(keyWithDots, ".") v := reflect.ValueOf(obj) diff --git a/pathfind_test.go b/pathfind_test.go index dab17a8..24eb42d 100644 --- a/pathfind_test.go +++ b/pathfind_test.go @@ -55,7 +55,7 @@ func TestPathFindSetValueString(t *testing.T) { for k, v := range testData { s := &PathfindTestStruct1{} - err := setTheValueOverPath[*PathfindTestStruct1](s, k, v) + err := setValueUsingPath[*PathfindTestStruct1](s, k, v) if err != nil { t.Error(err) } @@ -195,10 +195,10 @@ func TestPathFindGetValueFrom(t *testing.T) { func TestPathFindSetValueFrom(t *testing.T) { s := &PathfindTestStruct1{} - setTheValueOverPath[*PathfindTestStruct1](s, "Sub1.B", "true") - setTheValueOverPath[*PathfindTestStruct1](s, "Sub1.Bi", "2") - setTheValueOverPath[*PathfindTestStruct1](s, "Sub1.Bs", "3") - setTheValueOverPath[*PathfindTestStruct1](s, "Sub1.Bf", "4.0") + setValueUsingPath[*PathfindTestStruct1](s, "Sub1.B", "true") + setValueUsingPath[*PathfindTestStruct1](s, "Sub1.Bi", "2") + setValueUsingPath[*PathfindTestStruct1](s, "Sub1.Bs", "3") + setValueUsingPath[*PathfindTestStruct1](s, "Sub1.Bf", "4.0") if s.Sub1.B != true { t.Error("s.Sub1.B != true") @@ -217,10 +217,10 @@ func TestPathFindSetValueFrom(t *testing.T) { t.Error("s.Sub1.Bf != 4.0") } - setTheValueOverPath[*PathfindTestStruct1](s, "Sub1.Sub2.C", "true") - setTheValueOverPath[*PathfindTestStruct1](s, "Sub1.Sub2.Ci", "2") - setTheValueOverPath[*PathfindTestStruct1](s, "Sub1.Sub2.Cs", "3") - setTheValueOverPath[*PathfindTestStruct1](s, "Sub1.Sub2.Cf", "4.0") + setValueUsingPath[*PathfindTestStruct1](s, "Sub1.Sub2.C", "true") + setValueUsingPath[*PathfindTestStruct1](s, "Sub1.Sub2.Ci", "2") + setValueUsingPath[*PathfindTestStruct1](s, "Sub1.Sub2.Cs", "3") + setValueUsingPath[*PathfindTestStruct1](s, "Sub1.Sub2.Cf", "4.0") if s.Sub1.Sub2.C != true { t.Error("s.Sub1.Sub2.C != true") @@ -247,10 +247,10 @@ func TestPathFindSetValueFrom(t *testing.T) { } - setTheValueOverPath[*PathfindTestStruct1](s, "Sub1.Sub2.Sub3.D", "true") - setTheValueOverPath[*PathfindTestStruct1](s, "Sub1.Sub2.Sub3.Di", "2") - setTheValueOverPath[*PathfindTestStruct1](s, "Sub1.Sub2.Sub3.Ds", "3") - setTheValueOverPath[*PathfindTestStruct1](s, "Sub1.Sub2.Sub3.Df", "4.0") + setValueUsingPath[*PathfindTestStruct1](s, "Sub1.Sub2.Sub3.D", "true") + setValueUsingPath[*PathfindTestStruct1](s, "Sub1.Sub2.Sub3.Di", "2") + setValueUsingPath[*PathfindTestStruct1](s, "Sub1.Sub2.Sub3.Ds", "3") + setValueUsingPath[*PathfindTestStruct1](s, "Sub1.Sub2.Sub3.Df", "4.0") if s.Sub1.Sub2.Sub3.D != true { t.Error("s.Sub1.Sub2.Sub3.D != true") diff --git a/setting.go b/setting.go index f9c218d..4a73c4b 100644 --- a/setting.go +++ b/setting.go @@ -25,6 +25,8 @@ type setting[C any] struct { args []string config config + + shadow any } func (s *setting[C]) GetValues() C { diff --git a/setting_test.go b/setting_test.go index 6ca3021..23b1265 100644 --- a/setting_test.go +++ b/setting_test.go @@ -21,5 +21,5 @@ func TestInitCommands(t *testing.T) { assert.Equal(t, 0, len(s.errors)) assert.Equal(t, "test", s.command.name) assert.Equal(t, 1, len(s.command.commands)) - assert.Equal(t, 3, len(s.command.mapping)) + assert.Equal(t, 3, len(s.command.tagMapping)) } diff --git a/shadow.go b/shadow.go new file mode 100644 index 0000000..b41717f --- /dev/null +++ b/shadow.go @@ -0,0 +1,20 @@ +package xflags + +import "reflect" + +// SetShadow sets the shadow struct for the flag configuration. +func (s *setting[C]) SetShadow(shadow any) *setting[C] { + + if reflect.TypeOf(shadow).Kind() != reflect.Ptr { + s.errors = append(s.errors, ShadowMustBePointerError) + return s + } + + if reflect.TypeOf(shadow).Elem().Kind() != reflect.Struct { + s.errors = append(s.errors, ShadowMustBePointerError) + return s + } + + s.shadow = shadow + return s +} diff --git a/shadow_test.go b/shadow_test.go new file mode 100644 index 0000000..8418e16 --- /dev/null +++ b/shadow_test.go @@ -0,0 +1,43 @@ +package xflags + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +type ConfigStruct6Sub1 struct { + Command3Flag1 bool +} + +type ConfigStruct6 struct { + ValGlobal1 bool + ValGlobal2 bool + ValCommand1Flag1 bool + ValCommand1Flag2 bool + ValSub ConfigStruct6Sub1 +} + +func TestFlagSetShadowError(t *testing.T) { + settings := New("test", testExecutionStruct{}) + settings.SetShadow(3) + assert.True(t, settings.HasErrors()) + +} + +func TestFlagCopyToShadow(t *testing.T) { + + c := ConfigStruct6{} + c.ValSub.Command3Flag1 = true + + settings := New("test", testExecutionStruct{}) + assert.NotNil(t, settings) + + settings.SetShadow(&c) + assert.False(t, settings.HasErrors()) + + settings.Parse([]string{"test", "-a", "command1", "-d"}) + + assert.True(t, c.ValGlobal1) + assert.True(t, c.ValCommand1Flag2) + +} diff --git a/tags.go b/tags.go index 0580191..dad791c 100644 --- a/tags.go +++ b/tags.go @@ -5,6 +5,16 @@ import ( "strconv" ) +const ( + tagIgnore = "ignore" + tagCall = "call" + tagCommand = "command" + tagShort = "short" + tagLong = "long" + tagDescription = "description" + tagShadow = "shadow" +) + func getTagMap(field reflect.StructField) (value map[string]string) { tagValues := map[string]string{} -- GitLab