strenum

package module
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 24, 2025 License: MIT Imports: 7 Imported by: 0

README

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/[email protected]

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.

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
}

Documentation

Overview

Package strenum provides strongly typed string enums using phantom-type generics. It supports raw ("value") and qualified ("Enum.value") wire formats, JSON/Text/SQL, and a registry.

Example
package main

import (
	"fmt"

	"github.com/JasonAcar/strenum"
)

func main() {
	// Qualified / "strong" example
	// In real code you'd declare the tag type and spec at package scope.
	// They're local here to keep the example self-contained for pkg.go.dev.
	type PTQTag struct{}
	type ProjectType = strenum.Enum[PTQTag]
	var ProjectTypeSpec = strenum.NewStrongEnum[PTQTag]("ProjectType", "go", "node")

	v := ProjectTypeSpec.Must("go")
	fmt.Println(v)
}
Output:

ProjectType.go

Index

Examples

Constants

View Source
const UnknownType = ""

Variables

This section is empty.

Functions

func RegistryKeys

func RegistryKeys() []reflect.Type

func RegistrySpecNames

func RegistrySpecNames() []string

func TagFields

func TagFields(t reflect.Type) []string

Types

type Enum

type Enum[T any] struct {
	// contains filtered or unexported fields
}

func (Enum[T]) IsValid

func (e Enum[T]) IsValid() bool

func (Enum[T]) MarshalJSON

func (e Enum[T]) MarshalJSON() ([]byte, error)

func (Enum[T]) MarshalText

func (e Enum[T]) MarshalText() ([]byte, error)

func (Enum[T]) Qualified

func (e Enum[T]) Qualified() string

func (Enum[T]) Raw

func (e Enum[T]) Raw() string

func (*Enum[T]) Scan

func (e *Enum[T]) Scan(src any) error

func (Enum[T]) String

func (e Enum[T]) String() string

func (*Enum[T]) UnmarshalJSON

func (e *Enum[T]) UnmarshalJSON(b []byte) error
Example (Qualified)
package main

import (
	"encoding/json"
	"fmt"

	"github.com/JasonAcar/strenum"
)

func main() {
	// Qualified / "strong" example
	// In real code you'd declare the tag type and spec at package scope.
	// They're local here to keep the example self-contained for pkg.go.dev.
	type PTQTag struct{}
	type ProjectType = strenum.Enum[PTQTag]
	var ProjectTypeSpec = strenum.NewStrongEnum[PTQTag]("ProjectType", "go", "node")
	_ = ProjectTypeSpec // to avoid unused error
	type S struct {
		T ProjectType `json:"t"`
	}
	var s S
	_ = json.Unmarshal([]byte(`{"t": "ProjectType.go"}`), &s)
	fmt.Println(s.T)
}
Output:

ProjectType.go
Example (Raw)
package main

import (
	"encoding/json"
	"fmt"

	"github.com/JasonAcar/strenum"
)

func main() {
	// raw example
	// In real code you'd declare the tag type and spec at package scope.
	// They're local here to keep the example self-contained for pkg.go.dev.
	type MSRawTag struct{}
	type MessageStatus = strenum.Enum[MSRawTag]
	var MessageStatusSpec = strenum.NewEnum[MSRawTag]("MessageStatus", "queued", "pending", "delivered", "failed")
	_ = MessageStatusSpec // to avoid unused error
	type M struct {
		Status MessageStatus `json:"status"`
	}
	var m M
	_ = json.Unmarshal([]byte(`{"status": "queued"}`), &m)
	fmt.Println(m.Status)
}
Output:

queued

func (*Enum[T]) UnmarshalText

func (e *Enum[T]) UnmarshalText(b []byte) error

func (Enum[T]) Value

func (e Enum[T]) Value() (driver.Value, error)

type RegistryEntry

type RegistryEntry struct {
	Tag       reflect.Type
	TagShort  string // e.g. "ProjectTypeTag"
	TagFull   string // e.g. "github.com/me/strenum/generic.ProjectTypeTag"
	SpecName  string // e.g. "ProjectType"
	Values    []string
	Qualified bool // if allTypedValues is present and values over-the-wire are "Enum.value"
}

func RegistryEntries

func RegistryEntries() []RegistryEntry

func (RegistryEntry) String

func (e RegistryEntry) String() string

type Spec

type Spec[T any] struct {
	Name string
	// contains filtered or unexported fields
}

func NewEnum

func NewEnum[T any](name string, values ...string) *Spec[T]

func NewStrongEnum

func NewStrongEnum[T any](name string, values ...string) *Spec[T]

func (*Spec[T]) All

func (sp *Spec[T]) All() []Enum[T]

func (*Spec[T]) AllStrings

func (sp *Spec[T]) AllStrings() []string

func (*Spec[T]) Contains

func (sp *Spec[T]) Contains(s string) bool

func (*Spec[T]) Must

func (sp *Spec[T]) Must(s string) Enum[T]

func (*Spec[T]) Parse

func (sp *Spec[T]) Parse(s string) (Enum[T], error)
Example (Qualified)
package main

import (
	"fmt"

	"github.com/JasonAcar/strenum"
)

func main() {
	// Qualified / "strong" example
	// In real code you'd declare the tag type and spec at package scope.
	// They're local here to keep the example self-contained for pkg.go.dev.
	type PTQTag struct{}
	type ProjectType = strenum.Enum[PTQTag]
	var ProjectTypeSpec = strenum.NewStrongEnum[PTQTag]("ProjectType", "go", "node")
	v, _ := ProjectTypeSpec.Parse("ProjectType.go")
	fmt.Println(v)
}
Output:

ProjectType.go
Example (Raw)
package main

import (
	"fmt"

	"github.com/JasonAcar/strenum"
)

func main() {
	// raw example
	// In real code you'd declare the tag type and spec at package scope.
	// They're local here to keep the example self-contained for pkg.go.dev.
	type MSRawTag struct{}
	type MessageStatus = strenum.Enum[MSRawTag]
	var MessageStatusSpec = strenum.NewEnum[MSRawTag]("MessageStatus", "queued", "pending", "delivered", "failed")
	v, _ := MessageStatusSpec.Parse("delivered")
	fmt.Println(v)
}
Output:

delivered

func (Spec[T]) String

func (sp Spec[T]) String() string

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL