diff --git a/enumer.go b/enumer.go index 2e122e3..18f48d9 100644 --- a/enumer.go +++ b/enumer.go @@ -59,6 +59,16 @@ const altStringValuesMethod = `func (%[1]s) Values() []string { } ` +// Arguments to format are: [1]: type name +const validateMethod = `// Validate returns an error if the value is not listed in the enum definition. +func (i %[1]s) Validate() error { + if !i.IsA%[1]s() { + return fmt.Errorf("%%v is not a valid value for %[1]s values", i) + } + return nil +} +` + func (g *Generator) buildAltStringValuesMethod(typeName string) { g.Printf("\n") g.Printf(altStringValuesMethod, typeName) @@ -215,3 +225,7 @@ func (g *Generator) buildYAMLMethods(runs [][]Value, typeName string, runsThresh // We rely on the %[1]sString method to provide typed errors when enabled g.Printf(yamlMethods, typeName) } + +func (g *Generator) buildValidateMethod(typeName string) { + g.Printf(validateMethod, typeName) +} diff --git a/golden_test.go b/golden_test.go index 2906f04..e400a4b 100644 --- a/golden_test.go +++ b/golden_test.go @@ -10,7 +10,7 @@ package main import ( - "io/ioutil" + "io" "os" "path/filepath" "strings" @@ -76,6 +76,10 @@ var goldenLinecomment = []Golden{ {"dayWithLinecomment", linecommentIn}, } +var goldenValidateMethod = []Golden{ + {name: "validate", input: dayIn}, +} + var goldenTypedErrors = []Golden{ {"typedErrors", typedErrorsIn}, } @@ -327,48 +331,51 @@ const ( func TestGolden(t *testing.T) { for _, test := range golden { - runGoldenTest(t, test, false, false, false, false, false, false, true, "", "", false) + runGoldenTest(t, test, false, false, false, false, false, false, true, false, "", "", false) } for _, test := range goldenJSON { - runGoldenTest(t, test, true, false, false, false, false, false, false, "", "", false) + runGoldenTest(t, test, true, false, false, false, false, false, false, false, "", "", false) } for _, test := range goldenText { - runGoldenTest(t, test, false, false, false, true, false, false, false, "", "", false) + runGoldenTest(t, test, false, false, false, true, false, false, false, false, "", "", false) } for _, test := range goldenYAML { - runGoldenTest(t, test, false, true, false, false, false, false, false, "", "", false) + runGoldenTest(t, test, false, true, false, false, false, false, false, false, "", "", false) } for _, test := range goldenSQL { - runGoldenTest(t, test, false, false, true, false, false, false, false, "", "", false) + runGoldenTest(t, test, false, false, true, false, false, false, false, false, "", "", false) } for _, test := range goldenJSONAndSQL { - runGoldenTest(t, test, true, false, true, false, false, false, false, "", "", false) + runGoldenTest(t, test, true, false, true, false, false, false, false, false, "", "", false) } for _, test := range goldenGQLGen { - runGoldenTest(t, test, false, false, false, false, false, true, false, "", "", false) + runGoldenTest(t, test, false, false, false, false, false, true, false, false, "", "", false) } for _, test := range goldenTrimPrefix { - runGoldenTest(t, test, false, false, false, false, false, false, false, "Day", "", false) + runGoldenTest(t, test, false, false, false, false, false, false, false, false, "Day", "", false) } for _, test := range goldenTrimPrefixMultiple { - runGoldenTest(t, test, false, false, false, false, false, false, false, "Day,Night", "", false) + runGoldenTest(t, test, false, false, false, false, false, false, false, false, "Day,Night", "", false) } for _, test := range goldenWithPrefix { - runGoldenTest(t, test, false, false, false, false, false, false, false, "", "Day", false) + runGoldenTest(t, test, false, false, false, false, false, false, false, false, "", "Day", false) } for _, test := range goldenTrimAndAddPrefix { - runGoldenTest(t, test, false, false, false, false, false, false, false, "Day", "Night", false) + runGoldenTest(t, test, false, false, false, false, false, false, false, false, "Day", "Night", false) } for _, test := range goldenLinecomment { - runGoldenTest(t, test, false, false, false, false, true, false, false, "", "", false) + runGoldenTest(t, test, false, false, false, false, true, false, false, false, "", "", false) + } + for _, test := range goldenValidateMethod { + runGoldenTest(t, test, false, false, false, false, false, false, false, true, "", "", false) } for _, test := range goldenTypedErrors { - runGoldenTest(t, test, false, false, false, false, false, false, false, "", "", true) + runGoldenTest(t, test, false, false, false, false, false, false, false, false, "", "", true) } } func runGoldenTest(t *testing.T, test Golden, - generateJSON, generateYAML, generateSQL, generateText, linecomment, generateGQLGen, generateValuesMethod bool, + generateJSON, generateYAML, generateSQL, generateText, linecomment, generateGQLGen, generateValuesMethod, generateIsValidMethod bool, trimPrefix string, prefix string, useTypedErrors bool) { var g Generator @@ -397,7 +404,7 @@ 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, useTypedErrors) + g.generate(tokens[1], generateJSON, generateYAML, generateSQL, generateText, generateGQLGen, "noop", trimPrefix, prefix, linecomment, generateValuesMethod, generateIsValidMethod, useTypedErrors) got := string(g.format()) if got != loadGolden(test.name) { // Use this to help build a golden text when changes are needed @@ -416,7 +423,7 @@ func loadGolden(name string) string { return "" } defer fh.Close() - b, err := ioutil.ReadAll(fh) + b, err := io.ReadAll(fh) if err != nil { return "" } diff --git a/stringer.go b/stringer.go index 4c99758..0065976 100644 --- a/stringer.go +++ b/stringer.go @@ -51,6 +51,7 @@ var ( 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") + validateFunc = flag.Bool("validate", false, "if true, a `Validate() error` 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: \"\"") @@ -140,7 +141,7 @@ func main() { // Run generate for each type. for _, typeName := range typs { - g.generate(typeName, *json, *yaml, *sql, *text, *gqlgen, *transformMethod, *trimPrefix, *addPrefix, *linecomment, *altValuesFunc, *typedErrors) + g.generate(typeName, *json, *yaml, *sql, *text, *gqlgen, *transformMethod, *trimPrefix, *addPrefix, *linecomment, *altValuesFunc, *validateFunc, *typedErrors) } // Format the output. @@ -420,7 +421,9 @@ 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, useTypedErrors bool) { + transformMethod string, trimPrefix string, addPrefix string, + lineComment, includeValuesMethod, includeValidateFunc, useTypedErrors bool, +) { values := make([]Value, 0, 100) for _, file := range g.pkg.files { file.lineComment = lineComment @@ -470,6 +473,9 @@ func (g *Generator) generate(typeName string, if includeValuesMethod { g.buildAltStringValuesMethod(typeName) } + if includeValidateFunc { + g.buildValidateMethod(typeName) + } g.buildNoOpOrderChangeDetect(runs, typeName) diff --git a/testdata/validate.golden b/testdata/validate.golden new file mode 100644 index 0000000..3f70b72 --- /dev/null +++ b/testdata/validate.golden @@ -0,0 +1,98 @@ + +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]] +} + +// Validate returns an error if the value is not listed in the enum definition. +func (i Day) Validate() error { + if !i.IsADay() { + return fmt.Errorf("%v is not a valid value for Day values", i) + } + return nil +} + +// 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 +}