diff --git a/README.md b/README.md index 642ae79..b3e738c 100644 --- a/README.md +++ b/README.md @@ -215,7 +215,39 @@ Output: } ``` +### Builder +`goerr` provides `goerr.NewBuilder()` to create an error with pre-defined contextual variables. It is useful when you want to create an error with the same contextual variables in multiple places. + +```go +type object struct { + id string + color string +} + +func (o *object) Validate() error { + eb := goerr.NewBuilder().With("id", o.id) + + if o.color == "" { + return eb.New("color is empty") + } + + return nil +} + +func main() { + obj := &object{id: "object-1"} + + if err := obj.Validate(); err != nil { + slog.Default().Error("Validation error", "err", err) + } +} +``` + +Output: +``` +2024/10/19 14:19:54 ERROR Validation error err.message="color is empty" err.values.id=object-1 (snip) +``` ## License diff --git a/builder.go b/builder.go new file mode 100644 index 0000000..8593ee1 --- /dev/null +++ b/builder.go @@ -0,0 +1,40 @@ +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 +} + +// NewBuilder creates a new Builder +func NewBuilder() *Builder { + return &Builder{values: make(values)} +} + +// With copies the current Builder and adds a new key-value pair. +func (x *Builder) With(key string, value any) *Builder { + newVS := &Builder{values: x.values.clone()} + newVS.values[key] = value + return newVS +} + +// 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() + + 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) + err.cause = cause + err.values = x.values.clone() + return err +} diff --git a/builder_test.go b/builder_test.go new file mode 100644 index 0000000..c43b160 --- /dev/null +++ b/builder_test.go @@ -0,0 +1,32 @@ +package goerr_test + +import ( + "testing" + + "github.com/m-mizutani/goerr" +) + +func newErrorWithBuilder() *goerr.Error { + return goerr.NewBuilder().With("color", "orange").New("error") +} + +func TestBuilderNew(t *testing.T) { + err := newErrorWithBuilder() + + if err.Values()["color"] != "orange" { + t.Errorf("Unexpected value: %v", err.Values()) + } +} + +func TestBuilderWrap(t *testing.T) { + cause := goerr.New("cause") + err := goerr.NewBuilder().With("color", "blue").Wrap(cause, "error") + + if err.Values()["color"] != "blue" { + t.Errorf("Unexpected value: %v", err.Values()) + } + + if err.Unwrap().Error() != "cause" { + t.Errorf("Unexpected cause: %v", err.Unwrap().Error()) + } +} diff --git a/errors.go b/errors.go index cfe5fb6..9a56503 100644 --- a/errors.go +++ b/errors.go @@ -18,18 +18,18 @@ func New(format string, args ...any) *Error { return err } +func toWrapMessage(msgs []any) string { + var newMsgs []string + for _, m := range msgs { + newMsgs = append(newMsgs, fmt.Sprintf("%v", m)) + } + return strings.Join(newMsgs, " ") +} + // Wrap creates a new Error and add message. func Wrap(cause error, msg ...any) *Error { err := newError() - - if len(msg) > 0 { - var newMsgs []string - for _, m := range msg { - newMsgs = append(newMsgs, fmt.Sprintf("%v", m)) - } - err.msg = strings.Join(newMsgs, " ") - } - + err.msg = toWrapMessage(msg) err.cause = cause return err @@ -56,19 +56,29 @@ func Unwrap(err error) *Error { return nil } +type values map[string]any + +func (x values) clone() values { + newValues := make(values) + for key, value := range x { + newValues[key] = value + } + return newValues +} + // Error is error interface for deepalert to handle related variables type Error struct { msg string id string st *stack cause error - values map[string]any + values values } func newError() *Error { return &Error{ st: callers(), - values: make(map[string]any), + values: make(values), id: uuid.New().String(), } } diff --git a/examples/builder/main.go b/examples/builder/main.go new file mode 100644 index 0000000..b289a10 --- /dev/null +++ b/examples/builder/main.go @@ -0,0 +1,30 @@ +package main + +import ( + "log/slog" + + "github.com/m-mizutani/goerr" +) + +type object struct { + id string + color string +} + +func (o *object) Validate() error { + eb := goerr.NewBuilder().With("id", o.id) + + if o.color == "" { + return eb.New("color is empty") + } + + return nil +} + +func main() { + obj := &object{id: "object-1"} + + if err := obj.Validate(); err != nil { + slog.Default().Error("Validation error", "err", err) + } +}