Compare commits

...

16 Commits

Author SHA1 Message Date
Adam Bouqdib b59fe7c6e6 Add transform to MACRO_CASE and phrase 2019-08-21 14:35:35 +01:00
Álvaro López Espinosa d27f80ae12
Merge pull request #48 from ltpquang/master
Fix runsThreshold limit condition
2019-07-18 11:50:21 +01:00
Quang Le 275c2734c9 Fix error when number of runs reaches the run threshold 2019-07-18 10:39:11 +07:00
Quang Le 814e4e4ad4 Add test cases that make the endtoend test failed 2019-07-18 10:37:23 +07:00
Quang Le 89dcacfe6f Fix typos 2019-07-18 09:53:12 +07:00
Álvaro cbdc8d0944 Minimal changes in Readme 2019-05-25 17:21:47 +01:00
Álvaro 642f2c0436 Add support for -linecomment 2019-05-25 17:02:36 +01:00
Álvaro 0f1dc27ec9 Add support for go modules 2019-05-25 16:05:55 +01:00
Álvaro 7820f3770a Remove specific version importers and use the default one 2019-05-25 14:58:46 +01:00
Álvaro c0f685c6bf Merge remote-tracking branch 'origin/master' 2019-05-25 14:30:39 +01:00
Álvaro b6edbe712f Use go modules 2019-05-25 14:30:28 +01:00
Álvaro López Espinosa ddc705194a
Merge pull request #47 from osctobe/master
Create tempfile in the output directory
2019-05-25 14:29:03 +01:00
Michał Mirosław 0ef6de982e Create tempfile in the output directory 2019-05-23 14:01:08 +02:00
Álvaro López Espinosa 0fa5a42e5f
Merge pull request #45 from sekky0905/add-how-to-install-to-readme
Add Install section to README
2019-05-20 17:25:57 +01:00
Álvaro López Espinosa 9875c3a8c3
Merge pull request #33 from dterei/master
Add comment support + temporary file before writing
2019-05-20 16:53:50 +01:00
sekky0905 5d94885c07 Add Install section to README 2019-05-20 12:48:56 +09:00
19 changed files with 438 additions and 374 deletions

View File

@ -2,10 +2,19 @@
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).
## Install
Enumer can be installed as any other go command:
```
go get github.com/alvaroloes/enumer
```
After that, the `enumer` executable will be in "$GOPATH/bin" folder and you can use it with `go generate`
## 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.
@ -18,7 +27,7 @@ be almost meaningless or hard to trace or use by a human.
* 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
@ -40,19 +49,19 @@ const (
``` ```
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 {
//... //...
} }
func PillString(s string) (Pill, error) { func PillString(s string) (Pill, error) {
//... //...
} }
func PillValues() []Pill { func PillValues() []Pill {
//... //...
} }
func (i Pill) IsAPill() bool { func (i Pill) IsAPill() bool {
//... //...
} }
@ -119,7 +128,7 @@ 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 from CamelCase to snake_case or kebab-case, not the other way around.
## 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)
@ -130,7 +139,8 @@ There are four boolean flags: `json`, `text`, `yaml` and `sql`. You can use any
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`, `kebab` `macro` and `phrase` for transformation
to snake_case, kebab-case, MACRO_CASE or a phrase (i.e. space-separated words) accordingly.
The default value for `transform` flag is `noop` which means no transformation will be performed. The default value for `transform` flag is `noop` which means no transformation will be performed.
If a prefix is provided via the `trimprefix` flag, it will be trimmed from the start of each name (before If a prefix is provided via the `trimprefix` flag, it will be trimmed from the start of each name (before

View File

@ -111,7 +111,14 @@ func copy(to, from string) error {
// run runs a single command and returns an error if it does not succeed. // run runs a single command and returns an error if it does not succeed.
// os/exec should have this function, to be honest. // os/exec should have this function, to be honest.
func run(name string, arg ...string) error { func run(name string, arg ...string) error {
return runInDir(".", name, arg...)
}
// runInDir runs a single command in directory dir and returns an error if
// it does not succeed.
func runInDir(dir, name string, arg ...string) error {
cmd := exec.Command(name, arg...) cmd := exec.Command(name, arg...)
cmd.Dir = dir
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
return cmd.Run() return cmd.Run()

View File

@ -39,7 +39,7 @@ func (i %[1]s) IsA%[1]s() bool {
// [1]: type name // [1]: type name
const stringBelongsMethodSet = `// IsA%[1]s returns "true" if the value is listed in the enum definition. "false" otherwise const stringBelongsMethodSet = `// IsA%[1]s returns "true" if the value is listed in the enum definition. "false" otherwise
func (i %[1]s) IsA%[1]s() bool { func (i %[1]s) IsA%[1]s() bool {
_, ok := _%[1]sMap[i] _, ok := _%[1]sMap[i]
return ok return ok
} }
` `
@ -79,7 +79,7 @@ func (g *Generator) buildBasicExtras(runs [][]Value, typeName string, runsThresh
// Print the basic extra methods // Print the basic extra methods
g.Printf(stringNameToValueMethod, typeName) g.Printf(stringNameToValueMethod, typeName)
g.Printf(stringValuesMethod, typeName) g.Printf(stringValuesMethod, typeName)
if len(runs) < runsThreshold { if len(runs) <= runsThreshold {
g.Printf(stringBelongsMethodLoop, typeName) g.Printf(stringBelongsMethodLoop, typeName)
} else { // There is a map of values, the code is simpler then } else { // There is a map of values, the code is simpler then
g.Printf(stringBelongsMethodSet, typeName) g.Printf(stringBelongsMethodSet, typeName)

8
go.mod Normal file
View File

@ -0,0 +1,8 @@
module github.com/alvaroloes/enumer
go 1.12
require (
github.com/pascaldekloe/name v0.0.0-20180628100202-0fd16699aae1
golang.org/x/tools v0.0.0-20190525145741-7be61e1b0e51
)

9
go.sum Normal file
View File

@ -0,0 +1,9 @@
github.com/pascaldekloe/name v0.0.0-20180628100202-0fd16699aae1 h1:/I3lTljEEDNYLho3/FUB7iD/oc2cEFgVmbHzV+O0PtU=
github.com/pascaldekloe/name v0.0.0-20180628100202-0fd16699aae1/go.mod h1:eD5JxqMiuNYyFNmyY9rkJ/slN8y59oEu4Ei7F8OoKWQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20190525145741-7be61e1b0e51 h1:RhYYBLDB5MoVkvoNGMNk+DSj7WoGhySvIvtEjTyiP74=
golang.org/x/tools v0.0.0-20190525145741-7be61e1b0e51/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=

View File

@ -10,6 +10,9 @@
package main package main
import ( import (
"io/ioutil"
"os"
"path/filepath"
"strings" "strings"
"testing" "testing"
) )
@ -18,7 +21,7 @@ import (
type Golden struct { type Golden struct {
name string name string
input string // input; the package clause is provided when running the test. input string // input; the package clause is provided when running the test.
output string // exected output. output string // expected output.
} }
var golden = []Golden{ var golden = []Golden{
@ -31,28 +34,32 @@ var golden = []Golden{
} }
var goldenJSON = []Golden{ var goldenJSON = []Golden{
{"prime", primeJsonIn, primeJsonOut}, {"prime with JSON", primeJsonIn, primeJsonOut},
} }
var goldenText = []Golden{ var goldenText = []Golden{
{"prime", primeTextIn, primeTextOut}, {"prime with Text", primeTextIn, primeTextOut},
} }
var goldenYAML = []Golden{ var goldenYAML = []Golden{
{"prime", primeYamlIn, primeYamlOut}, {"prime with YAML", primeYamlIn, primeYamlOut},
} }
var goldenSQL = []Golden{ var goldenSQL = []Golden{
{"prime", primeSqlIn, primeSqlOut}, {"prime with SQL", primeSqlIn, primeSqlOut},
} }
var goldenJSONAndSQL = []Golden{ var goldenJSONAndSQL = []Golden{
{"prime", primeJsonAndSqlIn, primeJsonAndSqlOut}, {"prime with JSONAndSQL", primeJsonAndSqlIn, primeJsonAndSqlOut},
} }
var goldenPrefix = []Golden{ var goldenPrefix = []Golden{
{"prefix", prefixIn, dayOut}, {"prefix", prefixIn, dayOut},
} }
var goldenWithLineComments = []Golden{
{"primer with line Comments", primeWithLineCommentIn, primeWithLineCommentOut},
}
// 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.
@ -1022,6 +1029,90 @@ const (
) )
` `
const primeWithLineCommentIn = `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 primeWithLineCommentOut = `
const _PrimeName = "p2p3GoodPrimep7p11p13p17p19p23p29p37TwinPrime41Twin prime 43"
var _PrimeMap = map[Prime]string{
2: _PrimeName[0:2],
3: _PrimeName[2:4],
5: _PrimeName[4:13],
7: _PrimeName[13:15],
11: _PrimeName[15:18],
13: _PrimeName[18:21],
17: _PrimeName[21:24],
19: _PrimeName[24:27],
23: _PrimeName[27:30],
29: _PrimeName[30:33],
31: _PrimeName[33:36],
41: _PrimeName[36:47],
43: _PrimeName[47:60],
}
func (i Prime) String() string {
if str, ok := _PrimeMap[i]; ok {
return str
}
return fmt.Sprintf("Prime(%d)", i)
}
var _PrimeValues = []Prime{2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 41, 43}
var _PrimeNameToValueMap = map[string]Prime{
_PrimeName[0:2]: 2,
_PrimeName[2:4]: 3,
_PrimeName[4:13]: 5,
_PrimeName[13:15]: 7,
_PrimeName[15:18]: 11,
_PrimeName[18:21]: 13,
_PrimeName[21:24]: 17,
_PrimeName[24:27]: 19,
_PrimeName[27:30]: 23,
_PrimeName[30:33]: 29,
_PrimeName[33:36]: 31,
_PrimeName[36:47]: 41,
_PrimeName[47:60]: 43,
}
// PrimeString retrieves an enum value from the enum constants string name.
// Throws an error if the param is not part of the enum.
func PrimeString(s string) (Prime, error) {
if val, ok := _PrimeNameToValueMap[s]; ok {
return val, nil
}
return 0, fmt.Errorf("%s does not belong to Prime values", s)
}
// PrimeValues returns all values of the enum
func PrimeValues() []Prime {
return _PrimeValues
}
// IsAPrime returns "true" if the value is listed in the enum definition. "false" otherwise
func (i Prime) IsAPrime() bool {
_, ok := _PrimeMap[i]
return ok
}
`
func TestGolden(t *testing.T) { func TestGolden(t *testing.T) {
for _, test := range golden { for _, test := range golden {
runGoldenTest(t, test, false, false, false, false, "") runGoldenTest(t, test, false, false, false, false, "")
@ -1050,13 +1141,30 @@ func runGoldenTest(t *testing.T, test Golden, generateJSON, generateYAML, genera
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"
g.parsePackage(".", []string{file}, input)
dir, err := ioutil.TempDir("", "stringer")
if err != nil {
t.Error(err)
}
defer func() {
err = os.RemoveAll(dir)
if err != nil {
t.Error(err)
}
}()
absFile := filepath.Join(dir, file)
err = ioutil.WriteFile(absFile, []byte(input), 0644)
if err != nil {
t.Error(err)
}
g.parsePackage([]string{absFile})
// Extract the name and type of the constant from the first line. // Extract the name and type of the constant from the first line.
tokens := strings.SplitN(test.input, " ", 3) tokens := strings.SplitN(test.input, " ", 3)
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, generateText, "noop", prefix) g.generate(tokens[1], generateJSON, generateYAML, generateSQL, generateText, "noop", prefix, false)
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

@ -1,16 +0,0 @@
// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build !go1.9
package main
import (
"go/importer"
"go/types"
)
func defaultImporter() types.Importer {
return importer.Default()
}

View File

@ -1,16 +0,0 @@
// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build go1.9
package main
import (
"go/importer"
"go/types"
)
func defaultImporter() types.Importer {
return importer.For("source", nil)
}

2
sql.go
View File

@ -26,7 +26,7 @@ const scanMethod = `func (i *%[1]s) Scan(value interface{}) error {
if err != nil { if err != nil {
return err return err
} }
*i = val *i = val
return nil return nil
} }

View File

@ -15,12 +15,12 @@ import (
"flag" "flag"
"fmt" "fmt"
"go/ast" "go/ast"
"go/build"
exact "go/constant" exact "go/constant"
"go/format" "go/format"
"go/parser" "go/importer"
"go/token" "go/token"
"go/types" "go/types"
"golang.org/x/tools/go/packages"
"io/ioutil" "io/ioutil"
"log" "log"
"os" "os"
@ -51,6 +51,7 @@ var (
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") transformMethod = flag.String("transform", "noop", "enum item name transformation method. Default: noop")
trimPrefix = flag.String("trimprefix", "", "transform each item name by removing a prefix. Default: \"\"") trimPrefix = flag.String("trimprefix", "", "transform each item name by removing a prefix. Default: \"\"")
lineComment = flag.Bool("linecomment", false, "use line comment text as printed text when present")
) )
var comments arrayFlags var comments arrayFlags
@ -96,12 +97,12 @@ func main() {
if len(args) == 1 && isDirectory(args[0]) { if len(args) == 1 && isDirectory(args[0]) {
dir = args[0] dir = args[0]
g.parsePackageDir(args[0])
} else { } else {
dir = filepath.Dir(args[0]) dir = filepath.Dir(args[0])
g.parsePackageFiles(args)
} }
g.parsePackage(args)
// Print the header and package clause. // Print the header and package clause.
g.Printf("// Code generated by \"enumer %s\"; DO NOT EDIT.\n", strings.Join(os.Args[1:], " ")) g.Printf("// Code generated by \"enumer %s\"; DO NOT EDIT.\n", strings.Join(os.Args[1:], " "))
g.Printf("\n") g.Printf("\n")
@ -120,7 +121,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, *text, *transformMethod, *trimPrefix) g.generate(typeName, *json, *yaml, *sql, *text, *transformMethod, *trimPrefix, *lineComment)
} }
// Format the output. // Format the output.
@ -134,7 +135,8 @@ func main() {
} }
// Write to tmpfile first // Write to tmpfile first
tmpFile, err := ioutil.TempFile("", fmt.Sprintf("%s_enumer_", types[0])) tmpName := fmt.Sprintf("%s_enumer_", filepath.Base(types[0]))
tmpFile, err := ioutil.TempFile(filepath.Dir(types[0]), tmpName)
if err != nil { if err != nil {
log.Fatalf("creating temporary file for output: %s", err) log.Fatalf("creating temporary file for output: %s", err)
} }
@ -192,76 +194,111 @@ type Package struct {
typesPkg *types.Package typesPkg *types.Package
} }
// parsePackageDir parses the package residing in the directory. //// parsePackageDir parses the package residing in the directory.
func (g *Generator) parsePackageDir(directory string) { //func (g *Generator) parsePackageDir(directory string) {
pkg, err := build.Default.ImportDir(directory, 0) // pkg, err := build.Default.ImportDir(directory, 0)
// if err != nil {
// log.Fatalf("cannot process directory %s: %s", directory, err)
// }
// var names []string
// names = append(names, pkg.GoFiles...)
// names = append(names, pkg.CgoFiles...)
// // TODO: Need to think about constants in test files. Maybe write type_string_test.go
// // in a separate pass? For later.
// // names = append(names, pkg.TestGoFiles...) // These are also in the "foo" package.
// names = append(names, pkg.SFiles...)
// names = prefixDirectory(directory, names)
// g.parsePackage(directory, names, nil)
//}
//
//// parsePackageFiles parses the package occupying the named files.
//func (g *Generator) parsePackageFiles(names []string) {
// g.parsePackage(".", names, nil)
//}
//
//// prefixDirectory places the directory name on the beginning of each name in the list.
//func prefixDirectory(directory string, names []string) []string {
// if directory == "." {
// return names
// }
// ret := make([]string, len(names))
// for i, name := range names {
// ret[i] = filepath.Join(directory, name)
// }
// return ret
//}
//// parsePackage analyzes the single package constructed from the named files.
//// If text is non-nil, it is a string to be used instead of the content of the file,
//// to be used for testing. parsePackage exits if there is an error.
//func (g *Generator) parsePackage(directory string, names []string, text interface{}) {
// var files []*File
// var astFiles []*ast.File
// g.pkg = new(Package)
// fs := token.NewFileSet()
// for _, name := range names {
// if !strings.HasSuffix(name, ".go") {
// continue
// }
// parsedFile, err := parser.ParseFile(fs, name, text, 0)
// if err != nil {
// log.Fatalf("parsing package: %s: %s", name, err)
// }
// astFiles = append(astFiles, parsedFile)
// files = append(files, &File{
// file: parsedFile,
// pkg: g.pkg,
// })
// }
// if len(astFiles) == 0 {
// log.Fatalf("%s: no buildable Go files", directory)
// }
// g.pkg.name = astFiles[0].Name.Name
// g.pkg.files = files
// g.pkg.dir = directory
// // Type check the package.
// g.pkg.check(fs, astFiles)
//}
// parsePackage analyzes the single package constructed from the patterns and tags.
// parsePackage exits if there is an error.
func (g *Generator) parsePackage(patterns []string) {
cfg := &packages.Config{
Mode: packages.LoadSyntax,
// TODO: Need to think about constants in test files. Maybe write type_string_test.go
// in a separate pass? For later.
Tests: false,
}
pkgs, err := packages.Load(cfg, patterns...)
if err != nil { if err != nil {
log.Fatalf("cannot process directory %s: %s", directory, err) log.Fatal(err)
} }
var names []string if len(pkgs) != 1 {
names = append(names, pkg.GoFiles...) log.Fatalf("error: %d packages found", len(pkgs))
names = append(names, pkg.CgoFiles...) }
// TODO: Need to think about constants in test files. Maybe write type_string_test.go g.addPackage(pkgs[0])
// in a separate pass? For later.
// names = append(names, pkg.TestGoFiles...) // These are also in the "foo" package.
names = append(names, pkg.SFiles...)
names = prefixDirectory(directory, names)
g.parsePackage(directory, names, nil)
} }
// parsePackageFiles parses the package occupying the named files. // addPackage adds a type checked Package and its syntax files to the generator.
func (g *Generator) parsePackageFiles(names []string) { func (g *Generator) addPackage(pkg *packages.Package) {
g.parsePackage(".", names, nil) g.pkg = &Package{
} name: pkg.Name,
defs: pkg.TypesInfo.Defs,
// prefixDirectory places the directory name on the beginning of each name in the list. files: make([]*File, len(pkg.Syntax)),
func prefixDirectory(directory string, names []string) []string {
if directory == "." {
return names
} }
ret := make([]string, len(names))
for i, name := range names {
ret[i] = filepath.Join(directory, name)
}
return ret
}
// parsePackage analyzes the single package constructed from the named files. for i, file := range pkg.Syntax {
// If text is non-nil, it is a string to be used instead of the content of the file, g.pkg.files[i] = &File{
// to be used for testing. parsePackage exits if there is an error. file: file,
func (g *Generator) parsePackage(directory string, names []string, text interface{}) {
var files []*File
var astFiles []*ast.File
g.pkg = new(Package)
fs := token.NewFileSet()
for _, name := range names {
if !strings.HasSuffix(name, ".go") {
continue
}
parsedFile, err := parser.ParseFile(fs, name, text, 0)
if err != nil {
log.Fatalf("parsing package: %s: %s", name, err)
}
astFiles = append(astFiles, parsedFile)
files = append(files, &File{
file: parsedFile,
pkg: g.pkg, pkg: g.pkg,
}) }
} }
if len(astFiles) == 0 {
log.Fatalf("%s: no buildable Go files", directory)
}
g.pkg.name = astFiles[0].Name.Name
g.pkg.files = files
g.pkg.dir = directory
// Type check the package.
g.pkg.check(fs, astFiles)
} }
// check type-checks the package. The package must be OK to proceed. // check type-checks the package. The package must be OK to proceed.
func (pkg *Package) check(fs *token.FileSet, astFiles []*ast.File) { func (pkg *Package) check(fs *token.FileSet, astFiles []*ast.File) {
pkg.defs = make(map[*ast.Ident]types.Object) pkg.defs = make(map[*ast.Ident]types.Object)
config := types.Config{Importer: defaultImporter(), FakeImportC: true} config := types.Config{Importer: importer.Default(), FakeImportC: true}
info := &types.Info{ info := &types.Info{
Defs: pkg.defs, Defs: pkg.defs,
} }
@ -279,6 +316,14 @@ func (g *Generator) transformValueNames(values []Value, transformMethod string)
sep = '_' sep = '_'
case "kebab": case "kebab":
sep = '-' sep = '-'
case "phrase":
sep = ' '
case "macro":
sep = '_'
for i := range values {
values[i].name = strings.ToUpper(name.Delimit(values[i].name, sep))
}
return
default: default:
return return
} }
@ -295,8 +340,16 @@ func (g *Generator) trimValueNames(values []Value, prefix string) {
} }
} }
func (g *Generator) replaceValuesWithLineComment(values []Value) {
for i, val := range values {
if val.comment != "" {
values[i].name = val.comment
}
}
}
// 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, includeText bool, transformMethod string, trimPrefix string) { func (g *Generator) generate(typeName string, includeJSON, includeYAML, includeSQL, includeText bool, transformMethod string, trimPrefix string, lineComment 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.
@ -316,6 +369,10 @@ func (g *Generator) generate(typeName string, includeJSON, includeYAML, includeS
g.transformValueNames(values, transformMethod) g.transformValueNames(values, transformMethod)
if lineComment {
g.replaceValuesWithLineComment(values)
}
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
@ -407,9 +464,10 @@ type Value struct {
// this matters is when sorting. // this matters is when sorting.
// Much of the time the str field is all we need; it is printed // Much of the time the str field is all we need; it is printed
// by Value.String. // by Value.String.
value uint64 // Will be converted to int64 when needed. value uint64 // Will be converted to int64 when needed.
signed bool // Whether the constant is a signed type. signed bool // Whether the constant is a signed type.
str string // The string representation given by the "go/exact" package. str string // The string representation given by the "go/exact" package.
comment string // The comment on the right of the constant
} }
func (v *Value) String() string { func (v *Value) String() string {
@ -494,11 +552,17 @@ func (f *File) genDecl(node ast.Node) bool {
if !isInt { if !isInt {
u64 = uint64(i64) u64 = uint64(i64)
} }
comment := ""
if c := vspec.Comment; c != nil && len(c.List) == 1 {
comment = strings.TrimSpace(c.Text())
}
v := Value{ v := Value{
name: name.Name, name: name.Name,
value: u64, value: u64,
signed: info&types.IsUnsigned == 0, signed: info&types.IsUnsigned == 0,
str: value.String(), str: value.String(),
comment: comment,
} }
f.values = append(f.values, v) f.values = append(f.values, v)
} }

47
testdata/thresholdeq.go vendored Normal file
View File

@ -0,0 +1,47 @@
package main
import "fmt"
type Thresholdeq int
const (
req1 Thresholdeq = 2
req2 Thresholdeq = 4
req3 Thresholdeq = 6
req4 Thresholdeq = 8
req5 Thresholdeq = 10
req6 Thresholdeq = 12
req7 Thresholdeq = 14
req8 Thresholdeq = 16
req9 Thresholdeq = 18
req10 Thresholdeq = 20
)
func main() {
ck(1, "Thresholdeq(1)")
ck(req1, "req1")
ck(3, "Thresholdeq(3)")
ck(req2, "req2")
ck(5, "Thresholdeq(5)")
ck(req3, "req3")
ck(7, "Thresholdeq(7)")
ck(req4, "req4")
ck(9, "Thresholdeq(9)")
ck(req5, "req5")
ck(11, "Thresholdeq(11)")
ck(req6, "req6")
ck(13, "Thresholdeq(13)")
ck(req7, "req7")
ck(15, "Thresholdeq(15)")
ck(req8, "req8")
ck(17, "Thresholdeq(17)")
ck(req9, "req9")
ck(19, "Thresholdeq(19)")
ck(req10, "req10")
}
func ck(thresholdeq Thresholdeq, str string) {
if fmt.Sprint(thresholdeq) != str {
panic("thresholdeq.go: " + str)
}
}

50
testdata/thresholdgt.go vendored Normal file
View File

@ -0,0 +1,50 @@
package main
import "fmt"
type Thresholdgt int
const (
rgt1 Thresholdgt = 2
rgt2 Thresholdgt = 4
rgt3 Thresholdgt = 6
rgt4 Thresholdgt = 8
rgt5 Thresholdgt = 10
rgt6 Thresholdgt = 12
rgt7 Thresholdgt = 14
rgt8 Thresholdgt = 16
rgt9 Thresholdgt = 18
rgt10 Thresholdgt = 20
rgt11 Thresholdgt = 22
)
func main() {
ck(1, "Thresholdgt(1)")
ck(rgt1, "rgt1")
ck(3, "Thresholdgt(3)")
ck(rgt2, "rgt2")
ck(5, "Thresholdgt(5)")
ck(rgt3, "rgt3")
ck(7, "Thresholdgt(7)")
ck(rgt4, "rgt4")
ck(9, "Thresholdgt(9)")
ck(rgt5, "rgt5")
ck(11, "Thresholdgt(11)")
ck(rgt6, "rgt6")
ck(13, "Thresholdgt(13)")
ck(rgt7, "rgt7")
ck(15, "Thresholdgt(15)")
ck(rgt8, "rgt8")
ck(17, "Thresholdgt(17)")
ck(rgt9, "rgt9")
ck(19, "Thresholdgt(19)")
ck(rgt10, "rgt10")
ck(21, "Thresholdgt(21)")
ck(rgt11, "rgt11")
}
func ck(thresholdgt Thresholdgt, str string) {
if fmt.Sprint(thresholdgt) != str {
panic("thresholdgt.go: " + str)
}
}

44
testdata/thresholdlt.go vendored Normal file
View File

@ -0,0 +1,44 @@
package main
import "fmt"
type Thresholdlt int
const (
rlt1 Thresholdlt = 2
rlt2 Thresholdlt = 4
rlt3 Thresholdlt = 6
rlt4 Thresholdlt = 8
rlt5 Thresholdlt = 10
rlt6 Thresholdlt = 12
rlt7 Thresholdlt = 14
rlt8 Thresholdlt = 16
rlt9 Thresholdlt = 18
)
func main() {
ck(1, "Thresholdlt(1)")
ck(rlt1, "rlt1")
ck(3, "Thresholdlt(3)")
ck(rlt2, "rlt2")
ck(5, "Thresholdlt(5)")
ck(rlt3, "rlt3")
ck(7, "Thresholdlt(7)")
ck(rlt4, "rlt4")
ck(9, "Thresholdlt(9)")
ck(rlt5, "rlt5")
ck(11, "Thresholdlt(11)")
ck(rlt6, "rlt6")
ck(13, "Thresholdlt(13)")
ck(rlt7, "rlt7")
ck(15, "Thresholdlt(15)")
ck(rlt8, "rlt8")
ck(17, "Thresholdlt(17)")
ck(rlt9, "rlt9")
}
func ck(thresholdlt Thresholdlt, str string) {
if fmt.Sprint(thresholdlt) != str {
panic("thresholdlt.go: " + str)
}
}

View File

@ -54,7 +54,7 @@ Outer:
for n, test := range splitTests { for n, test := range splitTests {
values := make([]Value, len(test.input)) values := make([]Value, len(test.input))
for i, v := range test.input { for i, v := range test.input {
values[i] = Value{"", v, test.signed, fmt.Sprint(v)} values[i] = Value{"", v, test.signed, fmt.Sprint(v), ""}
} }
runs := splitIntoRuns(values) runs := splitIntoRuns(values)
if len(runs) != len(test.output) { if len(runs) != len(test.output) {

View File

@ -1,3 +0,0 @@
To the extent possible under law, Pascal S. de Kloe has waived all
copyright and related or neighboring rights to Colfer. This work is
published from The Netherlands.

View File

@ -1,6 +0,0 @@
[![GoDoc](https://godoc.org/github.com/pascaldekloe/name?status.svg)](https://godoc.org/github.com/pascaldekloe/name)
Naming convention library for the Go programming language (golang).
This is free and unencumbered software released into the
[public domain](http://creativecommons.org/publicdomain/zero/1.0).

View File

@ -1,112 +0,0 @@
// Package name implements naming conventions.
package name
import "unicode"
// CamelCase returns the medial capitals form of word sequence s.
// The input can be any case or even just a bunch of words.
// Upper case abbreviations are preserved.
// Argument upper sets the casing for the first rune.
func CamelCase(s string, upper bool) string {
if s == "" {
return ""
}
out := make([]rune, 1, len(s)+5)
for i, r := range s {
if i == 0 {
if upper {
r = unicode.ToUpper(r)
}
out[0] = r
continue
}
if i == 1 {
if !upper && unicode.Is(unicode.Lower, r) {
out[0] = unicode.ToLower(out[0])
}
upper = false
}
switch {
case unicode.IsLetter(r):
if upper {
r = unicode.ToUpper(r)
}
fallthrough
case unicode.IsNumber(r):
upper = false
out = append(out, r)
default:
upper = true
}
}
return string(out)
}
// SnakeCase is an alias for Delimit(s, '_').
func SnakeCase(s string) string {
return Delimit(s, '_')
}
// DotSeparated is an alias for Delimit(s, '.').
func DotSeparated(s string) string {
return Delimit(s, '.')
}
// Delimit returns word sequence s delimited with sep.
// The input can be any case or even just a bunch of words.
// Upper case abbreviations are preserved. Use strings.ToLower
// and strings.ToUpper to enforce a letter case.
func Delimit(s string, sep rune) string {
out := make([]rune, 0, len(s)+5)
for _, r := range s {
switch {
case unicode.IsUpper(r):
if last := len(out) - 1; last >= 0 && unicode.IsLower(out[last]) {
out = append(out, sep)
}
case unicode.IsLetter(r):
if i := len(out) - 1; i >= 0 {
if last := out[i]; unicode.IsUpper(last) {
out = out[:i]
if i > 0 && out[i-1] != sep {
out = append(out, sep)
}
out = append(out, unicode.ToLower(last))
}
}
case !unicode.IsNumber(r):
if i := len(out); i != 0 && out[i-1] != sep {
out = append(out, sep)
}
continue
}
out = append(out, r)
}
if len(out) == 0 {
return ""
}
// trim tailing separator
if i := len(out) - 1; out[i] == sep {
out = out[:i]
}
if len(out) == 1 {
out[0] = unicode.ToLower(out[0])
}
return string(out)
}

View File

@ -1,103 +0,0 @@
package name
import (
"testing"
)
type goldenCase struct {
snake, lowerCamel, upperCamel string
}
var goldenCases = []goldenCase{
{"", "", ""},
{"i", "i", "I"},
{"name", "name", "Name"},
{"ID", "ID", "ID"},
{"wi_fi", "wiFi", "WiFi"},
// single outer abbreviation
{"vitamin_C", "vitaminC", "VitaminC"},
{"T_cell", "TCell", "TCell"},
// double outer abbreviation
{"master_DB", "masterDB", "MasterDB"},
{"IO_bounds", "IOBounds", "IOBounds"},
// tripple outer abbreviation
{"main_API", "mainAPI", "MainAPI"},
{"TCP_conn", "TCPConn", "TCPConn"},
// inner abbreviation
{"raw_URL_query", "rawURLQuery", "RawURLQuery"},
// numbers
{"4x4", "4x4", "4x4"},
{"no5", "no5", "No5"},
{"DB2", "DB2", "DB2"},
{"3M", "3M", "3M"},
{"7_up", "7Up", "7Up"},
{"20th", "20th", "20th"},
}
func TestSnakeToSnake(t *testing.T) {
for _, golden := range goldenCases {
s := golden.snake
if got := SnakeCase(s); got != s {
t.Errorf("%q: got %q", s, got)
}
}
}
func TestLowerCamelToLowerCamel(t *testing.T) {
for _, golden := range goldenCases {
s := golden.lowerCamel
if got := CamelCase(s, false); got != s {
t.Errorf("%q: got %q", s, got)
}
}
}
func TestUpperCamelToUpperCamel(t *testing.T) {
for _, golden := range goldenCases {
s := golden.upperCamel
if got := CamelCase(s, true); got != s {
t.Errorf("%q: got %q", s, got)
}
}
}
func TestSnakeToLowerCamel(t *testing.T) {
for _, golden := range goldenCases {
snake, want := golden.snake, golden.lowerCamel
if got := CamelCase(snake, false); got != want {
t.Errorf("%q: got %q, want %q", snake, got, want)
}
}
}
func TestSnakeToUpperCamel(t *testing.T) {
for _, golden := range goldenCases {
snake, want := golden.snake, golden.upperCamel
if got := CamelCase(snake, true); got != want {
t.Errorf("%q: got %q, want %q", snake, got, want)
}
}
}
func TestLowerCamelToSnake(t *testing.T) {
for _, golden := range goldenCases {
camel, want := golden.lowerCamel, golden.snake
if got := SnakeCase(camel); got != want {
t.Errorf("%q: got %q, want %q", camel, got, want)
}
}
}
func TestUpperCamelToSnake(t *testing.T) {
for _, golden := range goldenCases {
camel, want := golden.upperCamel, golden.snake
if got := SnakeCase(camel); got != want {
t.Errorf("%q: got %q, want %q", camel, got, want)
}
}
}

View File

@ -1,27 +0,0 @@
package name_test
import (
"fmt"
"github.com/pascaldekloe/name"
)
func ExampleCamelCase() {
fmt.Println(name.CamelCase("pascal case", true))
fmt.Println(name.CamelCase("snake_to_camel AND CamelToCamel?", false))
// Output:
// PascalCase
// snakeToCamelANDCamelToCamel
}
func ExampleDelimit() {
// Garbage to Lisp-case:
fmt.Println(name.Delimit("* All Hype is aGoodThing (TM)", '-'))
// Builds a Java property key:
fmt.Println(name.DotSeparated("WebCrawler#socketTimeout"))
// Output:
// all-hype-is-a-good-thing-TM
// web.crawler.socket.timeout
}