Skip to content

Commit

Permalink
Starting v2 (#19)
Browse files Browse the repository at this point in the history
* Implement functional options pattern

* starting v2
  • Loading branch information
m-mizutani authored Jan 7, 2025
1 parent 6c79b8d commit 7d75169
Show file tree
Hide file tree
Showing 16 changed files with 179 additions and 101 deletions.
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ Package `goerr` provides more contextual error handling in Go.
- Stack traces
- Compatible with `github.com/pkg/errors`.
- Structured stack traces with `goerr.Stack` is available.
- Contextual variables to errors using `With(key, value)` and `WithTags(tags ...Tag)`.
- Contextual variables to errors using:
- Key value data by `goerr.Value(key, value)` (or `goerr.V(key, value)` as alias).
- Tag value data can be defined by `goerr.NewTag` and set into error by `goerr.Tag(tag)` (or `goerr.T(tag)` as alias).
- `errors.Is` to identify errors and `errors.As` to unwrap errors.
- `slog.LogValuer` interface to output structured logs with `slog`.

Expand Down Expand Up @@ -86,17 +88,15 @@ if err := someAction("no_such_file.txt"); err != nil {

### Add/Extract contextual variables

#### Key-Value pairs

`goerr` provides the `With(key, value)` method to add contextual variables to errors. The standard way to handle errors in Go is by injecting values into error messages. However, this approach makes it difficult to aggregate various errors. On the other hand, `goerr`'s `With` method allows for adding contextual information to errors without changing error message, making it easier to aggregate error logs. Additionally, error handling services like Sentry.io can handle errors more accurately with this feature.
`goerr` provides the `Value(key, value)` method to add contextual variables to errors. The standard way to handle errors in Go is by injecting values into error messages. However, this approach makes it difficult to aggregate various errors. On the other hand, `goerr`'s `Value` method allows for adding contextual information to errors without changing error message, making it easier to aggregate error logs. Additionally, error handling services like Sentry.io can handle errors more accurately with this feature.

```go
var errFormatMismatch = errors.New("format mismatch")

func someAction(tasks []task) error {
for _, t := range tasks {
if err := validateData(t.Data); err != nil {
return goerr.Wrap(err, "failed to validate data").With("name", t.Name)
return goerr.Wrap(err, "failed to validate data", goerr.Value("name", t.Name))
}
}
// ....
Expand All @@ -105,7 +105,7 @@ func someAction(tasks []task) error {

func validateData(data string) error {
if !strings.HasPrefix(data, "data:") {
return goerr.Wrap(errFormatMismatch).With("data", data)
return goerr.Wrap(errFormatMismatch, goerr.Value("data", data))
}
return nil
}
Expand Down Expand Up @@ -220,7 +220,7 @@ func someAction(input string) error {

func validate(input string) error {
if input != "OK" {
return goerr.Wrap(errRuntime, "invalid input").With("input", input)
return goerr.Wrap(errRuntime, "invalid input", goerr.V("input", input))
}
return nil
}
Expand Down Expand Up @@ -276,7 +276,7 @@ type object struct {
}

func (o *object) Validate() error {
eb := goerr.NewBuilder().With("id", o.id)
eb := goerr.NewBuilder(goerr.Value("id", o.id))

if o.color == "" {
return eb.New("color is empty")
Expand Down
37 changes: 18 additions & 19 deletions builder.go
Original file line number Diff line number Diff line change
@@ -1,40 +1,39 @@
package goerr

import (
"fmt"
)

// Builder keeps a set of key-value pairs and can create a new error and wrap error with the key-value pairs.
type Builder struct {
values values
options []Option
}

// NewBuilder creates a new Builder
func NewBuilder() *Builder {
return &Builder{values: make(values)}
func NewBuilder(options ...Option) *Builder {
return &Builder{
options: options,
}
}

// With copies the current Builder and adds a new key-value pair.
//
// Deprecated: Use goerr.Value instead.
func (x *Builder) With(key string, value any) *Builder {
newVS := &Builder{values: x.values.clone()}
newVS.values[key] = value
return newVS
newBuilder := &Builder{
options: x.options[:],
}
newBuilder.options = append(newBuilder.options, Value(key, value))
return newBuilder
}

// New creates a new error with message
func (x *Builder) New(format string, args ...any) *Error {
err := newError()
err.msg = fmt.Sprintf(format, args...)
err.values = x.values.clone()

func (x *Builder) New(msg string, options ...Option) *Error {
err := newError(append(x.options, options...)...)
err.msg = msg
return err
}

// Wrap creates a new Error with caused error and add message.
func (x *Builder) Wrap(cause error, msg ...any) *Error {
err := newError()
err.msg = toWrapMessage(msg)
func (x *Builder) Wrap(cause error, msg string, options ...Option) *Error {
err := newError(append(x.options, options...)...)
err.msg = msg
err.cause = cause
err.values = x.values.clone()
return err
}
6 changes: 3 additions & 3 deletions builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ package goerr_test
import (
"testing"

"github.com/m-mizutani/goerr"
"github.com/m-mizutani/goerr/v2"
)

func newErrorWithBuilder() *goerr.Error {
return goerr.NewBuilder().With("color", "orange").New("error")
return goerr.NewBuilder(goerr.V("color", "orange")).New("error")
}

func TestBuilderNew(t *testing.T) {
Expand All @@ -20,7 +20,7 @@ func TestBuilderNew(t *testing.T) {

func TestBuilderWrap(t *testing.T) {
cause := goerr.New("cause")
err := goerr.NewBuilder().With("color", "blue").Wrap(cause, "error")
err := goerr.NewBuilder(goerr.V("color", "blue")).Wrap(cause, "error")

if err.Values()["color"] != "blue" {
t.Errorf("Unexpected value: %v", err.Values())
Expand Down
59 changes: 38 additions & 21 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,42 +4,51 @@ import (
"errors"
"fmt"
"io"
"strings"

"log/slog"

"github.com/google/uuid"
)

// New creates a new error with message
func New(format string, args ...any) *Error {
err := newError()
err.msg = fmt.Sprintf(format, args...)
return err
type Option func(*Error)

// Value sets key and value to the error
func Value(key string, value any) Option {
return func(err *Error) {
err.values[key] = value
}
}

func toWrapMessage(msgs []any) string {
var newMsgs []string
for _, m := range msgs {
newMsgs = append(newMsgs, fmt.Sprintf("%v", m))
// V is alias of Value
func V(key string, value any) Option {
return Value(key, value)
}

// Tag sets tag to the error
func Tag(t tag) Option {
return func(err *Error) {
err.tags.add(t)
}
return strings.Join(newMsgs, " ")
}

// Wrap creates a new Error and add message.
func Wrap(cause error, msg ...any) *Error {
err := newError()
err.msg = toWrapMessage(msg)
err.cause = cause
// T is alias of Tag
func T(t tag) Option {
return Tag(t)
}

// New creates a new error with message
func New(msg string, options ...Option) *Error {
err := newError(options...)
err.msg = msg
return err
}

// Wrapf creates a new Error and add message. The error message is formatted by fmt.Sprintf.
func Wrapf(cause error, format string, args ...any) *Error {
// Wrap creates a new Error and add message.
func Wrap(cause error, msg string, options ...Option) *Error {
err := newError()
err.msg = fmt.Sprintf(format, args...)
err.msg = msg
err.cause = cause

return err
}

Expand Down Expand Up @@ -94,13 +103,19 @@ type Error struct {
tags tags
}

func newError() *Error {
return &Error{
func newError(options ...Option) *Error {
e := &Error{
st: callers(),
values: make(values),
id: uuid.New().String(),
tags: make(tags),
}

for _, opt := range options {
opt(e)
}

return e
}

func (x *Error) copy(dst *Error) {
Expand Down Expand Up @@ -186,6 +201,8 @@ func (x *Error) Unwrap() error {
}

// With adds key and value related to the error event
//
// Deprecated: Use goerr.Value instead.
func (x *Error) With(key string, value any) *Error {
x.values[key] = value
return x
Expand Down
Loading

0 comments on commit 7d75169

Please sign in to comment.