serum

package module
v0.8.0 Latest Latest
Warning

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

Go to latest
Published: Nov 24, 2022 License: Apache-2.0, MIT, Apache-2.0, + 1 more Imports: 9 Imported by: 15

README

the go-serum library

The go-serum library is an easy implementation of the Serum Errors Specification for use in Golang development.

The Serum errors spec is meant to be a "just enough" spec -- easy to adopt, easy to extend, easy to describe. It specifies enough to be meaningful, but not so much that it becomes complicated.

This implementation is meant to be similarly "just enough":

  • it's a golang type;
  • it implements interfaces for error and also interfaces that let tools like go-serum-analyzer do static analysis for you;
  • it implements serialization to JSON;
  • and that's about it.

The library is written with the trust you can put those basics to good use.

The library also provides package-scope functions which can be used to access any of the attributes of a Serum-convention error -- Code, Message, Details, etc -- which also work on any golang error, making incremental adoption easy.

Examples!

We'll give a couple of examples of creating errors, in increasing order of complexity. Then, at the bottom, a quick example of how we suggest handling errors.

This is golang code to produce an error:

serum.Errorf("myapp-error-foobar", "this is a foobar error, with more info: %s", "somethingsomething")

If you print the result as JSON, you'll get:

{
	"code": "myapp-error-foobar",
	"message": "this is a foobar error, with more info: somethingsomething"
}

You can use the %w syntax to wrap other errors, too -- just like with standard fmt.Errorf:

serum.Errorf("myapp-error-frobnoz", "this is a bigger error, with cause: %w", otherErrorAbove)

If you print the result as JSON, you'll get:

{
	"code": "myapp-error-frobnoz",
	"message": "this is a bigger error, with cause: this is a foobar error, with more info: somethingsomething",
	"cause": {
		"code": "myapp-error-foobar",
		"message": "this is a foobar error, with more info: somethingsomething"
	}
}

(Note that the templating of messages is resolved in advance at all times. So typically, to a user, you just print the outermost message.)

What's above is just the shorthand API.

You can also produce richer errors:

serum.Error("myapp-error-jobnotfound",
	serum.WithMessageTemplate("job ID {{ID}} not found"),
	serum.WithDetail("ID", "asdf-qwer-zxcv"),
)

The result of this, as JSON, is:

{
	"code": "myapp-error-jobnotfound",
	"message": "job ID asdf-qwer-zxcv not found",
	"details": {
		"ID": "asdf-qwer-zxcv"
	}
}

Notice how with this syntax, you could attach details to the error. This makes for easier programmatic transmission of complex, rich errors. The brief templating syntax -- {{this}} -- just substitutes in values. It means the message prepared for human readers can still include the details, without the developer having to repeat themself too much.

(Note that you can use WithMessageLiteral instead of WithMessageTemplate, if you don't want to use the templating system at all!)

The templating language is not rich (intentionally! You shouldn't be doing complex logic during error production!), but it does support a few critical things, like quoting:

serum.Error("myapp-error-withquotedstuff",
	serum.WithMessageTemplate("message detail {{thedetail | q}} should be quoted"),
	serum.WithDetail("thedetail", "whee! wow!"),
)

(A pipe character -- | -- is how we insert a formatting directive; and "q" means "quote this".)

If you stringify this (i.e. with just .Error()), you'll get:

myapp-error-withquotedstuff: message detail "whee! wow!" should be quoted

When we serialize this one as JSON, notice that the the value in the details map is unquoted (it's still a clear value on its own!), but the composed message is quoted (which then ends up escaped in JSON):

{
	"code": "demo-error-withquotes",
	"message": "message detail \"whee! wow!\" should be quoted",
	"details": {
		"thedetail": "whee! wow!"
	}
}

Now how do we handle all these errors? Easy: the typical way is to switch on their "code" field. That looks like this:

switch serum.Code(theError) {
	case "myapp-error-foobar":
		// ...handle foobar...
	case "myapp-error-frobnoz":
		// ...handle frobnoz...
	case "myapp-error-jobnotfound":
		// ...handle jobnotfound...
	default:
		panic("unhandled error :(") // shouldn't happen because go-serum-analyzer can catch it at compile time!  :D
}

Status

This library is considered in "beta" status. Please try it out, and see if it suits your needs.

The API may change in the future, as we discover more about how to make it the smoothest it can be. However, we will take any changes carefully, as we do understand that this library may end up at the base of deep dependency tress; we will definitely aim to minimize breaking changes, provide smooth migration windows, and generally avoid creating any painful "diamond problems" in dependency graphs.

But this is too heavyweight...

Well, most people don't say that :) It depends only on the standard library!

But yes, we allowed several dependencies from the standard library to creep in. Namely, encoding/json and reflect. (And strings and strconv, though usually people don't mind that.)

You can most definitely implement the Serum conventions without such dependencies. But this library uses them. Using them makes it possible for us to give you something that's easier to use than if we had avoided those stdlib packages. Especially in the case of JSON: playing nice with stdlib's encoding/json just feels enormously valuable.

If you would like a variant of this library without those dependencies, you can write another package that does exactly what you want! It's certainly possible. Or, patches/PRs for adding build tags to conditionally remove those features from this library would likely be accepted as well.

License

SPDX-License-Identifier: Apache-2.0 OR MIT

Documentation

Overview

The serum package provides helper functions and handy types for error handling that works in accordance with the Serum Errors Convention.

You don't need to use this package to implement the Serum Errors Convention! (A key goal of the Convention is that you *do not* need any specific implementation; the convention is based on the serial forms, and in Golang, even all of the static analysis tooling is based on interfaces.) However, you may find it handy.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Cause

func Cause(err error) error

Cause returns the cause of any Serum-style error.

This function takes the general "error" type and feature-detects for Serum behaviors, but still has fallback behaviors for any error value.

If the given error is not recognizably Serum-styled, this function falls back to golang's standard `errors.Unwrap`.

(This function is really only provided for completeness and consistency of naming; it's functionally identical to golang's standard `errors.Unwrap`.)

func Code

func Code(err error) string

Code will access and return the error code for any Serum-style error.

This function takes the general "error" type and feature-detects for Serum behaviors, but still has fallback behaviors for any error value.

If the given error is _not_ recognizably Serum-styled, a code string will be invented on the fly. This invented code string will have the prefix "bestguess-golang-" followed by a munge of the golang type name. This fallback is meant to be minimally functional and help find the source of coding mistakes that lead to its creation, but should not be seen in a well-formed program.

func Detail

func Detail(err error, whichDetail string) string

Detail gets a detail value out of an error, or returns empty string if there isn't one. It's functionally equal to `DetailsMap()[whichDetail]`, but may be more efficient.

func Details

func Details(err error) [][2]string

Details returns the details key-values of an error as a slice of pairs of strings.

This function takes the general "error" type and feature-detects for Serum behaviors, but still has fallback behaviors for any error value.

If the given error is not recognizably Serum-styled, this function returns nil.

Note that you may also be able to use the DetailsMap to get the same content as a golang map, for convenience, but be aware that access mechanism does not support order-preservation, and may often be slightly slower performance.

The result should not be mutated; it may be the original memory from the error value.

func DetailsMap

func DetailsMap(err error) map[string]string

DetailsMap returns the details of an error as a map.

This function takes the general "error" type and feature-detects for Serum behaviors, but still has fallback behaviors for any error value.

Note that you may wish to use the Details function instead, if the original ordering of the details fields is important; because it uses golang maps, this function is not order-preserving.

If the given error is not recognizably Serum-styled, this function returns an empty map.

The map should not be mutated; it may be the original memory from the error value.

func Error

func Error(ecode string, params ...WithConstruction) error

Error is a constructor for new Serum-style error values, supporting use of templated messages, and attachment of details, causes, and the enter suite of Serum features.

The error code parameter is required, and all other parameters are optional. The `serum.With*()` functions are used to create descriptions of messages and details attachements, and these are then provided as varargs as desired.

See the examples for usage.

Errors:

  • param: ecode -- the error code to construct.
Example
const ErrJobNotFound = "demo-error-job-not-found"
constructor := func(param int) error {
	return serum.Error(ErrJobNotFound,
		serum.WithMessageTemplate("job ID {{ID}} not found"),
		serum.WithDetail("ID", strconv.Itoa(param)),
	)
}
err := constructor(12)
fmt.Printf("the error as a string:\n\t%v\n", err)
jb, jsonErr := json.MarshalIndent(err, "\t", "\t")
if jsonErr != nil {
	panic(jsonErr)
}
fmt.Printf("the error as json:\n\t%s\n", jb)
Output:

the error as a string:
	demo-error-job-not-found: job ID 12 not found
the error as json:
	{
		"code": "demo-error-job-not-found",
		"message": "job ID 12 not found",
		"details": {
			"ID": "12"
		}
	}

func Errorf

func Errorf(ecode string, fmtPattern string, args ...interface{}) error

Errorf produces new Serum-style error values, and attaches a message, which may use a formatting pattern.

Provide the error code string as the first parameter, and a message as the second parameter. The message may be a format string as per `fmt.Errorf` and friends, and additional parameters can be given as varargs.

If a %w verb is used, Errorf will take an error parameter in the args and attach it as "cause", similarly to the behavior of `fmt.Errorf`. However, if that error is not already a Serum-style error (concretely: if it does not implement ErrorInterface), it will be coerced into one, by use of the Standardize function. (We consider this coersion appropriate to perform immediately, because otherwise the resulting value would fail to round-trip through serialization.)

Errors:

  • param: ecode -- the error code to construct.
Example
const ErrFoobar = "demo-error-foobar"
err := serum.Errorf(ErrFoobar, "freetext goes here (%s)", "and can interpolate")

fmt.Printf("the error as a string:\n\t%v\n", err)
jb, jsonErr := json.MarshalIndent(err, "\t", "\t")
if jsonErr != nil {
	panic(jsonErr)
}
fmt.Printf("the error as json:\n\t%s\n", jb)
Output:

the error as a string:
	demo-error-foobar: freetext goes here (and can interpolate)
the error as json:
	{
		"code": "demo-error-foobar",
		"message": "freetext goes here (and can interpolate)"
	}

func Message

func Message(err error) string

Message returns the message field of a Serum-style error.

This function takes the general "error" type and feature-detects for Serum behaviors, but still has fallback behaviors for any error value. It returns the same as (error).Error() for other errors.

If you are producing text for a user, consider the SynthesizeString function. Since the message field is optional in Serum, it may be blank; it is also defined as _not_ including the error's code. The SynthesizeString function will produce a human-readable string containing the code as well as the message, if it's present.

func SynthesizeString

func SynthesizeString(err ErrorInterface) string

SynthesizeString generates a string for an error, suitable for return as the golang `Error() string` result. SynthesizeString will detect properties of a Serum error, and synthesize a string using them. The string will contain the code, the message, and the string of the cause if present, in roughly the form "{code}[: {message}][: caused by: {cause}]". Entries from a details map will not be present (unless the message includes them), as per the Serum standard's recommendation.

You can use this function to implement the `Error() string` method of a Serum error type conveniently.

The resultant string is hoped to be human-readable. It is not expected to be mechanically parsible. The form is primarily meant to match Golang community norms; it is not a Serum convention.

The exact behavior of this function may change over time. For example, currently, it disregards all linebreaks (it neither strips nor introduces them itself), but in the future, if a Serum convention for multiline errors is introduced, then this function will likely change in behavior to match.

func ToJSON

func ToJSON(err error) ([]byte, error)

ToJSON is a helper function to turn any error into JSON. It is suitable to use in implementing `encoding/json.Marshaler.MarshalJSON` if implementing your own error types, and it is used for that purpose in the ErrorValue type provided by this package. If is also suitable for freestanding use on any error value (even non-Serum error values).

If the error is a Serum error (per ErrorInterface), we'll serialize it completely, including message, details, code, and cause, all distinctly. If the error is not a Serum error, we'll serialize it anyway, but fudge as necessary to produce a result that will at least be Serum serial spec compliant and able to be deserialized as a Serum error.

In fudge mode: the golang type will appear as part of the serum code; the `Error() string` will be used as a message; `errors.Unwrap` will be used to find a cause; etc.

func ToJSONString added in v0.6.0

func ToJSONString(err error) string

ToJSONString is similar to ToJSON, but returns exactly one value, which makes it easy to use in chained calls, or to hand to a Printf parameter.

fmt.Sprintf(os.Stderr, "%s\n", serum.ToJSONString(err))

... is an easy way to wrap up your program!

Types

type Data

type Data struct {
	Code    string
	Message string
	Details [][2]string
	Cause   ErrorInterface
}

Data is the body of the ErrorValue type.

It is a separate type mainly for naming purposes (it allows us to have the same name for fields here as we use for the accessor methods on ErrorValue).

Most user code will not see this type. Although it is exported, and referencing it is allowed, it is not usually necessary. User code can construct these values if desired, but using constructor functions from the go-serum package is often syntactically easier. User code may access these values directly if it's known that the code is handling ErrorValue concretely, but most code is not writen in such a way, and the serum accessor functions are used instead.

type ErrorInterface

type ErrorInterface interface {
	error
	Code() string
}

ErrorInterface is the minimal interface that must be implemented to be a Serum error. This is also the interface that go-serum-analyzer is looking for, if you use that tool (although not by name; matching the pattern is sufficient).

This interface is not often seen in user code. Usually, we still recommend user code mostly refers to "error", as is normal in golang. Functions throughout this package will accept and return error as a parameter, and apply serum behaviors to those values by use of feature detection, which removes all need to refer to this interface in user code.

func Standardize

func Standardize(other error) ErrorInterface

Standardize returns a value that's guaranteed to be a Serum-style error, and use the concrete type of *ErrorValue from this package.

This isn't often necessary to use, because all the functions in this package accept any error implementation and figure out how to do the right thing -- but is provided for your convenience if needed for creating new errors or just moving things into a standard memory layout for some reason.

If given a value that implements the Serum interfaces, all data will be copied, using those interfaces to access it.

If given a golang error that's not a Serum-style error at all, the same procedure is followed: a new value will be created, where the code is set to what `serum.Code` returns on the old value; etc. (In practice, this means you'll end up with an ErrorValue that contains a code string that is prefixed with "golang-bestguess-"; etc.)

If given a value that is already of type *ErrorValue, it is returned unchanged.

This function returns ErrorInterface rather than concretely *ErrorValue, to reduce the chance of creating "untyped nil" problems in practical usage, but it is valid to directly cast the result to *ErrorValue if you wish.

type ErrorInterfaceWithCause

type ErrorInterfaceWithCause interface {
	ErrorInterface
	Unwrap() error
}

type ErrorInterfaceWithDetailsMap

type ErrorInterfaceWithDetailsMap interface {
	ErrorInterface
	Details() map[string]string
}

type ErrorInterfaceWithDetailsOrdered

type ErrorInterfaceWithDetailsOrdered interface {
	ErrorInterface
	Details() [][2]string
}

type ErrorInterfaceWithMessage

type ErrorInterfaceWithMessage interface {
	ErrorInterface
	Message() string
}

type ErrorValue

type ErrorValue struct {
	Data
}

ErrorValue is a concrete type that implements the Serum conventions for errors.

It can contain message and details fields in addition to the essential "code" field, and implements convenient features like automatic synthesis of a good message for golang `Error() string`, as well as supporting json marshalling and unmarshalling.

Accessor methods can be used to inspect the values inside this type, but typically, the package-scope functions in serum should be used instead -- `serum.Code`, `serum.Message`, `serum.Details`, etc -- because they are easier to use without refering to any concrete types. (Using the package-scope functions will save you from any syntactical line-noise of casting!)

The fields of this type are exported, but mutating them is inadvisable. (The go-serum-analyzer tool becomes much less useful if you do so; it does not support tracking the effects of such mutations.)

func (*ErrorValue) Code

func (e *ErrorValue) Code() string

Code returns the Serum errorcode. Use the `serum.Code` package function to access this without referring to the concrete type.

func (*ErrorValue) Details

func (e *ErrorValue) Details() [][2]string

Details returns the Serum details key-values. Use the `serum.Details` or `serum.DetailsMap` package function to access this without referring to the concrete type.

func (*ErrorValue) Error

func (e *ErrorValue) Error() string

Error implements the golang error interface. The returned string will contain the code, the message if present, and the string of the cause. Per Serum convention, it does not include any of the details fields.

func (*ErrorValue) MarshalJSON

func (e *ErrorValue) MarshalJSON() ([]byte, error)

func (*ErrorValue) Message

func (e *ErrorValue) Message() string

Message returns the Serum message. Use the `serum.Message` package function to access this without referring to the concrete type.

func (*ErrorValue) UnmarshalJSON

func (e *ErrorValue) UnmarshalJSON(b []byte) error

func (*ErrorValue) Unwrap

func (e *ErrorValue) Unwrap() error

Unwrap returns the Serum cause. Use the `serum.Cause` package function, or the golang `errors.Unwrap` function, to access this without referring to the concrete type.

type WithConstruction

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

WithConstruction is a data carrier type used as part of the Error constructor system. It is not usually seen directly in user code; only passed between the With*() functions, and directly into the Error constructor function.

See the examples of the Error function for complete demonstrations of usage.

func WithCause

func WithCause(cause error) WithConstruction

WithDetail is part of the system for constructing an error with the serum.Error function. It can accept any golang error value and will attach it as a cause to the newly produced Serum error.

As with Errorf's behavior when attaching causes, if the given error is not already Serum-style error, it will be coerced into one. This may result in a generated error code, which is prefixed with the string "bestguess-golang-" and some type name information.

func WithDetail

func WithDetail(key, value string) WithConstruction

WithDetail is part of the system for constructing an error with the serum.Error function. It allows attaching simple key:value string pairs to the error.

In addition to being stored as details in Serum convention (e.g., these values will be serialized as entries in a map when serializing the error), the WithMessageTemplate system can reference the detail values.

See the examples of the Error function for complete demonstrations of usage.

func WithMessageLiteral

func WithMessageLiteral(s string) WithConstruction

WithMessageLiteral is part of the system for constructing an error with the serum.Error function.

In contrast with WithMessageTemplate, this string is always passed verbatim into the message of the error.

func WithMessageTemplate

func WithMessageTemplate(tmpl string) WithConstruction

WithMessageTemplate is part of the system for constructing an error with the serum.Error function.

WithMessageTemplate describes how to produce a message for the error, and allows values from the error's details to be incorporated into the message smoothly.

The templates used are very simple: "{{x}}" will look up the detail labelled "x" and place the corresponding value in that position in the string. The templates will never error; if a detail is not found with the given label, then the output will just contain the template syntax and missing label (e.g., "{{x}}" will be emitted as output).

See the examples of the Error function for complete demonstrations of usage.

Jump to

Keyboard shortcuts

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