From 60a6ef914ca03edd39b9f0f07bc523d9444f6230 Mon Sep 17 00:00:00 2001 From: Nick Craig-Wood Date: Tue, 26 Sep 2023 16:49:09 +0100 Subject: [PATCH] fs: create fs.Enum for easy creation of parameters from a list of choices --- fs/config/configstruct/configstruct.go | 3 +- fs/enum.go | 107 +++++++++++++++++++ fs/enum_test.go | 139 +++++++++++++++++++++++++ fs/registry.go | 9 ++ 4 files changed, 256 insertions(+), 2 deletions(-) create mode 100644 fs/enum.go create mode 100644 fs/enum_test.go diff --git a/fs/config/configstruct/configstruct.go b/fs/config/configstruct/configstruct.go index 637e5e4ef..e8692b1b6 100644 --- a/fs/config/configstruct/configstruct.go +++ b/fs/config/configstruct/configstruct.go @@ -24,8 +24,7 @@ func camelToSnake(in string) string { // StringToInterface turns in into an interface{} the same type as def func StringToInterface(def interface{}, in string) (newValue interface{}, err error) { typ := reflect.TypeOf(def) - switch typ.Kind() { - case reflect.String: + if typ.Kind() == reflect.String && typ.Name() == "string" { // Pass strings unmodified return in, nil } diff --git a/fs/enum.go b/fs/enum.go new file mode 100644 index 000000000..fe54405e1 --- /dev/null +++ b/fs/enum.go @@ -0,0 +1,107 @@ +package fs + +import ( + "encoding/json" + "fmt" + "strings" +) + +// Enum is an option which can only be one of the Choices. +// +// Suggested implementation is something like this: +// +// type choice = Enum[choices] +// +// const ( +// choiceA choice = iota +// choiceB +// choiceC +// ) +// +// type choices struct{} +// +// func (choices) Choices() []string { +// return []string{ +// choiceA: "A", +// choiceB: "B", +// choiceC: "C", +// } +// } +type Enum[C Choices] byte + +// Choices returns the valid choices for this type. +// +// It must work on the zero value. +// +// Note that when using this in an Option the ExampleChoices will be +// filled in automatically. +type Choices interface { + // Choices returns the valid choices for this type + Choices() []string +} + +// String renders the Enum as a string +func (e Enum[C]) String() string { + choices := e.Choices() + if int(e) >= len(choices) { + return fmt.Sprintf("Unknown(%d)", e) + } + return choices[e] +} + +// Choices returns the possible values of the Enum. +func (e Enum[C]) Choices() []string { + var c C + return c.Choices() +} + +// Help returns a comma separated list of all possible states. +func (e Enum[C]) Help() string { + return strings.Join(e.Choices(), ", ") +} + +// Set the Enum entries +func (e *Enum[C]) Set(s string) error { + for i, choice := range e.Choices() { + if strings.EqualFold(s, choice) { + *e = Enum[C](i) + return nil + } + } + return fmt.Errorf("invalid choice %q from: %s", s, e.Help()) +} + +// Type of the value. +// +// If C has a Type() string method then it will be used instead. +func (e Enum[C]) Type() string { + var c C + if do, ok := any(c).(typer); ok { + return do.Type() + } + return strings.Join(e.Choices(), "|") +} + +// Scan implements the fmt.Scanner interface +func (e *Enum[C]) Scan(s fmt.ScanState, ch rune) error { + token, err := s.Token(true, nil) + if err != nil { + return err + } + return e.Set(string(token)) +} + +// UnmarshalJSON parses it as a string +func (e *Enum[C]) UnmarshalJSON(in []byte) error { + var choice string + err := json.Unmarshal(in, &choice) + if err != nil { + return err + } + return e.Set(choice) +} + +// MarshalJSON encodes it as string +func (e *Enum[C]) MarshalJSON() ([]byte, error) { + return json.Marshal(e.String()) +} diff --git a/fs/enum_test.go b/fs/enum_test.go new file mode 100644 index 000000000..3413ff259 --- /dev/null +++ b/fs/enum_test.go @@ -0,0 +1,139 @@ +package fs + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type choices struct{} + +func (choices) Choices() []string { + return []string{ + choiceA: "A", + choiceB: "B", + choiceC: "C", + } +} + +type choice = Enum[choices] + +const ( + choiceA choice = iota + choiceB + choiceC +) + +// Check it satisfies the interfaces +var ( + _ flagger = (*choice)(nil) + _ flaggerNP = choice(0) +) + +func TestEnumString(t *testing.T) { + for _, test := range []struct { + in choice + want string + }{ + {choiceA, "A"}, + {choiceB, "B"}, + {choiceC, "C"}, + {choice(100), "Unknown(100)"}, + } { + got := test.in.String() + assert.Equal(t, test.want, got) + } +} + +func TestEnumType(t *testing.T) { + assert.Equal(t, "A|B|C", choiceA.Type()) +} + +// Enum with Type() on the choices +type choicestype struct{} + +func (choicestype) Choices() []string { + return []string{} +} + +func (choicestype) Type() string { + return "potato" +} + +type choicetype = Enum[choicestype] + +func TestEnumTypeWithFunction(t *testing.T) { + assert.Equal(t, "potato", choicetype(0).Type()) +} + +func TestEnumHelp(t *testing.T) { + assert.Equal(t, "A, B, C", choice(0).Help()) +} + +func TestEnumSet(t *testing.T) { + for _, test := range []struct { + in string + want choice + err bool + }{ + {"A", choiceA, false}, + {"B", choiceB, false}, + {"C", choiceC, false}, + {"D", choice(100), true}, + } { + var got choice + err := got.Set(test.in) + if test.err { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, test.want, got) + } + } +} + +func TestEnumScan(t *testing.T) { + var v choice + n, err := fmt.Sscan(" A ", &v) + require.NoError(t, err) + assert.Equal(t, 1, n) + assert.Equal(t, choiceA, v) +} + +func TestEnumUnmarshalJSON(t *testing.T) { + for _, test := range []struct { + in string + want choice + err bool + }{ + {`"A"`, choiceA, false}, + {`"B"`, choiceB, false}, + {`"D"`, choice(0), true}, + } { + var got choice + err := json.Unmarshal([]byte(test.in), &got) + if test.err { + require.Error(t, err, test.in) + } else { + require.NoError(t, err, test.in) + } + assert.Equal(t, test.want, got, test.in) + } +} + +func TestEnumMarshalJSON(t *testing.T) { + for _, test := range []struct { + in choice + want string + }{ + {choiceA, `"A"`}, + {choiceB, `"B"`}, + } { + got, err := json.Marshal(&test.in) + require.NoError(t, err) + assert.Equal(t, test.want, string(got), fmt.Sprintf("%#v", test.in)) + } +} diff --git a/fs/registry.go b/fs/registry.go index 8bd8cc1a8..1ecd2c305 100644 --- a/fs/registry.go +++ b/fs/registry.go @@ -60,6 +60,15 @@ func (os Options) setValues() { if o.Default == nil { o.Default = "" } + // Create options for Enums + if do, ok := o.Default.(Choices); ok && len(o.Examples) == 0 { + o.Exclusive = true + o.Required = true + o.Examples = make(OptionExamples, len(do.Choices())) + for i, choice := range do.Choices() { + o.Examples[i].Value = choice + } + } } }