initial commit

This commit is contained in:
Francesc Campoy 2015-01-29 11:35:15 +01:00
parent fb5644d041
commit 66a9dd356f
6 changed files with 587 additions and 1 deletions

View File

@ -1,2 +1,71 @@
# jsonenums
This tool is similar to golang.org/x/tools/cmd/stringer but generates MarshalJSON and UnmarshalJSON methods.
JSONenums is a tool to automate the creation of methods that satisfy the
`fmt.Stringer`, `json.Marshaler` and `json.Unmarshaler` interfaces.
Given the name of a (signed or unsigned) integer type T that has constants
defined, stringer will create a new self-contained Go source file implementing
```
func (t T) String() string
func (t T) MarshalJSON() ([]byte, error)
func (t *T) UnmarshalJSON([]byte) error
```
The file is created in the same package and directory as the package that
defines T. It has helpful defaults designed for use with go generate.
JSONenums is a simple implementation of a concept and the code might not be the
most performant or beautiful to read.
For example, given this snippet,
```
package painkiller
type Pill int
const (
Placebo Pill = iota
Aspirin
Ibuprofen
Paracetamol
Acetaminophen = Paracetamol
)
```
running this command
```
jsonenums -type=Pill
```
in the same directory will create the file `pill_jsonenums.go`, in package
`painkiller`, containing a definition of
```
func (r Pill) String() string
func (r Pill) MarshalJSON() ([]byte, error)
func (r *Pill) UnmarshalJSON([]byte) error
```
That method will translate the value of a Pill constant to the string
representation of the respective constant name, so that the call
`fmt.Print(painkiller.Aspirin) will print the string "Aspirin".
Typically this process would be run using go generate, like this:
```
//go:generate stringer -type=Pill
```
If multiple constants have the same value, the lexically first matching name
will be used (in the example, Acetaminophen will print as "Paracetamol").
With no arguments, it processes the package in the current directory. Otherwise,
the arguments must name a single directory holding a Go package or a set of Go
source files that represent a single Go package.
The `-type` flag accepts a comma-separated list of types so a single run can
generate methods for multiple types. The default output file is t_string.go,
where t is the lower-cased name of the first type listed. THe suffix can be
overridden with the `-suffix` flag.

30
example/shirtsize.go Normal file
View File

@ -0,0 +1,30 @@
// Copyright 2014 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.
package testdata
//go:generate jsonenums -type=ShirtSize
type ShirtSize byte
const (
NA ShirtSize = iota
XS
S
M
L
XL
)
//go:generate jsonenums -type=WeekDay
type WeekDay int
const (
Monday WeekDay = iota
Tuesday
Wednesday
Thursday
Friday
Saturday
Sunday
)

View File

@ -0,0 +1,69 @@
// generated by jsonenums -type=ShirtSize; DO NOT EDIT
package testdata
import (
"encoding/json"
"fmt"
)
func (r ShirtSize) String() string {
switch r {
case NA:
return "NA"
case XS:
return "XS"
case S:
return "S"
case M:
return "M"
case L:
return "L"
case XL:
return "XL"
default:
return "unknown ShirtSize"
}
}
func (r ShirtSize) MarshalJSON() ([]byte, error) {
return json.Marshal(r.String())
}
func (r *ShirtSize) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return fmt.Errorf("ShirtSize should be a string, got %s", data)
}
switch s {
case "NA":
*r = NA
case "XS":
*r = XS
case "S":
*r = S
case "M":
*r = M
case "L":
*r = L
case "XL":
*r = XL
default:
return nil, fmt.Errorf("invalid ShirtSize %q", s)
}
return nil
}

View File

@ -0,0 +1,75 @@
// generated by jsonenums -type=WeekDay; DO NOT EDIT
package testdata
import (
"encoding/json"
"fmt"
)
func (r WeekDay) String() string {
switch r {
case Monday:
return "Monday"
case Tuesday:
return "Tuesday"
case Wednesday:
return "Wednesday"
case Thursday:
return "Thursday"
case Friday:
return "Friday"
case Saturday:
return "Saturday"
case Sunday:
return "Sunday"
default:
return "unknown WeekDay"
}
}
func (r WeekDay) MarshalJSON() ([]byte, error) {
return json.Marshal(r.String())
}
func (r *WeekDay) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return fmt.Errorf("WeekDay should be a string, got %s", data)
}
switch s {
case "Monday":
*r = Monday
case "Tuesday":
*r = Tuesday
case "Wednesday":
*r = Wednesday
case "Thursday":
*r = Thursday
case "Friday":
*r = Friday
case "Saturday":
*r = Saturday
case "Sunday":
*r = Sunday
default:
return nil, fmt.Errorf("invalid WeekDay %q", s)
}
return nil
}

289
jsonenums.go Normal file
View File

@ -0,0 +1,289 @@
// Copyright 2014 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.
// JSONenums is a tool to automate the creation of methods that satisfy the
// fmt.Stringer, json.Marshaler and json.Unmarshaler interfaces.
// Given the name of a (signed or unsigned) integer type T that has constants
// defined, stringer will create a new self-contained Go source file implementing
//
// func (t T) String() string
// func (t T) MarshalJSON() ([]byte, error)
// func (t *T) UnmarshalJSON([]byte) error
//
// The file is created in the same package and directory as the package that defines T.
// It has helpful defaults designed for use with go generate.
//
// JSONenums is a simple implementation of a concept and the code might not be
// the most performant or beautiful to read.
//
// For example, given this snippet,
//
// package painkiller
//
// type Pill int
//
// const (
// Placebo Pill = iota
// Aspirin
// Ibuprofen
// Paracetamol
// Acetaminophen = Paracetamol
// )
//
// running this command
//
// jsonenums -type=Pill
//
// in the same directory will create the file pill_jsonenums.go, in package painkiller,
// containing a definition of
//
// func (r Pill) String() string
// func (r Pill) MarshalJSON() ([]byte, error)
// func (r *Pill) UnmarshalJSON([]byte) error
//
// That method will translate the value of a Pill constant to the string representation
// of the respective constant name, so that the call fmt.Print(painkiller.Aspirin) will
// print the string "Aspirin".
//
// Typically this process would be run using go generate, like this:
//
// //go:generate stringer -type=Pill
//
// If multiple constants have the same value, the lexically first matching name will
// be used (in the example, Acetaminophen will print as "Paracetamol").
//
// With no arguments, it processes the package in the current directory.
// Otherwise, the arguments must name a single directory holding a Go package
// or a set of Go source files that represent a single Go package.
//
// The -type flag accepts a comma-separated list of types so a single run can
// generate methods for multiple types. The default output file is t_string.go,
// where t is the lower-cased name of the first type listed. THe suffix can be
// overridden with the -suffix flag.
//
package main
import (
"bytes"
"flag"
"fmt"
"go/ast"
"go/build"
"go/format"
"go/parser"
"go/token"
"io/ioutil"
"log"
"os"
"path/filepath"
"strings"
"golang.org/x/tools/go/exact"
"golang.org/x/tools/go/types"
_ "golang.org/x/tools/go/gcimporter"
)
var (
typeNames = flag.String("type", "", "comma-separated list of type names; must be set")
outputSuffix = flag.String("suffix", "_jsonenums", "suffix to be added to the output file")
)
func main() {
flag.Parse()
if len(*typeNames) == 0 {
log.Fatalf("the flag -type must be set")
}
types := strings.Split(*typeNames, ",")
// Only one directory at a time can be processed, and the default is ".".
dir := "."
if args := flag.Args(); len(args) == 1 {
dir = args[0]
} else if len(args) > 1 {
log.Fatalf("only one directory at a time")
}
pkg, err := parsePackage(dir, *outputSuffix+".go")
if err != nil {
log.Fatalf("parsing package: %v", err)
}
var analysis = struct {
Command string
PackageName string
TypesAndValues map[string][]string
}{
Command: strings.Join(os.Args[1:], " "),
PackageName: pkg.name,
TypesAndValues: make(map[string][]string),
}
// Run generate for each type.
for _, typeName := range types {
values, err := pkg.valuesOfType(typeName)
if err != nil {
log.Fatalf("finding values for type %v: %v", typeName, err)
}
analysis.TypesAndValues[typeName] = values
var buf bytes.Buffer
if err := generatedTmpl.Execute(&buf, analysis); err != nil {
log.Fatalf("generating code: %v", err)
}
src, err := format.Source(buf.Bytes())
if err != nil {
// Should never happen, but can arise when developing this code.
// The user can compile the output to see the error.
log.Printf("warning: internal error: invalid Go generated: %s", err)
log.Printf("warning: compile the package to analyze the error")
src = buf.Bytes()
}
output := strings.ToLower(typeName + *outputSuffix + ".go")
outputPath := filepath.Join(dir, output)
if err := ioutil.WriteFile(outputPath, src, 0644); err != nil {
log.Fatalf("writing output: %s", err)
}
}
}
type Package struct {
name string
files []*ast.File
defs map[*ast.Ident]types.Object
types *types.Package
}
// parsePackage parses the package in the given directory and returns it.
func parsePackage(directory string, skipSuffix string) (*Package, error) {
pkgDir, err := build.Default.ImportDir(directory, 0)
if err != nil {
return nil, fmt.Errorf("cannot process directory %s: %s", directory, err)
}
var files []*ast.File
fs := token.NewFileSet()
for _, name := range pkgDir.GoFiles {
if !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, skipSuffix) {
continue
}
if directory != "." {
name = filepath.Join(directory, name)
}
f, err := parser.ParseFile(fs, name, nil, 0)
if err != nil {
return nil, fmt.Errorf("parsing file %v: %v", name, err)
}
files = append(files, f)
}
if len(files) == 0 {
return nil, fmt.Errorf("%s: no buildable Go files", directory)
}
// type-check the package
defs := make(map[*ast.Ident]types.Object)
config := types.Config{FakeImportC: true}
info := &types.Info{Defs: defs}
types, err := config.Check(directory, fs, files, info)
if err != nil {
return nil, fmt.Errorf("type-checking package: %v", err)
}
return &Package{
name: files[0].Name.Name,
files: files,
defs: defs,
types: types,
}, nil
}
// generate produces the String method for the named type.
func (pkg *Package) valuesOfType(typeName string) ([]string, error) {
var values, inspectErrs []string
for _, file := range pkg.files {
ast.Inspect(file, func(node ast.Node) bool {
decl, ok := node.(*ast.GenDecl)
if !ok || decl.Tok != token.CONST {
// We only care about const declarations.
return true
}
if vs, err := pkg.valuesOfTypeIn(typeName, decl); err != nil {
inspectErrs = append(inspectErrs, err.Error())
} else {
values = append(values, vs...)
}
return false
})
}
if len(inspectErrs) > 0 {
return nil, fmt.Errorf("inspecting code:\n\t%v", strings.Join(inspectErrs, "\n\t"))
}
if len(values) == 0 {
return nil, fmt.Errorf("no values defined for type %s", typeName)
}
return values, nil
}
func (pkg *Package) valuesOfTypeIn(typeName string, decl *ast.GenDecl) ([]string, error) {
var values []string
// The name of the type of the constants we are declaring.
// Can change if this is a multi-element declaration.
typ := ""
// Loop over the elements of the declaration. Each element is a ValueSpec:
// a list of names possibly followed by a type, possibly followed by values.
// If the type and value are both missing, we carry down the type (and value,
// but the "go/types" package takes care of that).
for _, spec := range decl.Specs {
vspec := spec.(*ast.ValueSpec) // Guaranteed to succeed as this is CONST.
if vspec.Type == nil && len(vspec.Values) > 0 {
// "X = 1". With no type but a value, the constant is untyped.
// Skip this vspec and reset the remembered type.
typ = ""
continue
}
if vspec.Type != nil {
// "X T". We have a type. Remember it.
ident, ok := vspec.Type.(*ast.Ident)
if !ok {
continue
}
typ = ident.Name
}
if typ != typeName {
// This is not the type we're looking for.
continue
}
// We now have a list of names (from one line of source code) all being
// declared with the desired type.
// Grab their names and actual values and store them in f.values.
for _, name := range vspec.Names {
if name.Name == "_" {
continue
}
// This dance lets the type checker find the values for us. It's a
// bit tricky: look up the object declared by the name, find its
// types.Const, and extract its value.
obj, ok := pkg.defs[name]
if !ok {
return nil, fmt.Errorf("no value for constant %s", name)
}
info := obj.Type().Underlying().(*types.Basic).Info()
if info&types.IsInteger == 0 {
return nil, fmt.Errorf("can't handle non-integer constant type %s", typ)
}
value := obj.(*types.Const).Val() // Guaranteed to succeed as this is CONST.
if value.Kind() != exact.Int {
log.Fatalf("can't happen: constant is not an integer %s", name)
}
values = append(values, name.Name)
}
}
return values, nil
}

54
template.go Normal file
View File

@ -0,0 +1,54 @@
// Copyright 2014 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.
// Added as a .go file to avoid embedding issues of the template.
package main
import "text/template"
var generatedTmpl = template.Must(template.New("generated").Parse(`
// generated by jsonenums {{.Command}}; DO NOT EDIT
package {{.PackageName}}
import (
"encoding/json"
"fmt"
)
{{range $typename, $values := .TypesAndValues}}
func (r {{$typename}}) String() string {
switch r {
{{range $values}}
case {{.}}:
return "{{.}}"
{{end}}
default:
return "unknown {{$typename}}"
}
}
func (r {{$typename}}) MarshalJSON() ([]byte, error) {
return json.Marshal(r.String())
}
func (r *{{$typename}}) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return fmt.Errorf("{{$typename}} should be a string, got %s", data)
}
switch s {
{{range $values}}
case "{{.}}":
*r = {{.}}
{{end}}
default:
return fmt.Errorf("invalid {{$typename}} %q", s)
}
return nil
}
{{end}}
`))