enum

package module
v0.0.0-...-99ca27b Latest Latest
Warning

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

Go to latest
Published: Jun 14, 2026 License: GPL-2.0 Imports: 9 Imported by: 0

README

🌊 Go-enums

Universal, generics-based enum library for Go.

Go has no built-in enums.

The usual workaround — type Foo string + a const (...) block + hand-written switch/validation boilerplate — is repetitive and easy to get wrong.

So go-enums turns that pattern into a small, reusable registry built once via a constructor (a map, neverreflect), and offers two ergonomic styles on top of it.

  • Module: github.com/OlenEnkeli/go-enums (package enum)
  • Requires: Go 1.24+

🎯️ Install

go get github.com/OlenEnkeli/go-enums
import enum "github.com/OlenEnkeli/go-enums"

🍏️ Two styles, one core

Style Per-type code Boundary validation Underlying types
🏔️ StrictEnum ✍🏻 ~5 one-line methods yesScan/Value/JSON reject ~string, signed ~int…~int64
🦄 AutoEnum ✅ none (zero boilerplate) 🚫 no — validate explicitly via Enum any comparable

Pick StrictEnum when you want the type to police itself at the SQL/JSON boundary.

Pick AutoEnum when you want zero per-type code and are happy to validate explicitly.

🏔️ StrictEnum — validating delegates

The enum type stays a bare ~string (or signed integer), so JSON and SQL are native.

Each method is a one-line delegate to a reflect-free helper that validates membership in both directions:

  • Unknown values are rejected on the way in (Scan, UnmarshalJSON)
  • And on the way out (Value,MarshalJSON).
type OperationStatus string

const (
	StatusNew        OperationStatus = "NEW"
	StatusProcessing OperationStatus = "PROCESSING"
	StatusDone       OperationStatus = "DONE"
)

// The registry of allowed values, built once.
var operationStatusSet = enum.NewSet(StatusNew, StatusProcessing, StatusDone)

func (status OperationStatus) IsValid() bool {
	return operationStatusSet.Has(status)
}

func (status *OperationStatus) Scan(src any) error {
	return enum.ScanString(operationStatusSet, status, src)
}

func (status OperationStatus) Value() (driver.Value, error) {
	return enum.ValueString(operationStatusSet, status)
}

func (status OperationStatus) MarshalJSON() ([]byte, error) {
	return enum.MarshalJSONString(operationStatusSet, status)
}

func (status *OperationStatus) UnmarshalJSON(data []byte) error {
	return enum.UnmarshalJSONString(operationStatusSet, status, data)
}
var status OperationStatus

_ = status.Scan("PROCESSING")          // status == StatusProcessing
err := status.Scan("BOGUS")            // err wraps enum.ErrInvalidValue — rejected
ok := StatusDone.IsValid()             // true

for _, value := range operationStatusSet.Values() { /* ... */ }
🔟 Integer based

For signed integer enums (~int … ~int64, constrained by enum.Signed), use the Int helpers.

They map onto the int64 scalar of driver.Value and JSON numbers, and reject values that do not fit the type or are not members.

type Priority int

const (
	PriorityLow    Priority = 1
	PriorityMedium Priority = 2
	PriorityHigh   Priority = 3
)

var prioritySet = enum.NewSet(PriorityLow, PriorityMedium, PriorityHigh)

func (level Priority) IsValid() bool                  { return prioritySet.Has(level) }
func (level *Priority) Scan(src any) error            { return enum.ScanInt(prioritySet, level, src) }
func (level Priority) Value() (driver.Value, error)   { return enum.ValueInt(prioritySet, level) }
func (level Priority) MarshalJSON() ([]byte, error)   { return enum.MarshalJSONInt(prioritySet, level) }
func (level *Priority) UnmarshalJSON(b []byte) error  { return enum.UnmarshalJSONInt(prioritySet, level, b) }

🦄 AutoEnum — zero boilerplate

Member[T] wraps a value and implements sql.Scanner, driver.Valuer, json.Marshaler and json.Unmarshaler once for every enum.

These methods are native but do not validate membership — by Go's type system a method on a generic Member[T] cannot reach the specific container.

Validate explicitly through the Enum[T] registry (Parse / Contains).

// The alias keeps Member's methods.
type Color = enum.Member[string]

var (
	Red   = enum.Of("red")
	Green = enum.Of("green")
	Blue  = enum.Of("blue")

	// The registry (container) of allowed members.
	Colors = enum.New(Red, Green, Blue)
)
parsed, ok := Colors.Parse("red")   // parsed == Red, ok == true
_, ok = Colors.Parse("pink")        // ok == false
Colors.Contains(Red)                // true
Red.Get()                           // "red"

for member := range Colors.All() { /* iter.Seq[Member[string]] */ }

// Native in SQL/JSON, but Scan/Marshal here do NOT validate membership:
type Row struct{ Favorite Color }   // stored as a plain varchar
data, _ := json.Marshal(Red)        // "red"

Why no self-validating Member?

Boundary self-validation and "one method set for all enums" are mutually exclusive in Go:

Two enums sharing an underlying type are the same Member[string] and cannot be told apart

A distinct named type does not inherit Member's methods.

So AutoEnum trades boundary validation for zero boilerplate;

StrictEnum makes the opposite trade.

✨ API at a glance

  • Set[T comparable] (core registry): NewSet, Has, Values, Len, All() iter.Seq[T], String.
  • String helpers (~string): ScanString, ValueString, MarshalJSONString, UnmarshalJSONString.
  • Integer helpers (Signed = ~int … ~int64): ScanInt, ValueInt, MarshalJSONInt, UnmarshalJSONInt.
  • Member[T comparable]: Of, Get, String, plus native Scan / Value / MarshalJSON / UnmarshalJSON.
  • Enum[T comparable]: New, Parse, Contains, Members, Len, All() iter.Seq[Member[T]].
  • Errors: ErrInvalidValue, ErrNullValue, ErrUnsupportedType (all wrapped; match with errors.Is).

Scan accepts:

  • The natural driver sources (string/[]byte for string enums; int64 or textual []byte/string for integer enums);
  • A nil source yields ErrNullValue
  • Anything else ErrUnsupportedType.

🤔 Examples

Runnable programs live under examples/:

go run ./examples/strictenum   # StrictEnum over a string
go run ./examples/intenum      # StrictEnum over a signed int
go run ./examples/autoenum     # AutoEnum via the Member wrapper

🛠️ Development

The workflow is driven by Task:

The discussions and PRs are highly welcome!

task init          # install pinned golangci-lint and tidy modules (first run)
task lint          # standard linting (format check, go vet) + golangci-lint
task test          # go test -race -cover ./...
task check         # full gate: lint + test + build
task run-examples  # run all example programs

™️ License

See LICENSE.

Documentation

Overview

Package enum turns the usual `type Foo string` + const + hand-written switch boilerplate into a reusable, generics-based registry.

It offers two styles over one shared core:

  • StrictEnum: a bare ~string type with one-line delegate methods that validate at the boundary (ScanString, ValueString, MarshalJSONString, UnmarshalJSONString).
  • AutoEnum: the Member[T] wrapper with zero per-type code and explicit validation via the Enum[T] container.

The set of allowed values is a registry (Set[T]) built once via a constructor, never via reflect.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrInvalidValue is returned when a value is not a registered member of an enum.
	ErrInvalidValue = errors.New("enum: invalid value")
	// ErrNullValue is returned when a NULL/nil source is scanned into a non-pointer enum.
	ErrNullValue = errors.New("enum: null value")
	// ErrUnsupportedType is returned when a source type cannot be converted to the enum value.
	ErrUnsupportedType = errors.New("enum: unsupported type")
)

Functions

func MarshalJSONInt

func MarshalJSONInt[T Signed](set *Set[T], value T) ([]byte, error)

MarshalJSONInt implements json.Marshaler for a signed integer enum type and validates membership before encoding the value as a JSON number. It marshals the base int64, never the enum-typed value, to avoid recursing into the delegate.

func MarshalJSONString

func MarshalJSONString[T ~string](set *Set[T], value T) ([]byte, error)

MarshalJSONString implements json.Marshaler for a ~string enum type and validates membership before encoding the value as a JSON string.

func ScanInt

func ScanInt[T Signed](set *Set[T], dst *T, src any) error

ScanInt implements sql.Scanner for a signed integer enum type and validates membership. It accepts an int64 source (the canonical driver representation) or a textual []byte/string; nil yields ErrNullValue and any other type yields ErrUnsupportedType.

func ScanString

func ScanString[T ~string](set *Set[T], dst *T, src any) error

ScanString implements sql.Scanner for a ~string enum type and validates membership. It accepts a string or []byte source; nil yields ErrNullValue and any other type yields ErrUnsupportedType.

func UnmarshalJSONInt

func UnmarshalJSONInt[T Signed](set *Set[T], dst *T, data []byte) error

UnmarshalJSONInt implements json.Unmarshaler for a signed integer enum type. It decodes a JSON number into an int64 and validates membership before storing it into dst. Non-integer JSON numbers (e.g. 2.5) are rejected by the decoder.

func UnmarshalJSONString

func UnmarshalJSONString[T ~string](set *Set[T], dst *T, data []byte) error

UnmarshalJSONString implements json.Unmarshaler for a ~string enum type. It decodes a JSON string and validates membership before storing the value into dst.

func ValueInt

func ValueInt[T Signed](set *Set[T], value T) (driver.Value, error)

ValueInt implements driver.Valuer for a signed integer enum type. It validates membership and returns a plain int64, which is a valid driver.Value regardless of how a given driver's converter treats named types.

func ValueString

func ValueString[T ~string](set *Set[T], value T) (driver.Value, error)

ValueString implements driver.Valuer for a ~string enum type. It validates membership and returns a plain string, which is a valid driver.Value regardless of how a given driver's converter treats named types.

Types

type Enum

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

Enum is the registry (container) of allowed Members. It is the explicit-validation counterpart to the non-validating Member methods.

func New

func New[T comparable](members ...Member[T]) *Enum[T]

New builds an Enum from the given members, preserving first-seen order.

func (*Enum[T]) All

func (enumeration *Enum[T]) All() iter.Seq[Member[T]]

All returns an iterator over the registered members in insertion order.

func (*Enum[T]) Contains

func (enumeration *Enum[T]) Contains(member Member[T]) bool

Contains reports whether member is registered.

func (*Enum[T]) Len

func (enumeration *Enum[T]) Len() int

Len returns the number of registered members.

func (*Enum[T]) Members

func (enumeration *Enum[T]) Members() []Member[T]

Members returns the registered members in insertion order. The result is a fresh copy.

func (*Enum[T]) Parse

func (enumeration *Enum[T]) Parse(value T) (Member[T], bool)

Parse returns the registered Member for the raw value. The bool reports whether value is a member; on false the zero Member is returned.

type Member

type Member[T comparable] struct {
	// contains filtered or unexported fields
}

Member is a generic enum member wrapping a single comparable value of type T.

It implements sql.Scanner, driver.Valuer, json.Marshaler and json.Unmarshaler once for every enum, so member types need zero per-type code. By Go's type system these methods cannot reach the specific Enum container, so they do NOT validate membership. Validate explicitly via Enum.Parse / Enum.Contains.

func Of

func Of[T comparable](value T) Member[T]

Of constructs a Member holding value. T is normally inferred from value.

func (Member[T]) Get

func (member Member[T]) Get() T

Get returns the underlying value.

func (Member[T]) MarshalJSON

func (member Member[T]) MarshalJSON() ([]byte, error)

MarshalJSON encodes the underlying value as JSON. It is reflect-free: encoding/json handles T directly. Membership is not validated here.

func (*Member[T]) Scan

func (member *Member[T]) Scan(src any) error

Scan implements sql.Scanner via the reflect scalar bridge. A nil source yields ErrNullValue. Membership is not validated here.

func (Member[T]) String

func (member Member[T]) String() string

String returns the underlying value formatted with the %v verb.

func (*Member[T]) UnmarshalJSON

func (member *Member[T]) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes JSON into the underlying value. It is reflect-free and does not validate membership.

func (Member[T]) Value

func (member Member[T]) Value() (driver.Value, error)

Value implements driver.Valuer via the reflect scalar bridge. Membership is not validated here.

type Set

type Set[T comparable] struct {
	// contains filtered or unexported fields
}

Set is a reflect-free registry of allowed enum values for any comparable type T.

It is the core shared by both enum styles (StrictEnum and AutoEnum): a membership check plus deterministic enumeration. The registry is built once via NewSet and is read-only afterwards, so it is safe for concurrent reads.

func NewSet

func NewSet[T comparable](values ...T) *Set[T]

NewSet builds a Set from the given values. Duplicates are stored once and the first-seen insertion order is preserved for Values, All and String.

func (*Set[T]) All

func (set *Set[T]) All() iter.Seq[T]

All returns an iterator over the registered values in insertion order.

func (*Set[T]) Has

func (set *Set[T]) Has(value T) bool

Has reports whether value is a registered member of the Set.

func (*Set[T]) Len

func (set *Set[T]) Len() int

Len returns the number of registered values.

func (*Set[T]) String

func (set *Set[T]) String() string

String returns a human-readable representation of the Set, e.g. "Set[a, b, c]".

func (*Set[T]) Values

func (set *Set[T]) Values() []T

Values returns the registered values in insertion order. The result is a fresh copy, so callers may modify it without affecting the Set.

type Signed

type Signed interface {
	~int | ~int8 | ~int16 | ~int32 | ~int64
}

Signed is the constraint for the integer underlying kinds supported by the integer StrictEnum helpers.

Directories

Path Synopsis
examples
autoenum command
Command autoenum demonstrates the AutoEnum style of go-enums: zero per-type code via the Member wrapper, with explicit validation through the Enum container.
Command autoenum demonstrates the AutoEnum style of go-enums: zero per-type code via the Member wrapper, with explicit validation through the Enum container.
intenum command
Command intenum demonstrates the StrictEnum style over a signed integer type: a bare int whose one-line delegate methods validate every value at the boundary.
Command intenum demonstrates the StrictEnum style over a signed integer type: a bare int whose one-line delegate methods validate every value at the boundary.
strictenum command
Command strictenum demonstrates the StrictEnum style of go-enums: a bare string type whose one-line delegate methods validate every value at the boundary.
Command strictenum demonstrates the StrictEnum style of go-enums: a bare string type whose one-line delegate methods validate every value at the boundary.

Jump to

Keyboard shortcuts

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