From 66a9dd356f9958c89b83512a07f4964187981419 Mon Sep 17 00:00:00 2001 From: Francesc Campoy Date: Thu, 29 Jan 2015 11:35:15 +0100 Subject: [PATCH 1/7] initial commit --- README.md | 71 +++++++- example/shirtsize.go | 30 ++++ example/shirtsize_jsonenums.go | 69 ++++++++ example/weekday_jsonenums.go | 75 +++++++++ jsonenums.go | 289 +++++++++++++++++++++++++++++++++ template.go | 54 ++++++ 6 files changed, 587 insertions(+), 1 deletion(-) create mode 100644 example/shirtsize.go create mode 100644 example/shirtsize_jsonenums.go create mode 100644 example/weekday_jsonenums.go create mode 100644 jsonenums.go create mode 100644 template.go diff --git a/README.md b/README.md index 48ad29a..f4b7c38 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/example/shirtsize.go b/example/shirtsize.go new file mode 100644 index 0000000..41ed848 --- /dev/null +++ b/example/shirtsize.go @@ -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 +) diff --git a/example/shirtsize_jsonenums.go b/example/shirtsize_jsonenums.go new file mode 100644 index 0000000..7fc4f47 --- /dev/null +++ b/example/shirtsize_jsonenums.go @@ -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 +} diff --git a/example/weekday_jsonenums.go b/example/weekday_jsonenums.go new file mode 100644 index 0000000..1ce2f9b --- /dev/null +++ b/example/weekday_jsonenums.go @@ -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 +} diff --git a/jsonenums.go b/jsonenums.go new file mode 100644 index 0000000..7585a51 --- /dev/null +++ b/jsonenums.go @@ -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 +} diff --git a/template.go b/template.go new file mode 100644 index 0000000..243d01a --- /dev/null +++ b/template.go @@ -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}} +`)) From 10933b856e570af2c6482962d71e516dcad12828 Mon Sep 17 00:00:00 2001 From: Francesc Campoy Date: Thu, 29 Jan 2015 17:00:50 +0100 Subject: [PATCH 2/7] added demo file --- example/shirtsize.go | 18 +++++++++++++++++- example/shirtsize_jsonenums.go | 4 ++-- example/weekday_jsonenums.go | 4 ++-- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/example/shirtsize.go b/example/shirtsize.go index 41ed848..4bdf15e 100644 --- a/example/shirtsize.go +++ b/example/shirtsize.go @@ -2,7 +2,13 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package testdata +package main + +import ( + "encoding/json" + "log" + "os" +) //go:generate jsonenums -type=ShirtSize type ShirtSize byte @@ -28,3 +34,13 @@ const ( Saturday Sunday ) + +func main() { + v := struct { + Size ShirtSize + Day WeekDay + }{M, Friday} + if err := json.NewEncoder(os.Stdout).Encode(v); err != nil { + log.Fatal(err) + } +} diff --git a/example/shirtsize_jsonenums.go b/example/shirtsize_jsonenums.go index 7fc4f47..a2432d6 100644 --- a/example/shirtsize_jsonenums.go +++ b/example/shirtsize_jsonenums.go @@ -1,6 +1,6 @@ // generated by jsonenums -type=ShirtSize; DO NOT EDIT -package testdata +package main import ( "encoding/json" @@ -63,7 +63,7 @@ func (r *ShirtSize) UnmarshalJSON(data []byte) error { *r = XL default: - return nil, fmt.Errorf("invalid ShirtSize %q", s) + return fmt.Errorf("invalid ShirtSize %q", s) } return nil } diff --git a/example/weekday_jsonenums.go b/example/weekday_jsonenums.go index 1ce2f9b..38b923f 100644 --- a/example/weekday_jsonenums.go +++ b/example/weekday_jsonenums.go @@ -1,6 +1,6 @@ // generated by jsonenums -type=WeekDay; DO NOT EDIT -package testdata +package main import ( "encoding/json" @@ -69,7 +69,7 @@ func (r *WeekDay) UnmarshalJSON(data []byte) error { *r = Sunday default: - return nil, fmt.Errorf("invalid WeekDay %q", s) + return fmt.Errorf("invalid WeekDay %q", s) } return nil } From 3da4e90d4393be281cf409bead9aa3860415a87b Mon Sep 17 00:00:00 2001 From: Francesc Campoy Date: Sat, 31 Jan 2015 10:21:59 +0100 Subject: [PATCH 3/7] Move the go:generate comments one line up to avoid documenting the type --- example/shirtsize.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/example/shirtsize.go b/example/shirtsize.go index 4bdf15e..e0bfc26 100644 --- a/example/shirtsize.go +++ b/example/shirtsize.go @@ -11,6 +11,7 @@ import ( ) //go:generate jsonenums -type=ShirtSize + type ShirtSize byte const ( @@ -23,6 +24,7 @@ const ( ) //go:generate jsonenums -type=WeekDay + type WeekDay int const ( From a673e223843b8a04285a854579b25aa99cc7370d Mon Sep 17 00:00:00 2001 From: Francesc Campoy Date: Sat, 31 Jan 2015 11:09:02 +0100 Subject: [PATCH 4/7] improved the generated code by using maps and using existing String method if available --- example/shirtsize_jsonenums.go | 63 +++++++++---------------------- example/weekday_jsonenums.go | 69 +++++++++------------------------- template.go | 33 ++++++++-------- 3 files changed, 51 insertions(+), 114 deletions(-) diff --git a/example/shirtsize_jsonenums.go b/example/shirtsize_jsonenums.go index a2432d6..65ac8ad 100644 --- a/example/shirtsize_jsonenums.go +++ b/example/shirtsize_jsonenums.go @@ -7,34 +7,23 @@ import ( "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" - } +var _ShirtSizeValueToName = map[ShirtSize]string{ + NA: "NA", XS: "XS", S: "S", M: "M", L: "L", XL: "XL", } func (r ShirtSize) MarshalJSON() ([]byte, error) { - return json.Marshal(r.String()) + if s, ok := r.(fmt.Stringer); ok { + return json.Marshal(s.String()) + } + s, ok := _ShirtSizeValueToName[r] + if !ok { + return nil, fmt.Errorf("invalid ShirtSize: %d", r) + } + return json.Marshal(s) +} + +var _ShirtSizeNameToValue = map[string]ShirtSize{ + "NA": NA, "XS": XS, "S": S, "M": M, "L": L, "XL": XL, } func (r *ShirtSize) UnmarshalJSON(data []byte) error { @@ -42,28 +31,10 @@ func (r *ShirtSize) UnmarshalJSON(data []byte) error { 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: + v, ok := _ShirtSizeNameToValue[s] + if !ok { return fmt.Errorf("invalid ShirtSize %q", s) } + *r = v return nil } diff --git a/example/weekday_jsonenums.go b/example/weekday_jsonenums.go index 38b923f..5e6f0b5 100644 --- a/example/weekday_jsonenums.go +++ b/example/weekday_jsonenums.go @@ -7,37 +7,23 @@ import ( "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" - } +var _WeekDayValueToName = map[WeekDay]string{ + Monday: "Monday", Tuesday: "Tuesday", Wednesday: "Wednesday", Thursday: "Thursday", Friday: "Friday", Saturday: "Saturday", Sunday: "Sunday", } func (r WeekDay) MarshalJSON() ([]byte, error) { - return json.Marshal(r.String()) + if s, ok := r.(fmt.Stringer); ok { + return json.Marshal(s.String()) + } + s, ok := _WeekDayValueToName[r] + if !ok { + return nil, fmt.Errorf("invalid WeekDay: %d", r) + } + return json.Marshal(s) +} + +var _WeekDayNameToValue = map[string]WeekDay{ + "Monday": Monday, "Tuesday": Tuesday, "Wednesday": Wednesday, "Thursday": Thursday, "Friday": Friday, "Saturday": Saturday, "Sunday": Sunday, } func (r *WeekDay) UnmarshalJSON(data []byte) error { @@ -45,31 +31,10 @@ func (r *WeekDay) UnmarshalJSON(data []byte) error { 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: + v, ok := _WeekDayNameToValue[s] + if !ok { return fmt.Errorf("invalid WeekDay %q", s) } + *r = v return nil } diff --git a/template.go b/template.go index 243d01a..e2ca209 100644 --- a/template.go +++ b/template.go @@ -19,19 +19,23 @@ import ( ) {{range $typename, $values := .TypesAndValues}} -func (r {{$typename}}) String() string { - switch r { - {{range $values}} - case {{.}}: - return "{{.}}" - {{end}} - default: - return "unknown {{$typename}}" - } +var _{{$typename}}ValueToName = map[{{$typename}}]string { + {{range $values}}{{.}}: "{{.}}",{{end}} } func (r {{$typename}}) MarshalJSON() ([]byte, error) { - return json.Marshal(r.String()) + if s, ok := r.(fmt.Stringer); ok { + return json.Marshal(s.String()) + } + s, ok := _{{$typename}}ValueToName[r] + if !ok { + return nil, fmt.Errorf("invalid {{$typename}}: %d", r) + } + return json.Marshal(s) +} + +var _{{$typename}}NameToValue = map[string]{{$typename}} { + {{range $values}}"{{.}}": {{.}},{{end}} } func (r *{{$typename}}) UnmarshalJSON(data []byte) error { @@ -39,14 +43,11 @@ func (r *{{$typename}}) UnmarshalJSON(data []byte) error { 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: + v, ok := _{{$typename}}NameToValue[s] + if !ok { return fmt.Errorf("invalid {{$typename}} %q", s) } + *r = v return nil } From 5cd7ec204a329856ab14137f90a3341622582a7d Mon Sep 17 00:00:00 2001 From: Francesc Campoy Date: Sat, 31 Jan 2015 12:21:55 +0100 Subject: [PATCH 5/7] support of decoding for Stringers --- example/shirtsize.go | 29 +++++++++++++++++++++++++++++ example/shirtsize_jsonenums.go | 19 +++++++++++++------ example/weekday_jsonenums.go | 19 +++++++++++++------ jsonenums.go | 7 ++----- template.go | 18 +++++++++++++----- 5 files changed, 70 insertions(+), 22 deletions(-) diff --git a/example/shirtsize.go b/example/shirtsize.go index e0bfc26..58bb247 100644 --- a/example/shirtsize.go +++ b/example/shirtsize.go @@ -6,8 +6,10 @@ package main import ( "encoding/json" + "fmt" "log" "os" + "strings" ) //go:generate jsonenums -type=ShirtSize @@ -37,6 +39,27 @@ const ( Sunday ) +func (d WeekDay) String() string { + switch d { + case Monday: + return "Dilluns" + case Tuesday: + return "Dimarts" + case Wednesday: + return "Dimecres" + case Thursday: + return "Dijous" + case Friday: + return "Divendres" + case Saturday: + return "Dissabte" + case Sunday: + return "Diumenge" + default: + return "invalid WeekDay" + } +} + func main() { v := struct { Size ShirtSize @@ -45,4 +68,10 @@ func main() { if err := json.NewEncoder(os.Stdout).Encode(v); err != nil { log.Fatal(err) } + + input := `{"Size":"XL", "Day":"Dimarts"}` + if err := json.NewDecoder(strings.NewReader(input)).Decode(&v); err != nil { + log.Fatal(err) + } + fmt.Printf("decoded %s as %+v\n", input, v) } diff --git a/example/shirtsize_jsonenums.go b/example/shirtsize_jsonenums.go index 65ac8ad..41938cb 100644 --- a/example/shirtsize_jsonenums.go +++ b/example/shirtsize_jsonenums.go @@ -7,15 +7,13 @@ import ( "fmt" ) -var _ShirtSizeValueToName = map[ShirtSize]string{ - NA: "NA", XS: "XS", S: "S", M: "M", L: "L", XL: "XL", -} - func (r ShirtSize) MarshalJSON() ([]byte, error) { - if s, ok := r.(fmt.Stringer); ok { + if s, ok := interface{}(r).(fmt.Stringer); ok { return json.Marshal(s.String()) } - s, ok := _ShirtSizeValueToName[r] + s, ok := map[ShirtSize]string{ + NA: "NA", XS: "XS", S: "S", M: "M", L: "L", XL: "XL", + }[r] if !ok { return nil, fmt.Errorf("invalid ShirtSize: %d", r) } @@ -26,6 +24,15 @@ var _ShirtSizeNameToValue = map[string]ShirtSize{ "NA": NA, "XS": XS, "S": S, "M": M, "L": L, "XL": XL, } +func init() { + var v ShirtSize + if _, ok := interface{}(v).(fmt.Stringer); ok { + _ShirtSizeNameToValue = map[string]ShirtSize{ + interface{}(NA).(fmt.Stringer).String(): NA, interface{}(XS).(fmt.Stringer).String(): XS, interface{}(S).(fmt.Stringer).String(): S, interface{}(M).(fmt.Stringer).String(): M, interface{}(L).(fmt.Stringer).String(): L, interface{}(XL).(fmt.Stringer).String(): XL, + } + } +} + func (r *ShirtSize) UnmarshalJSON(data []byte) error { var s string if err := json.Unmarshal(data, &s); err != nil { diff --git a/example/weekday_jsonenums.go b/example/weekday_jsonenums.go index 5e6f0b5..a58aa14 100644 --- a/example/weekday_jsonenums.go +++ b/example/weekday_jsonenums.go @@ -7,15 +7,13 @@ import ( "fmt" ) -var _WeekDayValueToName = map[WeekDay]string{ - Monday: "Monday", Tuesday: "Tuesday", Wednesday: "Wednesday", Thursday: "Thursday", Friday: "Friday", Saturday: "Saturday", Sunday: "Sunday", -} - func (r WeekDay) MarshalJSON() ([]byte, error) { - if s, ok := r.(fmt.Stringer); ok { + if s, ok := interface{}(r).(fmt.Stringer); ok { return json.Marshal(s.String()) } - s, ok := _WeekDayValueToName[r] + s, ok := map[WeekDay]string{ + Monday: "Monday", Tuesday: "Tuesday", Wednesday: "Wednesday", Thursday: "Thursday", Friday: "Friday", Saturday: "Saturday", Sunday: "Sunday", + }[r] if !ok { return nil, fmt.Errorf("invalid WeekDay: %d", r) } @@ -26,6 +24,15 @@ var _WeekDayNameToValue = map[string]WeekDay{ "Monday": Monday, "Tuesday": Tuesday, "Wednesday": Wednesday, "Thursday": Thursday, "Friday": Friday, "Saturday": Saturday, "Sunday": Sunday, } +func init() { + var v WeekDay + if _, ok := interface{}(v).(fmt.Stringer); ok { + _WeekDayNameToValue = map[string]WeekDay{ + interface{}(Monday).(fmt.Stringer).String(): Monday, interface{}(Tuesday).(fmt.Stringer).String(): Tuesday, interface{}(Wednesday).(fmt.Stringer).String(): Wednesday, interface{}(Thursday).(fmt.Stringer).String(): Thursday, interface{}(Friday).(fmt.Stringer).String(): Friday, interface{}(Saturday).(fmt.Stringer).String(): Saturday, interface{}(Sunday).(fmt.Stringer).String(): Sunday, + } + } +} + func (r *WeekDay) UnmarshalJSON(data []byte) error { var s string if err := json.Unmarshal(data, &s); err != nil { diff --git a/jsonenums.go b/jsonenums.go index 7585a51..53798c2 100644 --- a/jsonenums.go +++ b/jsonenums.go @@ -154,8 +154,7 @@ type Package struct { name string files []*ast.File - defs map[*ast.Ident]types.Object - types *types.Package + defs map[*ast.Ident]types.Object } // parsePackage parses the package in the given directory and returns it. @@ -188,8 +187,7 @@ func parsePackage(directory string, skipSuffix string) (*Package, error) { 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 { + if _, err := config.Check(directory, fs, files, info); err != nil { return nil, fmt.Errorf("type-checking package: %v", err) } @@ -197,7 +195,6 @@ func parsePackage(directory string, skipSuffix string) (*Package, error) { name: files[0].Name.Name, files: files, defs: defs, - types: types, }, nil } diff --git a/template.go b/template.go index e2ca209..dd78689 100644 --- a/template.go +++ b/template.go @@ -19,15 +19,14 @@ import ( ) {{range $typename, $values := .TypesAndValues}} -var _{{$typename}}ValueToName = map[{{$typename}}]string { - {{range $values}}{{.}}: "{{.}}",{{end}} -} func (r {{$typename}}) MarshalJSON() ([]byte, error) { - if s, ok := r.(fmt.Stringer); ok { + if s, ok := interface{}(r).(fmt.Stringer); ok { return json.Marshal(s.String()) } - s, ok := _{{$typename}}ValueToName[r] + s, ok := map[{{$typename}}]string { + {{range $values}}{{.}}: "{{.}}",{{end}} + }[r] if !ok { return nil, fmt.Errorf("invalid {{$typename}}: %d", r) } @@ -38,6 +37,15 @@ var _{{$typename}}NameToValue = map[string]{{$typename}} { {{range $values}}"{{.}}": {{.}},{{end}} } +func init() { + var v {{$typename}} + if _, ok := interface{}(v).(fmt.Stringer); ok { + _{{$typename}}NameToValue = map[string]{{$typename}} { + {{range $values}}interface{}({{.}}).(fmt.Stringer).String(): {{.}},{{end}} + } + } +} + func (r *{{$typename}}) UnmarshalJSON(data []byte) error { var s string if err := json.Unmarshal(data, &s); err != nil { From 90659514af30288ba5e542f7c5c2b6ccf3143655 Mon Sep 17 00:00:00 2001 From: Francesc Campoy Date: Sat, 31 Jan 2015 12:23:24 +0100 Subject: [PATCH 6/7] corrected case of jsonenums --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f4b7c38..4758d1d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # jsonenums -JSONenums is a tool to automate the creation of methods that satisfy the +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 @@ -14,7 +14,7 @@ defined, stringer will create a new self-contained Go source file implementing 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 +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, From 63815420d04303751eb1d4c040fdc9a8c282a68d Mon Sep 17 00:00:00 2001 From: Francesc Campoy Date: Sun, 1 Feb 2015 09:37:31 +0100 Subject: [PATCH 7/7] Update README.md --- README.md | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 4758d1d..b305b03 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,11 @@ # jsonenums jsonenums is a tool to automate the creation of methods that satisfy the -`fmt.Stringer`, `json.Marshaler` and `json.Unmarshaler` interfaces. +`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 +defined, jsonenums 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 ``` @@ -43,19 +42,23 @@ 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 +`MarshalJSON` will translate the value of a `Pill` constant to the `[]byte` representation of the respective constant name, so that the call -`fmt.Print(painkiller.Aspirin) will print the string "Aspirin". +`json.Marshal(painkiller.Aspirin) will return the bytes `[]byte("\"Aspirin\"")`. + +`UnmarshalJSON` performs the opposite operation; given the `[]byte` +representation of a `Pill` constant it will change the receiver to equal the +corresponding constant. So given `[]byte("\"Aspirin\"")` the receiver will +change to `Aspirin` and the returned error will be `nil`. Typically this process would be run using go generate, like this: ``` - //go:generate stringer -type=Pill + //go:generate jsonenums -type=Pill ``` If multiple constants have the same value, the lexically first matching name @@ -66,6 +69,6 @@ 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, +generate methods for multiple types. The default output file is t_jsonenums.go, where t is the lower-cased name of the first type listed. THe suffix can be overridden with the `-suffix` flag.