Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add package to handle end-user error messages. #13

Merged
merged 3 commits into from
Dec 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:

- uses: actions/setup-go@v1
with:
go-version: 1.17
go-version: 1.21

- uses: actions/checkout@v3

Expand Down
50 changes: 50 additions & 0 deletions errmsg/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# errmsg

Add end-user messages to errors.

`err.Error()` doesn't usually provide a human-friendly output. `errmsg` allows
errors to carry an (extendable) end-user message that can be used in e.g.
handlers.

Here is an example on how it can be used:

```go
package connector

import (
// ...
"github.com/instill-ai/x/errmsg"
)

func (c *Client) sendReq(reqURL, method, contentType string, data io.Reader) ([]byte, error) {
// ...

res, err := c.HTTPClient.Do(req)
if err != nil {
err := fmt.Errorf("failed to call connector vendor: %w", err)
return nil, errmsg.AddMessage(err, "Failed to call Vendor API.")
}

if res.StatusCode < 200 || res.StatusCode >= 300 {
err := fmt.Errorf("vendor responded with status code %d", res.StatusCode)
msg := fmt.Sprintf("Vendor responded with a %d status code.", res.StatusCode)
return nil, errmsg.AddMessage(err, msg)
}

// ...
}
```

```go
package handler

func (h *PublicHandler) DoAction(ctx context.Context, req *pb.DoActionRequest) (*pb.DoActionResponse, error) {
resp, err := h.triggerActionSteps(ctx, req)
if err != nil {
resp.Outputs, resp.Metadata, err = h.triggerNamespacePipeline(ctx, req)
return nil, status.Error(asGRPCStatus(err), errmsg.MessageOrErr(err))
}

return resp, nil
}
```
67 changes: 67 additions & 0 deletions errmsg/errmsg.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package errmsg

import (
"errors"
"fmt"
)

// endUserError is an error that holds an end-user message.
type endUserError struct {
message string
cause error
}

// Error implements the error interface by returning the internal error message.
func (e *endUserError) Error() string { return e.cause.Error() }

// Unwrap implements the Unwrap interface.
func (e *endUserError) Unwrap() error { return e.cause }

Check warning on line 18 in errmsg/errmsg.go

View check run for this annotation

Codecov / codecov/patch

errmsg/errmsg.go#L18

Added line #L18 was not covered by tests

// As implements the required function to ensure errors.As can properly match
// endUserEror targets.
func (e *endUserError) As(target any) bool {
if tgt, ok := target.(**endUserError); ok {
*tgt = e
return true
}

Check warning on line 26 in errmsg/errmsg.go

View check run for this annotation

Codecov / codecov/patch

errmsg/errmsg.go#L22-L26

Added lines #L22 - L26 were not covered by tests

return false

Check warning on line 28 in errmsg/errmsg.go

View check run for this annotation

Codecov / codecov/patch

errmsg/errmsg.go#L28

Added line #L28 was not covered by tests
}

// AddMessage adds an end-user message to an error, prepending it to any
// potential existing message.
func AddMessage(err error, msg string) error {
if msgInCause := Message(err); msgInCause != "" {
msg = fmt.Sprintf("%s %s", msg, msgInCause)
}

return &endUserError{
cause: err,
message: msg,
}
}

// Message extracts an end-user message from the error.
func Message(err error) string {
for err != nil {
eu := new(endUserError)
if errors.As(err, &eu) && eu.message != "" {
return eu.message
}

err = errors.Unwrap(err)
}

return ""
}

// MessageOrErr extracts an end-user message from the error. If no message is
// found, err.Error() is returned.
func MessageOrErr(err error) string {
msg := Message(err)
if msg == "" {
return err.Error()
}

return msg
}
72 changes: 72 additions & 0 deletions errmsg/errmsg_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package errmsg

import (
"errors"
"fmt"
"testing"

qt "github.com/frankban/quicktest"
)

func TestAddAndExtractMessage(t *testing.T) {
c := qt.New(t)

testcases := []struct {
name string
wantMsg string
wantErr string
err error
}{
{
name: "no message",
wantMsg: "boom",
wantErr: "boom",
err: errors.New("boom"),
},
{
name: "message on top of stack",
wantMsg: "Something went wrong.",
wantErr: "boom",
err: AddMessage(errors.New("boom"), "Something went wrong."),
},
{
name: "message in wrapped error (fmt)",
wantMsg: "Something went wrong.",
wantErr: "bang: boom",
err: fmt.Errorf(
"bang: %w",
AddMessage(errors.New("boom"), "Something went wrong."),
),
},
{
name: "message in joint error",
wantMsg: "Something went wrong.",
wantErr: "bang\nboom",
err: errors.Join(
errors.New("bang"),
AddMessage(errors.New("boom"), "Something went wrong."),
),
},
{
name: "multi-message error",
wantMsg: "An error happened. Something went wrong.",
wantErr: "bang: boom",
err: AddMessage(
// handle error coming from downstream
fmt.Errorf("bang: %w",
// downstream error also contains message
AddMessage(errors.New("boom"), "Something went wrong."),
),
// add message to downstream error
"An error happened.",
),
},
}

for _, tc := range testcases {
c.Run(tc.name, func(c *qt.C) {
c.Check(MessageOrErr(tc.err), qt.Equals, tc.wantMsg)
c.Check(tc.err, qt.ErrorMatches, tc.wantErr)
})
}
}
8 changes: 7 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
module github.com/instill-ai/x

go 1.19
go 1.21

require (
github.com/frankban/quicktest v1.14.6
github.com/google/uuid v1.3.0
github.com/iancoleman/strcase v0.2.0
github.com/pkg/errors v0.8.1
github.com/stretchr/testify v1.7.0
go.temporal.io/sdk v1.13.1
go.uber.org/zap v1.21.0
Expand All @@ -21,10 +23,14 @@ require (
github.com/gogo/status v1.1.0 // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/pborman/uuid v1.2.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/robfig/cron v1.2.0 // indirect
github.com/rogpeppe/go-internal v1.9.0 // indirect
github.com/stretchr/objx v0.3.0 // indirect
go.temporal.io/api v1.6.1-0.20211110205628-60c98e9cbfe2 // indirect
go.uber.org/atomic v1.9.0 // indirect
Expand Down
11 changes: 9 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw=
github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a/go.mod h1:7Ga40egUymuWXxAe151lTNnCv97MddSOVsjpPPkityA=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
Expand Down Expand Up @@ -65,8 +67,9 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
Expand All @@ -81,6 +84,8 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
Expand All @@ -90,6 +95,7 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLA
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw=
github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
Expand All @@ -98,6 +104,8 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:
github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
Expand Down Expand Up @@ -201,7 +209,6 @@ golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
Expand Down
Loading