renum

package module
v1.0.1 Latest Latest
Warning

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

Go to latest
Published: Jul 26, 2023 License: GPL-3.0 Imports: 9 Imported by: 0

README

renum - strongly typed Go enums

Overview GoDoc Sourcegraph

Go package that provides a rich, descriptive interface for developers to use in order to allow enums to cross package boundries without loosing important details and metadata.

Also a CLI utility to generate idiomatic Go enums with a diverse set of features and options (that allow you to easily satisfy the renum interface ^.^)

NOTE: This library is in it's early stage, so I wouldn't call it production stable yet. But any PRs and comments are welcome!

Background

Go's language, while expressive, has shortcomings around propogation of type information with commonly used code. A great example of this is the error interface that's built into the language.

While Go lets you define custom error types (*os.PathError is one example), generally developers end up simply using the standard errors.New to generate a type that satisfies error, but is basically a string containing whatever you passed to New().

As Go (rightly) attempts to force you to handle your errors, it often involves passing errors around, with the expectation that the caller likely wants to make decisions about what to do. This is an incredibly powerful paradigm, and why I fully support not including constructs like exception handling into the runtime.

An example of this occurred for myself recently with the github.com/masterzen/winrm package. I was using it to make WinRM connections to a Windows host, but I kept getting an error relating to response header timeouts. Is this happening within winrm or net/http or net? The only way to answer that question was to print the error to the console and begin grepping through source trees, looking for string literals that use those words.

While generically this system is cheap, efficient, and allows broad adoption - it begins to age when working with large, complex codebases where error propagation becomes a lot of manual logging of error messages, with human review consuming considerable time. Forgetting how much return nil, errors.New("this is bad") you see, typically this is the "idiomatic" way to define error types in Go:

var (
  // ErrUnauthorized is thrown when a request is not authorized to perform a function.
  ErrUnauthorized = errors.New("request unauthorized")

  // ErrInvalidSQLQuery is thrown when the provided SQL query was not a valid SQL expression.
  ErrInvalidSQLQuery = errors.New("invalid sql query")
)

This creats code that is easy to read and now is comparable (even type comparable), hear me out. Imagine a situation where this is printed to a log. You'd see a message of "requested unauthorized". What happens though when another package does this:

return nil, errors.New("request unauthorized")
Solution

renum aims to solve this by allow users define "constant" (types that don't change after compilation) types that conform to a more machine friendly and descriptive interface. While errors are a great use case, they certainly aren't the only paradigm where this benefits. The interface aims to push users not to write these type definitions by hand, but to generate them with codegen. You certainly could write a type that satisfies renum.Enum or renum.Error, but after seeing how easy it is to generate, I think you'll gladly let the renum utility do the heavy lifting 😃

Simply define your types in in YAML:

# Enum configuration
go:
  name: ErrorCode
  package_name: lib
  package_path: github.com/gen0cide/renum/example/lib
plugins:
  error: true
  text: true
  json: true
  yaml: true
  sql: true
  description: true
values:
  - name: unauthorized
    message: The request was unauthorized.
    comment: Unauthorized is thrown when the request action cannot be taken.
    description: This error is used to signify that the request was made by an *authenticated* requester, but that requester is not authorized to perform the requested action.
  - name: invalid_sql_query
    message: The provided query was not valid SQL.
    comment: InvalidSQLQuery is thrown when a user supplied SQL query is not valid.
    description: This error often means the caller should perform further validation in order to locate situations where they're taking unsanitized input from users and interpolating that value directly into the SQL query.


and use the renum generate command in order to codegen a much better error paradigm:

$ renum -c error_code.yaml generate -o .
[✓] parsed configuration
[✓] initialized generator
[✓] generated Go code
[✓] successfully wrote code to generated_error_codes.go
$

And if you opened up generated_error_codes.go, you'd see something that looks like this:

// ErrorCode is a generated type alias for the ErrorCode enum.
type ErrorCode int

const (
  // ErrorCodeUnknown is an enum value for type ErrorCode.
  // ErrorCodeUnknown is the default value for enum type ErrorCode. It is meant to be a placeholder and default for unknown values.
  // This value is a default placeholder for any unknown type for the lib.ErrorCode enum.
  ErrorCodeUnknown ErrorCode = iota

  // ErrorCodeUnauthorized is an enum value for type ErrorCode.
  // Unauthorized is thrown when the request action cannot be taken.
  // This error is used to signify that the request was made by an *authenticated* requester, but that requester is not authorized to perform the requested action.
  ErrorCodeUnauthorized

  // ErrorCodeInvalidSQLQuery is an enum value for type ErrorCode.
  // InvalidSQLQuery is thrown when a user supplied SQL query is not valid.
  // This error often means the caller should perform further validation in order to locate situations where they're taking unsanitized input from users and interpolating that value directly into the SQL query.
  ErrorCodeInvalidSQLQuery

// ... more code below

To demonstrate how this is now a much richer error interface, I've created a small example program that shows how this now looks to the human eye. I've pasted the output of the example program to demonstrate what features you now have:

$ go run main.go
[+] renum.Coder interface
[✓] Code() = 2

[+] renum.Namespacer interface
[✓] Namespace() = github.com.gen0cide.renum.cmd.renum.example.lib
[✓]      Path() = github.com.gen0cide.renum.cmd.renum.example.lib.error_code_invalid_sql_query

[+] renum.Typer interface
[✓]        Kind() = lib.ErrorCodeInvalidSQLQuery
[✓]      Source() = github.com/gen0cide/renum/cmd/renum/example/lib.ErrorCodeInvalidSQLQuery
[✓] PackageName() = lib
[✓]  ImportPath() = github.com/gen0cide/renum/cmd/renum/example/lib

[+] renum.Descriptioner interface
[✓] Description() = This error often means the caller should perform further validation in order to locate situations where they're taking unsanitized input from users and interpolating that value directly into the SQL query.

[+] fmt.Stringer interface
[✓] String() = invalid_sql_query

[+] error interface
[✓] Error() = github.com.gen0cide.renum.cmd.renum.example.lib.error_code_invalid_sql_query (2): The provided query was not valid SQL.
$

We've effectively created a situation where errors are isolated into their namespace - they have identity, lineage, descriptive information, and satisfy the interface correctly. And of course, great Godocs. This is the power of strongly typed enums in Go.

Generating Enums w/ CLI

To install the CLI:

go get github.com/gen0cide/renum/cmd/renum
YAML Configuration Format

An example and YAML configuration file outlining all options can be found in the config_spec.yaml file inside this repo.

Example

Below shows an example of how you can write your enums in YAML, then with the renum codegen tool, generate your Go code. The example files can be found in the examples folder of the CLI.

# write your YAML configuration file...
$ renum -c error_code.yaml generate
[✓] parsed configuration
[✓] initialized generator
[✓] generated Go code
[✓] successfully wrote code to generated_error_codes.go
$

Library

To use the interfaces, simply install the library with:

go get github.com/gen0cide/renum

Inspiration / Prior Works

Both go-enum and enumer do very similar things, but have some shortcomings. Both of them rely on AST parsing - meaning if your code is not parsable due to errors, you cannot generate your enums. Secondly, they don't provide easy mechanisms to enrich your types with additional methods and functionality.

This project started as a fork of go-enum, but ended up on it's own trajectory given the number of interfaces I wanted the enums to be able to implement. Their work is great and I think they have their uses, but were too limited to implement the strict paradigm of renum.

Author

Shoutouts

  • mbm
  • davehughes
  • ychen
  • emperorcow
  • m0
  • vyrus001
  • hecfblog

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	// Version describes the version of the library.
	Version = `1.0.1`

	// Build describes the git revision for this build.
	// Getting read of this.
	Build = ``
)

Functions

func IsErr

func IsErr(err error) bool

IsErr checks to see if an error is either a renum.Error or a renum.Wrapped type.

func IsErrorUndefinedEnum

func IsErrorUndefinedEnum(err error) bool

IsErrorUndefinedEnum is used to check if an error is because an enum value was undefined.

func IsUndefined

func IsUndefined(e Enum) bool

IsUndefined is used to check if an enum value is undefined.

func VersionString

func VersionString() string

VersionString returns the renum library version, using semantic versioning (https://semver.org) decorating the Version with the Build if a build is provided.

Types

type Caser

type Caser interface {
	// SnakeCase should return enum names formatted as "snake_case" representations
	SnakeCase() string

	// PascalCase should return enum names formatted as "PascalCase" representations
	PascalCase() string

	// CamelCase should return enum names formatted as "camelCase" representations
	CamelCase() string

	// ScreamingCase should return enum names formatted as "SCREAMING_CASE" representations
	ScreamingCase() string

	// CommandCase should return enum names formatted as "command-case" representations
	CommandCase() string

	// TrainCase should return enum names formatted as "TRAIN-CASE" representations
	TrainCase() string

	// DottedCase should renum enum names formatted as "dotted.case" representations
	DottedCase() string
}

Caser defines an interface for types (specifically enums) to be able to describe their name typically returned by the fmt.Stringer interface as various text casings. This is helpful in situations where there are character restrictions that are enforced.

type Coder

type Coder interface {
	Code() int
}

Coder allows an enum to retrieve it's builtin underlying numeric value.

type Descriptioner

type Descriptioner interface {
	Description() string
}

Descriptioner allows types to describe themselves in more detail when asked, without having to embed this information in otherwise unstructured ways like fmt.Errorf("foo info: %v", err).

type EmbeddedError

type EmbeddedError Error

EmbeddedError is a type alias for renum.Error that allows the renum.Wrapped interface to directly embed the EmbeddedError into Wrapped without conflicting with the Go built-in error interface's Error() string method.

type Enum

type Enum interface {
	// Coder requires enums to be able to describe their underlying integer representation.
	Coder

	// Namespacer requires enums to be uniquely identifiable with namespace and path values.
	Namespacer

	// Sourcer requires enums to be able to self describe aspects of the Go source and package
	// which they're located. This makes Enum's great for tracing and error handling.
	Sourcer

	// Typer requires enums to describe their type.
	Typer

	// Descriptioner requires enums to describe themselves in detail, upon request.
	Descriptioner

	// Caser requires enums to describe their names in multiple
	// string case semantics.
	Caser

	// Stringer implements fmt.Print handling
	fmt.Stringer

	// Marshaler requires that enums be able to support encoding/decoding for
	// a variety of common formats. The expectation is that if your Enum implements
	// Marshaler, that it will also implement the pointer recievers for Unmarshaler.
	// If you're generating your Enum with the Renum CLI, this will happen
	// automatically for you.
	Marshaler
}

Enum forms the basis for a strongly typed enum class that allows for good cross-package interoperability. This creates enums that play nice with things like loggers and metrics emitters.

type EnumTypeInfo

type EnumTypeInfo struct {
	Name    string      `json:"name,omitempty" mapstructure:"name,omitempty" yaml:"name,omitempty" toml:"name,omitempty"`
	Code    int         `json:"code,omitempty" mapstructure:"code,omitempty" yaml:"code,omitempty" toml:"code,omitempty"`
	Details TypeDetails `json:"details,omitempty" mapstructure:"details,omitempty" yaml:"details,omitempty" toml:"details,omitempty"`
}

EnumTypeInfo is a type used to hold all the metadata associated with a given renum.Enum where the fields of this structure are associated directly with the return values from the renum.Enum interface. This acts as a convenience to help things like structured loggers or HTTP JSON responses to be have information extracted into a self contained object.

func ExtractEnumTypeInfo

func ExtractEnumTypeInfo(e Enum) EnumTypeInfo

ExtractEnumTypeInfo is used to take a renum.Enum type and expand it's details into a more annotated structure. The primary purpose of this is to act as a helper to loggers who wish to expand interface methods of the renum.Enum type into a nested, flat structure.

type Error

type Error interface {
	Enum
	error
	Message() string
}

Error allows types to conform to a strongly defined interface, as well as act as enriched error builtins. The point of this is that as types that satisfy Error pass across package boundry, context and metadata is not lost.

func ToError

func ToError(err error) (Error, bool)

ToError attempts to extract a renum.Error type out of an error. That error can either be of type renum.Error or renum.Wrapped.

type ErrorTypeInfo

type ErrorTypeInfo struct {
	Name    string      `json:"name,omitempty" mapstructure:"name,omitempty" yaml:"name,omitempty" toml:"name,omitempty"`
	Code    int         `json:"code,omitempty" mapstructure:"code,omitempty" yaml:"code,omitempty" toml:"code,omitempty"`
	Details TypeDetails `json:"details,omitempty" mapstructure:"details,omitempty" yaml:"details,omitempty" toml:"details,omitempty"`
	Message string      `json:"message,omitempty" mapstructure:"message,omitempty" yaml:"message,omitempty" toml:"message,omitempty"`
}

ErrorTypeInfo is a type used to hold all the metadata associated with a given renum.Error where the fields of this structure are associated directly with the return values from the renum.Error interface. This acts as a convenience to help things like structured loggers or HTTP JSON responses to be have information extracted into a self contained object.

func ExtractErrorTypeInfo

func ExtractErrorTypeInfo(e error) []ErrorTypeInfo

ExtractErrorTypeInfo is used to take a renum.Error type and expand it's details into a more annotated structure. The primary purpose of this is to act as a helper to loggers who wish to expand interface methods of the renum.Error type into a nested, flat structure.

func ExtractErrorsFromYARPCStatus

func ExtractErrorsFromYARPCStatus(status *yarpcerrors.Status) ([]ErrorTypeInfo, bool)

ExtractErrorsFromYARPCStatus is a helper method to read in a YARPC Status has been transmitted across application boundries and attempts to unpack an error stack of the foreign services wrapped errors. Note that this should *not* be used to programmatically type check errors, but rather in presenting the remote error's contexts in ways that are easily formatted for a user.

type FlagUnmarshaler

type FlagUnmarshaler interface {
	String() string
	Set(string) error
	Get() interface{}
	Type() string
}

FlagUnmarshaler is used to enforce that enum types can be properly encoded and decoded into command line flags without custom implementations. This requires renum.Enums to conform to pflag.Value (github.com/spf13/pflag), as well as the standard library flag package.

type HTTPError

type HTTPError interface {
	Error
	HTTPResponder
}

HTTPError extends the Error interface to allow a type to additionally self-report an HTTP Response code in order to enrich a net/http handler's ability to respond with the proper status code when an error of this type is encountered.

type HTTPResponder

type HTTPResponder interface {
	ToHTTP() int
}

HTTPResponder allows a type to define a specified HTTP status code so that a handler can automatically act on it's behalf without having to maintain a separate mapping.

type Marshaler

type Marshaler interface {
	encoding.TextMarshaler
	json.Marshaler
	yaml.Marshaler
	driver.Valuer
}

Marshaler defines the required methods for a renum.Enum type implementation to properly support serialization to various encoding formats (text, json, yaml, sql, flags). This allows for easy integration with existing data structures that are commonly serialized into these formats.

type Namespacer

type Namespacer interface {
	Name() string      // val
	ID() string        // type_val
	Path() string      // github.com.gen0cide.foo.type_val
	Namespace() string // github.com.gen0cide.foo
}

Namespacer requires a type be able to produce information relating to the package or component it's defined by. This allows tracing of errors to propogate across package boundries without loosing the ability to easily identify the owner of a type.

type PointerUnmarshaler

type PointerUnmarshaler interface {
	PointerUnmarshal() Unmarshaler
}

PointerUnmarshaler defines how a concrete non-pointer value can conform to the Unmarshaler contract by returning a pointer to it's value receiver. This interface has no practical use and should not need to be used. It simply exists to allow the Go compiler to ensure compliance with the Unmarshaler interface by renum.Enum types.

type ProcessError

type ProcessError interface {
	Error
	ProcessResponder
}

ProcessError extends the Error interface to allow a type to additionally self-report a specific exit code it wishes the handler to exit the process with.

type ProcessResponder

type ProcessResponder interface {
	ToOSExit() int
}

ProcessResponder allows a type to define the exit code that a process should exit with upon encountering said type. This primarily targets error handling.

type Sourcer

type Sourcer interface {
	PackageName() string // foo
	PackagePath() string // github.com/gen0cide/foo
	ExportType() string  // foo.TypeVal
	ExportRef() string   // github.com/gen0cide/foo.TypeVal
}

Sourcer requires enums to be able to self describe aspects of the Go source and package which they're located. This makes Enum's great for tracing and error handling. This interface allows callers to retrieve additional context of the Enum value without having to take up excess memory space, since the enum is just a type alias to a builtin numeric.

type TypeDetails

type TypeDetails struct {
	Namespace   string `json:"namespace,omitempty" mapstructure:"namespace,omitempty" yaml:"namespace,omitempty" toml:"namespace,omitempty"`
	Path        string `json:"path,omitempty" mapstructure:"path,omitempty" yaml:"path,omitempty" toml:"path,omitempty"`
	Kind        string `json:"kind,omitempty" mapstructure:"kind,omitempty" yaml:"kind,omitempty" toml:"kind,omitempty"`
	Source      string `json:"source,omitempty" mapstructure:"source,omitempty" yaml:"source,omitempty" toml:"source,omitempty"`
	ImportPath  string `json:"import_path,omitempty" mapstructure:"import_path,omitempty" yaml:"import_path,omitempty" toml:"import_path,omitempty"`
	Description string `json:"description,omitempty" mapstructure:"description,omitempty" yaml:"description,omitempty" toml:"description,omitempty"`
}

TypeDetails allows for detailed renum.Enum/renum.Error type information to be embedded in a nested structure for TypeInfo structs.

type Typer

type Typer interface {
	Type() string // Type
}

Typer can be implemented by types to allow their callers to get a string reference to the type that they are. Here's an example of what that means:

type Foo int           // enum type alias
const FooValA Foo = 1  // enum value assignment
Foo(1).Type() = "Foo"  // Type() returns the name of the type.

type Unmarshaler

type Unmarshaler interface {
	encoding.TextUnmarshaler
	json.Unmarshaler
	yaml.Unmarshaler
	sql.Scanner
	FlagUnmarshaler
}

Unmarshaler defines the required methods for a renum.Enum type implementation to properly support de-serialization out of various encoding foramts (text, json, yaml, sql, flags). This allows for easy integration with existing data structures that are commonly unmarshaled from these formats.

type Wrapped

type Wrapped interface {
	EmbeddedError

	// Typed returns the renum.Err typed error for this wrapped error.
	Typed() Error

	// Cause implements github.com/pkg/errors.Causer interface
	Cause() error

	// Unwrap implements the golang.org/x/xerrors.Wrapper interface.
	Unwrap() error

	// Is implements the golang.org/x/xerrors.Is interface.
	Is(e error) bool

	// Format implements fmt.Formatter interface (old error handling)
	Format(f fmt.State, c rune)

	// FormatError implements golang.org/x/xerrors.Formatter interface (new error handling)
	FormatError(p xerrors.Printer) error

	// Errors implements the github.com/uber-go/multierr.errorGroup interface.
	Errors() []error

	// YARPCError implements the go.uber.org/yarpc/yarpcerrors interface for creating
	// custom YARPC errors.
	YARPCError() *yarpcerrors.Status
}

Wrapped defines a type implementation that allows for wrapped Errors to be wrapped and unwrapped following Go convention (both old and new).

func Wrap

func Wrap(e Error, err error) Wrapped

Wrap combines a renum.Error type as well as a standard library error in order to allow for contextual information.

type YARPCError

type YARPCError interface {
	Error
	YARPCResponder
}

YARPCError extends the Error interface to allow a type to additionally self-report a YARPC error code in order to enrich the handler's ability to respond with the proper code when an error of this type is encountered.

type YARPCResponder

type YARPCResponder interface {
	ToYARPC() yarpcerrors.Code
	YARPCError() *yarpcerrors.Status
}

YARPCResponder allows a type to define a specified YARPC error code so that a handler can automatically act on it's behalf without having to maintain a separate mapping.

Directories

Path Synopsis
cmd
lib

Jump to

Keyboard shortcuts

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