Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
*.prof
.idea
mise.toml
coverage.out
coverage.html
coverage_*.out
coverage_*.html
*.coverprofile
37 changes: 33 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
- Case-sensitive or case-insensitive string representations
- Panic-free parsing with error handling
- Must-style parsing variants for convenience
- Easy value enumeration with Values() and Names() functions
- Declaration order preservation (enums maintain source code order, not alphabetical)
- Type fidelity preservation (generated code uses the same underlying type as your enum)
- Optimized parsing with O(1) map-based lookups
- Smart SQL null handling (uses zero value when available, errors otherwise)
- Generated code is fully tested and documented
- No external runtime dependencies
- Supports Go 1.23's range-over-func iteration
Expand Down Expand Up @@ -109,10 +112,12 @@ The generator creates a new type with the following features:

- String representation (implements `fmt.Stringer`)
- Text marshaling (implements `encoding.TextMarshaler` and `encoding.TextUnmarshaler`)
- Parse function with error handling (`ParseStatus`)
- SQL support (implements `database/sql/driver.Valuer` and `sql.Scanner`)
- Parse function with error handling (`ParseStatus`) - uses efficient O(1) map lookup
- Must-style parse function that panics on error (`MustStatus`)
- All possible values slice (`StatusValues`)
- All possible names slice (`StatusNames`)
- All possible values as package variable (`StatusValues`) - preserves declaration order
- All possible names as package variable (`StatusNames`) - preserves declaration order
- Index method to get underlying integer value (`Status.Index()`)
- Go 1.23 iterator support (`StatusIter()`) for range-over-func syntax
- Public constants for each value (`StatusActive`, `StatusInactive`, etc.) - note that these are capitalized versions of your original constants

Expand Down Expand Up @@ -151,6 +156,30 @@ if err != nil {
status := MustStatus("active") // panics if invalid
```

### SQL Database Support

The generated enums implement `database/sql/driver.Valuer` and `sql.Scanner` interfaces for seamless database integration:

```go
// Scanning from database
var s Status
err := db.QueryRow("SELECT status FROM users WHERE id = ?", userID).Scan(&s)

// Writing to database
_, err = db.Exec("UPDATE users SET status = ? WHERE id = ?", StatusActive, userID)

// Handling NULL values
// If the enum has a zero value (value = 0), NULL will scan to that value
// Otherwise, scanning NULL returns an error
```

### Performance Characteristics

- **Parsing**: O(1) constant time using map lookup (previously O(n) with switch statement)
- **Values/Names access**: Zero allocation - returns pre-computed package variables
- **Memory efficient**: Single shared instance for each enum value
- **Declaration order**: Preserved from source code, not alphabetically sorted

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ go 1.24

require (
github.com/stretchr/testify v1.10.0
golang.org/x/text v0.23.0
golang.org/x/text v0.28.0
)

require (
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
Expand Down
6 changes: 6 additions & 0 deletions go.work.sum
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,24 @@ github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457 h1:zf5N6UOrA487eEFacMePxjXAJctxKmyjKUsjA11Uzuk=
golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/cc/v3 v3.41.0 h1:QoR1Sn3YWlmA1T4vLaKZfawdVtSiGx8H+cEojbC7v1Q=
Expand Down
67 changes: 37 additions & 30 deletions internal/generator/enum.go.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,14 @@ import (
// {{.Type | title}} is the exported type for the enum
type {{.Type | title}} struct {
name string
value int
value {{if .UnderlyingType}}{{.UnderlyingType}}{{else}}int{{end}}
}

func (e {{.Type | title}}) String() string { return e.name }

// Index returns the underlying integer value
func (e {{.Type | title}}) Index() {{if .UnderlyingType}}{{.UnderlyingType}}{{else}}int{{end}} { return e.value }

// MarshalText implements encoding.TextMarshaler
func (e {{.Type | title}}) MarshalText() ([]byte, error) {
return []byte(e.name), nil
Expand All @@ -38,8 +41,15 @@ func (e {{.Type | title}}) Value() (driver.Value, error) {
// Scan implements the sql.Scanner interface
func (e *{{.Type | title}}) Scan(value interface{}) error {
if value == nil {
*e = {{.Type | title}}Values()[0]
return nil
// try to find zero value
for _, v := range {{.Type | title}}Values {
if v.Index() == 0 {
*e = v
return nil
}
}
// no zero value found, return error
return fmt.Errorf("cannot scan nil into {{.Type | title}}: no zero value defined")
}

str, ok := value.(string)
Expand All @@ -60,21 +70,22 @@ func (e *{{.Type | title}}) Scan(value interface{}) error {
return nil
}

// _{{.Type}}ParseMap is used for efficient string to enum conversion
var _{{.Type}}ParseMap = map[string]{{.Type | title}}{
{{range .Values -}}
"{{.Name | ToLower}}": {{.PublicName}},
{{end}}
}

// Parse{{.Type | title}} converts string to {{.Type}} enum value
func Parse{{.Type | title}}(v string) ({{.Type | title}}, error) {
{{if .LowerCase}}
switch v {
{{range .Values -}}
case "{{.Name | ToLower}}":
return {{.PublicName}}, nil
{{end}}
if val, ok := _{{.Type}}ParseMap[v]; ok {
return val, nil
}
{{else}}
switch strings.ToLower(v) {
{{range .Values -}}
case strings.ToLower("{{.Name}}"):
return {{.PublicName}}, nil
{{end}}
if val, ok := _{{.Type}}ParseMap[strings.ToLower(v)]; ok {
return val, nil
}
{{end}}
return {{.Type | title}}{}, fmt.Errorf("invalid {{.Type}}: %s", v)
Expand All @@ -91,7 +102,7 @@ func Must{{.Type | title}}(v string) {{.Type | title}} {

{{if .GenerateGetter -}}
// Get{{.Type | title}}ByID gets the correspondent {{.Type}} enum value by its ID (raw integer value)
func Get{{.Type | title}}ByID(v int) ({{.Type | title}}, error) {
func Get{{.Type | title}}ByID(v {{if .UnderlyingType}}{{.UnderlyingType}}{{else}}int{{end}}) ({{.Type | title}}, error) {
switch v {
{{range .Values -}}
case {{.Index}}:
Expand All @@ -109,22 +120,18 @@ var (
{{end -}}
)

// {{.Type | title}}Values returns all possible enum values
func {{.Type | title}}Values() []{{.Type | title}} {
return []{{.Type | title}}{
{{range .Values -}}
{{.PublicName}},
{{end -}}
}
// {{.Type | title}}Values contains all possible enum values
var {{.Type | title}}Values = []{{.Type | title}}{
{{range .Values -}}
{{.PublicName}},
{{end -}}
}

// {{.Type | title}}Names returns all possible enum names
func {{.Type | title}}Names() []string {
return []string{
{{range .Values -}}
"{{if $.LowerCase}}{{.Name | ToLower}}{{else}}{{.Name}}{{end}}",
{{end -}}
}
// {{.Type | title}}Names contains all possible enum names
var {{.Type | title}}Names = []string{
{{range .Values -}}
"{{if $.LowerCase}}{{.Name | ToLower}}{{else}}{{.Name}}{{end}}",
{{end -}}
}

// {{.Type | title}}Iter returns a function compatible with Go 1.23's range-over-func syntax.
Expand All @@ -136,7 +143,7 @@ func {{.Type | title}}Names() []string {
//
func {{.Type | title}}Iter() func(yield func({{.Type | title}}) bool) {
return func(yield func({{.Type | title}}) bool) {
for _, v := range {{.Type | title}}Values() {
for _, v := range {{.Type | title}}Values {
if !yield(v) {
break
}
Expand All @@ -148,7 +155,7 @@ func {{.Type | title}}Iter() func(yield func({{.Type | title}}) bool) {
// for the original enum constants. They are intentionally placed in a var block
// that is compiled away by the Go compiler.
var _ = func() bool {
var _ {{.Type}} = 0
var _ {{.Type}} = {{if .UnderlyingType}}{{.Type}}(0){{else}}0{{end}}
{{range .Values -}}
// This avoids "defined but not used" linter error for {{.PrivateName}}
var _ {{$.Type}} = {{.PrivateName}}
Expand Down
Loading
Loading