From 41d1d8c6c67a8ad055823eab928efef3ab3bd00b Mon Sep 17 00:00:00 2001 From: Tiago Peczenyj Date: Fri, 21 Mar 2025 12:18:17 +0100 Subject: [PATCH] add options to implement flag.Value and pflag.Value methods --- enumer.go | 40 ++++++++++++++ golden_test.go | 107 +++++++++++++++++++++++++++++-------- stringer.go | 92 +++++++++++++++++++------------ testdata/flagvalue.golden | 97 +++++++++++++++++++++++++++++++++ testdata/pflagvalue.golden | 102 +++++++++++++++++++++++++++++++++++ 5 files changed, 382 insertions(+), 56 deletions(-) create mode 100644 testdata/flagvalue.golden create mode 100644 testdata/pflagvalue.golden diff --git a/enumer.go b/enumer.go index c234209..bf8b7a6 100644 --- a/enumer.go +++ b/enumer.go @@ -3,6 +3,7 @@ package main import "fmt" // Arguments to format are: +// // [1]: type name const stringNameToValueMethod = `// %[1]sString retrieves an enum value from the enum constants string name. // Throws an error if the param is not part of the enum. @@ -19,6 +20,7 @@ func %[1]sString(s string) (%[1]s, error) { ` // Arguments to format are: +// // [1]: type name const stringValuesMethod = `// %[1]sValues returns all values of the enum func %[1]sValues() []%[1]s { @@ -27,6 +29,7 @@ func %[1]sValues() []%[1]s { ` // Arguments to format are: +// // [1]: type name const stringsMethod = `// %[1]sStrings returns a slice of all String values of the enum func %[1]sStrings() []string { @@ -37,6 +40,7 @@ func %[1]sStrings() []string { ` // Arguments to format are: +// // [1]: type name const stringBelongsMethodLoop = `// IsA%[1]s returns "true" if the value is listed in the enum definition. "false" otherwise func (i %[1]s) IsA%[1]s() bool { @@ -50,6 +54,7 @@ func (i %[1]s) IsA%[1]s() bool { ` // Arguments to format are: +// // [1]: type name const stringBelongsMethodSet = `// IsA%[1]s returns "true" if the value is listed in the enum definition. "false" otherwise func (i %[1]s) IsA%[1]s() bool { @@ -59,6 +64,7 @@ func (i %[1]s) IsA%[1]s() bool { ` // Arguments to format are: +// // [1]: type name const altStringValuesMethod = `func (%[1]s) Values() []string { return %[1]sStrings() @@ -144,6 +150,7 @@ func (g *Generator) printNamesSlice(runs [][]Value, typeName string, runsThresho } // Arguments to format are: +// // [1]: type name const jsonMethods = ` // MarshalJSON implements the json.Marshaler interface for %[1]s @@ -169,6 +176,7 @@ func (g *Generator) buildJSONMethods(runs [][]Value, typeName string, runsThresh } // Arguments to format are: +// // [1]: type name const textMethods = ` // MarshalText implements the encoding.TextMarshaler interface for %[1]s @@ -189,6 +197,7 @@ func (g *Generator) buildTextMethods(runs [][]Value, typeName string, runsThresh } // Arguments to format are: +// // [1]: type name const yamlMethods = ` // MarshalYAML implements a YAML Marshaler for %[1]s @@ -212,3 +221,34 @@ func (i *%[1]s) UnmarshalYAML(unmarshal func(interface{}) error) error { func (g *Generator) buildYAMLMethods(runs [][]Value, typeName string, runsThreshold int) { g.Printf(yamlMethods, typeName) } + +// Arguments to format are: +// +// [1]: type name +const flagValueMethodSet = ` +// Set allows flag and pflag libraries to set a value dinamically. +func (i *%[1]s) Set(value string) error { + var err error + *i, err = %[1]sString(s) + return err +} +` + +// Arguments to format are: +// +// [1]: type name +const pflagValueMethodType = ` +// Type returns a string that represents all possible values to this type joined by '|'. +func (%[1]s) Type() string { + return strings.Join(_%[1]sNames, "|") +} +` + +func (g *Generator) buildFlagMethods(runs [][]Value, typeName string, runsThreshold int) { + g.Printf(flagValueMethodSet, typeName) +} + +func (g *Generator) buildPflagMethods(runs [][]Value, typeName string, runsThreshold int) { + g.Printf(flagValueMethodSet, typeName) + g.Printf(pflagValueMethodType, typeName) +} diff --git a/golden_test.go b/golden_test.go index 03479d2..f9054c5 100644 --- a/golden_test.go +++ b/golden_test.go @@ -76,6 +76,14 @@ var goldenLinecomment = []Golden{ {"dayWithLinecomment", linecommentIn}, } +var goldenFlagValue = []Golden{ + {"flagvalue", dayIn}, +} + +var goldenPflagValue = []Golden{ + {"pflagvalue", dayIn}, +} + // Each example starts with "type XXX [u]int", with a single space separating them. // Simple test: enumeration of type int starting at 0. @@ -315,46 +323,95 @@ const ( func TestGolden(t *testing.T) { for _, test := range golden { - runGoldenTest(t, test, false, false, false, false, false, false, true, "", "") + runGoldenTest(t, test, generateOptions{ + transformMethod: "noop", + includeValuesMethod: true, + }) } for _, test := range goldenJSON { - runGoldenTest(t, test, true, false, false, false, false, false, false, "", "") + runGoldenTest(t, test, generateOptions{ + includeJSON: true, + transformMethod: "noop", + }) } for _, test := range goldenText { - runGoldenTest(t, test, false, false, false, true, false, false, false, "", "") + runGoldenTest(t, test, generateOptions{ + includeText: true, + transformMethod: "noop", + }) } for _, test := range goldenYAML { - runGoldenTest(t, test, false, true, false, false, false, false, false, "", "") + runGoldenTest(t, test, generateOptions{ + includeYAML: true, + transformMethod: "noop", + }) } for _, test := range goldenSQL { - runGoldenTest(t, test, false, false, true, false, false, false, false, "", "") + runGoldenTest(t, test, generateOptions{ + includeSQL: true, + transformMethod: "noop", + }) } for _, test := range goldenJSONAndSQL { - runGoldenTest(t, test, true, false, true, false, false, false, false, "", "") + runGoldenTest(t, test, generateOptions{ + includeJSON: true, + includeSQL: true, + transformMethod: "noop", + }) } for _, test := range goldenGQLGen { - runGoldenTest(t, test, false, false, false, false, false, true, false, "", "") + runGoldenTest(t, test, generateOptions{ + includeGQLGen: true, + transformMethod: "noop", + }) } for _, test := range goldenTrimPrefix { - runGoldenTest(t, test, false, false, false, false, false, false, false, "Day", "") + runGoldenTest(t, test, generateOptions{ + trimPrefix: "Day", + transformMethod: "noop", + }) } for _, test := range goldenTrimPrefixMultiple { - runGoldenTest(t, test, false, false, false, false, false, false, false, "Day,Night", "") + runGoldenTest(t, test, generateOptions{ + trimPrefix: "Day,Night", + transformMethod: "noop", + }) } for _, test := range goldenWithPrefix { - runGoldenTest(t, test, false, false, false, false, false, false, false, "", "Day") + runGoldenTest(t, test, generateOptions{ + addPrefix: "Day", + transformMethod: "noop", + }) } for _, test := range goldenTrimAndAddPrefix { - runGoldenTest(t, test, false, false, false, false, false, false, false, "Day", "Night") + runGoldenTest(t, test, generateOptions{ + trimPrefix: "Day", + addPrefix: "Night", + transformMethod: "noop", + }) } for _, test := range goldenLinecomment { - runGoldenTest(t, test, false, false, false, false, true, false, false, "", "") + runGoldenTest(t, test, generateOptions{ + transformMethod: "noop", + lineComment: true, + }) + } + for _, test := range goldenFlagValue { + runGoldenTest(t, test, generateOptions{ + transformMethod: "noop", + includeFlagMethods: true, + }) + } + for _, test := range goldenPflagValue { + runGoldenTest(t, test, generateOptions{ + transformMethod: "noop", + includePflagMethods: true, + }) } } -func runGoldenTest(t *testing.T, test Golden, - generateJSON, generateYAML, generateSQL, generateText, linecomment, generateGQLGen, generateValuesMethod bool, - trimPrefix string, prefix string) { +func runGoldenTest(t *testing.T, test Golden, opts generateOptions) { + t.Helper() var g Generator file := test.name + ".go" @@ -382,29 +439,35 @@ func runGoldenTest(t *testing.T, test Golden, if len(tokens) != 3 { t.Fatalf("%s: need type declaration on first line", test.name) } - g.generate(tokens[1], generateJSON, generateYAML, generateSQL, generateText, generateGQLGen, "noop", trimPrefix, prefix, linecomment, generateValuesMethod) + g.generate(tokens[1], opts) + got := string(g.format()) - if got != loadGolden(test.name) { + expected, err := loadGolden(test.name) + if err != nil { + t.Fatalf("unexpected error while loading golden %q: %v", test.name, err) + } + + if got != expected { // Use this to help build a golden text when changes are needed //goldenFile := fmt.Sprintf("./testdata/%v.golden", test.name) //err = ioutil.WriteFile(goldenFile, []byte(got), 0644) //if err != nil { // t.Error(err) //} - t.Errorf("%s: got\n====\n%s====\nexpected\n====%s", test.name, got, loadGolden(test.name)) + t.Errorf("%s: got\n====\n%s====\nexpected\n====%s", test.name, got, expected) } } -func loadGolden(name string) string { +func loadGolden(name string) (string, error) { fh, err := os.Open("testdata/" + name + ".golden") if err != nil { - return "" + return "", err } defer fh.Close() b, err := ioutil.ReadAll(fh) if err != nil { - return "" + return "", err } - return string(b) + return string(b), nil } diff --git a/stringer.go b/stringer.go index 33d3d09..c8f7a5d 100644 --- a/stringer.go +++ b/stringer.go @@ -43,24 +43,45 @@ func (af *arrayFlags) Set(value string) error { return nil } +type generateOptions struct { + includeJSON bool + includeYAML bool + includeSQL bool + includeText bool + includeGQLGen bool + transformMethod string + trimPrefix string + addPrefix string + lineComment bool + includeValuesMethod bool + includeFlagMethods bool + includePflagMethods bool +} + var ( - typeNames = flag.String("type", "", "comma-separated list of type names; must be set") - sql = flag.Bool("sql", false, "if true, the Scanner and Valuer interface will be implemented.") - json = flag.Bool("json", false, "if true, json marshaling methods will be generated. Default: false") - yaml = flag.Bool("yaml", false, "if true, yaml marshaling methods will be generated. Default: false") - text = flag.Bool("text", false, "if true, text marshaling methods will be generated. Default: false") - gqlgen = flag.Bool("gqlgen", false, "if true, GraphQL marshaling methods for gqlgen will be generated. Default: false") - altValuesFunc = flag.Bool("values", false, "if true, alternative string values method will be generated. Default: false") - output = flag.String("output", "", "output file name; default srcdir/_string.go") - transformMethod = flag.String("transform", "noop", "enum item name transformation method. Default: noop") - trimPrefix = flag.String("trimprefix", "", "transform each item name by removing a prefix or comma separated list of prefixes. Default: \"\"") - addPrefix = flag.String("addprefix", "", "transform each item name by adding a prefix. Default: \"\"") - linecomment = flag.Bool("linecomment", false, "use line comment text as printed text when present") + typeNames string + opts generateOptions + output string + comments arrayFlags ) -var comments arrayFlags - func init() { + flag.StringVar(&typeNames, "type", "", "comma-separated list of type names; must be set") + + flag.BoolVar(&opts.includeSQL, "sql", false, "if true, the Scanner and Valuer interface will be implemented.") + flag.BoolVar(&opts.includeJSON, "json", false, "if true, json marshaling methods will be generated. Default: false") + flag.BoolVar(&opts.includeYAML, "yaml", false, "if true, yaml marshaling methods will be generated. Default: false") + flag.BoolVar(&opts.includeText, "text", false, "if true, text marshaling methods will be generated. Default: false") + flag.BoolVar(&opts.includeGQLGen, "gqlgen", false, "if true, GraphQL marshaling methods for gqlgen will be generated. Default: false") + flag.BoolVar(&opts.includeValuesMethod, "values", false, "if true, alternative string values method will be generated. Default: false") + flag.BoolVar(&opts.includeFlagMethods, "flag.value", false, "if true, ensure that the enumeration type implements stdlib flag.Value interface. Default: false") + flag.BoolVar(&opts.includePflagMethods, "pflag.value", false, "if true, ensure that the enumeration type implements pflag.Value interface, see: https://pkg.go.dev/github.com/spf13/pflag#Value Default: false") + flag.StringVar(&output, "output", "", "output file name; default srcdir/_string.go") + flag.StringVar(&opts.transformMethod, "transform", "noop", "enum item name transformation method. Default: noop") + flag.StringVar(&opts.trimPrefix, "trimprefix", "", "transform each item name by removing a prefix or comma separated list of prefixes. Default: \"\"") + flag.StringVar(&opts.addPrefix, "addprefix", "", "transform each item name by adding a prefix. Default: \"\"") + flag.BoolVar(&opts.lineComment, "linecomment", false, "use line comment text as printed text when present") + flag.Var(&comments, "comment", "comments to include in generated code, can repeat. Default: \"\"") } @@ -81,11 +102,11 @@ func main() { log.SetPrefix("enumer: ") flag.Usage = Usage flag.Parse() - if len(*typeNames) == 0 { + if len(typeNames) == 0 { flag.Usage() os.Exit(2) } - typs := strings.Split(*typeNames, ",") + typs := strings.Split(typeNames, ",") // We accept either one directory or a list of files. Which do we have? args := flag.Args() @@ -121,13 +142,13 @@ func main() { g.Printf("import (\n") g.Printf("\t\"fmt\"\n") g.Printf("\t\"strings\"\n") - if *sql { + if opts.includeSQL { g.Printf("\t\"database/sql/driver\"\n") } - if *json { + if opts.includeJSON { g.Printf("\t\"encoding/json\"\n") } - if *gqlgen { + if opts.includeGQLGen { g.Printf("\t\"io\"\n") g.Printf("\t\"strconv\"\n") } @@ -135,14 +156,14 @@ func main() { // Run generate for each type. for _, typeName := range typs { - g.generate(typeName, *json, *yaml, *sql, *text, *gqlgen, *transformMethod, *trimPrefix, *addPrefix, *linecomment, *altValuesFunc) + g.generate(typeName, opts) } // Format the output. src := g.format() // Figure out filename to write to - outputName := *output + outputName := output if outputName == "" { baseName := fmt.Sprintf("%s_enumer.go", typs[0]) outputName = filepath.Join(dir, strings.ToLower(baseName)) @@ -413,12 +434,10 @@ func (g *Generator) prefixValueNames(values []Value, prefix string) { } // generate produces the String method for the named type. -func (g *Generator) generate(typeName string, - includeJSON, includeYAML, includeSQL, includeText, includeGQLGen bool, - transformMethod string, trimPrefix string, addPrefix string, lineComment bool, includeValuesMethod bool) { +func (g *Generator) generate(typeName string, opts generateOptions) { values := make([]Value, 0, 100) for _, file := range g.pkg.files { - file.lineComment = lineComment + file.lineComment = opts.lineComment // Set the state for this run of the walker. file.typeName = typeName file.values = nil @@ -432,13 +451,13 @@ func (g *Generator) generate(typeName string, log.Fatalf("no values defined for type %s", typeName) } - for _, prefix := range strings.Split(trimPrefix, ",") { + for _, prefix := range strings.Split(opts.trimPrefix, ",") { g.trimValueNames(values, prefix) } - g.transformValueNames(values, transformMethod) + g.transformValueNames(values, opts.transformMethod) - g.prefixValueNames(values, addPrefix) + g.prefixValueNames(values, opts.addPrefix) runs := splitIntoRuns(values) // The decision of which pattern to use depends on the number of @@ -462,28 +481,33 @@ func (g *Generator) generate(typeName string, default: g.buildMap(runs, typeName) } - if includeValuesMethod { + if opts.includeValuesMethod { g.buildAltStringValuesMethod(typeName) } g.buildNoOpOrderChangeDetect(runs, typeName) g.buildBasicExtras(runs, typeName, runsThreshold) - if includeJSON { + if opts.includeJSON { g.buildJSONMethods(runs, typeName, runsThreshold) } - if includeText { + if opts.includeText { g.buildTextMethods(runs, typeName, runsThreshold) } - if includeYAML { + if opts.includeYAML { g.buildYAMLMethods(runs, typeName, runsThreshold) } - if includeSQL { + if opts.includeSQL { g.addValueAndScanMethod(typeName) } - if includeGQLGen { + if opts.includeGQLGen { g.buildGQLGenMethods(runs, typeName) } + if opts.includePflagMethods { + g.buildPflagMethods(runs, typeName, runsThreshold) + } else if opts.includeFlagMethods { + g.buildFlagMethods(runs, typeName, runsThreshold) + } } // splitIntoRuns breaks the values into runs of contiguous sequences. diff --git a/testdata/flagvalue.golden b/testdata/flagvalue.golden new file mode 100644 index 0000000..b7021c8 --- /dev/null +++ b/testdata/flagvalue.golden @@ -0,0 +1,97 @@ + +const _DayName = "MondayTuesdayWednesdayThursdayFridaySaturdaySunday" + +var _DayIndex = [...]uint8{0, 6, 13, 22, 30, 36, 44, 50} + +const _DayLowerName = "mondaytuesdaywednesdaythursdayfridaysaturdaysunday" + +func (i Day) String() string { + if i < 0 || i >= Day(len(_DayIndex)-1) { + return fmt.Sprintf("Day(%d)", i) + } + return _DayName[_DayIndex[i]:_DayIndex[i+1]] +} + +// An "invalid array index" compiler error signifies that the constant values have changed. +// Re-run the stringer command to generate them again. +func _DayNoOp() { + var x [1]struct{} + _ = x[Monday-(0)] + _ = x[Tuesday-(1)] + _ = x[Wednesday-(2)] + _ = x[Thursday-(3)] + _ = x[Friday-(4)] + _ = x[Saturday-(5)] + _ = x[Sunday-(6)] +} + +var _DayValues = []Day{Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday} + +var _DayNameToValueMap = map[string]Day{ + _DayName[0:6]: Monday, + _DayLowerName[0:6]: Monday, + _DayName[6:13]: Tuesday, + _DayLowerName[6:13]: Tuesday, + _DayName[13:22]: Wednesday, + _DayLowerName[13:22]: Wednesday, + _DayName[22:30]: Thursday, + _DayLowerName[22:30]: Thursday, + _DayName[30:36]: Friday, + _DayLowerName[30:36]: Friday, + _DayName[36:44]: Saturday, + _DayLowerName[36:44]: Saturday, + _DayName[44:50]: Sunday, + _DayLowerName[44:50]: Sunday, +} + +var _DayNames = []string{ + _DayName[0:6], + _DayName[6:13], + _DayName[13:22], + _DayName[22:30], + _DayName[30:36], + _DayName[36:44], + _DayName[44:50], +} + +// DayString retrieves an enum value from the enum constants string name. +// Throws an error if the param is not part of the enum. +func DayString(s string) (Day, error) { + if val, ok := _DayNameToValueMap[s]; ok { + return val, nil + } + + if val, ok := _DayNameToValueMap[strings.ToLower(s)]; ok { + return val, nil + } + return 0, fmt.Errorf("%s does not belong to Day values", s) +} + +// DayValues returns all values of the enum +func DayValues() []Day { + return _DayValues +} + +// DayStrings returns a slice of all String values of the enum +func DayStrings() []string { + strs := make([]string, len(_DayNames)) + copy(strs, _DayNames) + return strs +} + +// IsADay returns "true" if the value is listed in the enum definition. "false" otherwise +func (i Day) IsADay() bool { + for _, v := range _DayValues { + if i == v { + return true + } + } + return false +} + +// Set allows flag and pflag libraries to set a value dinamically. +func (i *Day) Set(value string) error { + var err error + *i, err = DayString(s) + return err +} diff --git a/testdata/pflagvalue.golden b/testdata/pflagvalue.golden new file mode 100644 index 0000000..30f60cf --- /dev/null +++ b/testdata/pflagvalue.golden @@ -0,0 +1,102 @@ + +const _DayName = "MondayTuesdayWednesdayThursdayFridaySaturdaySunday" + +var _DayIndex = [...]uint8{0, 6, 13, 22, 30, 36, 44, 50} + +const _DayLowerName = "mondaytuesdaywednesdaythursdayfridaysaturdaysunday" + +func (i Day) String() string { + if i < 0 || i >= Day(len(_DayIndex)-1) { + return fmt.Sprintf("Day(%d)", i) + } + return _DayName[_DayIndex[i]:_DayIndex[i+1]] +} + +// An "invalid array index" compiler error signifies that the constant values have changed. +// Re-run the stringer command to generate them again. +func _DayNoOp() { + var x [1]struct{} + _ = x[Monday-(0)] + _ = x[Tuesday-(1)] + _ = x[Wednesday-(2)] + _ = x[Thursday-(3)] + _ = x[Friday-(4)] + _ = x[Saturday-(5)] + _ = x[Sunday-(6)] +} + +var _DayValues = []Day{Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday} + +var _DayNameToValueMap = map[string]Day{ + _DayName[0:6]: Monday, + _DayLowerName[0:6]: Monday, + _DayName[6:13]: Tuesday, + _DayLowerName[6:13]: Tuesday, + _DayName[13:22]: Wednesday, + _DayLowerName[13:22]: Wednesday, + _DayName[22:30]: Thursday, + _DayLowerName[22:30]: Thursday, + _DayName[30:36]: Friday, + _DayLowerName[30:36]: Friday, + _DayName[36:44]: Saturday, + _DayLowerName[36:44]: Saturday, + _DayName[44:50]: Sunday, + _DayLowerName[44:50]: Sunday, +} + +var _DayNames = []string{ + _DayName[0:6], + _DayName[6:13], + _DayName[13:22], + _DayName[22:30], + _DayName[30:36], + _DayName[36:44], + _DayName[44:50], +} + +// DayString retrieves an enum value from the enum constants string name. +// Throws an error if the param is not part of the enum. +func DayString(s string) (Day, error) { + if val, ok := _DayNameToValueMap[s]; ok { + return val, nil + } + + if val, ok := _DayNameToValueMap[strings.ToLower(s)]; ok { + return val, nil + } + return 0, fmt.Errorf("%s does not belong to Day values", s) +} + +// DayValues returns all values of the enum +func DayValues() []Day { + return _DayValues +} + +// DayStrings returns a slice of all String values of the enum +func DayStrings() []string { + strs := make([]string, len(_DayNames)) + copy(strs, _DayNames) + return strs +} + +// IsADay returns "true" if the value is listed in the enum definition. "false" otherwise +func (i Day) IsADay() bool { + for _, v := range _DayValues { + if i == v { + return true + } + } + return false +} + +// Set allows flag and pflag libraries to set a value dinamically. +func (i *Day) Set(value string) error { + var err error + *i, err = DayString(s) + return err +} + +// Type returns a string that represents all possible values to this type joined by '|'. +func (Day) Type() string { + return strings.Join(_DayNames, "|") +}