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/README.md b/README.md index 6f78c36..83ad7b0 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -#Enumer +# Enumer Enumer is a tool to generate Go code that adds useful methods to Go enums (constants with a specific type). It started as a fork of [Rob Pike’s Stringer tool](https://godoc.org/golang.org/x/tools/cmd/stringer). -##Generated functions and methods +## Generated functions and methods When Enumer is applied to a type, it will generate: * A method `String()` that returns the string representation of the enum value. This makes the enum conform @@ -72,6 +72,28 @@ pillJSON := Aspirin.MarshalJSON() The generated code is exactly the same as the Stringer tool plus the mentioned additions, so you can use **Enumer** where you are already using **Stringer** without any code change. +## Enum value string representation transformation + +Stringer tool uses the same name for enum value string representation (usually CamelCase in Go). + +```go +type MyType int + + ... + +name := MyTypeValue.String() // name => "MyTypeValue" +``` + +Sometimes you need to use some other string representation format then CamelCase (i.e. in JSON). + +To transform enum value string representation from CamelCase to snake_case or kebab-case `transform` flag could be used. + +For example, for `enumer -type=MyType -json -transform=snake` command the next string representation will be generated: + +```go +name := MyTypeValue.String() // name => "my_type_value" +``` + ## How to use The usage of Enumer is the same as Stringer, so you can refer to the [Stringer docs](https://godoc.org/golang.org/x/tools/cmd/stringer) for more information. @@ -81,6 +103,10 @@ 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. +The default value for `transform` flag is `noop` which means no transformation will be performed. + ## 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 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"} +}