├── .github ├── CODEOWNERS ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── tests.yml │ └── scorecard.yml ├── go.mod ├── SECURITY.md ├── enum_string.go.template ├── enum_string_test.go.template ├── internal └── testdata │ ├── image.go │ ├── color_enum_encoding.go │ ├── image_size_enum_encoding.go │ ├── color_enum_encoding_test.go │ └── image_size_enum_encoding_test.go ├── enum.go.template ├── LICENSE ├── README.md ├── enum_test.go.template ├── main.go └── main_test.go /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @nikolaydubina 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: nikolaydubina 2 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nikolaydubina/go-enum-encoding 2 | 3 | go 1.25 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | Contact [@nikolaydubina](https://github.com/nikolaydubina) over email or linkedin. 6 | -------------------------------------------------------------------------------- /enum_string.go.template: -------------------------------------------------------------------------------- 1 | var seq_string_{{.Type}} = [...]string{{{.seq_string}}} 2 | 3 | func (s {{.Type}}) String() string { 4 | switch s { 5 | {{.value_to_string_switch}} 6 | default: 7 | return "" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /enum_string_test.go.template: -------------------------------------------------------------------------------- 1 | func Test{{.Type}}_String(t *testing.T) { 2 | values := []{{.Type}}{{{.Values}}} 3 | tags := []string{{{.Tags}}} 4 | 5 | for i := range values { 6 | if s := values[i].String(); s != tags[i] { 7 | t.Error(s , tags[i]) 8 | } 9 | } 10 | } 11 | 12 | func Benchmark{{.Type}}_String(b *testing.B) { 13 | vs := []{{.Type}}{{{.Values}}} 14 | v := vs[rand.Intn(len(vs))] 15 | 16 | for b.Loop() { 17 | _ = v.String() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /internal/testdata/image.go: -------------------------------------------------------------------------------- 1 | package image 2 | 3 | type Color struct{ c uint8 } 4 | 5 | var ( 6 | UndefinedColor = Color{} // json:"" 7 | Red = Color{1} // json:"red" 8 | Green = Color{2} // json:"green" 9 | Blue = Color{3} // json:"blue" 10 | Purple, Orange = Color{4}, Color{5} // json:"blue" 11 | ) 12 | 13 | type V struct { 14 | Color Color `json:"color"` 15 | } 16 | 17 | type ImageSize uint8 18 | 19 | const ( 20 | UndefinedSize ImageSize = iota // json:"" 21 | Small // json:"small" 22 | Large // json:"large" 23 | XLarge // json:"xlarge" 24 | ) 25 | -------------------------------------------------------------------------------- /enum.go.template: -------------------------------------------------------------------------------- 1 | // Code generated by go-enum-encoding; DO NOT EDIT. 2 | 3 | package {{.Package}} 4 | 5 | import "errors" 6 | 7 | var ErrUnknown{{.Type}} = errors.New("unknown {{.Type}}") 8 | 9 | func (s *{{.Type}}) UnmarshalText(text []byte) error { 10 | switch string(text) { 11 | {{.string_to_value_switch}} 12 | default: 13 | return ErrUnknown{{.Type}} 14 | } 15 | return nil 16 | } 17 | 18 | var seq_bytes_{{.Type}} = [...][]byte{{{.seq_bytes}}} 19 | 20 | func (s {{.Type}}) MarshalText() ([]byte, error) { return s.AppendText(nil) } 21 | 22 | func (s {{.Type}}) AppendText(b []byte) ([]byte, error) { 23 | switch s { 24 | {{.value_to_append_bytes_switch}} 25 | default: 26 | return nil, ErrUnknown{{.Type}} 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | permissions: read-all 10 | 11 | jobs: 12 | build: 13 | name: Tests 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | 19 | - name: Set up Go 1.x 20 | uses: actions/setup-go@v5 21 | with: 22 | go-version: ^1.25 23 | 24 | - name: test 25 | env: 26 | GOCOVERPROFILE: coverage.out 27 | GOEXPERIMENT: jsonv2 28 | run: go test ./... 29 | 30 | - name: upload coverage to Codecov 31 | uses: codecov/codecov-action@v4.1.1 32 | with: 33 | token: ${{ secrets.CODECOV_TOKEN }} 34 | files: coverage.out 35 | -------------------------------------------------------------------------------- /internal/testdata/color_enum_encoding.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-enum-encoding; DO NOT EDIT. 2 | 3 | package image 4 | 5 | import "errors" 6 | 7 | var ErrUnknownColor = errors.New("unknown Color") 8 | 9 | func (s *Color) UnmarshalText(text []byte) error { 10 | switch string(text) { 11 | case "": 12 | *s = UndefinedColor 13 | case "red": 14 | *s = Red 15 | case "green": 16 | *s = Green 17 | case "blue": 18 | *s = Blue 19 | default: 20 | return ErrUnknownColor 21 | } 22 | return nil 23 | } 24 | 25 | var seq_bytes_Color = [...][]byte{[]byte(""), []byte("red"), []byte("green"), []byte("blue")} 26 | 27 | func (s Color) MarshalText() ([]byte, error) { return s.AppendText(nil) } 28 | 29 | func (s Color) AppendText(b []byte) ([]byte, error) { 30 | switch s { 31 | case UndefinedColor: 32 | return append(b, seq_bytes_Color[0]...), nil 33 | case Red: 34 | return append(b, seq_bytes_Color[1]...), nil 35 | case Green: 36 | return append(b, seq_bytes_Color[2]...), nil 37 | case Blue: 38 | return append(b, seq_bytes_Color[3]...), nil 39 | default: 40 | return nil, ErrUnknownColor 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Nikolay Dubina 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/scorecard.yml: -------------------------------------------------------------------------------- 1 | name: Scorecard supply-chain security 2 | on: 3 | branch_protection_rule: 4 | schedule: 5 | - cron: "42 10 * * 3" 6 | push: 7 | branches: ["master"] 8 | 9 | permissions: read-all 10 | 11 | jobs: 12 | analysis: 13 | name: Scorecard analysis 14 | runs-on: ubuntu-latest 15 | permissions: 16 | security-events: write 17 | id-token: write 18 | 19 | steps: 20 | - name: "Checkout code" 21 | uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # v3.1.0 22 | with: 23 | persist-credentials: false 24 | 25 | - name: "Run analysis" 26 | uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1 27 | with: 28 | results_file: results.sarif 29 | results_format: sarif 30 | publish_results: true 31 | 32 | - name: "Upload artifact" 33 | uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 # v3.1.0 34 | with: 35 | name: SARIF file 36 | path: results.sarif 37 | retention-days: 5 38 | 39 | - name: "Upload to code-scanning" 40 | uses: github/codeql-action/upload-sarif@17573ee1cc1b9d061760f3a006fc4aac4f944fd5 # v2.2.4 41 | with: 42 | sarif_file: results.sarif 43 | -------------------------------------------------------------------------------- /internal/testdata/image_size_enum_encoding.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-enum-encoding; DO NOT EDIT. 2 | 3 | package image 4 | 5 | import "errors" 6 | 7 | var ErrUnknownImageSize = errors.New("unknown ImageSize") 8 | 9 | func (s *ImageSize) UnmarshalText(text []byte) error { 10 | switch string(text) { 11 | case "": 12 | *s = UndefinedSize 13 | case "small": 14 | *s = Small 15 | case "large": 16 | *s = Large 17 | case "xlarge": 18 | *s = XLarge 19 | default: 20 | return ErrUnknownImageSize 21 | } 22 | return nil 23 | } 24 | 25 | var seq_bytes_ImageSize = [...][]byte{[]byte(""), []byte("small"), []byte("large"), []byte("xlarge")} 26 | 27 | func (s ImageSize) MarshalText() ([]byte, error) { return s.AppendText(nil) } 28 | 29 | func (s ImageSize) AppendText(b []byte) ([]byte, error) { 30 | switch s { 31 | case UndefinedSize: 32 | return append(b, seq_bytes_ImageSize[0]...), nil 33 | case Small: 34 | return append(b, seq_bytes_ImageSize[1]...), nil 35 | case Large: 36 | return append(b, seq_bytes_ImageSize[2]...), nil 37 | case XLarge: 38 | return append(b, seq_bytes_ImageSize[3]...), nil 39 | default: 40 | return nil, ErrUnknownImageSize 41 | } 42 | } 43 | 44 | var seq_string_ImageSize = [...]string{"", "small", "large", "xlarge"} 45 | 46 | func (s ImageSize) String() string { 47 | switch s { 48 | case UndefinedSize: 49 | return seq_string_ImageSize[0] 50 | case Small: 51 | return seq_string_ImageSize[1] 52 | case Large: 53 | return seq_string_ImageSize[2] 54 | case XLarge: 55 | return seq_string_ImageSize[3] 56 | default: 57 | return "" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-enum-encoding 2 | 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/nikolaydubina/go-enum-encoding)](https://goreportcard.com/report/github.com/nikolaydubina/go-enum-encoding) 4 | [![Go Reference](https://pkg.go.dev/badge/github.com/nikolaydubina/go-enum-encoding.svg)](https://pkg.go.dev/github.com/nikolaydubina/go-enum-encoding) 5 | [![codecov](https://codecov.io/gh/nikolaydubina/go-enum-encoding/graph/badge.svg?token=asZfIddrLV)](https://codecov.io/gh/nikolaydubina/go-enum-encoding) 6 | [![go-recipes](https://raw.githubusercontent.com/nikolaydubina/go-recipes/main/badge.svg?raw=true)](https://github.com/nikolaydubina/go-recipes) 7 | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/nikolaydubina/go-enum-encoding/badge)](https://securityscorecards.dev/viewer/?uri=github.com/nikolaydubina/go-enum-encoding) 8 | 9 | ```bash 10 | go install github.com/nikolaydubina/go-enum-encoding@latest 11 | ``` 12 | 13 | * 200 LOC 14 | * simple, fast, strict 15 | * generate encoding/decoding, tests, benchmarks 16 | 17 | ```go 18 | type Color struct{ c uint8 } 19 | 20 | //go:generate go-enum-encoding -type=Color 21 | var ( 22 | UndefinedColor = Color{} // json:"" 23 | Red = Color{1} // json:"red" 24 | Green = Color{2} // json:"green" 25 | Blue = Color{3} // json:"blue" 26 | ) 27 | ``` 28 | 29 | `iota` is ok too 30 | 31 | ```go 32 | type Size uint8 33 | 34 | //go:generate go-enum-encoding -type=Size 35 | const ( 36 | UndefinedSize Size = iota // json:"" 37 | Small // json:"small" 38 | Large // json:"large" 39 | XLarge // json:"xlarge" 40 | ) 41 | ``` 42 | 43 | generated benchmarks 44 | 45 | ```bash 46 | $ go test -bench=. -benchmem . 47 | goos: darwin 48 | goarch: arm64 49 | pkg: github.com/nikolaydubina/go-enum-encoding/internal/testdata 50 | cpu: Apple M3 Max 51 | BenchmarkColor_UnmarshalText-16 752573839 1.374 ns/op 0 B/op 0 allocs/op 52 | BenchmarkColor_AppendText-16 450123993 2.676 ns/op 0 B/op 0 allocs/op 53 | BenchmarkColor_MarshalText-16 80059376 13.68 ns/op 8 B/op 1 allocs/op 54 | BenchmarkImageSize_UnmarshalText-16 751743885 1.601 ns/op 0 B/op 0 allocs/op 55 | BenchmarkImageSize_AppendText-16 500286883 2.402 ns/op 0 B/op 0 allocs/op 56 | BenchmarkImageSize_MarshalText-16 81467318 16.46 ns/op 8 B/op 1 allocs/op 57 | BenchmarkImageSize_String-16 856463289 1.330 ns/op 0 B/op 0 allocs/op 58 | PASS 59 | ok github.com/nikolaydubina/go-enum-encoding/internal/testdata 8.561s 60 | ``` 61 | 62 | ## References 63 | 64 | - http://github.com/zarldev/goenums - does much more advanced struct generation, generates all enum utilities besides encoding, does not generate tests, uses similar notation to trigger go:generate but with different comment directives (non-json field tags) 65 | - http://github.com/nikolaydubina/go-enum-example 66 | -------------------------------------------------------------------------------- /enum_test.go.template: -------------------------------------------------------------------------------- 1 | // Code generated by go-enum-encoding; DO NOT EDIT. 2 | 3 | package {{.Package}} 4 | 5 | import ( 6 | "encoding/json/v2" 7 | "errors" 8 | "fmt" 9 | "slices" 10 | "testing" 11 | "math/rand" 12 | ) 13 | 14 | func Example{{.Type}}_MarshalText() { 15 | for _, v := range []{{.Type}}{{{.Values}}} { 16 | b, _ := v.MarshalText() 17 | fmt.Printf("%s ", string(b)) 18 | } 19 | // Output: {{.TagsNaked}} 20 | } 21 | 22 | func Example{{.Type}}_UnmarshalText() { 23 | for _, s := range []string{{{.Tags}}} { 24 | var v {{.Type}} 25 | if err := (&v).UnmarshalText([]byte(s)); err != nil { 26 | fmt.Println(err) 27 | } 28 | } 29 | } 30 | 31 | func Test{{.Type}}_MarshalText_UnmarshalText(t *testing.T) { 32 | for _, v := range []{{.Type}}{{{.Values}}} { 33 | b, err := v.MarshalText() 34 | if err != nil { 35 | t.Errorf("cannot encode: %s", err) 36 | } 37 | 38 | var d {{.Type}} 39 | if err := (&d).UnmarshalText(b); err != nil { 40 | t.Errorf("cannot decode: %s", err) 41 | } 42 | 43 | if d != v { 44 | t.Errorf("exp(%v) != got(%v)", v, d) 45 | } 46 | } 47 | 48 | t.Run("when unknown value, then error", func(t *testing.T) { 49 | s := `something` 50 | var v {{.Type}} 51 | err := (&v).UnmarshalText([]byte(s)) 52 | if err == nil { 53 | t.Error("must be error") 54 | } 55 | if !errors.Is(err, ErrUnknown{{.Type}}) { 56 | t.Error("wrong error", err) 57 | } 58 | }) 59 | } 60 | 61 | func Test{{.Type}}_JSON(t *testing.T) { 62 | type V struct { 63 | Values []{{.Type}} `json:"values"` 64 | } 65 | 66 | values := []{{.Type}}{{{.Values}}} 67 | 68 | var v V 69 | s := `{"values":[{{.Tags}}]}` 70 | json.Unmarshal([]byte(s), &v) 71 | 72 | if len(v.Values) != len(values) { 73 | t.Errorf("cannot decode: %d", len(v.Values)) 74 | } 75 | if !slices.Equal(v.Values, values) { 76 | t.Errorf("wrong decoded: %v", v.Values) 77 | } 78 | 79 | b, err := json.Marshal(v) 80 | if err != nil { 81 | t.Fatalf("cannot encode: %s", err) 82 | } 83 | if string(b) != s { 84 | t.Errorf("wrong encoded: %s != %s", string(b), s) 85 | } 86 | 87 | t.Run("when unknown value, then error", func(t *testing.T) { 88 | s := `{"values":["something"]}` 89 | var v V 90 | err := json.Unmarshal([]byte(s), &v) 91 | if err == nil { 92 | t.Error("must be error") 93 | } 94 | if !errors.Is(err, ErrUnknown{{.Type}}) { 95 | t.Error("wrong error", err) 96 | } 97 | }) 98 | } 99 | 100 | 101 | func Benchmark{{.Type}}_UnmarshalText(b *testing.B) { 102 | vb := seq_bytes_{{.Type}}[rand.Intn(len(seq_bytes_{{.Type}}))] 103 | 104 | var x {{.Type}} 105 | 106 | for b.Loop() { 107 | _ = x.UnmarshalText(vb) 108 | } 109 | } 110 | 111 | func Benchmark{{.Type}}_AppendText(b *testing.B) { 112 | bb := make([]byte, 10, 1000) 113 | 114 | vs := []{{.Type}}{{{.Values}}} 115 | v := vs[rand.Intn(len(vs))] 116 | 117 | for b.Loop() { 118 | _, _ = v.AppendText(bb) 119 | } 120 | } 121 | 122 | func Benchmark{{.Type}}_MarshalText(b *testing.B) { 123 | vs := []{{.Type}}{{{.Values}}} 124 | v := vs[rand.Intn(len(vs))] 125 | 126 | for b.Loop() { 127 | _, _ = v.MarshalText() 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /internal/testdata/color_enum_encoding_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-enum-encoding; DO NOT EDIT. 2 | 3 | package image 4 | 5 | import ( 6 | "encoding/json/v2" 7 | "errors" 8 | "fmt" 9 | "math/rand" 10 | "slices" 11 | "testing" 12 | ) 13 | 14 | func ExampleColor_MarshalText() { 15 | for _, v := range []Color{UndefinedColor, Red, Green, Blue} { 16 | b, _ := v.MarshalText() 17 | fmt.Printf("%s ", string(b)) 18 | } 19 | // Output: red green blue 20 | } 21 | 22 | func ExampleColor_UnmarshalText() { 23 | for _, s := range []string{"", "red", "green", "blue"} { 24 | var v Color 25 | if err := (&v).UnmarshalText([]byte(s)); err != nil { 26 | fmt.Println(err) 27 | } 28 | } 29 | } 30 | 31 | func TestColor_MarshalText_UnmarshalText(t *testing.T) { 32 | for _, v := range []Color{UndefinedColor, Red, Green, Blue} { 33 | b, err := v.MarshalText() 34 | if err != nil { 35 | t.Errorf("cannot encode: %s", err) 36 | } 37 | 38 | var d Color 39 | if err := (&d).UnmarshalText(b); err != nil { 40 | t.Errorf("cannot decode: %s", err) 41 | } 42 | 43 | if d != v { 44 | t.Errorf("exp(%v) != got(%v)", v, d) 45 | } 46 | } 47 | 48 | t.Run("when unknown value, then error", func(t *testing.T) { 49 | s := `something` 50 | var v Color 51 | err := (&v).UnmarshalText([]byte(s)) 52 | if err == nil { 53 | t.Error("must be error") 54 | } 55 | if !errors.Is(err, ErrUnknownColor) { 56 | t.Error("wrong error", err) 57 | } 58 | }) 59 | } 60 | 61 | func TestColor_JSON(t *testing.T) { 62 | type V struct { 63 | Values []Color `json:"values"` 64 | } 65 | 66 | values := []Color{UndefinedColor, Red, Green, Blue} 67 | 68 | var v V 69 | s := `{"values":["","red","green","blue"]}` 70 | json.Unmarshal([]byte(s), &v) 71 | 72 | if len(v.Values) != len(values) { 73 | t.Errorf("cannot decode: %d", len(v.Values)) 74 | } 75 | if !slices.Equal(v.Values, values) { 76 | t.Errorf("wrong decoded: %v", v.Values) 77 | } 78 | 79 | b, err := json.Marshal(v) 80 | if err != nil { 81 | t.Fatalf("cannot encode: %s", err) 82 | } 83 | if string(b) != s { 84 | t.Errorf("wrong encoded: %s != %s", string(b), s) 85 | } 86 | 87 | t.Run("when unknown value, then error", func(t *testing.T) { 88 | s := `{"values":["something"]}` 89 | var v V 90 | err := json.Unmarshal([]byte(s), &v) 91 | if err == nil { 92 | t.Error("must be error") 93 | } 94 | if !errors.Is(err, ErrUnknownColor) { 95 | t.Error("wrong error", err) 96 | } 97 | }) 98 | } 99 | 100 | func BenchmarkColor_UnmarshalText(b *testing.B) { 101 | vb := seq_bytes_Color[rand.Intn(len(seq_bytes_Color))] 102 | 103 | var x Color 104 | 105 | for b.Loop() { 106 | _ = x.UnmarshalText(vb) 107 | } 108 | } 109 | 110 | func BenchmarkColor_AppendText(b *testing.B) { 111 | bb := make([]byte, 10, 1000) 112 | 113 | vs := []Color{UndefinedColor, Red, Green, Blue} 114 | v := vs[rand.Intn(len(vs))] 115 | 116 | for b.Loop() { 117 | _, _ = v.AppendText(bb) 118 | } 119 | } 120 | 121 | func BenchmarkColor_MarshalText(b *testing.B) { 122 | vs := []Color{UndefinedColor, Red, Green, Blue} 123 | v := vs[rand.Intn(len(vs))] 124 | 125 | for b.Loop() { 126 | _, _ = v.MarshalText() 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /internal/testdata/image_size_enum_encoding_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-enum-encoding; DO NOT EDIT. 2 | 3 | package image 4 | 5 | import ( 6 | "encoding/json/v2" 7 | "errors" 8 | "fmt" 9 | "math/rand" 10 | "slices" 11 | "testing" 12 | ) 13 | 14 | func ExampleImageSize_MarshalText() { 15 | for _, v := range []ImageSize{UndefinedSize, Small, Large, XLarge} { 16 | b, _ := v.MarshalText() 17 | fmt.Printf("%s ", string(b)) 18 | } 19 | // Output: small large xlarge 20 | } 21 | 22 | func ExampleImageSize_UnmarshalText() { 23 | for _, s := range []string{"", "small", "large", "xlarge"} { 24 | var v ImageSize 25 | if err := (&v).UnmarshalText([]byte(s)); err != nil { 26 | fmt.Println(err) 27 | } 28 | } 29 | } 30 | 31 | func TestImageSize_MarshalText_UnmarshalText(t *testing.T) { 32 | for _, v := range []ImageSize{UndefinedSize, Small, Large, XLarge} { 33 | b, err := v.MarshalText() 34 | if err != nil { 35 | t.Errorf("cannot encode: %s", err) 36 | } 37 | 38 | var d ImageSize 39 | if err := (&d).UnmarshalText(b); err != nil { 40 | t.Errorf("cannot decode: %s", err) 41 | } 42 | 43 | if d != v { 44 | t.Errorf("exp(%v) != got(%v)", v, d) 45 | } 46 | } 47 | 48 | t.Run("when unknown value, then error", func(t *testing.T) { 49 | s := `something` 50 | var v ImageSize 51 | err := (&v).UnmarshalText([]byte(s)) 52 | if err == nil { 53 | t.Error("must be error") 54 | } 55 | if !errors.Is(err, ErrUnknownImageSize) { 56 | t.Error("wrong error", err) 57 | } 58 | }) 59 | } 60 | 61 | func TestImageSize_JSON(t *testing.T) { 62 | type V struct { 63 | Values []ImageSize `json:"values"` 64 | } 65 | 66 | values := []ImageSize{UndefinedSize, Small, Large, XLarge} 67 | 68 | var v V 69 | s := `{"values":["","small","large","xlarge"]}` 70 | json.Unmarshal([]byte(s), &v) 71 | 72 | if len(v.Values) != len(values) { 73 | t.Errorf("cannot decode: %d", len(v.Values)) 74 | } 75 | if !slices.Equal(v.Values, values) { 76 | t.Errorf("wrong decoded: %v", v.Values) 77 | } 78 | 79 | b, err := json.Marshal(v) 80 | if err != nil { 81 | t.Fatalf("cannot encode: %s", err) 82 | } 83 | if string(b) != s { 84 | t.Errorf("wrong encoded: %s != %s", string(b), s) 85 | } 86 | 87 | t.Run("when unknown value, then error", func(t *testing.T) { 88 | s := `{"values":["something"]}` 89 | var v V 90 | err := json.Unmarshal([]byte(s), &v) 91 | if err == nil { 92 | t.Error("must be error") 93 | } 94 | if !errors.Is(err, ErrUnknownImageSize) { 95 | t.Error("wrong error", err) 96 | } 97 | }) 98 | } 99 | 100 | func BenchmarkImageSize_UnmarshalText(b *testing.B) { 101 | vb := seq_bytes_ImageSize[rand.Intn(len(seq_bytes_ImageSize))] 102 | 103 | var x ImageSize 104 | 105 | for b.Loop() { 106 | _ = x.UnmarshalText(vb) 107 | } 108 | } 109 | 110 | func BenchmarkImageSize_AppendText(b *testing.B) { 111 | bb := make([]byte, 10, 1000) 112 | 113 | vs := []ImageSize{UndefinedSize, Small, Large, XLarge} 114 | v := vs[rand.Intn(len(vs))] 115 | 116 | for b.Loop() { 117 | _, _ = v.AppendText(bb) 118 | } 119 | } 120 | 121 | func BenchmarkImageSize_MarshalText(b *testing.B) { 122 | vs := []ImageSize{UndefinedSize, Small, Large, XLarge} 123 | v := vs[rand.Intn(len(vs))] 124 | 125 | for b.Loop() { 126 | _, _ = v.MarshalText() 127 | } 128 | } 129 | 130 | func TestImageSize_String(t *testing.T) { 131 | values := []ImageSize{UndefinedSize, Small, Large, XLarge} 132 | tags := []string{"", "small", "large", "xlarge"} 133 | 134 | for i := range values { 135 | if s := values[i].String(); s != tags[i] { 136 | t.Error(s, tags[i]) 137 | } 138 | } 139 | } 140 | 141 | func BenchmarkImageSize_String(b *testing.B) { 142 | vs := []ImageSize{UndefinedSize, Small, Large, XLarge} 143 | v := vs[rand.Intn(len(vs))] 144 | 145 | for b.Loop() { 146 | _ = v.String() 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | _ "embed" 5 | "errors" 6 | "flag" 7 | "go/ast" 8 | "go/format" 9 | "go/parser" 10 | "go/token" 11 | "os" 12 | "path/filepath" 13 | "strconv" 14 | "strings" 15 | "unicode" 16 | ) 17 | 18 | //go:embed enum.go.template 19 | var templateEnum string 20 | 21 | //go:embed enum_test.go.template 22 | var templateEnumTest string 23 | 24 | //go:embed enum_string.go.template 25 | var templateString string 26 | 27 | //go:embed enum_string_test.go.template 28 | var templateStringTest string 29 | 30 | func main() { 31 | var ( 32 | typeName string 33 | enableString bool 34 | fileName = os.Getenv("GOFILE") 35 | lineNum = os.Getenv("GOLINE") 36 | packageName = os.Getenv("GOPACKAGE") 37 | ) 38 | flag.StringVar(&typeName, "type", "", "type to be generated for") 39 | flag.BoolVar(&enableString, "string", false, "generate String() method") 40 | flag.Parse() 41 | 42 | if err := process(packageName, fileName, typeName, lineNum, enableString); err != nil { 43 | os.Stderr.WriteString(err.Error()) 44 | os.Exit(1) 45 | } 46 | } 47 | 48 | func process(packageName, fileName, typeName, lineNum string, enableString bool) error { 49 | if typeName == "" || fileName == "" || packageName == "" || lineNum == "" { 50 | return errors.New("missing parameters") 51 | } 52 | 53 | inputCode, err := os.ReadFile(fileName) 54 | if err != nil { 55 | return err 56 | } 57 | 58 | fset := token.NewFileSet() 59 | f, err := parser.ParseFile(fset, fileName, inputCode, parser.ParseComments) 60 | if err != nil { 61 | return err 62 | } 63 | 64 | expectedLine, _ := strconv.Atoi(lineNum) 65 | expectedLine += 1 66 | 67 | var specs [][2]string 68 | 69 | ast.Inspect(f, func(astNode ast.Node) bool { 70 | node, ok := astNode.(*ast.GenDecl) 71 | if !ok || node == nil || (node.Tok != token.CONST && node.Tok != token.VAR) { 72 | return true 73 | } 74 | 75 | if position := fset.Position(node.Pos()); position.Line != expectedLine { 76 | return false 77 | } 78 | 79 | for _, astSpec := range node.Specs { 80 | spec, ok := astSpec.(*ast.ValueSpec) 81 | if !ok || spec == nil { 82 | continue 83 | } 84 | 85 | if len(spec.Names) != 1 { 86 | break 87 | } 88 | 89 | tag, ok := "", false 90 | for _, field := range strings.Fields(spec.Comment.Text()) { 91 | if strings.HasPrefix(field, "json:") { 92 | tag, ok = field[len("json:\""):len(field)-1], true 93 | break 94 | } 95 | } 96 | if ok { 97 | specs = append(specs, [2]string{spec.Names[0].Name, tag}) 98 | } 99 | } 100 | 101 | return false 102 | }) 103 | 104 | if len(specs) == 0 { 105 | return errors.New(fileName + ": unable to find values for enum type: " + typeName) 106 | } 107 | 108 | templateCode := templateEnum 109 | templateTest := templateEnumTest 110 | if enableString { 111 | templateCode += "\n" + templateString 112 | templateTest += "\n" + templateStringTest 113 | } 114 | 115 | replacer := strings.NewReplacer( 116 | "{{.Type}}", typeName, 117 | "{{.Package}}", packageName, 118 | "{{.Values}}", mapJoin(specs, func(_ int, v [2]string) string { return v[0] }, ", "), 119 | "{{.Tags}}", mapJoin(specs, func(_ int, v [2]string) string { return `"` + v[1] + `"` }, ","), 120 | "{{.TagsNaked}}", mapJoin(specs, func(_ int, v [2]string) string { return v[1] }, " "), 121 | "{{.seq_bytes}}", mapJoin(specs, func(_ int, v [2]string) string { return `[]byte("` + v[1] + `")` }, ", "), 122 | "{{.seq_string}}", mapJoin(specs, func(_ int, v [2]string) string { return `"` + v[1] + `"` }, ", "), 123 | "{{.string_to_value_switch}}", mapJoin(specs, func(_ int, v [2]string) string { return `case "` + v[1] + "\":\n *s = " + v[0] }, "\n"), 124 | "{{.value_to_append_bytes_switch}}", mapJoin(specs, func(i int, v [2]string) string { 125 | return `case ` + v[0] + ":\n return append(b, seq_bytes_" + typeName + "[" + strconv.Itoa(i) + `]...), nil` 126 | }, "\n"), 127 | "{{.value_to_string_switch}}", mapJoin(specs, func(i int, v [2]string) string { 128 | return `case ` + v[0] + ":\n return seq_string_" + typeName + "[" + strconv.Itoa(i) + `]` 129 | }, "\n"), 130 | ) 131 | 132 | bastPath := filepath.Join(filepath.Dir(fileName), camelCaseToSnakeCase(typeName)) 133 | 134 | return errors.Join( 135 | writeCode([]byte(replacer.Replace(templateCode)), bastPath+"_enum_encoding.go"), 136 | writeCode([]byte(replacer.Replace(templateTest)), bastPath+"_enum_encoding_test.go"), 137 | ) 138 | } 139 | 140 | func mapJoin(vs [][2]string, f func(idx int, val [2]string) string, sep string) string { 141 | var b strings.Builder 142 | for i, v := range vs { 143 | if i > 0 { 144 | b.WriteString(sep) 145 | } 146 | b.WriteString(f(i, v)) 147 | } 148 | return b.String() 149 | } 150 | 151 | func writeCode(code []byte, outFilePath string) error { 152 | formattedCode, err := format.Source(code) 153 | if err != nil { 154 | return err 155 | } 156 | return os.WriteFile(outFilePath, formattedCode, 0644) 157 | } 158 | 159 | func camelCaseToSnakeCase(s string) string { 160 | var b strings.Builder 161 | var isPrevUpper bool 162 | for i, r := range s { 163 | if unicode.IsUpper(r) { 164 | if i > 0 && !isPrevUpper { 165 | b.WriteRune('_') 166 | } 167 | isPrevUpper = true 168 | b.WriteRune(unicode.ToLower(r)) 169 | } else { 170 | isPrevUpper = false 171 | b.WriteRune(r) 172 | } 173 | } 174 | return b.String() 175 | } 176 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main_test 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | "path" 7 | "path/filepath" 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | func TestMain(t *testing.T) { 13 | var covdirs []string 14 | testdir := t.TempDir() 15 | testbin := path.Join(testdir, "go-enum-encoding-test") 16 | 17 | if b, err := exec.Command("go", "build", "-cover", "-o", testbin, "main.go").CombinedOutput(); err != nil { 18 | t.Fatal(err, string(b)) 19 | } 20 | 21 | defer func() { 22 | if b, err := exec.Command("go", "tool", "covdata", "textfmt", "-i="+strings.Join(covdirs, ","), "-o", os.Getenv("GOCOVERPROFILE")).CombinedOutput(); err != nil { 23 | t.Error(err, string(b)) 24 | } 25 | }() 26 | 27 | t.Run("ok", func(t *testing.T) { 28 | if b, err := exec.Command("cp", filepath.Join("internal", "testdata", "image.go"), filepath.Join(testdir, "image.go")).CombinedOutput(); err != nil { 29 | t.Fatal(err, string(b)) 30 | } 31 | 32 | t.Run("struct", func(t *testing.T) { 33 | covdir := "cov_struct" 34 | exec.Command("mkdir", covdir).Run() 35 | covdirs = append(covdirs, covdir) 36 | 37 | cmd := exec.Command(testbin, "--type", "Color") 38 | cmd.Env = append(cmd.Environ(), "GOFILE="+filepath.Join(testdir, "image.go"), "GOLINE=4", "GOPACKAGE=image", "GOTESTDIR="+testdir, "GOCOVERDIR="+covdir) 39 | if err := cmd.Run(); err != nil { 40 | t.Error(err) 41 | } 42 | 43 | assertEqFile(t, filepath.Join(testdir, "color_enum_encoding.go"), filepath.Join("internal", "testdata", "color_enum_encoding.go")) 44 | assertEqFile(t, filepath.Join(testdir, "color_enum_encoding_test.go"), filepath.Join("internal", "testdata", "color_enum_encoding_test.go")) 45 | }) 46 | 47 | t.Run("iota, string", func(t *testing.T) { 48 | covdir := "cov_iota" 49 | exec.Command("mkdir", covdir).Run() 50 | covdirs = append(covdirs, covdir) 51 | 52 | cmd := exec.Command(testbin, "--type", "ImageSize", "--string") 53 | cmd.Env = append(cmd.Environ(), "GOFILE="+filepath.Join(testdir, "image.go"), "GOLINE=18", "GOPACKAGE=image", "GOTESTDIR="+testdir, "GOCOVERDIR="+covdir) 54 | if err := cmd.Run(); err != nil { 55 | t.Error(err) 56 | } 57 | 58 | assertEqFile(t, filepath.Join(testdir, "image_size_enum_encoding.go"), filepath.Join("internal", "testdata", "image_size_enum_encoding.go")) 59 | assertEqFile(t, filepath.Join(testdir, "image_size_enum_encoding_test.go"), filepath.Join("internal", "testdata", "image_size_enum_encoding_test.go")) 60 | }) 61 | 62 | t.Run("run tests within generated code", func(t *testing.T) { 63 | from, _ := os.Getwd() 64 | os.Chdir(testdir) 65 | t.Cleanup(func() { os.Chdir(from) }) 66 | 67 | os.WriteFile(filepath.Join(testdir, "go.mod"), []byte("module test\ngo 1.24"), 0644) 68 | 69 | if b, err := exec.Command("go", "test", ".").CombinedOutput(); err != nil { 70 | t.Error(err, string(b)) 71 | } 72 | }) 73 | }) 74 | 75 | t.Run("when bad go file, then error", func(t *testing.T) { 76 | covdir := "cov_err_bad_file" 77 | exec.Command("mkdir", covdir).Run() 78 | covdirs = append(covdirs, covdir) 79 | 80 | exec.Command("cp", filepath.Join("internal", "README.md"), filepath.Join(testdir, "README.md")).Run() 81 | 82 | cmd := exec.Command(testbin, "--type", "Color") 83 | cmd.Env = append(cmd.Environ(), "GOFILE=README.md", "GOLINE=5", "GOPACKAGE=image", "GOTESTDIR="+testdir, "GOCOVERDIR="+covdir) 84 | if err := cmd.Run(); err == nil { 85 | t.Fatal("must be error") 86 | } 87 | }) 88 | 89 | t.Run("when enum values not immediately after go:generate line, then error", func(t *testing.T) { 90 | covdir := "cov_err_enum" 91 | exec.Command("mkdir", covdir).Run() 92 | covdirs = append(covdirs, covdir) 93 | 94 | cmd := exec.Command(testbin, "--type", "Color") 95 | cmd.Env = append(cmd.Environ(), "GOFILE="+filepath.Join(testdir, "image.go"), "GOLINE=1", "GOPACKAGE=image", "GOTESTDIR="+testdir, "GOCOVERDIR="+covdir) 96 | if err := cmd.Run(); err == nil { 97 | t.Fatal("must be error") 98 | } 99 | }) 100 | 101 | t.Run("when invalid package name, then error", func(t *testing.T) { 102 | covdir := "cov_err_invalid_pkg" 103 | exec.Command("mkdir", covdir).Run() 104 | covdirs = append(covdirs, covdir) 105 | 106 | cmd := exec.Command(testbin, "--type", "Color") 107 | cmd.Env = append(cmd.Environ(), "GOFILE="+filepath.Join(testdir, "image.go"), "GOLINE=5", "GOPACKAGE=\"", "GOTESTDIR="+testdir, "GOCOVERDIR="+covdir) 108 | if err := cmd.Run(); err == nil { 109 | t.Fatal("must be error") 110 | } 111 | }) 112 | 113 | t.Run("when not found file, then error", func(t *testing.T) { 114 | covdir := "cov_err_file_not_found" 115 | exec.Command("mkdir", covdir).Run() 116 | covdirs = append(covdirs, covdir) 117 | 118 | cmd := exec.Command(testbin, "--type", "Color") 119 | cmd.Env = append(cmd.Environ(), "GOFILE=asdf.asdf", "GOPACKAGE=image", "GOLINE=5", "GOTESTDIR="+testdir, "GOCOVERDIR="+covdir) 120 | if err := cmd.Run(); err == nil { 121 | t.Fatal("must be error") 122 | } 123 | }) 124 | 125 | t.Run("when wrong params, then error", func(t *testing.T) { 126 | covdir := "cov_err_wrong_params" 127 | exec.Command("mkdir", covdir).Run() 128 | covdirs = append(covdirs, covdir) 129 | 130 | cmd := exec.Command(testbin) 131 | cmd.Env = append(cmd.Environ(), "GOFILE="+filepath.Join(testdir, "image.go"), "GOLINE=5", "GOPACKAGE=color", "GOTESTDIR="+testdir, "GOCOVERDIR="+covdir) 132 | if err := cmd.Run(); err == nil { 133 | t.Fatal("must be error") 134 | } 135 | }) 136 | } 137 | 138 | func assertEqFile(t *testing.T, a, b string) { 139 | t.Helper() 140 | fa, err := os.ReadFile(a) 141 | if err != nil { 142 | t.Error(err) 143 | } 144 | fb, err := os.ReadFile(b) 145 | if err != nil { 146 | t.Error(err) 147 | } 148 | if len(fa) == 0 || len(fb) == 0 { 149 | t.Error("empty file: " + a + " or " + b) 150 | } 151 | if string(fa) != string(fb) { 152 | t.Error("files different: " + a + " != " + b + ": " + string(fa) + " <> " + string(fb)) 153 | } 154 | } 155 | --------------------------------------------------------------------------------