Merge pull request #6 from THE108/transform

Enum value string representation transformation feature
This commit is contained in:
Álvaro López Espinosa 2017-02-09 10:48:40 +00:00 committed by GitHub
commit 5b38bf6a65
13 changed files with 382 additions and 15 deletions

28
.gitignore vendored Normal file
View File

@ -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

View File

@ -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 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. **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 ## 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) 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. 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 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. 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 ## Inspiring projects
* [Stringer](https://godoc.org/golang.org/x/tools/cmd/stringer) * [Stringer](https://godoc.org/golang.org/x/tools/cmd/stringer)
* [jsonenums](https://github.com/campoy/jsonenums) * [jsonenums](https://github.com/campoy/jsonenums)

View File

@ -33,7 +33,7 @@ func TestEndToEnd(t *testing.T) {
defer os.RemoveAll(dir) defer os.RemoveAll(dir)
// Create stringer in temporary directory. // Create stringer in temporary directory.
stringer := filepath.Join(dir, "stringer.exe") 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 { if err != nil {
t.Fatalf("building stringer: %s", err) t.Fatalf("building stringer: %s", err)
} }
@ -59,13 +59,20 @@ func TestEndToEnd(t *testing.T) {
} }
// Names are known to be ASCII and long enough. // Names are known to be ASCII and long enough.
typeName := fmt.Sprintf("%c%s", name[0]+'A'-'a', name[1:len(name)-len(".go")]) 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 // 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. // 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) t.Logf("run: %s %s\n", fileName, typeName)
source := filepath.Join(dir, fileName) source := filepath.Join(dir, fileName)
err := copy(source, filepath.Join("testdata", 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") stringSource := filepath.Join(dir, typeName+"_string.go")
// Run stringer in temporary directory. // 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -759,7 +759,7 @@ func runGoldenTest(t *testing.T, test Golden, generateJSON, generateYAML, genera
if len(tokens) != 3 { if len(tokens) != 3 {
t.Fatalf("%s: need type declaration on first line", test.name) 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()) got := string(g.format())
if got != test.output { if got != test.output {
t.Errorf("%s: got\n====\n%s====\nexpected\n====%s", test.name, got, test.output) t.Errorf("%s: got\n====\n%s====\nexpected\n====%s", test.name, got, test.output)

View File

@ -86,6 +86,7 @@ var (
json = flag.Bool("json", false, "if true, json marshaling methods will be generated. Default: false") 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") yaml = flag.Bool("yaml", false, "if true, yaml marshaling methods will be generated. Default: false")
output = flag.String("output", "", "output file name; default srcdir/<type>_string.go") output = flag.String("output", "", "output file name; default srcdir/<type>_string.go")
transformMethod = flag.String("transform", "noop", "enum item name transformation method. Default: noop")
) )
// Usage is a replacement usage function for the flags package. // Usage is a replacement usage function for the flags package.
@ -122,6 +123,7 @@ func main() {
dir string dir string
g Generator g Generator
) )
if len(args) == 1 && isDirectory(args[0]) { if len(args) == 1 && isDirectory(args[0]) {
dir = args[0] dir = args[0]
g.parsePackageDir(args[0]) g.parsePackageDir(args[0])
@ -147,7 +149,7 @@ func main() {
// Run generate for each type. // Run generate for each type.
for _, typeName := range types { for _, typeName := range types {
g.generate(typeName, *json, *yaml, *sql) g.generate(typeName, *json, *yaml, *sql, *transformMethod)
} }
// Format the output. // Format the output.
@ -282,8 +284,24 @@ func (pkg *Package) check(fs *token.FileSet, astFiles []*ast.File) {
pkg.typesPkg = typesPkg 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. // 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) values := make([]Value, 0, 100)
for _, file := range g.pkg.files { for _, file := range g.pkg.files {
// Set the state for this run of the walker. // 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 { if len(values) == 0 {
log.Fatalf("no values defined for type %s", typeName) log.Fatalf("no values defined for type %s", typeName)
} }
g.transformValueNames(values, transformMethod)
runs := splitIntoRuns(values) runs := splitIntoRuns(values)
// The decision of which pattern to use depends on the number of // 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 // 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. // Value represents a declared constant.
type Value struct { 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 // 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 // whether to interpret it as an int64 or a uint64; the only place
// this matters is when sorting. // this matters is when sorting.

25
testdata/transform.go vendored Normal file
View File

@ -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)
}
}

28
transformer.go Normal file
View File

@ -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, "-")
}

14
transformer_test.go Normal file
View File

@ -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)
}
}

3
vendor/github.com/fatih/camelcase/.travis.yml generated vendored Normal file
View File

@ -0,0 +1,3 @@
language: go
go: 1.4

20
vendor/github.com/fatih/camelcase/LICENSE.md generated vendored Normal file
View File

@ -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.

58
vendor/github.com/fatih/camelcase/README.md generated vendored Normal file
View File

@ -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"]
```

90
vendor/github.com/fatih/camelcase/camelcase.go generated vendored Normal file
View File

@ -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
}

47
vendor/github.com/fatih/camelcase/camelcase_test.go generated vendored Normal file
View File

@ -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"}
}