├── .travis.yml
├── .gitignore
├── server
├── app.yaml
├── main.go
├── static
│ └── home.html
└── server.go
├── parser
├── parser_test.go
└── parser.go
├── example
├── shirtsize_jsonenums.go
├── shirtsize.go
└── weekday_jsonenums.go
├── template.go
├── README.md
├── jsonenums.go
└── LICENSE
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: go
2 |
3 | go:
4 | - 1.7
5 | - 1.8
6 | - master
7 | script:
8 | # The GOPATH is for testing #21
9 | - GOPATH="$GOPATH:/tmp/jsonenums-test/go1:/tmp/jsonenums-test/go2" go test ./...
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiled Object files, Static and Dynamic libs (Shared Objects)
2 | *.o
3 | *.a
4 | *.so
5 |
6 | # Folders
7 | _obj
8 | _test
9 |
10 | # Architecture specific extensions/prefixes
11 | *.[568vq]
12 | [568vq].out
13 |
14 | *.cgo1.go
15 | *.cgo2.c
16 | _cgo_defun.c
17 | _cgo_gotypes.go
18 | _cgo_export.*
19 |
20 | _testmain.go
21 |
22 | *.exe
23 | *.test
24 | *.prof
25 |
--------------------------------------------------------------------------------
/server/app.yaml:
--------------------------------------------------------------------------------
1 | # Unless required by applicable law or agreed to writing, software distributed
2 | # under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR
3 | # CONDITIONS OF ANY KIND, either express or implied.
4 |
5 | # See the License for the specific language governing permissions and
6 | # limitations under the License.
7 |
8 | version: gae
9 | runtime: go
10 | api_version: go1
11 |
12 | handlers:
13 | - url: /generate
14 | script: _go_app
15 |
16 | - url: /
17 | static_files: static/home.html
18 | upload: static/home.html
19 |
20 | - url: /
21 | static_dir: static
22 |
--------------------------------------------------------------------------------
/server/main.go:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Google Inc. All rights reserved.
2 | // Licensed under the Apache License, Version 2.0 (the "License");
3 | // you may not use this file except in compliance with the License.
4 | // You may obtain a copy of the License at
5 | // http://www.apache.org/licenses/LICENSE-2.0
6 | //
7 | // Unless required by applicable law or agreed to writing, software distributed
8 | // under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR
9 | // CONDITIONS OF ANY KIND, either express or implied.
10 | //
11 | // See the License for the specific language governing permissions and
12 | // limitations under the License.
13 |
14 | // +build !appengine
15 |
16 | package main
17 |
18 | import (
19 | "flag"
20 | "net/http"
21 | )
22 |
23 | func main() {
24 | port := flag.String("http", "127.0.0.1:8080", "ip and port to listen to")
25 | flag.Parse()
26 | http.HandleFunc("/", homeHandler)
27 | http.ListenAndServe(*port, nil)
28 | }
29 |
30 | func homeHandler(w http.ResponseWriter, r *http.Request) {
31 | http.ServeFile(w, r, "static/home.html")
32 | }
33 |
--------------------------------------------------------------------------------
/parser/parser_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Google Inc. All rights reserved.
2 | // Licensed under the Apache License, Version 2.0 (the "License");
3 | // you may not use this file except in compliance with the License.
4 | // You may obtain a copy of the License at
5 | // http://www.apache.org/licenses/LICENSE-2.0
6 | //
7 | // Unless required by applicable law or agreed to writing, software distributed
8 | // under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR
9 | // CONDITIONS OF ANY KIND, either express or implied.
10 | //
11 | // See the License for the specific language governing permissions and
12 | // limitations under the License.
13 |
14 | package parser
15 |
16 | import (
17 | "go/build"
18 | "io/ioutil"
19 | "os"
20 | "path/filepath"
21 | "testing"
22 | )
23 |
24 | func must(t *testing.T, err error) {
25 | if err != nil {
26 | t.Fatal(err)
27 | }
28 | }
29 |
30 | var fakeCode = `
31 | package main
32 | import "fmt"
33 | func main() {
34 | fmt.Println("Hello world!")
35 | }
36 | `
37 |
38 | func TestParseFromMultipleGopath(t *testing.T) {
39 | gopaths := filepath.SplitList(build.Default.GOPATH)
40 | if len(gopaths) < 2 {
41 | t.Skipf("No multiple GOPATH (%s) exists, skiping..", build.Default.GOPATH)
42 | }
43 | gopath := gopaths[len(gopaths)-1]
44 | dir := filepath.Join(gopath, "src", "foo")
45 | defer func() { must(t, os.RemoveAll(dir)) }()
46 | must(t, os.MkdirAll(dir, 0755))
47 | must(t, ioutil.WriteFile(filepath.Join(dir, "main.go"), []byte(fakeCode), 0644))
48 |
49 | if _, err := ParsePackage(dir); err != nil {
50 | t.Fatalf("Parse package (%v): %v", dir, err)
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/example/shirtsize_jsonenums.go:
--------------------------------------------------------------------------------
1 | // Code generated by jsonenums -type=ShirtSize; DO NOT EDIT.
2 |
3 | package main
4 |
5 | import (
6 | "encoding/json"
7 | "fmt"
8 | )
9 |
10 | var (
11 | _ShirtSizeNameToValue = map[string]ShirtSize{
12 | "NA": NA,
13 | "XS": XS,
14 | "S": S,
15 | "M": M,
16 | "L": L,
17 | "XL": XL,
18 | }
19 |
20 | _ShirtSizeValueToName = map[ShirtSize]string{
21 | NA: "NA",
22 | XS: "XS",
23 | S: "S",
24 | M: "M",
25 | L: "L",
26 | XL: "XL",
27 | }
28 | )
29 |
30 | func init() {
31 | var v ShirtSize
32 | if _, ok := interface{}(v).(fmt.Stringer); ok {
33 | _ShirtSizeNameToValue = map[string]ShirtSize{
34 | interface{}(NA).(fmt.Stringer).String(): NA,
35 | interface{}(XS).(fmt.Stringer).String(): XS,
36 | interface{}(S).(fmt.Stringer).String(): S,
37 | interface{}(M).(fmt.Stringer).String(): M,
38 | interface{}(L).(fmt.Stringer).String(): L,
39 | interface{}(XL).(fmt.Stringer).String(): XL,
40 | }
41 | }
42 | }
43 |
44 | // MarshalJSON is generated so ShirtSize satisfies json.Marshaler.
45 | func (r ShirtSize) MarshalJSON() ([]byte, error) {
46 | if s, ok := interface{}(r).(fmt.Stringer); ok {
47 | return json.Marshal(s.String())
48 | }
49 | s, ok := _ShirtSizeValueToName[r]
50 | if !ok {
51 | return nil, fmt.Errorf("invalid ShirtSize: %d", r)
52 | }
53 | return json.Marshal(s)
54 | }
55 |
56 | // UnmarshalJSON is generated so ShirtSize satisfies json.Unmarshaler.
57 | func (r *ShirtSize) UnmarshalJSON(data []byte) error {
58 | var s string
59 | if err := json.Unmarshal(data, &s); err != nil {
60 | return fmt.Errorf("ShirtSize should be a string, got %s", data)
61 | }
62 | v, ok := _ShirtSizeNameToValue[s]
63 | if !ok {
64 | return fmt.Errorf("invalid ShirtSize %q", s)
65 | }
66 | *r = v
67 | return nil
68 | }
69 |
--------------------------------------------------------------------------------
/example/shirtsize.go:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Google Inc. All rights reserved.
2 | // Licensed under the Apache License, Version 2.0 (the "License");
3 | // you may not use this file except in compliance with the License.
4 | // You may obtain a copy of the License at
5 | // http://www.apache.org/licenses/LICENSE-2.0
6 | //
7 | // Unless required by applicable law or agreed to writing, software distributed
8 | // under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR
9 | // CONDITIONS OF ANY KIND, either express or implied.
10 | //
11 | // See the License for the specific language governing permissions and
12 | // limitations under the License.
13 |
14 | package main
15 |
16 | import (
17 | "encoding/json"
18 | "fmt"
19 | "log"
20 | "os"
21 | "strings"
22 | )
23 |
24 | //go:generate jsonenums -type=ShirtSize
25 |
26 | type ShirtSize byte
27 |
28 | const (
29 | NA ShirtSize = iota
30 | XS
31 | S
32 | M
33 | L
34 | XL
35 | )
36 |
37 | //go:generate jsonenums -type=WeekDay
38 |
39 | type WeekDay int
40 |
41 | const (
42 | Monday WeekDay = iota
43 | Tuesday
44 | Wednesday
45 | Thursday
46 | Friday
47 | Saturday
48 | Sunday
49 | )
50 |
51 | func (d WeekDay) String() string {
52 | switch d {
53 | case Monday:
54 | return "Dilluns"
55 | case Tuesday:
56 | return "Dimarts"
57 | case Wednesday:
58 | return "Dimecres"
59 | case Thursday:
60 | return "Dijous"
61 | case Friday:
62 | return "Divendres"
63 | case Saturday:
64 | return "Dissabte"
65 | case Sunday:
66 | return "Diumenge"
67 | default:
68 | return "invalid WeekDay"
69 | }
70 | }
71 |
72 | func main() {
73 | v := struct {
74 | Size ShirtSize
75 | Day WeekDay
76 | }{M, Friday}
77 | if err := json.NewEncoder(os.Stdout).Encode(v); err != nil {
78 | log.Fatal(err)
79 | }
80 |
81 | input := `{"Size":"XL", "Day":"Dimarts"}`
82 | if err := json.NewDecoder(strings.NewReader(input)).Decode(&v); err != nil {
83 | log.Fatal(err)
84 | }
85 | fmt.Printf("decoded %s as %+v\n", input, v)
86 | }
87 |
--------------------------------------------------------------------------------
/example/weekday_jsonenums.go:
--------------------------------------------------------------------------------
1 | // Code generated by jsonenums -type=WeekDay; DO NOT EDIT.
2 |
3 | package main
4 |
5 | import (
6 | "encoding/json"
7 | "fmt"
8 | )
9 |
10 | var (
11 | _WeekDayNameToValue = map[string]WeekDay{
12 | "Monday": Monday,
13 | "Tuesday": Tuesday,
14 | "Wednesday": Wednesday,
15 | "Thursday": Thursday,
16 | "Friday": Friday,
17 | "Saturday": Saturday,
18 | "Sunday": Sunday,
19 | }
20 |
21 | _WeekDayValueToName = map[WeekDay]string{
22 | Monday: "Monday",
23 | Tuesday: "Tuesday",
24 | Wednesday: "Wednesday",
25 | Thursday: "Thursday",
26 | Friday: "Friday",
27 | Saturday: "Saturday",
28 | Sunday: "Sunday",
29 | }
30 | )
31 |
32 | func init() {
33 | var v WeekDay
34 | if _, ok := interface{}(v).(fmt.Stringer); ok {
35 | _WeekDayNameToValue = map[string]WeekDay{
36 | interface{}(Monday).(fmt.Stringer).String(): Monday,
37 | interface{}(Tuesday).(fmt.Stringer).String(): Tuesday,
38 | interface{}(Wednesday).(fmt.Stringer).String(): Wednesday,
39 | interface{}(Thursday).(fmt.Stringer).String(): Thursday,
40 | interface{}(Friday).(fmt.Stringer).String(): Friday,
41 | interface{}(Saturday).(fmt.Stringer).String(): Saturday,
42 | interface{}(Sunday).(fmt.Stringer).String(): Sunday,
43 | }
44 | }
45 | }
46 |
47 | // MarshalJSON is generated so WeekDay satisfies json.Marshaler.
48 | func (r WeekDay) MarshalJSON() ([]byte, error) {
49 | if s, ok := interface{}(r).(fmt.Stringer); ok {
50 | return json.Marshal(s.String())
51 | }
52 | s, ok := _WeekDayValueToName[r]
53 | if !ok {
54 | return nil, fmt.Errorf("invalid WeekDay: %d", r)
55 | }
56 | return json.Marshal(s)
57 | }
58 |
59 | // UnmarshalJSON is generated so WeekDay satisfies json.Unmarshaler.
60 | func (r *WeekDay) UnmarshalJSON(data []byte) error {
61 | var s string
62 | if err := json.Unmarshal(data, &s); err != nil {
63 | return fmt.Errorf("WeekDay should be a string, got %s", data)
64 | }
65 | v, ok := _WeekDayNameToValue[s]
66 | if !ok {
67 | return fmt.Errorf("invalid WeekDay %q", s)
68 | }
69 | *r = v
70 | return nil
71 | }
72 |
--------------------------------------------------------------------------------
/server/static/home.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
15 |
16 |
17 |
jsonenums
18 |
30 |
31 |
32 |
47 |
48 |
49 |
50 |
81 |
82 |
83 |
84 |
86 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/template.go:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Google Inc. All rights reserved.
2 | // Licensed under the Apache License, Version 2.0 (the "License");
3 | // you may not use this file except in compliance with the License.
4 | // You may obtain a copy of the License at
5 | // http://www.apache.org/licenses/LICENSE-2.0
6 | //
7 | // Unless required by applicable law or agreed to writing, software distributed
8 | // under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR
9 | // CONDITIONS OF ANY KIND, either express or implied.
10 | //
11 | // See the License for the specific language governing permissions and
12 | // limitations under the License.
13 |
14 | // Added as a .go file to avoid embedding issues of the template.
15 |
16 | package main
17 |
18 | import "text/template"
19 |
20 | var generatedTmpl = template.Must(template.New("generated").Parse(`
21 | // Code generated by jsonenums {{.Command}}; DO NOT EDIT.
22 |
23 | package {{.PackageName}}
24 |
25 | import (
26 | "encoding/json"
27 | "fmt"
28 | )
29 |
30 | {{range $typename, $values := .TypesAndValues}}
31 |
32 | var (
33 | _{{$typename}}NameToValue = map[string]{{$typename}} {
34 | {{range $values}}"{{.}}": {{.}},
35 | {{end}}
36 | }
37 |
38 | _{{$typename}}ValueToName = map[{{$typename}}]string {
39 | {{range $values}}{{.}}: "{{.}}",
40 | {{end}}
41 | }
42 | )
43 |
44 | func init() {
45 | var v {{$typename}}
46 | if _, ok := interface{}(v).(fmt.Stringer); ok {
47 | _{{$typename}}NameToValue = map[string]{{$typename}} {
48 | {{range $values}}interface{}({{.}}).(fmt.Stringer).String(): {{.}},
49 | {{end}}
50 | }
51 | }
52 | }
53 |
54 | // MarshalJSON is generated so {{$typename}} satisfies json.Marshaler.
55 | func (r {{$typename}}) MarshalJSON() ([]byte, error) {
56 | if s, ok := interface{}(r).(fmt.Stringer); ok {
57 | return json.Marshal(s.String())
58 | }
59 | s, ok := _{{$typename}}ValueToName[r]
60 | if !ok {
61 | return nil, fmt.Errorf("invalid {{$typename}}: %d", r)
62 | }
63 | return json.Marshal(s)
64 | }
65 |
66 | // UnmarshalJSON is generated so {{$typename}} satisfies json.Unmarshaler.
67 | func (r *{{$typename}}) UnmarshalJSON(data []byte) error {
68 | var s string
69 | if err := json.Unmarshal(data, &s); err != nil {
70 | return fmt.Errorf("{{$typename}} should be a string, got %s", data)
71 | }
72 | v, ok := _{{$typename}}NameToValue[s]
73 | if !ok {
74 | return fmt.Errorf("invalid {{$typename}} %q", s)
75 | }
76 | *r = v
77 | return nil
78 | }
79 |
80 | {{end}}
81 | `))
82 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # jsonenums
2 |
3 | jsonenums is a tool to automate the creation of methods that satisfy the
4 | `json.Marshaler` and `json.Unmarshaler` interfaces.
5 | Given the name of a (signed or unsigned) integer type T that has constants
6 | defined, jsonenums will create a new self-contained Go source file implementing
7 |
8 | ```
9 | func (t T) MarshalJSON() ([]byte, error)
10 | func (t *T) UnmarshalJSON([]byte) error
11 | ```
12 |
13 | The file is created in the same package and directory as the package that
14 | defines T. It has helpful defaults designed for use with go generate.
15 |
16 | jsonenums is a simple implementation of a concept and the code might not be the
17 | most performant or beautiful to read.
18 |
19 | For example, given this snippet,
20 |
21 | ```Go
22 | package painkiller
23 |
24 | type Pill int
25 |
26 | const (
27 | Placebo Pill = iota
28 | Aspirin
29 | Ibuprofen
30 | Paracetamol
31 | Acetaminophen = Paracetamol
32 | )
33 | ```
34 |
35 | running this command
36 |
37 | ```
38 | jsonenums -type=Pill
39 | ```
40 |
41 | in the same directory will create the file `pill_jsonenums.go`, in package
42 | `painkiller`, containing a definition of
43 |
44 | ```
45 | func (r Pill) MarshalJSON() ([]byte, error)
46 | func (r *Pill) UnmarshalJSON([]byte) error
47 | ```
48 |
49 | `MarshalJSON` will translate the value of a `Pill` constant to the `[]byte`
50 | representation of the respective constant name, so that the call
51 | `json.Marshal(painkiller.Aspirin)` will return the bytes `[]byte("\"Aspirin\"")`.
52 |
53 | `UnmarshalJSON` performs the opposite operation; given the `[]byte`
54 | representation of a `Pill` constant it will change the receiver to equal the
55 | corresponding constant. So given `[]byte("\"Aspirin\"")` the receiver will
56 | change to `Aspirin` and the returned error will be `nil`.
57 |
58 | Typically this process would be run using go generate, like this:
59 |
60 | ```
61 | //go:generate jsonenums -type=Pill
62 | ```
63 |
64 | If multiple constants have the same value, the lexically first matching name
65 | will be used (in the example, Acetaminophen will print as "Paracetamol").
66 |
67 | With no arguments, it processes the package in the current directory. Otherwise,
68 | the arguments must name a single directory holding a Go package or a set of Go
69 | source files that represent a single Go package.
70 |
71 | The `-type` flag accepts a comma-separated list of types so a single run can
72 | generate methods for multiple types. The default output file is t_jsonenums.go,
73 | where t is the lower-cased name of the first type listed. The suffix can be
74 | overridden with the `-suffix` flag and a prefix may be added with the `-prefix`
75 | flag.
76 |
77 | This is not an official Google product (experimental or otherwise), it is just code that happens to be owned by Google.
78 |
--------------------------------------------------------------------------------
/server/server.go:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Google Inc. All rights reserved.
2 | // Licensed under the Apache License, Version 2.0 (the "License");
3 | // you may not use this file except in compliance with the License.
4 | // You may obtain a copy of the License at
5 | // http://www.apache.org/licenses/LICENSE-2.0
6 | //
7 | // Unless required by applicable law or agreed to writing, software distributed
8 | // under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR
9 | // CONDITIONS OF ANY KIND, either express or implied.
10 | //
11 | // See the License for the specific language governing permissions and
12 | // limitations under the License.
13 |
14 | // Server is an http server that provides an alternative way of generating code
15 | // based on int types and the constants defined with it.
16 | //
17 | // Use the http flag to change the address on which the server will listen for
18 | // requests. The default is 127.0.0.1:8080.
19 | package main
20 |
21 | import (
22 | "bytes"
23 | "fmt"
24 | "io/ioutil"
25 | "log"
26 | "net/http"
27 | "os"
28 | "path/filepath"
29 | "text/template"
30 |
31 | "go/format"
32 |
33 | "github.com/campoy/jsonenums/parser"
34 | )
35 |
36 | func init() {
37 | http.Handle("/generate", handler(generateHandler))
38 | }
39 |
40 | func generateHandler(w http.ResponseWriter, r *http.Request) error {
41 | if r.Method != "GET" {
42 | return codeError{fmt.Errorf("only GET accepted"), http.StatusMethodNotAllowed}
43 | }
44 | code := r.FormValue("code")
45 | if code == "" {
46 | return codeError{fmt.Errorf("no code to be parsed"), http.StatusBadRequest}
47 | }
48 | typ := r.FormValue("type")
49 | if typ == "" {
50 | return codeError{fmt.Errorf("no type to be analyzed"), http.StatusBadRequest}
51 | }
52 |
53 | dir, err := createDir(code)
54 | if err != nil {
55 | return err
56 | }
57 | defer os.RemoveAll(dir)
58 |
59 | pkg, err := parser.ParsePackage(dir)
60 | if err != nil {
61 | return fmt.Errorf("parse package: %v", err)
62 | }
63 |
64 | values, err := pkg.ValuesOfType(typ)
65 | if err != nil {
66 | return fmt.Errorf("find values: %v", err)
67 | }
68 |
69 | t, err := template.New("code").Parse(r.FormValue("template"))
70 | if err != nil {
71 | return codeError{fmt.Errorf("parse template: %v", err), http.StatusBadRequest}
72 | }
73 | var data = struct {
74 | PackageName string
75 | TypeName string
76 | Values []string
77 | }{pkg.Name, typ, values}
78 |
79 | var buf bytes.Buffer
80 | if err := t.Execute(&buf, data); err != nil {
81 | return codeError{fmt.Errorf("execute template: %v", err), http.StatusBadRequest}
82 | }
83 | src, err := format.Source(buf.Bytes())
84 | if err != nil {
85 | return codeError{fmt.Errorf("code generated is not valid: %v\n%v", err, buf.String()), http.StatusBadRequest}
86 | }
87 | w.Write(src)
88 | return nil
89 | }
90 |
91 | func createDir(content string) (string, error) {
92 | dir, err := ioutil.TempDir("", "jsonenums")
93 | if err != nil {
94 | return "", fmt.Errorf("create tmp dir: %v", err)
95 | }
96 | f, err := os.Create(filepath.Join(dir, "main.go"))
97 | if err != nil {
98 | os.RemoveAll(dir)
99 | return "", fmt.Errorf("create tmp file: %v", err)
100 | }
101 | f.WriteString(content)
102 | f.Close()
103 | return dir, err
104 | }
105 |
106 | type handler func(http.ResponseWriter, *http.Request) error
107 |
108 | func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
109 | err := h(w, r)
110 | if err != nil {
111 | code := http.StatusInternalServerError
112 | if cErr, ok := err.(codeError); ok {
113 | code = cErr.code
114 | } else {
115 | log.Printf("%v: %v", r.URL, code)
116 | }
117 | http.Error(w, err.Error(), code)
118 | }
119 | }
120 |
121 | type codeError struct {
122 | error
123 | code int
124 | }
125 |
--------------------------------------------------------------------------------
/parser/parser.go:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Google Inc. All rights reserved.
2 | // Licensed under the Apache License, Version 2.0 (the "License");
3 | // you may not use this file except in compliance with the License.
4 | // You may obtain a copy of the License at
5 | // http://www.apache.org/licenses/LICENSE-2.0
6 | //
7 | // Unless required by applicable law or agreed to writing, software distributed
8 | // under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR
9 | // CONDITIONS OF ANY KIND, either express or implied.
10 | //
11 | // See the License for the specific language governing permissions and
12 | // limitations under the License.
13 |
14 | // Package parser parses Go code and keeps track of all the types defined
15 | // and provides access to all the constants defined for an int type.
16 | package parser
17 |
18 | import (
19 | "fmt"
20 | "go/ast"
21 | "go/build"
22 | "go/constant"
23 | "go/token"
24 | "go/types"
25 | "log"
26 | "strings"
27 |
28 | "golang.org/x/tools/go/loader"
29 | )
30 |
31 | // A Package contains all the information related to a parsed package.
32 | type Package struct {
33 | Name string
34 | files []*ast.File
35 |
36 | defs map[*ast.Ident]types.Object
37 | }
38 |
39 | // ParsePackage parses the package in the given directory and returns it.
40 | func ParsePackage(directory string) (*Package, error) {
41 | p, err := build.ImportDir(directory, build.FindOnly)
42 | if err != nil {
43 | return nil, fmt.Errorf("provided directory (%s) may not under GOPATH (%s): %v",
44 | directory, build.Default.GOPATH, err)
45 | }
46 |
47 | conf := loader.Config{TypeChecker: types.Config{FakeImportC: true}}
48 | conf.Import(p.ImportPath)
49 | program, err := conf.Load()
50 | if err != nil {
51 | return nil, fmt.Errorf("couldn't load package: %v", err)
52 | }
53 |
54 | pkgInfo := program.Package(p.ImportPath)
55 | return &Package{
56 | Name: pkgInfo.Pkg.Name(),
57 | files: pkgInfo.Files,
58 | defs: pkgInfo.Defs,
59 | }, nil
60 | }
61 |
62 | // generate produces the String method for the named type.
63 | func (pkg *Package) ValuesOfType(typeName string) ([]string, error) {
64 | var values, inspectErrs []string
65 | for _, file := range pkg.files {
66 | ast.Inspect(file, func(node ast.Node) bool {
67 | decl, ok := node.(*ast.GenDecl)
68 | if !ok || decl.Tok != token.CONST {
69 | // We only care about const declarations.
70 | return true
71 | }
72 |
73 | if vs, err := pkg.valuesOfTypeIn(typeName, decl); err != nil {
74 | inspectErrs = append(inspectErrs, err.Error())
75 | } else {
76 | values = append(values, vs...)
77 | }
78 | return false
79 | })
80 | }
81 | if len(inspectErrs) > 0 {
82 | return nil, fmt.Errorf("inspecting code:\n\t%v", strings.Join(inspectErrs, "\n\t"))
83 | }
84 | if len(values) == 0 {
85 | return nil, fmt.Errorf("no values defined for type %s", typeName)
86 | }
87 | return values, nil
88 | }
89 |
90 | func (pkg *Package) valuesOfTypeIn(typeName string, decl *ast.GenDecl) ([]string, error) {
91 | var values []string
92 |
93 | // The name of the type of the constants we are declaring.
94 | // Can change if this is a multi-element declaration.
95 | typ := ""
96 | // Loop over the elements of the declaration. Each element is a ValueSpec:
97 | // a list of names possibly followed by a type, possibly followed by values.
98 | // If the type and value are both missing, we carry down the type (and value,
99 | // but the "go/types" package takes care of that).
100 | for _, spec := range decl.Specs {
101 | vspec := spec.(*ast.ValueSpec) // Guaranteed to succeed as this is CONST.
102 | if vspec.Type == nil && len(vspec.Values) > 0 {
103 | // "X = 1". With no type but a value, the constant is untyped.
104 | // Skip this vspec and reset the remembered type.
105 | typ = ""
106 | continue
107 | }
108 | if vspec.Type != nil {
109 | // "X T". We have a type. Remember it.
110 | ident, ok := vspec.Type.(*ast.Ident)
111 | if !ok {
112 | continue
113 | }
114 | typ = ident.Name
115 | }
116 | if typ != typeName {
117 | // This is not the type we're looking for.
118 | continue
119 | }
120 |
121 | // We now have a list of names (from one line of source code) all being
122 | // declared with the desired type.
123 | // Grab their names and actual values and store them in f.values.
124 | for _, name := range vspec.Names {
125 | if name.Name == "_" {
126 | continue
127 | }
128 | // This dance lets the type checker find the values for us. It's a
129 | // bit tricky: look up the object declared by the name, find its
130 | // types.Const, and extract its value.
131 | obj, ok := pkg.defs[name]
132 | if !ok {
133 | return nil, fmt.Errorf("no value for constant %s", name)
134 | }
135 | info := obj.Type().Underlying().(*types.Basic).Info()
136 | if info&types.IsInteger == 0 {
137 | return nil, fmt.Errorf("can't handle non-integer constant type %s", typ)
138 | }
139 | value := obj.(*types.Const).Val() // Guaranteed to succeed as this is CONST.
140 | if value.Kind() != constant.Int {
141 | log.Fatalf("can't happen: constant is not an integer %s", name)
142 | }
143 | values = append(values, name.Name)
144 | }
145 | }
146 | return values, nil
147 | }
148 |
--------------------------------------------------------------------------------
/jsonenums.go:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Google Inc. All rights reserved.
2 | // Licensed under the Apache License, Version 2.0 (the "License");
3 | // you may not use this file except in compliance with the License.
4 | // You may obtain a copy of the License at
5 | // http://www.apache.org/licenses/LICENSE-2.0
6 | //
7 | // Unless required by applicable law or agreed to writing, software distributed
8 | // under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR
9 | // CONDITIONS OF ANY KIND, either express or implied.
10 | //
11 | // See the License for the specific language governing permissions and
12 | // limitations under the License.
13 |
14 | // JSONenums is a tool to automate the creation of methods that satisfy the
15 | // fmt.Stringer, json.Marshaler and json.Unmarshaler interfaces.
16 | // Given the name of a (signed or unsigned) integer type T that has constants
17 | // defined, jsonenums will create a new self-contained Go source file implementing
18 | //
19 | // func (t T) String() string
20 | // func (t T) MarshalJSON() ([]byte, error)
21 | // func (t *T) UnmarshalJSON([]byte) error
22 | //
23 | // The file is created in the same package and directory as the package that defines T.
24 | // It has helpful defaults designed for use with go generate.
25 | //
26 | // JSONenums is a simple implementation of a concept and the code might not be
27 | // the most performant or beautiful to read.
28 | //
29 | // For example, given this snippet,
30 | //
31 | // package painkiller
32 | //
33 | // type Pill int
34 | //
35 | // const (
36 | // Placebo Pill = iota
37 | // Aspirin
38 | // Ibuprofen
39 | // Paracetamol
40 | // Acetaminophen = Paracetamol
41 | // )
42 | //
43 | // running this command
44 | //
45 | // jsonenums -type=Pill
46 | //
47 | // in the same directory will create the file pill_jsonenums.go, in package painkiller,
48 | // containing a definition of
49 | //
50 | // func (r Pill) String() string
51 | // func (r Pill) MarshalJSON() ([]byte, error)
52 | // func (r *Pill) UnmarshalJSON([]byte) error
53 | //
54 | // That method will translate the value of a Pill constant to the string representation
55 | // of the respective constant name, so that the call fmt.Print(painkiller.Aspirin) will
56 | // print the string "Aspirin".
57 | //
58 | // Typically this process would be run using go generate, like this:
59 | //
60 | // //go:generate jsonenums -type=Pill
61 | //
62 | // If multiple constants have the same value, the lexically first matching name will
63 | // be used (in the example, Acetaminophen will print as "Paracetamol").
64 | //
65 | // With no arguments, it processes the package in the current directory.
66 | // Otherwise, the arguments must name a single directory holding a Go package
67 | // or a set of Go source files that represent a single Go package.
68 | //
69 | // The -type flag accepts a comma-separated list of types so a single run can
70 | // generate methods for multiple types. The default output file is
71 | // t_jsonenums.go, where t is the lower-cased name of the first type listed.
72 | // The suffix can be overridden with the -suffix flag and a prefix may be added
73 | // with the -prefix flag.
74 | //
75 | package main
76 |
77 | import (
78 | "bytes"
79 | "flag"
80 | "go/format"
81 | "io/ioutil"
82 | "log"
83 | "os"
84 | "path/filepath"
85 | "strings"
86 |
87 | "github.com/campoy/jsonenums/parser"
88 | )
89 |
90 | var (
91 | typeNames = flag.String("type", "", "comma-separated list of type names; must be set")
92 | outputPrefix = flag.String("prefix", "", "prefix to be added to the output file")
93 | outputSuffix = flag.String("suffix", "_jsonenums", "suffix to be added to the output file")
94 | )
95 |
96 | func main() {
97 | flag.Parse()
98 | if len(*typeNames) == 0 {
99 | log.Fatalf("the flag -type must be set")
100 | }
101 | types := strings.Split(*typeNames, ",")
102 |
103 | // Only one directory at a time can be processed, and the default is ".".
104 | dir := "."
105 | if args := flag.Args(); len(args) == 1 {
106 | dir = args[0]
107 | } else if len(args) > 1 {
108 | log.Fatalf("only one directory at a time")
109 | }
110 | dir, err := filepath.Abs(dir)
111 | if err != nil {
112 | log.Fatalf("unable to determine absolute filepath for requested path %s: %v",
113 | dir, err)
114 | }
115 |
116 | pkg, err := parser.ParsePackage(dir)
117 | if err != nil {
118 | log.Fatalf("parsing package: %v", err)
119 | }
120 |
121 | var analysis = struct {
122 | Command string
123 | PackageName string
124 | TypesAndValues map[string][]string
125 | }{
126 | Command: strings.Join(os.Args[1:], " "),
127 | PackageName: pkg.Name,
128 | TypesAndValues: make(map[string][]string),
129 | }
130 |
131 | // Run generate for each type.
132 | for _, typeName := range types {
133 | values, err := pkg.ValuesOfType(typeName)
134 | if err != nil {
135 | log.Fatalf("finding values for type %v: %v", typeName, err)
136 | }
137 | analysis.TypesAndValues[typeName] = values
138 |
139 | var buf bytes.Buffer
140 | if err := generatedTmpl.Execute(&buf, analysis); err != nil {
141 | log.Fatalf("generating code: %v", err)
142 | }
143 |
144 | src, err := format.Source(buf.Bytes())
145 | if err != nil {
146 | // Should never happen, but can arise when developing this code.
147 | // The user can compile the output to see the error.
148 | log.Printf("warning: internal error: invalid Go generated: %s", err)
149 | log.Printf("warning: compile the package to analyze the error")
150 | src = buf.Bytes()
151 | }
152 |
153 | output := strings.ToLower(*outputPrefix + typeName +
154 | *outputSuffix + ".go")
155 | outputPath := filepath.Join(dir, output)
156 | if err := ioutil.WriteFile(outputPath, src, 0644); err != nil {
157 | log.Fatalf("writing output: %s", err)
158 | }
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
203 |
--------------------------------------------------------------------------------