From 51d23ad53116f524d3d96dde0b3b9a04b4c40cad Mon Sep 17 00:00:00 2001 From: Steve Atkins Date: Mon, 10 Apr 2017 11:37:23 -0700 Subject: [PATCH 1/3] add -trimprefix and -autotrimprefix --- README.md | 10 ++++++++-- endtoend_test.go | 2 +- golden_test.go | 2 +- stringer.go | 12 ++++++++++-- trim.go | 47 +++++++++++++++++++++++++++++++++++++++++++++++ trim_test.go | 32 ++++++++++++++++++++++++++++++++ 6 files changed, 99 insertions(+), 6 deletions(-) create mode 100644 trim.go create mode 100644 trim_test.go diff --git a/README.md b/README.md index 77ebb0d..751a89b 100644 --- a/README.md +++ b/README.md @@ -104,10 +104,16 @@ the JSON related methods will be generated. Similarly if the yaml flag is set to the YAML related methods will be generated. And if the sql flag is set to true, the Scanner and Valuer interface will be implemented to seamlessly use the enum in a database model. -For enum string representation transformation `transform` flag was added (i.e. `enumer -type=MyType -json -transform=snake`). -Possible values are `snake` and `kebab` for transformation to snake_case and kebab-case accordingly. +For enum string representation transformation the `transform`, `trimprefix` and `autotrimprefix` flags +were added (i.e. `enumer -type=MyType -json -transform=snake`). +Possible transform values are `snake` and `kebab` for transformation to snake_case and kebab-case accordingly. The default value for `transform` flag is `noop` which means no transformation will be performed. +If a prefix is provided via the `trimprefix` flag that will be trimmed from the start of each name (before +it is transformed). If a name doesn't have the prefix it will be passed unchanged. + +If the `autotrimprefix` flag is set then if all the names in an enum have a common prefix that prefix will be removed. + ## Inspiring projects * [Stringer](https://godoc.org/golang.org/x/tools/cmd/stringer) * [jsonenums](https://github.com/campoy/jsonenums) diff --git a/endtoend_test.go b/endtoend_test.go index 40c91f7..9f18e23 100644 --- a/endtoend_test.go +++ b/endtoend_test.go @@ -33,7 +33,7 @@ func TestEndToEnd(t *testing.T) { defer os.RemoveAll(dir) // Create stringer in temporary directory. stringer := filepath.Join(dir, "stringer.exe") - err = run("go", "build", "-o", stringer, "enumer.go", "sql.go", "stringer.go", "transformer.go") + err = run("go", "build", "-o", stringer, "enumer.go", "sql.go", "stringer.go", "transformer.go", "trim.go") if err != nil { t.Fatalf("building stringer: %s", err) } diff --git a/golden_test.go b/golden_test.go index 72f7bc0..8588ac7 100644 --- a/golden_test.go +++ b/golden_test.go @@ -759,7 +759,7 @@ func runGoldenTest(t *testing.T, test Golden, generateJSON, generateYAML, genera if len(tokens) != 3 { t.Fatalf("%s: need type declaration on first line", test.name) } - g.generate(tokens[1], generateJSON, generateYAML, generateSQL, "noop") + g.generate(tokens[1], generateJSON, generateYAML, generateSQL, "noop", "", false) got := string(g.format()) if got != test.output { t.Errorf("%s: got\n====\n%s====\nexpected\n====%s", test.name, got, test.output) diff --git a/stringer.go b/stringer.go index ccfb304..ffba563 100644 --- a/stringer.go +++ b/stringer.go @@ -87,6 +87,8 @@ var ( yaml = flag.Bool("yaml", false, "if true, yaml marshaling methods 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. Default: \"\"") + autoTrimPrefix = flag.Bool("autotrimprefix", false, "if true, remove a common prefix from each item name. Default: false") ) // Usage is a replacement usage function for the flags package. @@ -149,7 +151,7 @@ func main() { // Run generate for each type. for _, typeName := range types { - g.generate(typeName, *json, *yaml, *sql, *transformMethod) + g.generate(typeName, *json, *yaml, *sql, *transformMethod, *trimPrefix, *autoTrimPrefix) } // Format the output. @@ -301,7 +303,7 @@ func (g *Generator) transformValueNames(values []Value, transformMethod string) } // generate produces the String method for the named type. -func (g *Generator) generate(typeName string, includeJSON, includeYAML, includeSQL bool, transformMethod string) { +func (g *Generator) generate(typeName string, includeJSON, includeYAML, includeSQL bool, transformMethod string, trimPrefix string, autoTrimPrefix bool) { values := make([]Value, 0, 100) for _, file := range g.pkg.files { // Set the state for this run of the walker. @@ -317,6 +319,12 @@ func (g *Generator) generate(typeName string, includeJSON, includeYAML, includeS log.Fatalf("no values defined for type %s", typeName) } + g.trimValueNames(values, trimPrefix) + + if autoTrimPrefix { + g.autoTrimValueNames(values) + } + g.transformValueNames(values, transformMethod) runs := splitIntoRuns(values) diff --git a/trim.go b/trim.go new file mode 100644 index 0000000..2fb2af0 --- /dev/null +++ b/trim.go @@ -0,0 +1,47 @@ +package main + +import "strings" + +func longestCommonPrefix(values []Value) string { + // LCP of min and max (lexigraphically) + // is the LCP of the whole set. + min := values[0].name + max := min + for _, s := range values[1:] { + switch { + case s.name < min: + min = s.name + case s.name > max: + max = s.name + } + } + for i := 0; i < len(min) && i < len(max); i++ { + if min[i] != max[i] { + return min[:i] + } + } + // If all the bytes are equal but the lengths aren't then + // min is a prefix of max, and hence the lcp + return min +} + +func autoPrefix(values []Value) string { + // Can't trim a single value + if len(values) < 2 { + return "" + } + prefix := longestCommonPrefix(values) + + return prefix +} + +func (g *Generator) trimValueNames(values []Value, prefix string) { + for i := range values { + values[i].name = strings.TrimPrefix(values[i].name, prefix) + } +} + +func (g *Generator) autoTrimValueNames(values []Value) { + prefix := autoPrefix(values) + g.trimValueNames(values, prefix) +} diff --git a/trim_test.go b/trim_test.go new file mode 100644 index 0000000..d1b79e6 --- /dev/null +++ b/trim_test.go @@ -0,0 +1,32 @@ +package main + +import "testing" + +var lcpTests = []struct { + expected string + in []string +}{ + {"Proto", []string{"ProtoOne", "ProtoTwo", "ProtoThree"}}, + // An empty string is OK when one value is the prefix + {"Proto", []string{"Proto", "ProtoLonger"}}, + {"", []string{}}, + {"", []string{"aardvark"}}, + {"", []string{"abc", "def", "deg"}}, + {"ab", []string{"ab", "abc", "abcd"}}, +} + +// TestLcp checks that the longest common prefix is generated correctly +func TestLcp(t *testing.T) { + for _, tt := range lcpTests { + values := make([]Value, len(tt.in)) + for i := range tt.in { + values[i] = Value{ + name: tt.in[i], + } + } + prefix := autoPrefix(values) + if prefix != tt.expected { + t.Errorf("%q => %s, expected %s", tt.in, tt.expected, prefix) + } + } +} From e1f9d0844946bb8a8183c4d4671fbb1e0da9721e Mon Sep 17 00:00:00 2001 From: Steve Atkins Date: Mon, 5 Feb 2018 16:09:28 -0800 Subject: [PATCH 2/3] Add expected comments. --- golden_test.go | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/golden_test.go b/golden_test.go index bc90c20..d016548 100644 --- a/golden_test.go +++ b/golden_test.go @@ -83,6 +83,8 @@ var _DayNameToValueMap = map[string]Day{ _DayName[44:50]: 6, } +// 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 @@ -122,6 +124,8 @@ var _NumberNameToValueMap = map[string]Number{ _NumberName[6:11]: 3, } +// NumberString retrieves an enum value from the enum constants string name. +// Throws an error if the param is not part of the enum. func NumberString(s string) (Number, error) { if val, ok := _NumberNameToValueMap[s]; ok { return val, nil @@ -183,6 +187,8 @@ var _GapNameToValueMap = map[string]Gap{ _GapName_2[0:6]: 11, } +// GapString retrieves an enum value from the enum constants string name. +// Throws an error if the param is not part of the enum. func GapString(s string) (Gap, error) { if val, ok := _GapNameToValueMap[s]; ok { return val, nil @@ -223,6 +229,8 @@ var _NumNameToValueMap = map[string]Num{ _NumName[10:12]: 2, } +// NumString retrieves an enum value from the enum constants string name. +// Throws an error if the param is not part of the enum. func NumString(s string) (Num, error) { if val, ok := _NumNameToValueMap[s]; ok { return val, nil @@ -276,6 +284,8 @@ var _UnumNameToValueMap = map[string]Unum{ _UnumName_1[3:6]: 254, } +// UnumString retrieves an enum value from the enum constants string name. +// Throws an error if the param is not part of the enum. func UnumString(s string) (Unum, error) { if val, ok := _UnumNameToValueMap[s]; ok { return val, nil @@ -347,6 +357,8 @@ var _PrimeNameToValueMap = map[string]Prime{ _PrimeName[32:35]: 43, } +// PrimeString retrieves an enum value from the enum constants string name. +// Throws an error if the param is not part of the enum. func PrimeString(s string) (Prime, error) { if val, ok := _PrimeNameToValueMap[s]; ok { return val, nil @@ -415,6 +427,8 @@ var _PrimeNameToValueMap = map[string]Prime{ _PrimeName[32:35]: 43, } +// PrimeString retrieves an enum value from the enum constants string name. +// Throws an error if the param is not part of the enum. func PrimeString(s string) (Prime, error) { if val, ok := _PrimeNameToValueMap[s]; ok { return val, nil @@ -499,6 +513,8 @@ var _PrimeNameToValueMap = map[string]Prime{ _PrimeName[32:35]: 43, } +// PrimeString retrieves an enum value from the enum constants string name. +// Throws an error if the param is not part of the enum. func PrimeString(s string) (Prime, error) { if val, ok := _PrimeNameToValueMap[s]; ok { return val, nil @@ -583,6 +599,8 @@ var _PrimeNameToValueMap = map[string]Prime{ _PrimeName[32:35]: 43, } +// PrimeString retrieves an enum value from the enum constants string name. +// Throws an error if the param is not part of the enum. func PrimeString(s string) (Prime, error) { if val, ok := _PrimeNameToValueMap[s]; ok { return val, nil @@ -680,6 +698,8 @@ var _PrimeNameToValueMap = map[string]Prime{ _PrimeName[32:35]: 43, } +// PrimeString retrieves an enum value from the enum constants string name. +// Throws an error if the param is not part of the enum. func PrimeString(s string) (Prime, error) { if val, ok := _PrimeNameToValueMap[s]; ok { return val, nil @@ -759,7 +779,7 @@ func runGoldenTest(t *testing.T, test Golden, generateJSON, generateYAML, genera if len(tokens) != 3 { t.Fatalf("%s: need type declaration on first line", test.name) } - g.generate(tokens[1], generateJSON, generateYAML, generateSQL, "noop", "", false) + g.generate(tokens[1], generateJSON, generateYAML, generateSQL, "noop") got := string(g.format()) if got != test.output { t.Errorf("%s: got\n====\n%s====\nexpected\n====%s", test.name, got, test.output) From bd9938fa865a8b637a93323d949781608dd293c8 Mon Sep 17 00:00:00 2001 From: Steve Atkins Date: Mon, 5 Feb 2018 16:48:43 -0800 Subject: [PATCH 3/3] Remove autotrimprefix option; add basic tests for -trimprefix. --- README.md | 4 +--- golden_test.go | 33 ++++++++++++++++++++++++++------- stringer.go | 16 +++++++++------- trim.go | 47 ----------------------------------------------- trim_test.go | 32 -------------------------------- 5 files changed, 36 insertions(+), 96 deletions(-) delete mode 100644 trim.go delete mode 100644 trim_test.go diff --git a/README.md b/README.md index ff00948..1ce1548 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ the JSON related methods will be generated. Similarly if the yaml flag is set to the YAML related methods will be generated. And if the sql flag is set to true, the Scanner and Valuer interface will be implemented to seamlessly use the enum in a database model. -For enum string representation transformation the `transform`, `trimprefix` and `autotrimprefix` flags +For enum string representation transformation the `transform` and `trimprefix` flags were added (i.e. `enumer -type=MyType -json -transform=snake`). Possible transform values are `snake` and `kebab` for transformation to snake_case and kebab-case accordingly. The default value for `transform` flag is `noop` which means no transformation will be performed. @@ -112,8 +112,6 @@ The default value for `transform` flag is `noop` which means no transformation w If a prefix is provided via the `trimprefix` flag that will be trimmed from the start of each name (before it is transformed). If a name doesn't have the prefix it will be passed unchanged. -If the `autotrimprefix` flag is set then if all the names in an enum have a common prefix that prefix will be removed. - ## Inspiring projects * [Stringer](https://godoc.org/golang.org/x/tools/cmd/stringer) * [jsonenums](https://github.com/campoy/jsonenums) diff --git a/golden_test.go b/golden_test.go index d016548..1d1ac46 100644 --- a/golden_test.go +++ b/golden_test.go @@ -46,6 +46,10 @@ var goldenJSONAndSQL = []Golden{ {"prime", prime_json_and_sql_in, prime_json_and_sql_out}, } +var goldenPrefix = []Golden{ + {"prefix", prefix_in, day_out}, +} + // Each example starts with "type XXX [u]int", with a single space separating them. // Simple test: enumeration of type int starting at 0. @@ -751,25 +755,40 @@ func (i *Prime) Scan(value interface{}) error { } ` +const prefix_in = `type Day int +const ( + DayMonday Day = iota + DayTuesday + DayWednesday + DayThursday + DayFriday + DaySaturday + DaySunday +) +` + func TestGolden(t *testing.T) { for _, test := range golden { - runGoldenTest(t, test, false, false, false) + runGoldenTest(t, test, false, false, false, "") } for _, test := range goldenJSON { - runGoldenTest(t, test, true, false, false) + runGoldenTest(t, test, true, false, false, "") } for _, test := range goldenYAML { - runGoldenTest(t, test, false, true, false) + runGoldenTest(t, test, false, true, false, "") } for _, test := range goldenSQL { - runGoldenTest(t, test, false, false, true) + runGoldenTest(t, test, false, false, true, "") } for _, test := range goldenJSONAndSQL { - runGoldenTest(t, test, true, false, true) + runGoldenTest(t, test, true, false, true, "") + } + for _, test := range goldenPrefix { + runGoldenTest(t, test, false, false, false, "Day") } } -func runGoldenTest(t *testing.T, test Golden, generateJSON, generateYAML, generateSQL bool) { +func runGoldenTest(t *testing.T, test Golden, generateJSON, generateYAML, generateSQL bool, prefix string) { var g Generator input := "package test\n" + test.input file := test.name + ".go" @@ -779,7 +798,7 @@ func runGoldenTest(t *testing.T, test Golden, generateJSON, generateYAML, genera if len(tokens) != 3 { t.Fatalf("%s: need type declaration on first line", test.name) } - g.generate(tokens[1], generateJSON, generateYAML, generateSQL, "noop") + g.generate(tokens[1], generateJSON, generateYAML, generateSQL, "noop", prefix) got := string(g.format()) if got != test.output { t.Errorf("%s: got\n====\n%s====\nexpected\n====%s", test.name, got, test.output) diff --git a/stringer.go b/stringer.go index 9c36959..b257965 100644 --- a/stringer.go +++ b/stringer.go @@ -94,7 +94,6 @@ var ( 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. Default: \"\"") - autoTrimPrefix = flag.Bool("autotrimprefix", false, "if true, remove a common prefix from each item name. Default: false") ) // Usage is a replacement usage function for the flags package. @@ -157,7 +156,7 @@ func main() { // Run generate for each type. for _, typeName := range types { - g.generate(typeName, *json, *yaml, *sql, *transformMethod, *trimPrefix, *autoTrimPrefix) + g.generate(typeName, *json, *yaml, *sql, *transformMethod, *trimPrefix) } // Format the output. @@ -308,8 +307,15 @@ func (g *Generator) transformValueNames(values []Value, transformMethod string) } } +// trimValueNames removes a prefix from each name +func (g *Generator) trimValueNames(values []Value, prefix string) { + for i := range values { + values[i].name = strings.TrimPrefix(values[i].name, prefix) + } +} + // generate produces the String method for the named type. -func (g *Generator) generate(typeName string, includeJSON, includeYAML, includeSQL bool, transformMethod string, trimPrefix string, autoTrimPrefix bool) { +func (g *Generator) generate(typeName string, includeJSON, includeYAML, includeSQL bool, transformMethod string, trimPrefix string) { values := make([]Value, 0, 100) for _, file := range g.pkg.files { // Set the state for this run of the walker. @@ -327,10 +333,6 @@ func (g *Generator) generate(typeName string, includeJSON, includeYAML, includeS g.trimValueNames(values, trimPrefix) - if autoTrimPrefix { - g.autoTrimValueNames(values) - } - g.transformValueNames(values, transformMethod) runs := splitIntoRuns(values) diff --git a/trim.go b/trim.go deleted file mode 100644 index 2fb2af0..0000000 --- a/trim.go +++ /dev/null @@ -1,47 +0,0 @@ -package main - -import "strings" - -func longestCommonPrefix(values []Value) string { - // LCP of min and max (lexigraphically) - // is the LCP of the whole set. - min := values[0].name - max := min - for _, s := range values[1:] { - switch { - case s.name < min: - min = s.name - case s.name > max: - max = s.name - } - } - for i := 0; i < len(min) && i < len(max); i++ { - if min[i] != max[i] { - return min[:i] - } - } - // If all the bytes are equal but the lengths aren't then - // min is a prefix of max, and hence the lcp - return min -} - -func autoPrefix(values []Value) string { - // Can't trim a single value - if len(values) < 2 { - return "" - } - prefix := longestCommonPrefix(values) - - return prefix -} - -func (g *Generator) trimValueNames(values []Value, prefix string) { - for i := range values { - values[i].name = strings.TrimPrefix(values[i].name, prefix) - } -} - -func (g *Generator) autoTrimValueNames(values []Value) { - prefix := autoPrefix(values) - g.trimValueNames(values, prefix) -} diff --git a/trim_test.go b/trim_test.go deleted file mode 100644 index d1b79e6..0000000 --- a/trim_test.go +++ /dev/null @@ -1,32 +0,0 @@ -package main - -import "testing" - -var lcpTests = []struct { - expected string - in []string -}{ - {"Proto", []string{"ProtoOne", "ProtoTwo", "ProtoThree"}}, - // An empty string is OK when one value is the prefix - {"Proto", []string{"Proto", "ProtoLonger"}}, - {"", []string{}}, - {"", []string{"aardvark"}}, - {"", []string{"abc", "def", "deg"}}, - {"ab", []string{"ab", "abc", "abcd"}}, -} - -// TestLcp checks that the longest common prefix is generated correctly -func TestLcp(t *testing.T) { - for _, tt := range lcpTests { - values := make([]Value, len(tt.in)) - for i := range tt.in { - values[i] = Value{ - name: tt.in[i], - } - } - prefix := autoPrefix(values) - if prefix != tt.expected { - t.Errorf("%q => %s, expected %s", tt.in, tt.expected, prefix) - } - } -}