Skip to content

JasonAcar/strenum

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

strenum

Strongly-typed string enums for Go using phantom-type generics.

Two wire formats supported out of the box:

  • Raw: "value"
  • Qualified: "Enum.value" (pretty and self-describing)

Batteries included: JSON/Text marshal/unmarshal, database/sql Scan/Value, a tiny registry, helpers (All, Contains, Qualified, …), and examples.

Requires Go 1.18+ (generics). Recommended: 1.20+.

Install

go get github.com/JasonAcar/strenum@latest
# or pin a release
go get github.com/JasonAcar/strenum@v0.1.0

Import:

import "github.com/JasonAcar/strenum"

Quick start (Qualified / "strong" wire format)

package yourpkg

import "github.com/JasonAcar/strenum"

// 1) Define a unique phantom tag type for this enum:
type ProjectTypeTag struct{}

// 2) (Optional) Alias a friendly field type:
type ProjectType = strenum.Enum[ProjectTypeTag]

// 3) Declare the spec once (registers itself):
var ProjectTypeSpec = strenum.NewStrongEnum[ProjectTypeTag](
    "ProjectType", "go", "node", "python",
)

// 4) Use it in your structs:
type Project struct {
    ProjectType ProjectType `json:"project_type"`
    Repo        string      `json:"repo"`
}

Behavior:

v := ProjectTypeSpec.Must("go")
fmt.Println(v)            // ProjectType.go
fmt.Println(v.Raw())      // go
fmt.Println(v.Qualified())// ProjectType.go
fmt.Println(v.IsValid())  // true

JSON round-trip:

p := Project{ProjectType: ProjectTypeSpec.Must("node")}
b, _ := json.Marshal(p)                      // {"project_type":"ProjectType.node"}
var p2 Project
_ = json.Unmarshal([]byte(`{"project_type":"ProjectType.go"}`), &p2) // ok

Raw wire format

Prefer plain strings on the wire? Use NewEnum:

type MessageStatusTag struct{}
type MessageStatus = strenum.Enum[MessageStatusTag]

var MessageStatusSpec = strenum.NewEnum[MessageStatusTag](
    "MessageStatus", "queued", "delivered", "failed",
)

m := MessageStatusSpec.Must("queued")
fmt.Println(m)       // queued
fmt.Println(m.Raw()) // queued

type Message struct {
    Status MessageStatus `json:"status"`
}
b, _ := json.Marshal(Message{Status: m})     // {"status":"queued"}

Parsing accepts raw or qualified input in both modes:

_ = json.Unmarshal([]byte(`{"status":"delivered"}`), &msg)          // ok
_ = json.Unmarshal([]byte(`{"status":"MessageStatus.delivered"}`), &msg) // also ok

Helpers you'll actually use

ProjectTypeSpec.Contains("go") // true
ProjectTypeSpec.AllStrings()   // []string{"go","node","python"}
for _, v := range ProjectTypeSpec.All() {
    fmt.Println(v, v.Raw(), v.IsValid())
}

database/sql integration

Values store/read as their String():

var v ProjectType
_ = v.Scan("ProjectType.go")  // ok
s, _ := v.Value()             // "ProjectType.go"

var raw MessageStatus
_ = raw.Scan([]byte("queued")) // ok
s2, _ := raw.Value()           // "queued"

CLI flags (tiny wrapper)

type EnumFlag[T any] struct {
    Spec  *strenum.Spec[T]
    Value strenum.Enum[T]
}
func (f *EnumFlag[T]) String() string { return f.Value.String() }
func (f *EnumFlag[T]) Set(s string) error {
    v, err := f.Spec.Parse(s); if err != nil { return err }
    f.Value = v; return nil
}

// usage
var pt EnumFlag[ProjectTypeTag] = EnumFlag[ProjectTypeTag]{Spec: ProjectTypeSpec}
flag.Var(&pt, "project-type", "one of: "+strings.Join(ProjectTypeSpec.AllStrings(), ", "))

Accepts -project-type go or -project-type ProjectType.go.

Registry introspection (optional)

Every spec registers itself (keyed by the phantom tag type). You can list them for diagnostics:

for _, e := range strenum.RegistryEntries() {
    fmt.Println(e)
    // Example: ProjectTypeTag → ProjectType (qualified) [go node python]
}

How it works (design in 60 seconds)

  • You create a zero-size phantom tag type per enum (e.g., ProjectTypeTag).
  • Your field type is Enum[ProjectTypeTag].
  • A Spec[ProjectTypeTag] defines valid values and is auto-registered.
  • Enum[T] methods consult the registered Spec[T]—no parent pointers.

Wire format is decided by constructor:

  • NewEnumString()/JSON emit raw ("value").
  • NewStrongEnum → emit qualified ("Enum.value").

Parse/unmarshal accept raw or qualified input for convenience.

Thread-safety: Spec is immutable after construction; registry uses sync.Map. Safe to share across goroutines.

Error behavior

_, err := ProjectTypeSpec.Parse("ProjectType.rust")
// err: "rust is not a valid ProjectType (valid: [go node python])"

_, err = ProjectTypeSpec.Parse("WrongEnum.go")
// err: `enum "WrongEnum" does not match "ProjectType"`

On JSON/Text/SQL decode, the same validations are applied.

Real-world layout

.
├── go.mod                         # module github.com/JasonAcar/strenum
├── doc.go                         # package comment (optional)
├── generic.go                     # the library (package strenum)
├── generic_test.go                # examples & tests (pkg strenum_test/strenum)
└── examples/
    └── main.go                    # runnable demos (package main)

Run everything:

go test ./...
go run ./examples

Examples on pkg.go.dev: they define the tag + spec inside each Example… function so the snippet is self-contained. In real code, put them at package scope.

FAQ

Why this instead of iota enums?

Classic iota + String() lets any int sneak in. This gives you:

  • stringy ergonomics,
  • compile-time type separation per enum,
  • clear wire format,
  • built-in JSON/SQL support.

Can I extend enums at runtime?

Specs are intended to be immutable.

Do I have to use Qualified mode?

No—pick per enum. Many APIs prefer raw (NewEnum), internal logs prefer qualified (NewStrongEnum).

What if I forget to declare the spec?

Enum[T] still marshals to its raw string. On unmarshal/parse, behavior depends on whether a Spec[T] is registered: if found, it validates; otherwise it accepts the raw string.

Versioning

This module follows semver. For major versions v2+, Go requires a path suffix:

module github.com/JasonAcar/strenum/v2
import "github.com/JasonAcar/strenum/v2"

License

MIT, See LICENSE.

Links

Appendix: Copy-paste example (both modes)

// Qualified example
type PTTag struct{}
type ProjectType = strenum.Enum[PTTag]
var ProjectTypeSpec = strenum.NewStrongEnum[PTTag]("ProjectType", "go", "node")

// Raw example
type MSTag struct{}
type MessageStatus = strenum.Enum[MSTag]
var MessageStatusSpec = strenum.NewEnum[MSTag]("MessageStatus", "queued", "delivered")

type S struct{ T ProjectType `json:"t"` }
type M struct{ Status MessageStatus `json:"status"` }

func demo() {
    // Parse
    fmt.Println(ProjectTypeSpec.Must("go")) // ProjectType.go
    // JSON
    b, _ := json.Marshal(S{T: ProjectTypeSpec.Must("node")})
    fmt.Println(string(b)) // {"t":"ProjectType.node"}
    var m M
    _ = json.Unmarshal([]byte(`{"status":"delivered"}`), &m)
    fmt.Println(m.Status)  // delivered
}

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages