From 432dffaa9517c8333b98273a3744bd43aa218910 Mon Sep 17 00:00:00 2001 From: achernov Date: Wed, 1 Feb 2017 10:01:12 +0300 Subject: [PATCH] Add enum value string representation transformation feature --- .gitignore | 28 ++++++ endtoend_test.go | 15 +++- golden_test.go | 2 +- stringer.go | 37 ++++++-- testdata/transform.go | 25 ++++++ transformer.go | 28 ++++++ transformer_test.go | 14 +++ vendor/github.com/fatih/camelcase/.travis.yml | 3 + vendor/github.com/fatih/camelcase/LICENSE.md | 20 +++++ vendor/github.com/fatih/camelcase/README.md | 58 ++++++++++++ .../github.com/fatih/camelcase/camelcase.go | 90 +++++++++++++++++++ .../fatih/camelcase/camelcase_test.go | 47 ++++++++++ 12 files changed, 354 insertions(+), 13 deletions(-) create mode 100644 .gitignore create mode 100644 testdata/transform.go create mode 100644 transformer.go create mode 100644 transformer_test.go create mode 100644 vendor/github.com/fatih/camelcase/.travis.yml create mode 100644 vendor/github.com/fatih/camelcase/LICENSE.md create mode 100644 vendor/github.com/fatih/camelcase/README.md create mode 100644 vendor/github.com/fatih/camelcase/camelcase.go create mode 100644 vendor/github.com/fatih/camelcase/camelcase_test.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..99f7832 --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +# Created by .ignore support plugin (hsz.mobi) +### Go template +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof + +.idea diff --git a/endtoend_test.go b/endtoend_test.go index 9e0eddc..40c91f7 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") + err = run("go", "build", "-o", stringer, "enumer.go", "sql.go", "stringer.go", "transformer.go") if err != nil { t.Fatalf("building stringer: %s", err) } @@ -59,13 +59,20 @@ func TestEndToEnd(t *testing.T) { } // Names are known to be ASCII and long enough. typeName := fmt.Sprintf("%c%s", name[0]+'A'-'a', name[1:len(name)-len(".go")]) - stringerCompileAndRun(t, dir, stringer, typeName, name) + transformNameMethod := "noop" + + if name == "transform.go" { + typeName = "CamelCaseValue" + transformNameMethod = "snake" + } + + stringerCompileAndRun(t, dir, stringer, typeName, name, transformNameMethod) } } // stringerCompileAndRun runs stringer for the named file and compiles and // runs the target binary in directory dir. That binary will panic if the String method is incorrect. -func stringerCompileAndRun(t *testing.T, dir, stringer, typeName, fileName string) { +func stringerCompileAndRun(t *testing.T, dir, stringer, typeName, fileName, transformNameMethod string) { t.Logf("run: %s %s\n", fileName, typeName) source := filepath.Join(dir, fileName) err := copy(source, filepath.Join("testdata", fileName)) @@ -74,7 +81,7 @@ func stringerCompileAndRun(t *testing.T, dir, stringer, typeName, fileName strin } stringSource := filepath.Join(dir, typeName+"_string.go") // Run stringer in temporary directory. - err = run(stringer, "-type", typeName, "-output", stringSource, source) + err = run(stringer, "-type", typeName, "-output", stringSource, "-transform", transformNameMethod, source) if err != nil { t.Fatal(err) } diff --git a/golden_test.go b/golden_test.go index c64c231..72f7bc0 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) + 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) diff --git a/stringer.go b/stringer.go index 100198d..ccfb304 100644 --- a/stringer.go +++ b/stringer.go @@ -81,11 +81,12 @@ import ( ) 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") - output = flag.String("output", "", "output file name; default srcdir/_string.go") + 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") + output = flag.String("output", "", "output file name; default srcdir/_string.go") + transformMethod = flag.String("transform", "noop", "enum item name transformation method. Default: noop") ) // Usage is a replacement usage function for the flags package. @@ -122,6 +123,7 @@ func main() { dir string g Generator ) + if len(args) == 1 && isDirectory(args[0]) { dir = args[0] g.parsePackageDir(args[0]) @@ -147,7 +149,7 @@ func main() { // Run generate for each type. for _, typeName := range types { - g.generate(typeName, *json, *yaml, *sql) + g.generate(typeName, *json, *yaml, *sql, *transformMethod) } // Format the output. @@ -282,8 +284,24 @@ func (pkg *Package) check(fs *token.FileSet, astFiles []*ast.File) { pkg.typesPkg = typesPkg } +func (g *Generator) transformValueNames(values []Value, transformMethod string) { + var transform func(string) string + switch transformMethod { + case "snake": + transform = toSnakeCase + case "kebab": + transform = toKebabCase + default: + return + } + + for i := range values { + values[i].name = transform(values[i].name) + } +} + // generate produces the String method for the named type. -func (g *Generator) generate(typeName string, includeJSON, includeYAML, includeSQL bool) { +func (g *Generator) generate(typeName string, includeJSON, includeYAML, includeSQL bool, transformMethod string) { values := make([]Value, 0, 100) for _, file := range g.pkg.files { // Set the state for this run of the walker. @@ -298,6 +316,9 @@ func (g *Generator) generate(typeName string, includeJSON, includeYAML, includeS if len(values) == 0 { log.Fatalf("no values defined for type %s", typeName) } + + g.transformValueNames(values, transformMethod) + runs := splitIntoRuns(values) // The decision of which pattern to use depends on the number of // runs in the numbers. If there's only one, it's easy. For more than @@ -380,7 +401,7 @@ func (g *Generator) format() []byte { // Value represents a declared constant. type Value struct { - name string // The name of the constant. + name string // The name of the constant after transformation (i.e. camel case => snake case) // The value is stored as a bit pattern alone. The boolean tells us // whether to interpret it as an int64 or a uint64; the only place // this matters is when sorting. diff --git a/testdata/transform.go b/testdata/transform.go new file mode 100644 index 0000000..70d4b1f --- /dev/null +++ b/testdata/transform.go @@ -0,0 +1,25 @@ +package main + +import "fmt" + +type CamelCaseValue int + +const ( + CamelCaseValueOne CamelCaseValue = iota + CamelCaseValueTwo + CamelCaseValueThree +) + +func main() { + ck(CamelCaseValueOne, "camel_case_value_one") + ck(CamelCaseValueTwo, "camel_case_value_two") + ck(CamelCaseValueThree, "camel_case_value_three") + ck(-127, "CamelCaseValue(-127)") + ck(127, "CamelCaseValue(127)") +} + +func ck(value CamelCaseValue, str string) { + if fmt.Sprint(value) != str { + panic("transform.go: " + str) + } +} diff --git a/transformer.go b/transformer.go new file mode 100644 index 0000000..617b027 --- /dev/null +++ b/transformer.go @@ -0,0 +1,28 @@ +package main + +import ( + "strings" + + "github.com/fatih/camelcase" +) + +func transform(src, delim string) string { + entries := camelcase.Split(src) + if len(entries) <= 1 { + return strings.ToLower(src) + } + + result := strings.ToLower(entries[0]) + for i := 1; i < len(entries); i++ { + result += delim + strings.ToLower(entries[i]) + } + return result +} + +func toSnakeCase(src string) string { + return transform(src, "_") +} + +func toKebabCase(src string) string { + return transform(src, "-") +} diff --git a/transformer_test.go b/transformer_test.go new file mode 100644 index 0000000..0685f56 --- /dev/null +++ b/transformer_test.go @@ -0,0 +1,14 @@ +package main + +import ( + "testing" +) + +func TestTransform(t *testing.T) { + result := transform("CamelCaseStringValue", ".") + + const expected = "camel.case.string.value" + if result != expected { + t.Errorf("\ngot: %s\n====\nexpected: %s", result, expected) + } +} diff --git a/vendor/github.com/fatih/camelcase/.travis.yml b/vendor/github.com/fatih/camelcase/.travis.yml new file mode 100644 index 0000000..0244e46 --- /dev/null +++ b/vendor/github.com/fatih/camelcase/.travis.yml @@ -0,0 +1,3 @@ +language: go +go: 1.4 + diff --git a/vendor/github.com/fatih/camelcase/LICENSE.md b/vendor/github.com/fatih/camelcase/LICENSE.md new file mode 100644 index 0000000..aa4a536 --- /dev/null +++ b/vendor/github.com/fatih/camelcase/LICENSE.md @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2015 Fatih Arslan + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/fatih/camelcase/README.md b/vendor/github.com/fatih/camelcase/README.md new file mode 100644 index 0000000..105a6ae --- /dev/null +++ b/vendor/github.com/fatih/camelcase/README.md @@ -0,0 +1,58 @@ +# CamelCase [![GoDoc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](http://godoc.org/github.com/fatih/camelcase) [![Build Status](http://img.shields.io/travis/fatih/camelcase.svg?style=flat-square)](https://travis-ci.org/fatih/camelcase) + +CamelCase is a Golang (Go) package to split the words of a camelcase type +string into a slice of words. It can be used to convert a camelcase word (lower +or upper case) into any type of word. + +## Splitting rules: + +1. If string is not valid UTF-8, return it without splitting as + single item array. +2. Assign all unicode characters into one of 4 sets: lower case + letters, upper case letters, numbers, and all other characters. +3. Iterate through characters of string, introducing splits + between adjacent characters that belong to different sets. +4. Iterate through array of split strings, and if a given string + is upper case: + * if subsequent string is lower case: + * move last character of upper case string to beginning of + lower case string + +## Install + +```bash +go get github.com/fatih/camelcase +``` + +## Usage and examples + +```go +splitted := camelcase.Split("GolangPackage") + +fmt.Println(splitted[0], splitted[1]) // prints: "Golang", "Package" +``` + +Both lower camel case and upper camel case are supported. For more info please +check: [http://en.wikipedia.org/wiki/CamelCase](http://en.wikipedia.org/wiki/CamelCase) + +Below are some example cases: + +``` +"" => [] +"lowercase" => ["lowercase"] +"Class" => ["Class"] +"MyClass" => ["My", "Class"] +"MyC" => ["My", "C"] +"HTML" => ["HTML"] +"PDFLoader" => ["PDF", "Loader"] +"AString" => ["A", "String"] +"SimpleXMLParser" => ["Simple", "XML", "Parser"] +"vimRPCPlugin" => ["vim", "RPC", "Plugin"] +"GL11Version" => ["GL", "11", "Version"] +"99Bottles" => ["99", "Bottles"] +"May5" => ["May", "5"] +"BFG9000" => ["BFG", "9000"] +"BöseÜberraschung" => ["Böse", "Überraschung"] +"Two spaces" => ["Two", " ", "spaces"] +"BadUTF8\xe2\xe2\xa1" => ["BadUTF8\xe2\xe2\xa1"] +``` diff --git a/vendor/github.com/fatih/camelcase/camelcase.go b/vendor/github.com/fatih/camelcase/camelcase.go new file mode 100644 index 0000000..02160c9 --- /dev/null +++ b/vendor/github.com/fatih/camelcase/camelcase.go @@ -0,0 +1,90 @@ +// Package camelcase is a micro package to split the words of a camelcase type +// string into a slice of words. +package camelcase + +import ( + "unicode" + "unicode/utf8" +) + +// Split splits the camelcase word and returns a list of words. It also +// supports digits. Both lower camel case and upper camel case are supported. +// For more info please check: http://en.wikipedia.org/wiki/CamelCase +// +// Examples +// +// "" => [""] +// "lowercase" => ["lowercase"] +// "Class" => ["Class"] +// "MyClass" => ["My", "Class"] +// "MyC" => ["My", "C"] +// "HTML" => ["HTML"] +// "PDFLoader" => ["PDF", "Loader"] +// "AString" => ["A", "String"] +// "SimpleXMLParser" => ["Simple", "XML", "Parser"] +// "vimRPCPlugin" => ["vim", "RPC", "Plugin"] +// "GL11Version" => ["GL", "11", "Version"] +// "99Bottles" => ["99", "Bottles"] +// "May5" => ["May", "5"] +// "BFG9000" => ["BFG", "9000"] +// "BöseÜberraschung" => ["Böse", "Überraschung"] +// "Two spaces" => ["Two", " ", "spaces"] +// "BadUTF8\xe2\xe2\xa1" => ["BadUTF8\xe2\xe2\xa1"] +// +// Splitting rules +// +// 1) If string is not valid UTF-8, return it without splitting as +// single item array. +// 2) Assign all unicode characters into one of 4 sets: lower case +// letters, upper case letters, numbers, and all other characters. +// 3) Iterate through characters of string, introducing splits +// between adjacent characters that belong to different sets. +// 4) Iterate through array of split strings, and if a given string +// is upper case: +// if subsequent string is lower case: +// move last character of upper case string to beginning of +// lower case string +func Split(src string) (entries []string) { + // don't split invalid utf8 + if !utf8.ValidString(src) { + return []string{src} + } + entries = []string{} + var runes [][]rune + lastClass := 0 + class := 0 + // split into fields based on class of unicode character + for _, r := range src { + switch true { + case unicode.IsLower(r): + class = 1 + case unicode.IsUpper(r): + class = 2 + case unicode.IsDigit(r): + class = 3 + default: + class = 4 + } + if class == lastClass { + runes[len(runes)-1] = append(runes[len(runes)-1], r) + } else { + runes = append(runes, []rune{r}) + } + lastClass = class + } + // handle upper case -> lower case sequences, e.g. + // "PDFL", "oader" -> "PDF", "Loader" + for i := 0; i < len(runes)-1; i++ { + if unicode.IsUpper(runes[i][0]) && unicode.IsLower(runes[i+1][0]) { + runes[i+1] = append([]rune{runes[i][len(runes[i])-1]}, runes[i+1]...) + runes[i] = runes[i][:len(runes[i])-1] + } + } + // construct []string from results + for _, s := range runes { + if len(s) > 0 { + entries = append(entries, string(s)) + } + } + return +} diff --git a/vendor/github.com/fatih/camelcase/camelcase_test.go b/vendor/github.com/fatih/camelcase/camelcase_test.go new file mode 100644 index 0000000..79d3f3a --- /dev/null +++ b/vendor/github.com/fatih/camelcase/camelcase_test.go @@ -0,0 +1,47 @@ +package camelcase + +import "fmt" + +func ExampleSplit() { + + for _, c := range []string{ + "", + "lowercase", + "Class", + "MyClass", + "MyC", + "HTML", + "PDFLoader", + "AString", + "SimpleXMLParser", + "vimRPCPlugin", + "GL11Version", + "99Bottles", + "May5", + "BFG9000", + "BöseÜberraschung", + "Two spaces", + "BadUTF8\xe2\xe2\xa1", + } { + fmt.Printf("%#v => %#v\n", c, Split(c)) + } + + // Output: + // "" => []string{} + // "lowercase" => []string{"lowercase"} + // "Class" => []string{"Class"} + // "MyClass" => []string{"My", "Class"} + // "MyC" => []string{"My", "C"} + // "HTML" => []string{"HTML"} + // "PDFLoader" => []string{"PDF", "Loader"} + // "AString" => []string{"A", "String"} + // "SimpleXMLParser" => []string{"Simple", "XML", "Parser"} + // "vimRPCPlugin" => []string{"vim", "RPC", "Plugin"} + // "GL11Version" => []string{"GL", "11", "Version"} + // "99Bottles" => []string{"99", "Bottles"} + // "May5" => []string{"May", "5"} + // "BFG9000" => []string{"BFG", "9000"} + // "BöseÜberraschung" => []string{"Böse", "Überraschung"} + // "Two spaces" => []string{"Two", " ", "spaces"} + // "BadUTF8\xe2\xe2\xa1" => []string{"BadUTF8\xe2\xe2\xa1"} +}