Merge remote-tracking branch 'upstream/master'

This commit is contained in:
marco 2016-02-06 22:40:52 +01:00
commit cd583bfe41
4 changed files with 184 additions and 33 deletions

View File

@ -1,11 +1,18 @@
# Enumer #Enumer
Enumer generates Go code to get string names from enum values and viceversa. Enumer is a tool to generate Go code that adds useful methods to Go enums (constants with a specific type).
It is 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).
but adding a *"string to enum value"* method to the generated code.
This is useful when you need to read enum values from the command line arguments, from a configuration file, ##Generated functions and methods
When Enumer is applied to a type, it will generate three methods and one function:
* A method `String()` that 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.
* A function `<Type>String(s string)` to get the enum value from its string representation. This is useful
when you need to read enum values from the command line arguments, from a configuration file,
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.
* And two more methods, `MarshalJSON()` and `UnmarshalJSON()`, that makes the enum conform
the `json.Marshaler` and `json.Unmarshaler` interfaces. Very useful to use it in JSON APIs.
For example, if we have an enum type called `Pill`, For example, if we have an enum type called `Pill`,
```go ```go
@ -19,7 +26,7 @@ const (
Acetaminophen = Paracetamol Acetaminophen = Paracetamol
) )
``` ```
executing `enumer -type=Pill` will generate a new file with two methods: executing `enumer -type=Pill` will generate a new file with four methods:
```go ```go
func (i Pill) String() string { func (i Pill) String() string {
//... //...
@ -28,6 +35,14 @@ func (i Pill) String() string {
func PillString(s string) (Pill, error) { func PillString(s string) (Pill, error) {
//... //...
} }
func (i Pill) MarshalJSON() ([]byte, error) {
//...
}
func (i *Pill) UnmarshalJSON(data []byte) error {
//...
}
``` ```
From now on, we can: From now on, we can:
```go ```go
@ -43,17 +58,26 @@ if err != nil {
return return
} }
// Now pill == Ibuprofen // Now pill == Ibuprofen
// Marshal/unmarshal to/from json strings, either directly or automatically when
// the enum is a field of a struct
pillJSON := Aspirin.MarshalJSON()
// Now pillJSON == `"Aspirin"`
``` ```
The generated code is exactly the same as the Stringer tool plus the `<Type>String` method, 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.
## How to use ## How to use
The usage of Enumer is the same as Stringer, no changes were introduced. 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 please refer to the [Stringer docs](https://godoc.org/golang.org/x/tools/cmd/stringer) for more information.
There is only one flag added: `noJSON`. If this flag is set to true (i.e. `enumer -type=Pill -noJSON`),
the JSON related methods won't be generated.
## Additional functions of this fork ## Additional functions of this fork
This fork additionally implements the Scanner and Valuer interface to use a enum seamlessly in a database model. This fork additionally implements the Scanner and Valuer interface to use a enum seamlessly in a database model.
## TODO ## Inspiring projects
- Add a flag to optionally generate implementation of Scanner und Valuer interface * [Stringer](https://godoc.org/golang.org/x/tools/cmd/stringer)
* [jsonenums](https://github.com/campoy/jsonenums)

View File

@ -1,4 +1,5 @@
package main package main
import "fmt" import "fmt"
// Arguments to format are: // Arguments to format are:
@ -19,7 +20,7 @@ func (g *Generator) buildValueToNameMap(runs [][]Value, typeName string, runsThr
var runID string var runID string
for i, values := range runs { for i, values := range runs {
if thereAreRuns { if thereAreRuns {
runID = "_" + fmt.Sprintf("%d",i) runID = "_" + fmt.Sprintf("%d", i)
n = 0 n = 0
} else { } else {
runID = "" runID = ""
@ -33,3 +34,26 @@ func (g *Generator) buildValueToNameMap(runs [][]Value, typeName string, runsThr
g.Printf("}\n\n") g.Printf("}\n\n")
g.Printf(stringValueToNameMap, typeName) g.Printf(stringValueToNameMap, typeName)
} }
// Arguments to format are:
// [1]: type name
const jsonMethods = `
func (i %[1]s) MarshalJSON() ([]byte, error) {
return json.Marshal(i.String())
}
func (i *%[1]s) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return fmt.Errorf("%[1]s should be a string, got %%s", data)
}
var err error
*i, err = %[1]sString(s)
return err
}
`
func (g *Generator) buildJSONMethods(runs [][]Value, typeName string, runsThreshold int) {
g.Printf(jsonMethods, typeName)
}

View File

@ -30,6 +30,10 @@ var golden = []Golden{
{"prime", prime_in, prime_out}, {"prime", prime_in, prime_out},
} }
var goldenJSON = []Golden {
{"prime", prime_json_in, prime_json_out},
}
// Each example starts with "type XXX [u]int", with a single space separating them. // Each example starts with "type XXX [u]int", with a single space separating them.
// Simple test: enumeration of type int starting at 0. // Simple test: enumeration of type int starting at 0.
@ -338,9 +342,100 @@ func PrimeString(s string) (Prime, error) {
return 0, fmt.Errorf("%s does not belong to Prime values", s) return 0, fmt.Errorf("%s does not belong to Prime values", s)
} }
` `
const prime_json_in = `type Prime int
const (
p2 Prime = 2
p3 Prime = 3
p5 Prime = 5
p7 Prime = 7
p77 Prime = 7 // Duplicate; note that p77 doesn't appear below.
p11 Prime = 11
p13 Prime = 13
p17 Prime = 17
p19 Prime = 19
p23 Prime = 23
p29 Prime = 29
p37 Prime = 31
p41 Prime = 41
p43 Prime = 43
)
`
const prime_json_out = `
const _Prime_name = "p2p3p5p7p11p13p17p19p23p29p37p41p43"
var _Prime_map = map[Prime]string{
2: _Prime_name[0:2],
3: _Prime_name[2:4],
5: _Prime_name[4:6],
7: _Prime_name[6:8],
11: _Prime_name[8:11],
13: _Prime_name[11:14],
17: _Prime_name[14:17],
19: _Prime_name[17:20],
23: _Prime_name[20:23],
29: _Prime_name[23:26],
31: _Prime_name[26:29],
41: _Prime_name[29:32],
43: _Prime_name[32:35],
}
func (i Prime) String() string {
if str, ok := _Prime_map[i]; ok {
return str
}
return fmt.Sprintf("Prime(%d)", i)
}
var _PrimeNameToValue_map = map[string]Prime{
_Prime_name[0:2]: 2,
_Prime_name[2:4]: 3,
_Prime_name[4:6]: 5,
_Prime_name[6:8]: 7,
_Prime_name[8:11]: 11,
_Prime_name[11:14]: 13,
_Prime_name[14:17]: 17,
_Prime_name[17:20]: 19,
_Prime_name[20:23]: 23,
_Prime_name[23:26]: 29,
_Prime_name[26:29]: 31,
_Prime_name[29:32]: 41,
_Prime_name[32:35]: 43,
}
func PrimeString(s string) (Prime, error) {
if val, ok := _PrimeNameToValue_map[s]; ok {
return val, nil
}
return 0, fmt.Errorf("%s does not belong to Prime values", s)
}
func (i Prime) MarshalJSON() ([]byte, error) {
return json.Marshal(i.String())
}
func (i *Prime) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return fmt.Errorf("Prime should be a string, got %s", data)
}
var err error
*i, err = PrimeString(s)
return err
}
`
func TestGolden(t *testing.T) { func TestGolden(t *testing.T) {
for _, test := range golden { for _, test := range golden {
runGoldenTest(t, test, false)
}
for _, test := range goldenJSON {
runGoldenTest(t, test, true)
}
}
func runGoldenTest(t *testing.T, test Golden, generateJSON bool) {
var g Generator var g Generator
input := "package test\n" + test.input input := "package test\n" + test.input
file := test.name + ".go" file := test.name + ".go"
@ -350,10 +445,9 @@ func TestGolden(t *testing.T) {
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]) g.generate(tokens[1], generateJSON)
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

@ -82,6 +82,7 @@ import (
var ( var (
typeNames = flag.String("type", "", "comma-separated list of type names; must be set") typeNames = flag.String("type", "", "comma-separated list of type names; must be set")
noJSON = flag.Bool("noJSON", false, "if true, json marshaling methods will NOT be included. 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")
) )
@ -98,7 +99,7 @@ func Usage() {
func main() { func main() {
log.SetFlags(0) log.SetFlags(0)
log.SetPrefix("stringer: ") log.SetPrefix("enumer: ")
flag.Usage = Usage flag.Usage = Usage
flag.Parse() flag.Parse()
if len(*typeNames) == 0 { if len(*typeNames) == 0 {
@ -135,11 +136,14 @@ func main() {
g.Printf("import (\n") g.Printf("import (\n")
g.Printf("\t\"fmt\"\n") g.Printf("\t\"fmt\"\n")
g.Printf("\t\"database/sql/driver\"\n") g.Printf("\t\"database/sql/driver\"\n")
if !*noJSON {
g.Printf("\t\"encoding/json\"\n")
}
g.Printf(")\n") g.Printf(")\n")
// Run generate for each type. // Run generate for each type.
for _, typeName := range types { for _, typeName := range types {
g.generate(typeName) g.generate(typeName, !*noJSON)
} }
// Format the output. // Format the output.
@ -275,7 +279,7 @@ func (pkg *Package) check(fs *token.FileSet, astFiles []*ast.File) {
} }
// generate produces the String method for the named type. // generate produces the String method for the named type.
func (g *Generator) generate(typeName string) { func (g *Generator) generate(typeName string, includeJSON bool) {
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.
@ -303,16 +307,21 @@ func (g *Generator) generate(typeName string) {
// being necessary for any realistic example other than bitmasks // being necessary for any realistic example other than bitmasks
// is very low. And bitmasks probably deserve their own analysis, // is very low. And bitmasks probably deserve their own analysis,
// to be done some other day. // to be done some other day.
const runsThreshold = 10
switch { switch {
case len(runs) == 1: case len(runs) == 1:
g.buildOneRun(runs, typeName) g.buildOneRun(runs, typeName)
case len(runs) <= 10: case len(runs) <= runsThreshold:
g.buildMultipleRuns(runs, typeName) g.buildMultipleRuns(runs, typeName)
default: default:
g.buildMap(runs, typeName) g.buildMap(runs, typeName)
} }
// ENUMER: This is the only addition over the original stringer code. Everything else is in enumer.go
g.buildValueToNameMap(runs, typeName, 10) // ENUMER part
g.buildValueToNameMap(runs, typeName, runsThreshold)
if includeJSON {
g.buildJSONMethods(runs, typeName, runsThreshold)
}
// SQL // SQL
g.addValueAndScanMethod(typeName) g.addValueAndScanMethod(typeName)