fs: create fs.Enum for easy creation of parameters from a list of choices

This commit is contained in:
Nick Craig-Wood 2023-09-26 16:49:09 +01:00
parent 3553cc4a5f
commit 60a6ef914c
4 changed files with 256 additions and 2 deletions

View file

@ -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
}

107
fs/enum.go Normal file
View file

@ -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())
}

139
fs/enum_test.go Normal file
View file

@ -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))
}
}

View file

@ -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
}
}
}
}