Add Transformers

- snake-upper
- kebab-upper
- lower (Lowercase)
- upper (Uppercase)
- title (TitleCase)
- first (Use first character of string)
- first-lower (same as first only lower case)
- first-upper (same as first only upper case)
This commit is contained in:
Gert-Jan Timmer 2019-01-17 13:07:52 +01:00
parent 6bcfe2edaa
commit 78452a952a
14 changed files with 380 additions and 60 deletions

View File

@ -1,32 +1,36 @@
# Enumer [![GoDoc](https://godoc.org/github.com/alvaroloes/enumer?status.svg)](https://godoc.org/github.com/alvaroloes/enumer) [![Go Report Card](https://goreportcard.com/badge/github.com/alvaroloes/enumer)](https://goreportcard.com/report/github.com/alvaroloes/enumer) [![cover.run go](https://cover.run/go/github.com/alvaroloes/enumer.svg?tag=golang-1.10)](https://cover.run/go/github.com/alvaroloes/enumer?tag=golang-1.10) # Enumer [![GoDoc](https://godoc.org/github.com/alvaroloes/enumer?status.svg)](https://godoc.org/github.com/alvaroloes/enumer) [![Go Report Card](https://goreportcard.com/badge/github.com/alvaroloes/enumer)](https://goreportcard.com/report/github.com/alvaroloes/enumer) [![cover.run go](https://cover.run/go/github.com/alvaroloes/enumer.svg?tag=golang-1.10)](https://cover.run/go/github.com/alvaroloes/enumer?tag=golang-1.10)
Enumer is a tool to generate Go code that adds useful methods to Go enums (constants with a specific type). 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 Pikes Stringer tool](https://godoc.org/golang.org/x/tools/cmd/stringer). It started as a fork of [Rob Pikes 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: When Enumer is applied to a type, it will generate:
* The following basic methods/functions: - The following basic methods/functions:
* Method `String()`: returns the string representation of the enum value. This makes the enum conform - Method `String()`: returns the string representation of the enum value. This makes the enum conform
the `Stringer` interface, so whenever you print an enum value, you'll get the string name instead of a number. the `Stringer` interface, so whenever you print an enum value, you'll get the string name instead of a number.
* Function `<Type>String(s string)`: returns the enum value from its string representation. This is useful - Function `<Type>String(s string)`: returns the enum value from its string representation. This is useful
when you need to read enum values from command line arguments, from a configuration file, or when you need to read enum values from command line arguments, from a configuration file, or
from a REST API request... In short, from those places where using the real enum value (an integer) would from a REST API request... In short, from those places where using the real enum value (an integer) would
be almost meaningless or hard to trace or use by a human. be almost meaningless or hard to trace or use by a human.
* Function `<Type>Values()`: returns a slice with all the values of the enum - Function `<Type>Values()`: returns a slice with all the values of the enum
* Method `IsA<Type>()`: returns true only if the current value is among the values of the enum. Useful for validations. - Method `IsA<Type>()`: returns true only if the current value is among the values of the enum. Useful for validations.
* When the flag `json` is provided, two additional methods will be generated, `MarshalJSON()` and `UnmarshalJSON()`. These make
- When the flag `json` is provided, two additional methods will be generated, `MarshalJSON()` and `UnmarshalJSON()`. These make
the enum conform to the `json.Marshaler` and `json.Unmarshaler` interfaces. Very useful to use it in JSON APIs. the enum conform to the `json.Marshaler` and `json.Unmarshaler` interfaces. Very useful to use it in JSON APIs.
* When the flag `text` is provided, two additional methods will be generated, `MarshalText()` and `UnmarshalText()`. These make - When the flag `text` is provided, two additional methods will be generated, `MarshalText()` and `UnmarshalText()`. These make
the enum conform to the `encoding.TextMarshaler` and `encoding.TextUnmarshaler` interfaces. the enum conform to the `encoding.TextMarshaler` and `encoding.TextUnmarshaler` interfaces.
**Note:** If you use your enum values as keys in a map and you encode the map as _JSON_, you need this flag set to true to properly **Note:** If you use your enum values as keys in a map and you encode the map as _JSON_, you need this flag set to true to properly
convert the map keys to json (strings). If not, the numeric values will be used instead convert the map keys to json (strings). If not, the numeric values will be used instead
* When the flag `yaml` is provided, two additional methods will be generated, `MarshalYAML()` and `UnmarshalYAML()`. These make - When the flag `yaml` is provided, two additional methods will be generated, `MarshalYAML()` and `UnmarshalYAML()`. These make
the enum conform to the `gopkg.in/yaml.v2.Marshaler` and `gopkg.in/yaml.v2.Unmarshaler` interfaces. the enum conform to the `gopkg.in/yaml.v2.Marshaler` and `gopkg.in/yaml.v2.Unmarshaler` interfaces.
* When the flag `sql` is provided, the methods for implementing the Scanner and Valuer interfaces will be also generated. - When the flag `sql` is provided, the methods for implementing the Scanner and Valuer interfaces will be also generated.
Useful when storing the enum in a database. Useful when storing the enum in a database.
For example, if we have an enum type called `Pill`, For example, if we have an enum type called `Pill`,
```go ```go
type Pill int type Pill int
@ -38,7 +42,9 @@ const (
Acetaminophen = Paracetamol Acetaminophen = Paracetamol
) )
``` ```
executing `enumer -type=Pill -json` will generate a new file with four basic methods and two extra for JSON: executing `enumer -type=Pill -json` will generate a new file with four basic methods and two extra for JSON:
```go ```go
func (i Pill) String() string { func (i Pill) String() string {
//... //...
@ -64,7 +70,9 @@ func (i *Pill) UnmarshalJSON(data []byte) error {
//... //...
} }
``` ```
From now on, we can: From now on, we can:
```go ```go
// Convert any Pill value to string // Convert any Pill value to string
var aspirinString string = Aspirin.String() var aspirinString string = Aspirin.String()
@ -119,15 +127,29 @@ For example, the command `enumer -type=MyType -json -transform=snake` would gene
```go ```go
name := MyTypeValue.String() // name => "my_type_value" name := MyTypeValue.String() // name => "my_type_value"
``` ```
**Note**: The transformation only works form CamelCase to snake_case or kebab-case, not the other way around. **Note**: The transformation only works form CamelCase to snake_case or kebab-case, not the other way around.
### Transformers
- snake
- snake-upper
- kebab
- kebab-upper
- lower (Lowercase)
- upper (Uppercase)
- title (TitleCase)
- first (Use first character of string)
- first-lower (same as first only lower case)
- first-upper (same as first only upper case)
## 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.
There are four boolean flags: `json`, `text`, `yaml` and `sql`. You can use any combination of them (i.e. `enumer -type=Pill -json -text`), There are four boolean flags: `json`, `text`, `yaml` and `sql`. You can use any combination of them (i.e. `enumer -type=Pill -json -text`),
For enum string representation transformation the `transform` and `trimprefix` flags For enum string representation transformation the `transform` and `trimprefix` flags
were added (i.e. `enumer -type=MyType -json -transform=snake`). 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. Possible transform values are `snake` and `kebab` for transformation to snake_case and kebab-case accordingly.
@ -137,5 +159,6 @@ If a prefix is provided via the `trimprefix` flag, it will be trimmed from the s
it is transformed). If a name doesn't have the prefix it will be passed unchanged. it is transformed). If a name doesn't have the prefix it will be passed unchanged.
## Inspiring projects ## Inspiring projects
* [Stringer](https://godoc.org/golang.org/x/tools/cmd/stringer)
* [jsonenums](https://github.com/campoy/jsonenums) - [Stringer](https://godoc.org/golang.org/x/tools/cmd/stringer)
- [jsonenums](https://github.com/campoy/jsonenums)

View File

@ -57,13 +57,45 @@ func TestEndToEnd(t *testing.T) {
t.Logf("cgo is no enabled for %s", name) t.Logf("cgo is no enabled for %s", name)
continue continue
} }
// Names are known to be ASCII and long enough.
typeName := fmt.Sprintf("%c%s", name[0]+'A'-'a', name[1:len(name)-len(".go")])
transformNameMethod := "noop"
if name == "transform.go" { // Names are known to be ASCII and long enough.
typeName = "CamelCaseValue" var typeName string
var transformNameMethod string
switch name {
case "transform_snake.go":
typeName = "SnakeCaseValue"
transformNameMethod = "snake" transformNameMethod = "snake"
case "transform_snake_upper.go":
typeName = "SnakeUpperCaseValue"
transformNameMethod = "snake-upper"
case "transform_kebab.go":
typeName = "KebabCaseValue"
transformNameMethod = "kebab"
case "transform_kebab_upper.go":
typeName = "KebabUpperCaseValue"
transformNameMethod = "kebab-upper"
case "transform_upper.go":
typeName = "UpperCaseValue"
transformNameMethod = "upper"
case "transform_lower.go":
typeName = "LowerCaseValue"
transformNameMethod = "lower"
case "transform_title.go":
typeName = "TitleCaseValue"
transformNameMethod = "title"
case "transform_first.go":
typeName = "FirstCaseValue"
transformNameMethod = "first"
case "transform_first_upper.go":
typeName = "FirstUpperCaseValue"
transformNameMethod = "first-upper"
case "transform_first_lower.go":
typeName = "FirstLowerCaseValue"
transformNameMethod = "first-lower"
default:
typeName = fmt.Sprintf("%c%s", name[0]+'A'-'a', name[1:len(name)-len(".go")])
transformNameMethod = "noop"
} }
stringerCompileAndRun(t, dir, stringer, typeName, name, transformNameMethod) stringerCompileAndRun(t, dir, stringer, typeName, name, transformNameMethod)

View File

@ -27,6 +27,7 @@ import (
"path/filepath" "path/filepath"
"sort" "sort"
"strings" "strings"
"unicode/utf8"
"github.com/pascaldekloe/name" "github.com/pascaldekloe/name"
) )
@ -240,18 +241,57 @@ func (pkg *Package) check(fs *token.FileSet, astFiles []*ast.File) {
} }
func (g *Generator) transformValueNames(values []Value, transformMethod string) { func (g *Generator) transformValueNames(values []Value, transformMethod string) {
var sep rune var fn func(src string) string
switch transformMethod { switch transformMethod {
case "snake": case "snake":
sep = '_' fn = func(s string) string {
return strings.ToLower(name.Delimit(s, '_'))
}
case "snake_upper", "snake-upper":
fn = func(s string) string {
return strings.ToUpper(name.Delimit(s, '_'))
}
case "kebab": case "kebab":
sep = '-' fn = func(s string) string {
return strings.ToLower(name.Delimit(s, '-'))
}
case "kebab_upper", "kebab-upper":
fn = func(s string) string {
return strings.ToUpper(name.Delimit(s, '-'))
}
case "upper":
fn = func(s string) string {
return strings.ToUpper(s)
}
case "lower":
fn = func(s string) string {
return strings.ToLower(s)
}
case "title":
fn = func(s string) string {
return strings.Title(s)
}
case "first":
fn = func(s string) string {
r, _ := utf8.DecodeRuneInString(s)
return string(r)
}
case "first_upper", "first-upper":
fn = func(s string) string {
r, _ := utf8.DecodeRuneInString(s)
return strings.ToUpper(string(r))
}
case "first_lower", "first-lower":
fn = func(s string) string {
r, _ := utf8.DecodeRuneInString(s)
return strings.ToLower(string(r))
}
default: default:
return return
} }
for i := range values { for i := range values {
values[i].name = strings.ToLower(name.Delimit(values[i].name, sep)) values[i].name = fn(values[i].name)
} }
} }

25
testdata/transform.go vendored
View File

@ -1,25 +0,0 @@
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)
}
}

25
testdata/transform_first.go vendored Normal file
View File

@ -0,0 +1,25 @@
package main
import "fmt"
type FirstCaseValue int
const (
Male FirstCaseValue = iota
Female
unknown
)
func main() {
ck(Male, "M")
ck(Female, "F")
ck(unknown, "u")
ck(-127, "FirstCaseValue(-127)")
ck(127, "FirstCaseValue(127)")
}
func ck(value FirstCaseValue, str string) {
if fmt.Sprint(value) != str {
panic("transform_first.go: " + str)
}
}

25
testdata/transform_first_lower.go vendored Normal file
View File

@ -0,0 +1,25 @@
package main
import "fmt"
type FirstLowerCaseValue int
const (
Male FirstLowerCaseValue = iota
Female
Unknown
)
func main() {
ck(Male, "m")
ck(Female, "f")
ck(Unknown, "u")
ck(-127, "FirstLowerCaseValue(-127)")
ck(127, "FirstLowerCaseValue(127)")
}
func ck(value FirstLowerCaseValue, str string) {
if fmt.Sprint(value) != str {
panic("transform_first_lower.go: " + str)
}
}

25
testdata/transform_first_upper.go vendored Normal file
View File

@ -0,0 +1,25 @@
package main
import "fmt"
type FirstUpperCaseValue int
const (
male FirstUpperCaseValue = iota
female
unknown
)
func main() {
ck(male, "M")
ck(female, "F")
ck(unknown, "U")
ck(-127, "FirstUpperCaseValue(-127)")
ck(127, "FirstUpperCaseValue(127)")
}
func ck(value FirstUpperCaseValue, str string) {
if fmt.Sprint(value) != str {
panic("transform_first_upper.go: " + str)
}
}

25
testdata/transform_kebab.go vendored Normal file
View File

@ -0,0 +1,25 @@
package main
import "fmt"
type KebabCaseValue int
const (
KebabCaseValueOne KebabCaseValue = iota
KebabCaseValueTwo
KebabCaseValueThree
)
func main() {
ck(KebabCaseValueOne, "kebab-case-value-one")
ck(KebabCaseValueTwo, "kebab-case-value-two")
ck(KebabCaseValueThree, "kebab-case-value-three")
ck(-127, "KebabCaseValue(-127)")
ck(127, "KebabCaseValue(127)")
}
func ck(value KebabCaseValue, str string) {
if fmt.Sprint(value) != str {
panic("transform_kebab.go: " + str)
}
}

25
testdata/transform_kebab_upper.go vendored Normal file
View File

@ -0,0 +1,25 @@
package main
import "fmt"
type KebabUpperCaseValue int
const (
KebabUpperCaseValueOne KebabUpperCaseValue = iota
KebabUpperCaseValueTwo
KebabUpperCaseValueThree
)
func main() {
ck(KebabUpperCaseValueOne, "KEBAB-UPPER-CASE-VALUE-ONE")
ck(KebabUpperCaseValueTwo, "KEBAB-UPPER-CASE-VALUE-TWO")
ck(KebabUpperCaseValueThree, "KEBAB-UPPER-CASE-VALUE-THREE")
ck(-127, "KebabUpperCaseValue(-127)")
ck(127, "KebabUpperCaseValue(127)")
}
func ck(value KebabUpperCaseValue, str string) {
if fmt.Sprint(value) != str {
panic("transform_kebab_upper.go: " + str)
}
}

25
testdata/transform_lower.go vendored Normal file
View File

@ -0,0 +1,25 @@
package main
import "fmt"
type LowerCaseValue int
const (
LowerCaseValueOne LowerCaseValue = iota
LowerCaseValueTwo
LowerCaseValueThree
)
func main() {
ck(LowerCaseValueOne, "lowercasevalueone")
ck(LowerCaseValueTwo, "lowercasevaluetwo")
ck(LowerCaseValueThree, "lowercasevaluethree")
ck(-127, "LowerCaseValue(-127)")
ck(127, "LowerCaseValue(127)")
}
func ck(value LowerCaseValue, str string) {
if fmt.Sprint(value) != str {
panic("transform_lower.go: " + str)
}
}

25
testdata/transform_snake.go vendored Normal file
View File

@ -0,0 +1,25 @@
package main
import "fmt"
type SnakeCaseValue int
const (
SnakeCaseValueOne SnakeCaseValue = iota
SnakeCaseValueTwo
SnakeCaseValueThree
)
func main() {
ck(SnakeCaseValueOne, "snake_case_value_one")
ck(SnakeCaseValueTwo, "snake_case_value_two")
ck(SnakeCaseValueThree, "snake_case_value_three")
ck(-127, "SnakeCaseValue(-127)")
ck(127, "SnakeCaseValue(127)")
}
func ck(value SnakeCaseValue, str string) {
if fmt.Sprint(value) != str {
panic("transform_snake.go: " + str)
}
}

25
testdata/transform_snake_upper.go vendored Normal file
View File

@ -0,0 +1,25 @@
package main
import "fmt"
type SnakeUpperCaseValue int
const (
SnakeUpperCaseValueOne SnakeUpperCaseValue = iota
SnakeUpperCaseValueTwo
SnakeUpperCaseValueThree
)
func main() {
ck(SnakeUpperCaseValueOne, "SNAKE_UPPER_CASE_VALUE_ONE")
ck(SnakeUpperCaseValueTwo, "SNAKE_UPPER_CASE_VALUE_TWO")
ck(SnakeUpperCaseValueThree, "SNAKE_UPPER_CASE_VALUE_THREE")
ck(-127, "SnakeUpperCaseValue(-127)")
ck(127, "SnakeUpperCaseValue(127)")
}
func ck(value SnakeUpperCaseValue, str string) {
if fmt.Sprint(value) != str {
panic("transform_snake_upper.go: " + str)
}
}

25
testdata/transform_title.go vendored Normal file
View File

@ -0,0 +1,25 @@
package main
import "fmt"
type TitleCaseValue int
const (
titlecasevalueone TitleCaseValue = iota
titlecasevaluetwo
titlecasevaluethree
)
func main() {
ck(titlecasevalueone, "Titlecasevalueone")
ck(titlecasevaluetwo, "Titlecasevaluetwo")
ck(titlecasevaluethree, "Titlecasevaluethree")
ck(-127, "TitleCaseValue(-127)")
ck(127, "TitleCaseValue(127)")
}
func ck(value TitleCaseValue, str string) {
if fmt.Sprint(value) != str {
panic("transform_title.go: " + str)
}
}

25
testdata/transform_upper.go vendored Normal file
View File

@ -0,0 +1,25 @@
package main
import "fmt"
type UpperCaseValue int
const (
UpperCaseValueOne UpperCaseValue = iota
UpperCaseValueTwo
UpperCaseValueThree
)
func main() {
ck(UpperCaseValueOne, "UPPERCASEVALUEONE")
ck(UpperCaseValueTwo, "UPPERCASEVALUETWO")
ck(UpperCaseValueThree, "UPPERCASEVALUETHREE")
ck(-127, "UpperCaseValue(-127)")
ck(127, "UpperCaseValue(127)")
}
func ck(value UpperCaseValue, str string) {
if fmt.Sprint(value) != str {
panic("transform_upper.go: " + str)
}
}