enum

package module
v0.0.0-...-08ef443 Latest Latest
Warning

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

Go to latest
Published: Apr 15, 2025 License: BSD-2-Clause Imports: 5 Imported by: 14

README

enum

Go Reference

Package enum provides helpers to enable enumerable interfaces for Go. Sometimes, exhaustive checks and enumeration are required to simplify code and let this fact be checked by the compiler. In general, due to the way, the Go type system has been designed, this is not possible. However, for non-general cases, this can be done under specific conditions with limitations.

For further discussions and backgrounds see 5468 et al.

Our approach is novel in a way, that we don't use code generation or pre-created tuples or type parameters. Instead, it combines a functional approach and reflection to delegate the checks either to the compiler or to the runtime at package initialization time. The actual matching logic is also implemented using reflection.

To allow seamless interoperability with JSON encoding and decoding, we forked the stdlib json package to directly support the enumerable interface types.

sealed example

//...

import(
    "github.com/worldiety/enum"
    "github.com/worldiety/enum/json"
)

// Note, that AcceptedOffer and UncheckOffer must be assignable to Offer, which is checked at 
// package initialization time. Offer must not be the empty interface.
var OfferEnum = enum.Declare[Offer, func(func(AcceptedOffer), func(UncheckOffer), func(any))](
	// options over options, inspired by and compatible with https://serde.rs/enum-representations.html
	enum.Rename[AcceptedOffer]("aof"),    // provide custom names
	enum.Adjacently("t", "c"),            // default is externally tagged, like serde
	//enum.NoZero(),                      // panic, if switch finds a zero-interface, you can omit the func(any) branch
	enum.Sealed(),                        // do not accept future Variant declaration (see second example below)
)

// ...

func someFn(){
    var offer Offer
	
	// now we can use the type enum switch func which has been implemented by reflection above.
	// if the enum is changed, this will fail at compile-time, and we can be sure to be
	// exhaustive with respect to our declaration. There is still nil and arbitrary other types,
	// but we can express that these types are essential for our domain and each case has been handled.
	OfferEnum.Switch(offer)(
        func(offer AcceptedOffer) {
            fmt.Printf("acceppted offer: %v\n", offer)
		}, 
		func(offer UncheckOffer) {
            fmt.Printf("unchecked offer: %v\n", offer)
        }, 
		func(a any) {
            fmt.Printf("any offer: %v %T\n", offer, offer)
        },
	)
	
	// ...
	// encode/decode as usual, but note the different import
    buf, err := json.Marshal(&offer)
    if err != nil {
        t.Fatal(err)
    }
    
    fmt.Println(string(buf))
}

open type example

Sometimes, you just want to define a base interface in a supporting package, which others need to extend, which the supporting package needs to inspect. We can model this situation as follows:

//...

import(
    "github.com/worldiety/enum"
)

type Credentials interface {
    GetName() string
    Credentials() bool // open sum type which can be extended by anyone
    IsZero() bool
}

// declaring the variants associates the concrete types with the interface type
var (
    _ = enum.Variant[secret.Credentials, secret.Jira]()
    _ = enum.Variant[secret.Credentials, secret.BookStack]()
)

// you can even inspect the declared variants at runtime.
// note, that this cannot be exhaustive, but we find that to be reasonable enough
func someFn(){
    decl, ok := enum.DeclarationFor[secret.Credentials]()
    if !ok {
        panic("unreachable: secret.Credentials declaration not defined")
    }
    
    for rtype := range decl.Variants() {
		//...
    }
}

Documentation

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Variant

func Variant[Interface any, Member any](opts ...Option) any

Variant adds or updates another type variant to an Enum declaration. If no such Enum Declaration exists, a new one is created. Panics, if the Enum is sealed. Also, when adding a new variant, all global settings may be changed. This is intentional, e.g. to introduce future workarounds. Currently, always nil is returned, to allow simple execution at global variable declaration time.

Example
package main

import (
	"fmt"
	"github.com/worldiety/enum"
)

func main() {
	type Pet interface {
		Eat()
		Sleep()
	}

	type Dog struct {
		TaxNumber int
		Pet
	}

	type Cat struct {
		Name string
		Pet
	}

	// usually declare it at package level
	// it is only for illustration here
	var _ = enum.Variant[Pet, Dog]()
	var _ = enum.Variant[Pet, Cat]()

	decl, ok := enum.DeclarationFor[Pet]()
	if !ok {
		panic("unreachable in this example")
	}

	for variant := range decl.Variants() {
		fmt.Printf("pet type: %s\n", variant.Name())
	}
}
Output:

pet type: Dog
pet type: Cat

Types

type AdjacentlyOptions

type AdjacentlyOptions struct {
	Tag     string // name of the tag key in json
	Content string // name of the content key in json
}

type Declaration

type Declaration struct {
	// contains filtered or unexported fields
}

func DeclarationFor

func DeclarationFor[Interface any]() (Declaration, bool)

func DeclarationOf

func DeclarationOf(t reflect.Type) (Declaration, bool)

func (Declaration) EnumType

func (d Declaration) EnumType() reflect.Type

func (Declaration) JSON

func (d Declaration) JSON() JSON

func (Declaration) Name

func (d Declaration) Name(t reflect.Type) (string, bool)

func (Declaration) NoZero

func (d Declaration) NoZero() bool

func (Declaration) Type

func (d Declaration) Type(name string) (reflect.Type, bool)

func (Declaration) Variants

func (d Declaration) Variants() iter.Seq[reflect.Type]

type Enumeration

type Enumeration[Interface any, MatchFn any] struct {
	// contains filtered or unexported fields
}

Enumeration represents a union type declaration and enumerates the declared subtypes for a distinct interface type.

func Declare

func Declare[Interface any, MatchFn any](opts ...Option) Enumeration[Interface, MatchFn]

Declare specifies at runtime a sum type based on a (marker) interface type. The actual members are defined through a (anonymous) function type, whose callbacks parameters define the allowed member types. This declaration must ever occur once per Interface type and as early as possible, thus probably at package level. Afterward, the Enumeration can be used for an exhaustive type switch and the interface type can transparently be used with the included json encoder and decoder package.

MatchFn must look like

func(func(TypA), func(TypB), func(TypC))
Example
package main

import (
	"fmt"
	"github.com/worldiety/enum"
)

func main() {
	type Pet interface {
		Eat()
		Sleep()
	}

	type Dog struct {
		TaxNumber int
		Pet
	}

	type Cat struct {
		Name string
		Pet
	}

	// usually declare it at package level
	// it is only for illustration here
	var PetEnum = enum.Declare[Pet, func(func(Dog), func(Cat), func(any))]()

	var myPet Pet
	myPet = Cat{Name: "Simba"}

	PetEnum.Switch(myPet)(
		func(dog Dog) {
			fmt.Printf("pay tax: %v\n", dog.TaxNumber)
		},
		func(cat Cat) {
			fmt.Printf("clean litterbox: %v\n", cat.Name)
		},
		func(a any) {
			if a != nil {
				fmt.Printf("remove vermin: %v\n", a)
			}
		},
	)
}
Output:

clean litterbox: Simba

func (Enumeration[Interface, MatchFn]) IsZero

func (b Enumeration[Interface, MatchFn]) IsZero(value Interface) bool

func (Enumeration) NoZero

func (c Enumeration) NoZero() bool

func (Enumeration[Interface, MatchFn]) Ordinal

func (b Enumeration[Interface, MatchFn]) Ordinal(value Interface) int

Ordinal returns either the zero based index of the declared types by MatchFn or -1. If the zero value has not been allowed in the declaration, it panics. If sealed and a non-exact type has been found, it panics and otherwise returns -1.

func (Enumeration) Sealed

func (c Enumeration) Sealed() bool

func (Enumeration[Interface, MatchFn]) Switch

func (b Enumeration[Interface, MatchFn]) Switch(value Interface) MatchFn

func (Enumeration[Interface, MatchFn]) Types

func (b Enumeration[Interface, MatchFn]) Types() iter.Seq[reflect.Type]

type ExternallyOptions

type ExternallyOptions struct {
}

type InternallyOptions

type InternallyOptions struct {
	Tag string // name of the tag key in json
}

type JSON

type JSON interface {
	// contains filtered or unexported methods
}

type Option

type Option interface {
	// contains filtered or unexported methods
}

func Adjacently

func Adjacently(tagName string, contentName string) Option
Example
package main

import (
	"fmt"
	"github.com/worldiety/enum"
	"github.com/worldiety/enum/json"
)

func main() {
	type Pet interface {
		Eat()
		Sleep()
	}

	type Dog struct {
		TaxNumber int
		Pet
	}

	type Cat struct {
		Name string
		Pet
	}

	// usually declare it at package level
	// it is only for illustration here
	var _ = enum.Variant[Pet, Dog](
		enum.Adjacently("kind", "obj"),
	)
	var _ = enum.Variant[Pet, Cat]()

	var myPet Pet
	myPet = Cat{Name: "Simba"}
	// Note: to not lose the interface information, you MUST provide the pointer
	// to the interface variable, otherwise the type itself is marshalled.
	buf, err := json.Marshal(&myPet)
	if err != nil {
		panic(fmt.Errorf("unreachable in this example: %w", err))
	}

	fmt.Println(string(buf))

	var pet2 Pet
	if err := json.Unmarshal(buf, &pet2); err != nil {
		panic(fmt.Errorf("unreachable in this example: %w", err))
	}

	if pet2 != myPet {
		panic(fmt.Errorf("unreachable in this example: %w", err))
	}
}
Output:

{"kind":"Cat","obj":{"Name":"Simba","Pet":null}}

func Externally

func Externally() Option

Externally is the default

Example
package main

import (
	"fmt"
	"github.com/worldiety/enum"
	"github.com/worldiety/enum/json"
)

func main() {
	type Pet interface {
		Eat()
		Sleep()
	}

	type Dog struct {
		TaxNumber int
		Pet
	}

	type Cat struct {
		Name string
		Pet
	}

	// usually declare it at package level
	// it is only for illustration here
	var _ = enum.Variant[Pet, Dog](
		// this is the default and can be omitted
		enum.Externally(),
	)
	var _ = enum.Variant[Pet, Cat]()

	var myPet Pet
	myPet = Cat{Name: "Simba"}
	// Note: to not lose the interface information, you MUST provide the pointer
	// to the interface variable, otherwise the type itself is marshalled.
	buf, err := json.Marshal(&myPet)
	if err != nil {
		panic(fmt.Errorf("unreachable in this example: %w", err))
	}

	fmt.Println(string(buf))

	var pet2 Pet
	if err := json.Unmarshal(buf, &pet2); err != nil {
		panic(fmt.Errorf("unreachable in this example: %w", err))
	}

	if pet2 != myPet {
		panic(fmt.Errorf("unreachable in this example: %w", err))
	}
}
Output:

{"Cat":{"Name":"Simba","Pet":null}}

func Internally

func Internally(tagName string) Option
Example
package main

import (
	"fmt"
	"github.com/worldiety/enum"
	"github.com/worldiety/enum/json"
)

func main() {
	type Pet interface {
		Eat()
		Sleep()
	}

	type Dog struct {
		TaxNumber int
		Pet
	}

	type Cat struct {
		Name string
		Pet
	}

	// usually declare it at package level
	// it is only for illustration here
	var _ = enum.Variant[Pet, Dog](
		enum.Internally("type"),
	)
	var _ = enum.Variant[Pet, Cat]()

	var myPet Pet
	myPet = Cat{Name: "Simba"}
	// Note: to not lose the interface information, you MUST provide the pointer
	// to the interface variable, otherwise the type itself is marshalled.
	buf, err := json.Marshal(&myPet)
	if err != nil {
		panic(fmt.Errorf("unreachable in this example: %w", err))
	}

	fmt.Println(string(buf))

	var pet2 Pet
	if err := json.Unmarshal(buf, &pet2); err != nil {
		panic(fmt.Errorf("unreachable in this example: %w", err))
	}

	if pet2 != myPet {
		panic(fmt.Errorf("unreachable in this example: %w", err))
	}
}
Output:

{"Name":"Simba","Pet":null,"type":"Cat"}

func NoZero

func NoZero() Option
Example
package main

import (
	"fmt"
	"github.com/worldiety/enum"
)

func main() {
	type Pet interface {
		Eat()
		Sleep()
	}

	type Dog struct {
		TaxNumber int
		Pet
	}

	type Cat struct {
		Name string
		Pet
	}

	// usually declare it at package level
	// it is only for illustration here
	var PetEnum = enum.Declare[Pet, func(func(Dog), func(Cat))](
		enum.NoZero(),
	)

	var myPet Pet
	myPet = Dog{TaxNumber: 42}

	PetEnum.Switch(myPet)(
		func(dog Dog) {
			fmt.Printf("pay tax: %v\n", dog.TaxNumber)
		},
		func(cat Cat) {
			fmt.Printf("clean litterbox: %v\n", cat.Name)
		},
	)
}
Output:

pay tax: 42

func Rename

func Rename[T any](name string) Option

func Sealed

func Sealed() Option

func Untagged

func Untagged() Option

type UntaggedOptions

type UntaggedOptions struct {
}

Directories

Path Synopsis
Package json implements encoding and decoding of JSON as defined in RFC 7159.
Package json implements encoding and decoding of JSON as defined in RFC 7159.

Jump to

Keyboard shortcuts

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