diff --git a/.build.yml b/.build.yml new file mode 100644 index 0000000..639a974 --- /dev/null +++ b/.build.yml @@ -0,0 +1,16 @@ +image: archlinux +packages: + - go +sources: + - https://github.com/go-ap/errors +environment: + GO111MODULE: 'on' +tasks: + - tests: | + cd errors + make test + - coverage: | + set -a +x + cd errors && make coverage + GIT_SHA=$(git rev-parse --verify HEAD) + GIT_BRANCH=$(git name-rev --name-only HEAD) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..42d9ac4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +# Gogland +.idea/ + +# Binaries for programs and plugins +*.so + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tools +*.out +*.coverprofile + +*pkg diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..102e3b0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Golang ActitvityPub + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ffce493 --- /dev/null +++ b/Makefile @@ -0,0 +1,22 @@ +GO ?= go +TEST := $(GO) test +TEST_FLAGS ?= -v +TEST_TARGET ?= ./... +GO111MODULE=on +PROJECT_NAME := $(shell basename $(PWD)) + +.PHONY: test coverage clean download + +download: + $(GO) mod download all + +test: download + $(TEST) $(TEST_FLAGS) $(TEST_TARGET) + +coverage: TEST_TARGET := . +coverage: TEST_FLAGS += -covermode=count -coverprofile $(PROJECT_NAME).coverprofile +coverage: test + +clean: + $(RM) -v *.coverprofile + diff --git a/README.md b/README.md new file mode 100644 index 0000000..d13fe61 --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ + +[![MIT Licensed](https://img.shields.io/github/license/go-ap/errors.svg)](https://raw.githubusercontent.com/go-ap/errors/master/LICENSE) +[![Build Status](https://builds.sr.ht/~mariusor/errors.svg)](https://builds.sr.ht/~mariusor/errors) +[![Test Coverage](https://img.shields.io/codecov/c/github/go-ap/errors.svg)](https://codecov.io/gh/go-ap/errors) +[![Go Report Card](https://goreportcard.com/badge/github.com/go-ap/errors)](https://goreportcard.com/report/github.com/go-ap/errors) + diff --git a/decoding.go b/decoding.go new file mode 100644 index 0000000..54b0e65 --- /dev/null +++ b/decoding.go @@ -0,0 +1,85 @@ +package errors + +import ( + "github.com/valyala/fastjson" +) + +func UnmarshalJSON(data []byte) ([]error, error) { + if len(data) == 0 { + return nil, nil + } + p := fastjson.Parser{} + val, err := p.ParseBytes(data) + if err != nil { + return nil, err + } + + v := val.Get("errors") + if v == nil { + return nil, wrap(nil, "invalid errors array") + } + items := make([]error, 0) + switch v.Type() { + case fastjson.TypeArray: + for _, v := range v.GetArray() { + status := v.GetInt("status") + localErr := errorFromStatus(status) + if err := localErr.UnmarshalJSON([]byte(v.String())); err == nil { + items = append(items, localErr) + } + } + return items, err + case fastjson.TypeObject: + status := v.GetInt("status") + localErr := errorFromStatus(status) + if err := localErr.UnmarshalJSON([]byte(v.String())); err == nil { + items = append(items, localErr) + } + case fastjson.TypeString: + it := new(Err) + it.m = string(data) + items = append(items, it) + } + return items, nil +} + +func (e *Err) UnmarshalJSON(data []byte) error { + if m := fastjson.GetString(data, "message"); len(m) > 0 { + e.m = m + } + return nil +} + +func (n *notFound) UnmarshalJSON(data []byte) error { + return n.Err.UnmarshalJSON(data) +} +func (m *methodNotAllowed) UnmarshalJSON(data []byte) error { + return m.Err.UnmarshalJSON(data) +} +func (n *notValid) UnmarshalJSON(data []byte) error { + return n.Err.UnmarshalJSON(data) +} +func (f *forbidden) UnmarshalJSON(data []byte) error { + return f.Err.UnmarshalJSON(data) +} +func (n *notImplemented) UnmarshalJSON(data []byte) error { + return n.Err.UnmarshalJSON(data) +} +func (b *badRequest) UnmarshalJSON(data []byte) error { + return b.Err.UnmarshalJSON(data) +} +func (u *unauthorized) UnmarshalJSON(data []byte) error { + return u.Err.UnmarshalJSON(data) +} +func (n *notSupported) UnmarshalJSON(data []byte) error { + return n.Err.UnmarshalJSON(data) +} +func (t *timeout) UnmarshalJSON(data []byte) error { + return t.Err.UnmarshalJSON(data) +} +func (b *badGateway) UnmarshalJSON(data []byte) error { + return b.Err.UnmarshalJSON(data) +} +func (s *serviceUnavailable) UnmarshalJSON(data []byte) error { + return s.Err.UnmarshalJSON(data) +} diff --git a/decoding_test.go b/decoding_test.go new file mode 100644 index 0000000..ebcc9be --- /dev/null +++ b/decoding_test.go @@ -0,0 +1,99 @@ +package errors + +import ( + "reflect" + "testing" +) + +func Test_UnmarshalJSON(t *testing.T) { + tests := []struct { + name string + args []byte + want []Error + wantErr bool + }{ + { + name: "nil", + args: nil, + want: nil, + wantErr: false, + }, + { + name: "empty", + args: []byte{'{', '}'}, + want: []Error(nil), + wantErr: true, + }, + { + name: "no errors", + args: []byte(`{"errors":[]}`), + want: []Error{}, + wantErr: false, + }, + { + name: "rl-example", + args: []byte(`{"@context":"https://fedbox.git/ns#errors","errors":[{"status":403,"message":"Error processing OAuth2 request: The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed.","trace":[{"file":"/home/habarnam/workspace/go-ap/errors/errors.go","line":79,"calee":"github.com/go-ap/errors.wrap(0x114baa0, 0xc0005075c0, 0x1041334, 0x23, 0xc0003e5078, 0x1, 0x1, 0x0, 0x0, 0x0, ...)"},{"file":"/home/habarnam/workspace/go-ap/errors/errors.go","line":42,"calee":"github.com/go-ap/errors.Annotatef(0x114baa0, 0xc0005075c0, 0x1041334, 0x23, 0xc0003e5078, 0x1, 0x1, 0x0)"},{"file":"/home/habarnam/workspace/go-ap/errors/http.go","line":54,"calee":"github.com/go-ap/errors.wrapErr(0x114baa0, 0xc0005075c0, 0x1041334, 0x23, 0xc0003e5078, 0x1, 0x1, 0x0, 0x0, 0x0, ...)"},{"file":"/home/habarnam/workspace/go-ap/errors/http.go","line":130,"calee":"github.com/go-ap/errors.NewForbidden(0x114baa0, 0xc0005075c0, 0x1041334, 0x23, 0xc0003e5078, 0x1, 0x1, 0x0)"},{"file":"/home/habarnam/workspace/fedbox/app/oauth.go","line":163,"calee":"github.com/go-ap/fedbox/app.annotatedRsError(0x193, 0x114baa0, 0xc0005075c0, 0x1041334, 0x23, 0xc0003e5078, 0x1, 0x1, 0x0, 0x0)"},{"file":"/home/habarnam/workspace/fedbox/app/oauth.go","line":177,"calee":"github.com/go-ap/fedbox/app.redirectOrOutput(0xc0000a0120, 0x7fb8c9b0baa8, 0xc00009a6c0, 0xc00021cc00)"},{"file":"/home/habarnam/workspace/fedbox/app/oauth.go","line":83,"calee":"github.com/go-ap/fedbox/app.(*oauthHandler).Authorize(0xc00026dec0, 0x7fb8c9b0baa8, 0xc00009a6c0, 0xc00021cc00)"},{"file":"/usr/lib/go/src/net/http/server.go","line":2012,"calee":"net/http.HandlerFunc.ServeHTTP(0xc000206a50, 0x7fb8c9b0baa8, 0xc00009a6c0, 0xc00021cc00)"},{"file":"/home/habarnam/.local/share/go/pkg/mod/github.com/go-chi/chi@v4.1.2+incompatible/mux.go","line":431,"calee":"github.com/go-chi/chi.(*Mux).routeHTTP(0xc0002012c0, 0x7fb8c9b0baa8, 0xc00009a6c0, 0xc00021cc00)"},{"file":"/usr/lib/go/src/net/http/server.go","line":2012,"calee":"net/http.HandlerFunc.ServeHTTP(0xc000206a60, 0x7fb8c9b0baa8, 0xc00009a6c0, 0xc00021cc00)"},{"file":"/home/habarnam/.local/share/go/pkg/mod/github.com/go-chi/chi@v4.1.2+incompatible/mux.go","line":70,"calee":"github.com/go-chi/chi.(*Mux).ServeHTTP(0xc0002012c0, 0x7fb8c9b0baa8, 0xc00009a6c0, 0xc00021cc00)"},{"file":"/home/habarnam/.local/share/go/pkg/mod/github.com/go-chi/chi@v4.1.2+incompatible/mux.go","line":298,"calee":"github.com/go-chi/chi.(*Mux).Mount.func1(0x7fb8c9b0baa8, 0xc00009a6c0, 0xc00021cc00)"},{"file":"/usr/lib/go/src/net/http/server.go","line":2012,"calee":"net/http.HandlerFunc.ServeHTTP(0xc000209ec0, 0x7fb8c9b0baa8, 0xc00009a6c0, 0xc00021cc00)"},{"file":"/home/habarnam/.local/share/go/pkg/mod/github.com/go-chi/chi@v4.1.2+incompatible/mux.go","line":431,"calee":"github.com/go-chi/chi.(*Mux).routeHTTP(0xc000200f60, 0x7fb8c9b0baa8, 0xc00009a6c0, 0xc00021cc00)"},{"file":"/usr/lib/go/src/net/http/server.go","line":2012,"calee":"net/http.HandlerFunc.ServeHTTP(0xc000206890, 0x7fb8c9b0baa8, 0xc00009a6c0, 0xc00021cc00)"},{"file":"/home/habarnam/workspace/fedbox/app/middleware.go","line":61,"calee":"github.com/go-ap/fedbox/app.ActorFromAuthHeader.func1.1(0x7fb8c9b0baa8, 0xc00009a6c0, 0xc00021cc00)"},{"file":"/usr/lib/go/src/net/http/server.go","line":2012,"calee":"net/http.HandlerFunc.ServeHTTP(0xc000211940, 0x7fb8c9b0baa8, 0xc00009a6c0, 0xc00021cb00)"},{"file":"/home/habarnam/.local/share/go/pkg/mod/github.com/go-chi/chi@v4.1.2+incompatible/middleware/get_head.go","line":37,"calee":"github.com/go-chi/chi/middleware.GetHead.func1(0x7fb8c9b0baa8, 0xc00009a6c0, 0xc00021cb00)"},{"file":"/usr/lib/go/src/net/http/server.go","line":2012,"calee":"net/http.HandlerFunc.ServeHTTP(0xc000209980, 0x7fb8c9b0baa8, 0xc00009a6c0, 0xc00021cb00)"},{"file":"/home/habarnam/.local/share/go/pkg/mod/github.com/go-chi/chi@v4.1.2+incompatible/middleware/realip.go","line":34,"calee":"github.com/go-chi/chi/middleware.RealIP.func1(0x7fb8c9b0baa8, 0xc00009a6c0, 0xc00021cb00)"},{"file":"/usr/lib/go/src/net/http/server.go","line":2012,"calee":"net/http.HandlerFunc.ServeHTTP(0xc0002099a0, 0x7fb8c9b0baa8, 0xc00009a6c0, 0xc00021cb00)"},{"file":"/home/habarnam/.local/share/go/pkg/mod/github.com/go-chi/chi@v4.1.2+incompatible/mux.go","line":70,"calee":"github.com/go-chi/chi.(*Mux).ServeHTTP(0xc000200f60, 0x7fb8c9b0baa8, 0xc00009a6c0, 0xc00021cb00)"},{"file":"/home/habarnam/.local/share/go/pkg/mod/github.com/go-chi/chi@v4.1.2+incompatible/mux.go","line":298,"calee":"github.com/go-chi/chi.(*Mux).Mount.func1(0x7fb8c9b0baa8, 0xc00009a6c0, 0xc00021cb00)"},{"file":"/usr/lib/go/src/net/http/server.go","line":2012,"calee":"net/http.HandlerFunc.ServeHTTP(0xc000209f80, 0x7fb8c9b0baa8, 0xc00009a6c0, 0xc00021cb00)"},{"file":"/home/habarnam/.local/share/go/pkg/mod/github.com/go-chi/chi@v4.1.2+incompatible/mux.go","line":431,"calee":"github.com/go-chi/chi.(*Mux).routeHTTP(0xc0002004e0, 0x7fb8c9b0baa8, 0xc00009a6c0, 0xc00021cb00)"},{"file":"/usr/lib/go/src/net/http/server.go","line":2012,"calee":"net/http.HandlerFunc.ServeHTTP(0xc000206b80, 0x7fb8c9b0baa8, 0xc00009a6c0, 0xc00021cb00)"},{"file":"/home/habarnam/.local/share/go/pkg/mod/github.com/go-chi/chi@v4.1.2+incompatible/middleware/logger.go","line":46,"calee":"github.com/go-chi/chi/middleware.RequestLogger.func1.1(0x1158d00, 0xc000010028, 0xc00021ca00)"},{"file":"/usr/lib/go/src/net/http/server.go","line":2012,"calee":"net/http.HandlerFunc.ServeHTTP(0xc000274ae0, 0x1158d00, 0xc000010028, 0xc00021ca00)"},{"file":"/home/habarnam/.local/share/go/pkg/mod/github.com/go-chi/chi@v4.1.2+incompatible/middleware/request_id.go","line":76,"calee":"github.com/go-chi/chi/middleware.RequestID.func1(0x1158d00, 0xc000010028, 0xc00021c900)"},{"file":"/usr/lib/go/src/net/http/server.go","line":2012,"calee":"net/http.HandlerFunc.ServeHTTP(0xc000209fa0, 0x1158d00, 0xc000010028, 0xc00021c900)"},{"file":"/home/habarnam/workspace/fedbox/app/middleware.go","line":21,"calee":"github.com/go-ap/fedbox/app.Repo.func1.1(0x1158d00, 0xc000010028, 0xc00021c800)"},{"file":"/usr/lib/go/src/net/http/server.go","line":2012,"calee":"net/http.HandlerFunc.ServeHTTP(0xc000274b10, 0x1158d00, 0xc000010028, 0xc00021c800)"},{"file":"/home/habarnam/.local/share/go/pkg/mod/github.com/go-chi/chi@v4.1.2+incompatible/mux.go","line":86,"calee":"github.com/go-chi/chi.(*Mux).ServeHTTP(0xc0002004e0, 0x1158d00, 0xc000010028, 0xc00021c800)"},{"file":"/usr/lib/go/src/net/http/server.go","line":2807,"calee":"net/http.serverHandler.ServeHTTP(0xc0002820e0, 0x1158d00, 0xc000010028, 0xc000524100)"},{"file":"/usr/lib/go/src/net/http/server.go","line":3381,"calee":"net/http.initALPNRequest.ServeHTTP(0x115d000, 0xc000090630, 0xc000080380, 0xc0002820e0, 0x1158d00, 0xc000010028, 0xc000524100)"},{"file":"/usr/lib/go/src/net/http/h2_bundle.go","line":5720,"calee":"net/http.(*http2serverConn).runHandler(0xc000582600, 0xc000010028, 0xc000524100, 0xc00029e000)"},{"file":"/usr/lib/go/src/net/http/h2_bundle.go","line":5454,"calee":"created by net/http.(*http2serverConn).processHeaders"}],"location":"/home/habarnam/workspace/go-ap/errors/http.go:54"},{"status":500,"message":"urls don't validate: https://brutalinks.git/auth/fedbox/callback / http://brutalinks.git/auth/fedbox/callback"}]}`), + want: []Error{ + &forbidden{ + Err: Err{ + m: "Error processing OAuth2 request: The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed.", + }, + }, + &Err{ + m: "urls don't validate: https://brutalinks.git/auth/fedbox/callback / http://brutalinks.git/auth/fedbox/callback", + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := UnmarshalJSON(tt.args) + if (err != nil) != tt.wantErr { + t.Errorf("UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr) + return + } + if len(tt.want) != len(got) { + t.Errorf("UnmarshalJSON() different lengths got = %d, want %d", len(got), len(tt.want)) + } + for i := range tt.want { + g := got[i] + w := tt.want[i] + switch ww := w.(type) { + case *badRequest: + assertEqual(t, g.(*badRequest).Err, ww.Err) + case *unauthorized: + assertEqual(t, g.(*unauthorized).Err, ww.Err) + case *forbidden: + assertEqual(t, g.(*forbidden).Err, ww.Err) + case *notFound: + assertEqual(t, g.(*notFound).Err, ww.Err) + case *methodNotAllowed: + assertEqual(t, g.(*methodNotAllowed).Err, ww.Err) + case *notValid: + assertEqual(t, g.(*notValid).Err, ww.Err) + case *notImplemented: + assertEqual(t, g.(*notImplemented).Err, ww.Err) + case *badGateway: + assertEqual(t, g.(*badGateway).Err, ww.Err) + case *notSupported: + assertEqual(t, g.(*notSupported).Err, ww.Err) + case *Err: + assertEqual(t, *g.(*Err), *ww) + } + } + }) + } +} + +func assertEqual(t *testing.T, g Err, w Err) { + if w.m != g.m { + t.Errorf("UnmarshalJSON() Err.m got = %s, want %s", g.m, w.m) + } + if w.c != g.c { + t.Errorf("UnmarshalJSON() Err.c got = %s, want %s", g.c, w.c) + } + if !reflect.DeepEqual(w.t.StackTrace(), g.t.StackTrace()) { + t.Errorf("UnmarshalJSON() Err.t got = %v, want %v", g.t, w.t) + } +} diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..26ee551 --- /dev/null +++ b/errors.go @@ -0,0 +1,166 @@ +package errors + +import ( + "errors" + "fmt" + "io" + "strings" +) + +// Export a number of functions or variables from package errors. +var ( + As = errors.As + Is = errors.Is + Unwrap = errors.Unwrap + //Join = errors.Join +) + +// IncludeBacktrace is a static variable that decides if when creating an error we store the backtrace with it. +var IncludeBacktrace = true + +// Err is our custom error type that can store backtrace, file and line number +type Err struct { + m string + c error + t stack +} + +func (e Err) Format(s fmt.State, verb rune) { + switch verb { + case 's': + io.WriteString(s, e.m) + switch { + case s.Flag('+'): + if e.c != nil { + io.WriteString(s, ": ") + io.WriteString(s, fmt.Sprintf("%+s", e.c)) + } + } + case 'v': + e.Format(s, 's') + switch { + case s.Flag('+'): + if e.t != nil { + io.WriteString(s, "\n\t") + e.t.Format(s, 'v') + } + } + } +} + +// Error implements the error interface +func (e Err) Error() string { + if IncludeBacktrace { + return e.m + } + s := strings.Builder{} + s.WriteString(e.m) + if ch := errors.Unwrap(e); ch != nil { + s.WriteString(": ") + s.WriteString(ch.Error()) + } + return s.String() +} + +// Unwrap implements the errors.Wrapper interface +func (e Err) Unwrap() error { + return e.c +} + +// StackTrace returns the stack trace as returned by the debug.Stack function +func (e Err) StackTrace() StackTrace { + return e.t.StackTrace() +} + +// Annotatef wraps an error with new message +func Annotatef(e error, s string, args ...interface{}) *Err { + err := wrap(e, s, args...) + return &err +} + +// Newf creaates a new error +func Newf(s string, args ...interface{}) *Err { + err := wrap(nil, s, args...) + return &err +} + +// Errorf is an alias for Newf +func Errorf(s string, args ...interface{}) error { + err := wrap(nil, s, args...) + return &err +} + +// As implements support for errors.As +func (e *Err) As(err interface{}) bool { + switch x := err.(type) { + case **Err: + *x = e + case *Err: + *x = *e + default: + return false + } + return true +} + +type StackTracer interface { + StackTrace() StackTrace +} + +// ancestorOfCause returns true if the caller looks to be an ancestor of the given stack +// trace. We check this by seeing whether our stack prefix-matches the cause stack, which +// should imply the error was generated directly from our goroutine. +func ancestorOfCause(ourStack stack, causeStack StackTrace) bool { + // Stack traces are ordered such that the deepest frame is first. We'll want to check + // for prefix matching in reverse. + // + // As an example, imagine we have a prefix-matching stack for ourselves: + // [ + // "github.com/go-ap/processing/processing.Validate", + // "testing.tRunner", + // "runtime.goexit" + // ] + // + // We'll want to compare this against an error cause that will have happened further + // down the stack. An example stack trace from such an error might be: + // [ + // "github.com/go-ap/errors/errors.New", + // "testing.tRunner", + // "runtime.goexit" + // ] + // + // Their prefix matches, but we'll have to handle the match carefully as we need to match + // from back to forward. + + // We can't possibly prefix match if our stack is larger than the cause stack. + if len(ourStack) > len(causeStack) { + return false + } + + // We know the sizes are compatible, so compare program counters from back to front. + for idx := 0; idx < len(ourStack); idx++ { + if ourStack[len(ourStack)-1] != (uintptr)(causeStack[len(causeStack)-1]) { + return false + } + } + + return true +} + +func wrap(e error, s string, args ...interface{}) Err { + err := Err{ + c: e, + m: fmt.Sprintf(s, args...), + } + if IncludeBacktrace { + causeStackTracer := new(StackTracer) + // If our cause has set a stack trace, and that trace is a child of our own function + // as inferred by prefix matching our current program counter stack, then we only want + // to decorate the error message rather than add a redundant stack trace. + stack := callers(2) + if !(As(e, causeStackTracer) && ancestorOfCause(*stack, (*causeStackTracer).StackTrace())) { + err.t = *stack + } + } + return err +} diff --git a/errors_test.go b/errors_test.go new file mode 100644 index 0000000..7b0ad4c --- /dev/null +++ b/errors_test.go @@ -0,0 +1,238 @@ +package errors + +import ( + "encoding/json" + "fmt" + "strings" + "testing" +) + +var err = fmt.Errorf("test error") + +func TestFormat(t *testing.T) { + IncludeBacktrace = false + // %+s check for unwrapped error + e1 := Newf("test") + str := fmt.Sprintf("%+s", e1) + if str != e1.m { + t.Errorf("Error message invalid %s, expected %s", str, e1.m) + } + + // %+s check for wrapped error + e2 := Annotatef(e1, "another") + str = fmt.Sprintf("%+s", e2) + val := e2.m + ": " + e2.c.Error() + if str != val { + t.Errorf("Error message invalid %s, expected %s", str, val) + } + + // %v check for unwrapped error with trace + IncludeBacktrace = true + e3 := Newf("test1") + str = fmt.Sprintf("%+v", e3) + if !strings.Contains(str, e3.m) { + t.Errorf("Error message %s\n should contain %s", str, e3.m) + } + if !strings.Contains(str, fmt.Sprintf("%+v", e3.t.StackTrace())) { + t.Errorf("Error message %s\n should contain %+v", str, e3.t.StackTrace()) + } +} + +func TestMarshalJSON(t *testing.T) { + IncludeBacktrace = true + e := Newf("test") + + b, err := e.t.StackTrace().MarshalJSON() + if err != nil { + t.Errorf("MarshalJSON failed with error: %+s", err) + } + stack := make([]json.RawMessage, 0) + err = json.Unmarshal(b, &stack) + if err != nil { + t.Errorf("JSON message could not be unmarshaled: %+s", err) + } + if len(stack) != len(e.t) { + t.Errorf("Count of stack frames different after marshaling, expected %d got %d", len(e.t), len(stack)) + } +} + +func TestAnnotatef(t *testing.T) { + testStr := "Annotatef string" + te := Annotatef(err, testStr) + if te.c != err { + t.Errorf("Invalid parent error %T:%s, expected %T:%s", te.c, te.c, err, err) + } + if te.m != testStr { + t.Errorf("Invalid error message %s, expected %s", te.m, testStr) + } +} + +var homeVal = "$HOME" + +func TestNewf(t *testing.T) { + testStr := "Newf string" + te := Newf(testStr) + if te.c != nil { + t.Errorf("Invalid parent error %T:%s, expected nil", te.c, te.c) + } + if te.m != testStr { + t.Errorf("Invalid error message %s, expected %s", te.m, testStr) + } +} + +func TestErrorf(t *testing.T) { + testStr := "Errorf string" + err := Errorf(testStr) + if te, ok := err.(*Err); ok { + if te.c != nil { + t.Errorf("Invalid parent error %T:%s, expected nil", te.c, te.c) + } + if te.m != testStr { + t.Errorf("Invalid error message %s, expected %s", te.m, testStr) + } + } else { + t.Errorf("Invalid error type returned %T, expected type %T", err, &Err{}) + } +} + +/* +func TestErr_As(t *testing.T) { + e := Err{m: "test", l: 11, f: "random", t: []uintptr{0x6, 0x6, 0x6}, c: fmt.Errorf("ttt")} + if e.As(&err) { + t.Errorf("%T should not be assertable as %T", err, e) + } + type clone = Err + e1 := clone{} + if !e.As(&e1) { + t.Errorf("%T should be assertable as %T", e, e1) + } + if e1.m != e.m { + t.Errorf("%T message should equal %T's, received %s, expected %s", e1, e, e1.m, e.m) + } + if e1.l != e.l { + t.Errorf("%T line should equal %T's, received %d, expected %d", e1, e, e1.l, e.l) + } + if e1.f != e.f { + t.Errorf("%T file should equal %T's, received %s, expected %s", e1, e, e1.f, e.f) + } + if !bytes.Equal(e1.t, e.t) { + t.Errorf("%T trace should equal %T's, received %2x, expected %2x", e1, e, e1.t, e.t) + } + if e1.c != e.c { + t.Errorf("%T parent error should equal %T's, received %T[%s], expected %T[%s]", e1, e, e1.c, e1.c, e.c, e.c) + } + e2 := &e + if !e.As(&e2) { + t.Errorf("%T should be assertable as %T", e, e2) + } + if e2.m != e.m { + t.Errorf("%T message should equal %T's, received %s, expected %s", e2, e, e2.m, e.m) + } + if e2.l != e.l { + t.Errorf("%T line should equal %T's, received %d, expected %d", e2, e, e2.l, e.l) + } + if e2.f != e.f { + t.Errorf("%T file should equal %T's, received %s, expected %s", e2, e, e2.f, e.f) + } + if !bytes.Equal(e2.t, e.t) { + t.Errorf("%T trace should equal %T's, received %2x, expected %2x", e2, e, e2.t, e.t) + } + if e2.c != e.c { + t.Errorf("%T parent error should equal %T's, received %T[%s], expected %T[%s]", e2, e, e2.c, e2.c, e.c, e.c) + } +} + +func TestErr_Error(t *testing.T) { + e := Err{m: "test"} + if e.Error() != e.m { + t.Errorf("Error() returned %s, expected %s", e.Error(), e.m) + } +} + +func TestErr_Location(t *testing.T) { + e := Err{l: 11, f: "random"} + if f, l := e.Location(); l != e.l || f != e.f { + t.Errorf("Location() returned: %s:%d, expected: %s:%d", f, l, e.f, e.l) + } +} + +func TestErr_StackTrace(t *testing.T) { + e := Err{t: []byte{0x6, 0x6, 0x6}} + if !bytes.Equal(e.StackTrace(), e.t) { + t.Errorf("StackTrace() returned: %2x, expected: %2x", e.StackTrace(), e.t) + } +} + +func TestErr_Unwrap(t *testing.T) { + e := Err{c: fmt.Errorf("ttt")} + w := e.Unwrap() + if w != e.c { + t.Errorf("Unwrap() returned: %T[%s], expected: %T[%s]", w, w, e.c, e.c) + } +} +*/ + +func TestErr_Error(t *testing.T) { + type fields struct { + m string + c error + t stack + } + tests := []struct { + quiet bool + name string + fields fields + want string + }{ + { + name: "empty", + fields: fields{}, + want: "", + }, + { + name: "just text", + fields: fields{m: "test"}, + want: "test", + }, + { + name: "text with single wrapped error", + fields: fields{m: "test", c: fmt.Errorf("error")}, + want: "test: error", + }, + { + name: "text with two wrapped errors", + fields: fields{m: "test", c: fmt.Errorf("error: %w", Newf("some error"))}, + want: "test: error: some error", + }, + { + name: "text with two wrapped errors, but no unwrapping", + quiet: true, + fields: fields{m: "test", c: fmt.Errorf("error: %w", Newf("some error"))}, + want: "test", + }, + { + name: "text with two wrapped Err errors", + fields: fields{m: "test", c: &Err{m: "error", c: &Err{m: "another error"}}}, + want: "test: error: another error", + }, + { + name: "text with two wrapped Err errors, without unwrapping", + quiet: true, + fields: fields{m: "test", c: &Err{m: "error", c: &Err{m: "another error"}}}, + want: "test", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + IncludeBacktrace = tt.quiet + e := Err{ + m: tt.fields.m, + c: tt.fields.c, + t: tt.fields.t, + } + if got := e.Error(); got != tt.want { + t.Errorf("Error() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..0448562 --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module github.com/go-ap/errors + +go 1.20 + +require ( + github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73 + github.com/valyala/fastjson v1.6.4 +) diff --git a/http.go b/http.go new file mode 100644 index 0000000..d1d22e0 --- /dev/null +++ b/http.go @@ -0,0 +1,1001 @@ +package errors + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/go-ap/jsonld" +) + +const errorsPackageName = "github.com/go-ap/errors" +const runtimeDebugPackageName = "runtime/debug" + +type Error interface { + error + json.Unmarshaler +} + +type notFound struct { + Err + s int +} + +type methodNotAllowed struct { + Err + s int +} + +type notValid struct { + Err + s int +} + +type forbidden struct { + Err + s int +} + +type notImplemented struct { + Err + s int +} + +type conflict struct { + Err + s int +} + +type gone struct { + Err + s int +} + +type badRequest struct { + Err + s int +} + +type unauthorized struct { + Err + s int + challenge string +} + +type notSupported struct { + Err + s int +} + +type timeout struct { + Err + s int +} + +type badGateway struct { + Err + s int +} + +type serviceUnavailable struct { + Err + s int +} + +func wrapErr(err error, s string, args ...interface{}) Err { + e := Annotatef(err, s, args...) + asErr := Err{} + As(e, &asErr) + return asErr +} + +func FromResponse(resp *http.Response) error { + if resp.StatusCode < http.StatusBadRequest { + return nil + } + body := make([]byte, 0) + defer resp.Body.Close() + + body, _ = io.ReadAll(resp.Body) + + errors, err := UnmarshalJSON(body) + if err != nil { + return AnnotateFromStatus(nil, resp.StatusCode, string(body)) + } + if len(errors) == 0 { + return nil + } + + return AnnotateFromStatus(Join(errors...), resp.StatusCode, resp.Status) +} + +func AnnotateFromStatus(err error, status int, s string, args ...interface{}) error { + switch status { + case http.StatusNotModified: + return NewNotModified(err, fmt.Sprintf(s, args...)) + case http.StatusBadRequest: + return NewBadRequest(err, s, args...) + case http.StatusUnauthorized: + return NewUnauthorized(err, s, args...) + // http.StatusPaymentRequired + case http.StatusForbidden: + return NewForbidden(err, s, args...) + case http.StatusNotFound: + return NewNotFound(err, s, args...) + case http.StatusMethodNotAllowed: + return NewMethodNotAllowed(err, s, args...) + case http.StatusNotAcceptable: + return NewNotValid(err, s, args...) + //case http.StatusProxyAuthRequired + //case http.StatusRequestTimeout + case http.StatusConflict: + return NewConflict(err, s, args...) + case http.StatusGone: + return NewGone(err, s, args...) + //case http.StatusLengthRequres + //case http.StatusPreconditionFailed + //case http.StatusRequestEntityTooLarge + //case http.StatusRequestURITooLong + // TODO(marius): http.StatusUnsupportedMediaType + //case http.StatusRequestedRangeNotSatisfiable + //case http.StatusExpectationFailed + //case http.StatusTeapot + //case http.StatusMisdirectedRequest + //case http.StatusUnprocessableEntity + //case http.StatusLocked + //case http.StatusFailedDependency + //case http.StatusTooEarly + //case http.StatusTooManyRequests + //case http.StatusRequestHeaderFieldsTooLarge + //case http.StatusUnavailableForLegalReason + //case http.StatusInternalServerError + case http.StatusNotImplemented: + return NewNotImplemented(err, s, args...) + case http.StatusBadGateway: + return NewBadGateway(err, s, args...) + //case http.StatusServiceUnavailable + //case http.StatusGatewayTimeout + case http.StatusHTTPVersionNotSupported: + return NewNotSupported(err, s, args...) + case http.StatusGatewayTimeout: + return NewTimeout(err, s, args...) + } + return Annotatef(err, s, args...) +} + +func NewFromStatus(status int, s string, args ...interface{}) error { + switch status { + case http.StatusBadRequest: + return BadRequestf(s, args...) + case http.StatusUnauthorized: + return Unauthorizedf(s, args...) + // http.StatusPaymentRequired + case http.StatusForbidden: + return Forbiddenf(s, args...) + case http.StatusNotFound: + return NotFoundf(s, args...) + case http.StatusMethodNotAllowed: + return MethodNotAllowedf(s, args...) + case http.StatusNotAcceptable: + return NotValidf(s, args...) + //case http.StatusProxyAuthRequired + //case http.StatusRequestTimeout + case http.StatusConflict: + return Conflictf(s, args...) + case http.StatusGone: + return Gonef(s, args...) + //case http.StatusLengthRequres + //case http.StatusPreconditionFailed + //case http.StatusRequestEntityTooLarge + //case http.StatusRequestURITooLong + // TODO(marius): http.StatusUnsupportedMediaType + //case http.StatusRequestedRangeNotSatisfiable + //case http.StatusExpectationFailed + //case http.StatusTeapot + //case http.StatusMisdirectedRequest + //case http.StatusUnprocessableEntity + //case http.StatusLocked + //case http.StatusFailedDependency + //case http.StatusTooEarly + //case http.StatusTooManyRequests + //case http.StatusRequestHeaderFieldsTooLarge + //case http.StatusUnavailableForLegalReason + //case http.StatusInternalServerError + case http.StatusNotImplemented: + return NotImplementedf(s, args...) + case http.StatusBadGateway: + return BadGatewayf(s, args...) + //case http.StatusServiceUnavailable + //case http.StatusGatewayTimeout + case http.StatusHTTPVersionNotSupported: + return NotSupportedf(s, args...) + case http.StatusGatewayTimeout: + return Timeoutf(s, args...) + } + return Newf(s, args...) +} + +func WrapWithStatus(status int, err error, s string, args ...interface{}) error { + switch status { + case http.StatusBadRequest: + return NewBadRequest(err, s, args...) + case http.StatusUnauthorized: + return NewUnauthorized(err, s, args...) + // http.StatusPaymentRequired + case http.StatusForbidden: + return NewForbidden(err, s, args...) + case http.StatusNotFound: + return NewNotFound(err, s, args...) + case http.StatusMethodNotAllowed: + return NewMethodNotAllowed(err, s, args...) + case http.StatusNotAcceptable: + return NewNotValid(err, s, args...) + //case http.StatusProxyAuthRequired + //case http.StatusRequestTimeout + case http.StatusConflict: + return NewConflict(err, s, args...) + case http.StatusGone: + return NewGone(err, s, args...) + //case http.StatusLengthRequres + //case http.StatusPreconditionFailed + //case http.StatusRequestEntityTooLarge + //case http.StatusRequestURITooLong + // TODO(marius): http.StatusUnsupportedMediaType + //case http.StatusRequestedRangeNotSatisfiable + //case http.StatusExpectationFailed + //case http.StatusTeapot + //case http.StatusMisdirectedRequest + //case http.StatusUnprocessableEntity + //case http.StatusLocked + //case http.StatusFailedDependency + //case http.StatusTooEarly + //case http.StatusTooManyRequests + //case http.StatusRequestHeaderFieldsTooLarge + //case http.StatusUnavailableForLegalReason + //case http.StatusInternalServerError + case http.StatusNotImplemented: + return NewNotImplemented(err, s, args...) + case http.StatusBadGateway: + return NewBadGateway(err, s, args...) + case http.StatusServiceUnavailable: + return NewServiceUnavailable(err, s, args...) + //case http.StatusGatewayTimeout + case http.StatusHTTPVersionNotSupported: + return NewNotSupported(err, s, args...) + case http.StatusGatewayTimeout: + return NewTimeout(err, s, args...) + } + return wrapErr(err, s, args...) +} +func NotFoundf(s string, args ...interface{}) *notFound { + return ¬Found{Err: wrapErr(nil, s, args...), s: http.StatusNotFound} +} +func NewNotFound(e error, s string, args ...interface{}) *notFound { + return ¬Found{Err: wrapErr(e, s, args...), s: http.StatusNotFound} +} +func MethodNotAllowedf(s string, args ...interface{}) *methodNotAllowed { + return &methodNotAllowed{Err: wrapErr(nil, s, args...), s: http.StatusMethodNotAllowed} +} +func NewMethodNotAllowed(e error, s string, args ...interface{}) *methodNotAllowed { + return &methodNotAllowed{Err: wrapErr(e, s, args...), s: http.StatusMethodNotAllowed} +} +func NotValidf(s string, args ...interface{}) *notValid { + return ¬Valid{Err: wrapErr(nil, s, args...)} +} +func NewNotValid(e error, s string, args ...interface{}) *notValid { + return ¬Valid{Err: wrapErr(e, s, args...)} +} +func Conflictf(s string, args ...interface{}) *conflict { + return &conflict{Err: wrapErr(nil, s, args...), s: http.StatusConflict} +} +func NewConflict(e error, s string, args ...interface{}) *conflict { + return &conflict{Err: wrapErr(e, s, args...), s: http.StatusConflict} +} +func Gonef(s string, args ...interface{}) *gone { + return &gone{Err: wrapErr(nil, s, args...), s: http.StatusGone} +} +func NewGone(e error, s string, args ...interface{}) *gone { + return &gone{Err: wrapErr(e, s, args...), s: http.StatusGone} +} +func Forbiddenf(s string, args ...interface{}) *forbidden { + return &forbidden{Err: wrapErr(nil, s, args...), s: http.StatusForbidden} +} +func NewForbidden(e error, s string, args ...interface{}) *forbidden { + return &forbidden{Err: wrapErr(e, s, args...), s: http.StatusForbidden} +} +func NotImplementedf(s string, args ...interface{}) *notImplemented { + return ¬Implemented{Err: wrapErr(nil, s, args...), s: http.StatusNotImplemented} +} +func NewNotImplemented(e error, s string, args ...interface{}) *notImplemented { + return ¬Implemented{Err: wrapErr(e, s, args...), s: http.StatusNotImplemented} +} +func BadRequestf(s string, args ...interface{}) *badRequest { + return &badRequest{Err: wrapErr(nil, s, args...), s: http.StatusBadRequest} +} +func NewBadRequest(e error, s string, args ...interface{}) *badRequest { + return &badRequest{Err: wrapErr(e, s, args...), s: http.StatusBadRequest} +} +func Unauthorizedf(s string, args ...interface{}) *unauthorized { + return &unauthorized{Err: wrapErr(nil, s, args...), s: http.StatusUnauthorized} +} +func NewUnauthorized(e error, s string, args ...interface{}) *unauthorized { + return &unauthorized{Err: wrapErr(e, s, args...), s: http.StatusUnauthorized} +} +func NotSupportedf(s string, args ...interface{}) *notSupported { + return ¬Supported{Err: wrapErr(nil, s, args...), s: http.StatusHTTPVersionNotSupported} +} +func NewNotSupported(e error, s string, args ...interface{}) *notSupported { + return ¬Supported{Err: wrapErr(e, s, args...), s: http.StatusHTTPVersionNotSupported} +} +func Timeoutf(s string, args ...interface{}) *timeout { + return &timeout{Err: wrapErr(nil, s, args...), s: http.StatusRequestTimeout} +} +func NewTimeout(e error, s string, args ...interface{}) *timeout { + return &timeout{Err: wrapErr(e, s, args...), s: http.StatusRequestTimeout} +} +func BadGatewayf(s string, args ...interface{}) *badGateway { + return &badGateway{Err: wrapErr(nil, s, args...), s: http.StatusBadGateway} +} +func NewBadGateway(e error, s string, args ...interface{}) *badGateway { + return &badGateway{Err: wrapErr(e, s, args...), s: http.StatusBadGateway} +} +func ServiceUnavailablef(s string, args ...interface{}) *serviceUnavailable { + return &serviceUnavailable{Err: wrapErr(nil, s, args...), s: http.StatusServiceUnavailable} +} +func NewServiceUnavailable(e error, s string, args ...interface{}) *serviceUnavailable { + return &serviceUnavailable{Err: wrapErr(e, s, args...), s: http.StatusServiceUnavailable} +} +func IsServiceUnavailable(e error) bool { + _, okp := e.(*serviceUnavailable) + _, oks := e.(serviceUnavailable) + return okp || oks || As(e, &serviceUnavailable{}) +} +func IsBadRequest(e error) bool { + _, okp := e.(*badRequest) + _, oks := e.(badRequest) + return okp || oks || As(e, &badRequest{}) +} +func IsForbidden(e error) bool { + _, okp := e.(*forbidden) + _, oks := e.(forbidden) + return okp || oks || As(e, &forbidden{}) +} +func IsNotSupported(e error) bool { + _, okp := e.(*notSupported) + _, oks := e.(notSupported) + return okp || oks +} +func IsConflict(e error) bool { + _, okp := e.(*conflict) + _, oks := e.(conflict) + return okp || oks || As(e, &conflict{}) +} +func IsGone(e error) bool { + _, okp := e.(*gone) + _, oks := e.(gone) + return okp || oks || As(e, &gone{}) +} +func IsMethodNotAllowed(e error) bool { + _, okp := e.(*methodNotAllowed) + _, oks := e.(methodNotAllowed) + return okp || oks || As(e, &methodNotAllowed{}) +} +func IsNotFound(e error) bool { + _, okp := e.(*notFound) + _, oks := e.(notFound) + return okp || oks || As(e, ¬Found{}) +} +func IsNotImplemented(e error) bool { + _, okp := e.(*notImplemented) + _, oks := e.(notImplemented) + return okp || oks || As(e, ¬Implemented{}) +} +func IsUnauthorized(e error) bool { + _, okp := e.(*unauthorized) + _, oks := e.(unauthorized) + return okp || oks || As(e, &unauthorized{}) +} +func IsTimeout(e error) bool { + _, okp := e.(*timeout) + _, oks := e.(timeout) + return okp || oks || As(e, &timeout{}) +} +func IsNotValid(e error) bool { + _, okp := e.(*notValid) + _, oks := e.(notValid) + return okp || oks || As(e, ¬Valid{}) +} + +func IsBadGateway(e error) bool { + _, okp := e.(*badGateway) + _, oks := e.(badGateway) + return okp || oks || As(e, &badGateway{}) +} +func (n notFound) Is(e error) bool { + return IsNotFound(e) +} +func (n notValid) Is(e error) bool { + return IsNotValid(e) +} +func (n notImplemented) Is(e error) bool { + return IsNotImplemented(e) +} +func (n notSupported) Is(e error) bool { + return IsNotSupported(e) +} +func (b badRequest) Is(e error) bool { + return IsBadRequest(e) +} +func (s serviceUnavailable) Is(e error) bool { + return IsServiceUnavailable(e) +} +func (t timeout) Is(e error) bool { + return IsTimeout(e) +} +func (u unauthorized) Is(e error) bool { + return IsUnauthorized(e) +} +func (m methodNotAllowed) Is(e error) bool { + return IsMethodNotAllowed(e) +} +func (f forbidden) Is(e error) bool { + return IsForbidden(e) +} +func (b badGateway) Is(e error) bool { + return IsBadGateway(e) +} +func (g gone) Is(e error) bool { + return IsGone(e) +} +func (c conflict) Is(e error) bool { + return IsConflict(e) +} +func (n notFound) Unwrap() error { + return n.Err.Unwrap() +} +func (n notValid) Unwrap() error { + return n.Err.Unwrap() +} +func (n notImplemented) Unwrap() error { + return n.Err.Unwrap() +} +func (n notSupported) Unwrap() error { + return n.Err.Unwrap() +} +func (b badRequest) Unwrap() error { + return b.Err.Unwrap() +} +func (s serviceUnavailable) Unwrap() error { + return s.Err.Unwrap() +} +func (t timeout) Unwrap() error { + return t.Err.Unwrap() +} +func (u unauthorized) Unwrap() error { + return u.Err.Unwrap() +} +func (m methodNotAllowed) Unwrap() error { + return m.Err.Unwrap() +} +func (f forbidden) Unwrap() error { + return f.Err.Unwrap() +} +func (b badGateway) Unwrap() error { + return b.Err.Unwrap() +} +func (g gone) Unwrap() error { + return g.Err.Unwrap() +} +func (c conflict) Unwrap() error { + return c.Err.Unwrap() +} + +// As is used by the errors.As() function to coerce the method's parameter to the one of the receiver +// +// if the underlying logic of the receiver's type can understand it. +// +// In this case we're converting a notFound to its underlying type Err. +func (n *notFound) As(err interface{}) bool { + switch x := err.(type) { + case **notFound: + *x = n + case *notFound: + *x = *n + case *Err: + return n.Err.As(x) + default: + return false + } + return true +} + +// As is used by the errors.As() function to coerce the method's parameter to the one of the receiver +// +// if the underlying logic of the receiver's type can understand it. +// +// In this case we're converting a notValid to its underlying type Err. +func (n *notValid) As(err interface{}) bool { + switch x := err.(type) { + case **notValid: + *x = n + case *notValid: + *x = *n + case *Err: + return n.Err.As(x) + default: + return false + } + return true +} + +// As is used by the errors.As() function to coerce the method's parameter to the one of the receiver +// +// if the underlying logic of the receiver's type can understand it. +// +// In this case we're converting a notImplemented to its underlying type Err. +func (n *notImplemented) As(err interface{}) bool { + switch x := err.(type) { + case **notImplemented: + *x = n + case *notImplemented: + *x = *n + case *Err: + return n.Err.As(x) + default: + return false + } + return true +} + +// As is used by the errors.As() function to coerce the method's parameter to the one of the receiver +// +// if the underlying logic of the receiver's type can understand it. +// +// In this case we're converting a notSupported to its underlying type Err. +func (n *notSupported) As(err interface{}) bool { + switch x := err.(type) { + case **notSupported: + *x = n + case *notSupported: + *x = *n + case *Err: + return n.Err.As(x) + default: + return false + } + return true +} + +// As is used by the errors.As() function to coerce the method's parameter to the one of the receiver +// +// if the underlying logic of the receiver's type can understand it. +// +// In this case we're converting a badRequest to its underlying type Err. +func (b *badRequest) As(err interface{}) bool { + switch x := err.(type) { + case **badRequest: + *x = b + case *badRequest: + *x = *b + case *Err: + return b.Err.As(x) + default: + return false + } + return true +} + +func (s *serviceUnavailable) As(err interface{}) bool { + switch x := err.(type) { + case **serviceUnavailable: + *x = s + case *serviceUnavailable: + *x = *s + case *Err: + return s.Err.As(x) + default: + return false + } + return true +} + +// As is used by the errors.As() function to coerce the method's parameter to the one of the receiver +// +// if the underlying logic of the receiver's type can understand it. +// +// In this case we're converting a timeout to its underlying type Err. +func (t *timeout) As(err interface{}) bool { + switch x := err.(type) { + case **timeout: + *x = t + case *timeout: + *x = *t + case *Err: + return t.Err.As(x) + default: + return false + } + return true +} + +// As is used by the errors.As() function to coerce the method's parameter to the one of the receiver +// +// if the underlying logic of the receiver's type can understand it. +// +// In this case we're converting a unauthorized to its underlying type Err. +func (u *unauthorized) As(err interface{}) bool { + switch x := err.(type) { + case **unauthorized: + *x = u + case *unauthorized: + *x = *u + case *Err: + return u.Err.As(x) + default: + return false + } + return true +} + +// As is used by the errors.As() function to coerce the method's parameter to the one of the receiver +// +// if the underlying logic of the receiver's type can understand it. +// +// In this case we're converting a methodNotAllowed to its underlying type Err. +func (m *methodNotAllowed) As(err interface{}) bool { + switch x := err.(type) { + case **methodNotAllowed: + *x = m + case *methodNotAllowed: + *x = *m + case *Err: + return m.Err.As(x) + default: + return false + } + return true +} + +// As is used by the errors.As() function to coerce the method's parameter to the one of the receiver +// +// if the underlying logic of the receiver's type can understand it. +// +// In this case we're converting a forbidden to its underlying type Err. +func (f *forbidden) As(err interface{}) bool { + switch x := err.(type) { + case **forbidden: + *x = f + case *forbidden: + *x = *f + case *Err: + return f.Err.As(x) + default: + return false + } + return true +} + +// As is used by the errors.As() function to coerce the method's parameter to the one of the receiver +// +// if the underlying logic of the receiver's type can understand it. +// +// In this case we're converting a badGateway to its underlying type Err. +func (b *badGateway) As(err interface{}) bool { + switch x := err.(type) { + case **badGateway: + *x = b + case *badGateway: + *x = *b + case *Err: + return b.Err.As(x) + default: + return false + } + return true +} + +// As is used by the errors.As() function to coerce the method's parameter to the one of the receiver +// +// if the underlying logic of the receiver's type can understand it. +// +// In this case we're converting a gone error to its underlying type Err. +func (g *gone) As(err interface{}) bool { + switch x := err.(type) { + case **gone: + *x = g + case *gone: + *x = *g + case *Err: + return g.Err.As(x) + default: + return false + } + return true +} + +// As is used by the errors.As() function to coerce the method's parameter to the one of the receiver +// +// if the underlying logic of the receiver's type can understand it. +// +// In this case we're converting a conflict error to its underlying type Err. +func (c *conflict) As(err interface{}) bool { + switch x := err.(type) { + case **conflict: + *x = c + case *conflict: + *x = *c + case *Err: + return c.Err.As(x) + default: + return false + } + return true +} + +// Challenge adds a challenge token to be added to the HTTP response +func (u *unauthorized) Challenge(c string) *unauthorized { + u.challenge = c + return u +} + +// Challenge returns the challenge of the err parameter if it's an unauthorized type error +func Challenge(err error) string { + un := unauthorized{} + if ok := As(err, &un); ok { + return un.challenge + } + return "" +} + +// ErrorHandlerFn +type ErrorHandlerFn func(http.ResponseWriter, *http.Request) error + +// ServeHTTP implements the http.Handler interface for the ItemHandlerFn type +func (h ErrorHandlerFn) ServeHTTP(w http.ResponseWriter, r *http.Request) { + var dat []byte + var status int + + if err := h(w, r); err != nil { + if IsRedirect(err) { + w.Header().Set("Location", Location(err)) + status = HttpStatus(err) + } else { + if status, dat = RenderErrors(r, err); status == 0 { + status = http.StatusInternalServerError + } + w.Header().Set("Content-Type", "application/json") + } + } + w.WriteHeader(status) + w.Write(dat) +} + +func Location(err error) string { + r := new(redirect) + if As(err, r) { + return r.u + } + return "" +} + +// HandleError is a generic method to return an HTTP handler that passes an error up the chain +func HandleError(e error) ErrorHandlerFn { + return func(w http.ResponseWriter, r *http.Request) error { + return e + } +} + +// NotFound is a generic method to return an 404 error HTTP handler that +var NotFound = ErrorHandlerFn(func(w http.ResponseWriter, r *http.Request) error { + return NotFoundf("%s not found", r.URL.Path) +}) + +type Http struct { + Code int `jsonld:"status,omitempty"` + Message string `jsonld:"message"` + Trace StackTrace `jsonld:"trace,omitempty"` +} + +func HttpErrors(err error) []Http { + https := make([]Http, 0) + + load := func(err error) Http { + var trace StackTrace + var msg string + switch e := err.(type) { + case *Err: + msg = e.Error() + if IncludeBacktrace { + trace = e.StackTrace() + } + default: + local := new(Err) + if ok := As(err, local); ok { + if IncludeBacktrace { + trace = local.StackTrace() + } + } + msg = err.Error() + } + + return Http{ + Message: msg, + Trace: trace, + Code: HttpStatus(err), + } + } + https = append(https, load(err)) + for { + if err = Unwrap(err); err != nil { + https = append(https, load(err)) + } else { + break + } + } + + return https +} + +func HttpStatus(e error) int { + if IsRedirect(e) { + r := new(redirect) + if As(e, r) { + return r.s + } + } + if IsBadRequest(e) { + return http.StatusBadRequest + } + if IsUnauthorized(e) { + return http.StatusUnauthorized + } + // http.StatusPaymentRequired + if IsForbidden(e) { + return http.StatusForbidden + } + if IsNotFound(e) { + return http.StatusNotFound + } + if IsMethodNotAllowed(e) { + return http.StatusMethodNotAllowed + } + if IsNotValid(e) { + return http.StatusNotAcceptable + } + // http.StatusProxyAuthRequired + // http.StatusRequestTimeout + if IsConflict(e) { + return http.StatusConflict + } + if IsGone(e) { + return http.StatusGone + } + // http.StatusLengthRequires + // http.StatusPreconditionFailed + // http.StatusRequestEntityTooLarge + // http.StatusRequestURITooLong + // TODO(marius): http.StatusUnsupportedMediaType + // http.StatusRequestedRangeNotSatisfiable + // http.StatusExpectationFailed + // http.StatusTeapot + // http.StatusMisdirectedRequest + // http.StatusUnprocessableEntity + // http.StatusLocked + // http.StatusFailedDependency + // http.StatusTooEarly + // http.StatusTooManyRequests + // http.StatusRequestHeaderFieldsTooLarge + // http.StatusUnavailableForLegalReason + + // http.StatusInternalServerError + if IsNotImplemented(e) { + return http.StatusNotImplemented + } + if IsBadGateway(e) { + return http.StatusBadGateway + } + if IsServiceUnavailable(e) { + return http.StatusServiceUnavailable + } + // http.StatusGatewayTimeout + if IsNotSupported(e) { + return http.StatusHTTPVersionNotSupported + } + + if IsTimeout(e) { + return http.StatusGatewayTimeout + } + + return 0 +} + +func errorFromStatus(status int) Error { + switch status { + case http.StatusBadRequest: + return new(badRequest) + case http.StatusUnauthorized: + return new(unauthorized) + //case http.StatusPaymentRequired: + case http.StatusForbidden: + return new(forbidden) + case http.StatusNotFound: + return new(notFound) + case http.StatusMethodNotAllowed: + return new(methodNotAllowed) + case http.StatusNotAcceptable: + return new(notValid) + //case http.StatusProxyAuthRequired: + //case http.StatusRequestTimeout: + case http.StatusConflict: + return new(conflict) + case http.StatusGone: + return new(gone) + //case http.StatusLengthRequired: + //case http.StatusPreconditionFailed: + //case http.StatusRequestEntityTooLarge: + //case http.StatusRequestURITooLong: + //case http.StatusUnsupportedMediaType: // TODO(marius): + //case http.StatusRequestedRangeNotSatisfiable: + //case http.StatusExpectationFailed: + //case http.StatusTeapot: + //case http.StatusMisdirectedRequest: + //case http.StatusUnprocessableEntity: + //case http.StatusLocked: + //case http.StatusFailedDependency: + //case http.StatusTooEarly: + //case http.StatusTooManyRequests: + //case http.StatusRequestHeaderFieldsTooLarge: + //case http.StatusUnavailableForLegalReason: + //case http.StatusInternalServerError: + case http.StatusNotImplemented: + return new(notImplemented) + case http.StatusBadGateway: + return new(badGateway) + case http.StatusServiceUnavailable: + return new(serviceUnavailable) + case http.StatusHTTPVersionNotSupported: + return new(notSupported) + case http.StatusGatewayTimeout: + return new(badGateway) + case http.StatusInternalServerError: + fallthrough + default: + return new(Err) + } +} + +// TODO(marius): get a proper ctxt +func ctxt(r *http.Request) jsonld.Context { + scheme := "http" + if r.TLS != nil { + scheme = "https" + } + return jsonld.Context{ + jsonld.ContextElement{ + Term: "errors", + IRI: jsonld.IRI(fmt.Sprintf("%s://%s/ns#errors", scheme, r.Host)), + }, + } +} + +// RenderErrors outputs the json encoded errors, with the JsonLD ctxt for current +func RenderErrors(r *http.Request, errs ...error) (int, []byte) { + errMap := make([]Http, 0) + var status int + for _, err := range errs { + more := HttpErrors(err) + errMap = append(errMap, more...) + status = HttpStatus(err) + } + var dat []byte + var err error + + m := struct { + Errors []Http `jsonld:"errors"` + }{Errors: errMap} + if dat, err = jsonld.WithContext(ctxt(r)).Marshal(m); err != nil { + return http.StatusInternalServerError, dat + } + return status, dat +} diff --git a/http_test.go b/http_test.go new file mode 100644 index 0000000..4c02dec --- /dev/null +++ b/http_test.go @@ -0,0 +1,1063 @@ +package errors + +import ( + "fmt" + "net/http" + "reflect" + "testing" +) + +func TestBadRequestf(t *testing.T) { + errMsg := "test" + e := BadRequestf(errMsg) + if e.m != errMsg { + t.Errorf("Invalid %T message %s, expected %s", e, e.m, errMsg) + } + if e.c != nil { + t.Errorf("Invalid %T parent error %T[%s], expected nil", e, e.c, e.c) + } +} + +func TestForbiddenf(t *testing.T) { + errMsg := "test" + e := Forbiddenf(errMsg) + if e.m != errMsg { + t.Errorf("Invalid %T message %s, expected %s", e, e.m, errMsg) + } + if e.c != nil { + t.Errorf("Invalid %T parent error %T[%s], expected nil", e, e.c, e.c) + } +} + +func TestMethodNotAllowedf(t *testing.T) { + errMsg := "test" + e := MethodNotAllowedf(errMsg) + if e.m != errMsg { + t.Errorf("Invalid %T message %s, expected %s", e, e.m, errMsg) + } + if e.c != nil { + t.Errorf("Invalid %T parent error %T[%s], expected nil", e, e.c, e.c) + } +} + +func TestMethodNotFoundf(t *testing.T) { + errMsg := "test" + e := NotFoundf(errMsg) + if e.m != errMsg { + t.Errorf("Invalid %T message %s, expected %s", e, e.m, errMsg) + } + if e.c != nil { + t.Errorf("Invalid %T parent error %T[%s], expected nil", e, e.c, e.c) + } +} + +func TestNotImplementedf(t *testing.T) { + errMsg := "test" + e := NotImplementedf(errMsg) + if e.m != errMsg { + t.Errorf("Invalid %T message %s, expected %s", e, e.m, errMsg) + } + if e.c != nil { + t.Errorf("Invalid %T parent error %T[%s], expected nil", e, e.c, e.c) + } +} + +func TestNotSupportedf(t *testing.T) { + errMsg := "test" + e := NotSupportedf(errMsg) + if e.m != errMsg { + t.Errorf("Invalid %T message %s, expected %s", e, e.m, errMsg) + } + if e.c != nil { + t.Errorf("Invalid %T parent error %T[%s], expected nil", e, e.c, e.c) + } +} + +func TestNotValidf(t *testing.T) { + errMsg := "test" + e := NotValidf(errMsg) + if e.m != errMsg { + t.Errorf("Invalid %T message %s, expected %s", e, e.m, errMsg) + } + if e.c != nil { + t.Errorf("Invalid %T parent error %T[%s], expected nil", e, e.c, e.c) + } +} + +func TestTimeoutf(t *testing.T) { + errMsg := "test" + e := Timeoutf(errMsg) + if e.m != errMsg { + t.Errorf("Invalid %T message %s, expected %s", e, e.m, errMsg) + } + if e.c != nil { + t.Errorf("Invalid %T parent error %T[%s], expected nil", e, e.c, e.c) + } +} + +func TestUnauthorizedf(t *testing.T) { + errMsg := "test" + e := Unauthorizedf(errMsg) + if e.m != errMsg { + t.Errorf("Invalid %T message %s, expected %s", e, e.m, errMsg) + } + if e.c != nil { + t.Errorf("Invalid %T parent error %T[%s], expected nil", e, e.c, e.c) + } +} + +func TestNewBadRequest(t *testing.T) { + errMsg := "test" + e := NewBadRequest(err, errMsg) + if e.m != errMsg { + t.Errorf("Invalid %T message %s, expected %s", e, e.m, errMsg) + } + if e.c != err { + t.Errorf("Invalid %T parent error %T[%s], expected %T[%s]", e, e.c, e.c, err, err) + } +} + +func TestNewForbidden(t *testing.T) { + errMsg := "test" + e := NewForbidden(err, errMsg) + if e.m != errMsg { + t.Errorf("Invalid %T message %s, expected %s", e, e.m, errMsg) + } + if e.c != err { + t.Errorf("Invalid %T parent error %T[%s], expected %T[%s]", e, e.c, e.c, err, err) + } +} + +func TestNewMethodNotAllowed(t *testing.T) { + errMsg := "test" + e := NewMethodNotAllowed(err, errMsg) + if e.m != errMsg { + t.Errorf("Invalid %T message %s, expected %s", e, e.m, errMsg) + } + if e.c != err { + t.Errorf("Invalid %T parent error %T[%s], expected %T[%s]", e, e.c, e.c, err, err) + } +} + +func TestNewNotFound(t *testing.T) { + errMsg := "test" + e := NewNotFound(err, errMsg) + if e.m != errMsg { + t.Errorf("Invalid %T message %s, expected %s", e, e.m, errMsg) + } + if e.c != err { + t.Errorf("Invalid %T parent error %T[%s], expected %T[%s]", e, e.c, e.c, err, err) + } +} + +func TestNewNotImplemented(t *testing.T) { + errMsg := "test" + e := NewNotImplemented(err, errMsg) + if e.m != errMsg { + t.Errorf("Invalid %T message %s, expected %s", e, e.m, errMsg) + } + if e.c != err { + t.Errorf("Invalid %T parent error %T[%s], expected %T[%s]", e, e.c, e.c, err, err) + } +} + +func TestNewNotSupported(t *testing.T) { + errMsg := "test" + e := NewNotSupported(err, errMsg) + if e.m != errMsg { + t.Errorf("Invalid %T message %s, expected %s", e, e.m, errMsg) + } + if e.c != err { + t.Errorf("Invalid %T parent error %T[%s], expected %T[%s]", e, e.c, e.c, err, err) + } +} + +func TestNewNotValid(t *testing.T) { + errMsg := "test" + e := NewNotValid(err, errMsg) + if e.m != errMsg { + t.Errorf("Invalid %T message %s, expected %s", e, e.m, errMsg) + } + if e.c != err { + t.Errorf("Invalid %T parent error %T[%s], expected %T[%s]", e, e.c, e.c, err, err) + } +} + +func TestNewTimeout(t *testing.T) { + errMsg := "test" + e := NewTimeout(err, errMsg) + if e.m != errMsg { + t.Errorf("Invalid %T message %s, expected %s", e, e.m, errMsg) + } + if e.c != err { + t.Errorf("Invalid %T parent error %T[%s], expected %T[%s]", e, e.c, e.c, err, err) + } +} + +func TestNewUnauthorized(t *testing.T) { + errMsg := "test" + e := NewUnauthorized(err, errMsg) + if e.m != errMsg { + t.Errorf("Invalid %T message %s, expected %s", e, e.m, errMsg) + } + if e.c != err { + t.Errorf("Invalid %T parent error %T[%s], expected %T[%s]", e, e.c, e.c, err, err) + } +} + +func TestIsBadRequest(t *testing.T) { + e := badRequest{} + e1 := &Err{} + if IsBadRequest(e1) { + t.Errorf("%T should not be a valid %T", e1, e) + } + e2 := &badRequest{} + if !IsBadRequest(e2) { + t.Errorf("%T should be a valid %T", e2, e) + } + e3 := badRequest{} + if !IsBadRequest(e3) { + t.Errorf("%T should be a valid %T", e3, e) + } +} + +func TestBadRequest_As(t *testing.T) { + e := badRequest{Err: Err{m: "test", c: fmt.Errorf("ttt")}, s: http.StatusBadRequest} + e0 := err + if e.As(e0) { + t.Errorf("%T should not be assertable as %T", e, e0) + } + e1 := Err{} + if !e.As(&e1) { + t.Errorf("%T should be assertable as %T", e, e1) + } + if e1.m != e.m { + t.Errorf("%T message should equal %T's, received %s, expected %s", e1, e, e1.m, e.m) + } + if !reflect.DeepEqual(e1.t, e.t) { + t.Errorf("%T trace should equal %T's, received %v, expected %v", e1, e, e1.t, e.t) + } + if e1.c != e.c { + t.Errorf("%T parent error should equal %T's, received %T[%s], expected %T[%s]", e1, e, e1.c, e1.c, e.c, e.c) + } + e2 := &badRequest{} + if !e.As(e2) { + t.Errorf("%T should be assertable as %T", e, e2) + } + if e2.m != e.m { + t.Errorf("%T message should equal %T's, received %s, expected %s", e2, e, e2.m, e.m) + } + if !reflect.DeepEqual(e2.t, e.t) { + t.Errorf("%T trace should equal %T's, received %v, expected %v", e2, e, e2.t, e.t) + } + if e2.c != e.c { + t.Errorf("%T parent error should equal %T's, received %T[%s], expected %T[%s]", e2, e, e2.c, e2.c, e.c, e.c) + } + e3 := e2 + if !e.As(&e3) { + t.Errorf("%T should be assertable as %T", e, e3) + } + if e3.m != e.m { + t.Errorf("%T message should equal %T's, received %s, expected %s", e3, e, e3.m, e.m) + } + if !reflect.DeepEqual(e3.t, e.t) { + t.Errorf("%T trace should equal %T's, received %v, expected %v", e3, e, e3.t, e.t) + } + if e3.c != e.c { + t.Errorf("%T parent error should equal %T's, received %T[%s], expected %T[%s]", e3, e, e3.c, e3.c, e.c, e.c) + } +} + +func TestBadRequest_Is(t *testing.T) { + e := badRequest{} + if e.Is(err) { + t.Errorf("%T should not be a valid %T", err, e) + } + e1 := &Err{} + if e.Is(e1) { + t.Errorf("%T should not be a valid %T", e1, e) + } + e2 := &badRequest{} + if !e.Is(e2) { + t.Errorf("%T should be a valid %T", e2, e) + } + e3 := badRequest{} + if !e.Is(e3) { + t.Errorf("%T should be a valid %T", e3, e) + } +} + +func TestBadRequest_Unwrap(t *testing.T) { + e := badRequest{Err: Err{c: fmt.Errorf("ttt")}, s: http.StatusBadRequest} + w := e.Unwrap() + if w != e.c { + t.Errorf("Unwrap() returned: %T[%s], expected: %T[%s]", w, w, e.c, e.c) + } +} + +func TestIsForbidden(t *testing.T) { + e := forbidden{} + e1 := &Err{} + if IsForbidden(e1) { + t.Errorf("%T should not be a valid %T", e1, e) + } + e2 := &forbidden{} + if !IsForbidden(e2) { + t.Errorf("%T should be a valid %T", e2, e) + } + e3 := forbidden{} + if !IsForbidden(e3) { + t.Errorf("%T should be a valid %T", e3, e) + } +} + +func TestForbidden_As(t *testing.T) { + e := forbidden{Err: Err{m: "test", c: fmt.Errorf("ttt")}, s: http.StatusForbidden} + e0 := err + if e.As(e0) { + t.Errorf("%T should not be assertable as %T", e, e0) + } + e1 := Err{} + if !e.As(&e1) { + t.Errorf("%T should be assertable as %T", e, e1) + } + if e1.m != e.m { + t.Errorf("%T message should equal %T's, received %s, expected %s", e1, e, e1.m, e.m) + } + if !reflect.DeepEqual(e1.t, e.t) { + t.Errorf("%T trace should equal %T's, received %v, expected %v", e1, e, e1.t, e.t) + } + if e1.c != e.c { + t.Errorf("%T parent error should equal %T's, received %T[%s], expected %T[%s]", e1, e, e1.c, e1.c, e.c, e.c) + } + e2 := &forbidden{} + if !e.As(e2) { + t.Errorf("%T should be assertable as %T", e, e2) + } + if e2.m != e.m { + t.Errorf("%T message should equal %T's, received %s, expected %s", e2, e, e2.m, e.m) + } + if !reflect.DeepEqual(e2.t, e.t) { + t.Errorf("%T trace should equal %T's, received %v, expected %v", e2, e, e2.t, e.t) + } + if e2.c != e.c { + t.Errorf("%T parent error should equal %T's, received %T[%s], expected %T[%s]", e2, e, e2.c, e2.c, e.c, e.c) + } + e3 := e2 + if !e.As(&e3) { + t.Errorf("%T should be assertable as %T", e, e3) + } + if e3.m != e.m { + t.Errorf("%T message should equal %T's, received %s, expected %s", e3, e, e3.m, e.m) + } + if !reflect.DeepEqual(e3.t, e.t) { + t.Errorf("%T trace should equal %T's, received %v, expected %v", e3, e, e3.t, e.t) + } + if e3.c != e.c { + t.Errorf("%T parent error should equal %T's, received %T[%s], expected %T[%s]", e3, e, e3.c, e3.c, e.c, e.c) + } +} + +func TestForbidden_Is(t *testing.T) { + e := forbidden{} + if e.Is(err) { + t.Errorf("%T should not be a valid %T", err, e) + } + e1 := &Err{} + if e.Is(e1) { + t.Errorf("%T should not be a valid %T", e1, e) + } + e2 := &forbidden{} + if !e.Is(e2) { + t.Errorf("%T should be a valid %T", e2, e) + } + e3 := forbidden{} + if !e.Is(e3) { + t.Errorf("%T should be a valid %T", e3, e) + } +} + +func TestForbidden_Unwrap(t *testing.T) { + e := forbidden{Err: Err{c: fmt.Errorf("ttt")}, s: http.StatusForbidden} + w := e.Unwrap() + if w != e.c { + t.Errorf("Unwrap() returned: %T[%s], expected: %T[%s]", w, w, e.c, e.c) + } +} + +func TestIsMethodNotAllowed(t *testing.T) { + e := methodNotAllowed{} + e1 := &Err{} + if IsMethodNotAllowed(e1) { + t.Errorf("%T should not be a valid %T", e1, e) + } + e2 := &methodNotAllowed{} + if !IsMethodNotAllowed(e2) { + t.Errorf("%T should be a valid %T", e2, e) + } + e3 := methodNotAllowed{} + if !IsMethodNotAllowed(e3) { + t.Errorf("%T should be a valid %T", e3, e) + } +} + +func TestMethodNotAllowed_As(t *testing.T) { + e := methodNotAllowed{Err: Err{m: "test", c: fmt.Errorf("ttt")}} + e0 := err + if e.As(e0) { + t.Errorf("%T should not be assertable as %T", e, e0) + } + e1 := Err{} + if !e.As(&e1) { + t.Errorf("%T should be assertable as %T", e, e1) + } + if e1.m != e.m { + t.Errorf("%T message should equal %T's, received %s, expected %s", e1, e, e1.m, e.m) + } + if !reflect.DeepEqual(e1.t, e.t) { + t.Errorf("%T trace should equal %T's, received %v, expected %v", e1, e, e1.t, e.t) + } + if e1.c != e.c { + t.Errorf("%T parent error should equal %T's, received %T[%s], expected %T[%s]", e1, e, e1.c, e1.c, e.c, e.c) + } + e2 := &methodNotAllowed{} + if !e.As(e2) { + t.Errorf("%T should be assertable as %T", e, e2) + } + if e2.m != e.m { + t.Errorf("%T message should equal %T's, received %s, expected %s", e2, e, e2.m, e.m) + } + if !reflect.DeepEqual(e2.t, e.t) { + t.Errorf("%T trace should equal %T's, received %v, expected %v", e2, e, e2.t, e.t) + } + if e2.c != e.c { + t.Errorf("%T parent error should equal %T's, received %T[%s], expected %T[%s]", e2, e, e2.c, e2.c, e.c, e.c) + } + e3 := e2 + if !e.As(&e3) { + t.Errorf("%T should be assertable as %T", e, e3) + } + if e3.m != e.m { + t.Errorf("%T message should equal %T's, received %s, expected %s", e3, e, e3.m, e.m) + } + if !reflect.DeepEqual(e3.t, e.t) { + t.Errorf("%T trace should equal %T's, received %v, expected %v", e3, e, e3.t, e.t) + } + if e3.c != e.c { + t.Errorf("%T parent error should equal %T's, received %T[%s], expected %T[%s]", e3, e, e3.c, e3.c, e.c, e.c) + } +} + +func TestMethodNotAllowed_Is(t *testing.T) { + e := methodNotAllowed{} + if e.Is(err) { + t.Errorf("%T should not be a valid %T", err, e) + } + e1 := &Err{} + if e.Is(e1) { + t.Errorf("%T should not be a valid %T", e1, e) + } + e2 := &methodNotAllowed{} + if !e.Is(e2) { + t.Errorf("%T should be a valid %T", e2, e) + } + e3 := methodNotAllowed{} + if !e.Is(e3) { + t.Errorf("%T should be a valid %T", e3, e) + } +} + +func TestMethodNotAllowed_Unwrap(t *testing.T) { + e := methodNotAllowed{Err: Err{c: fmt.Errorf("ttt")}} + w := e.Unwrap() + if w != e.c { + t.Errorf("Unwrap() returned: %T[%s], expected: %T[%s]", w, w, e.c, e.c) + } +} + +func TestIsNotFound(t *testing.T) { + e := notFound{} + e1 := &Err{} + if IsNotFound(e1) { + t.Errorf("%T should not be a valid %T", e1, e) + } + e2 := ¬Found{} + if !IsNotFound(e2) { + t.Errorf("%T should be a valid %T", e2, e) + } + e3 := notFound{} + if !IsNotFound(e3) { + t.Errorf("%T should be a valid %T", e3, e) + } +} + +func TestNotFound_As(t *testing.T) { + e := notFound{Err: Err{m: "test", c: fmt.Errorf("ttt")}} + e0 := err + if e.As(e0) { + t.Errorf("%T should not be assertable as %T", e, e0) + } + e1 := Err{} + if !e.As(&e1) { + t.Errorf("%T should be assertable as %T", e, e1) + } + if e1.m != e.m { + t.Errorf("%T message should equal %T's, received %s, expected %s", e1, e, e1.m, e.m) + } + if !reflect.DeepEqual(e1.t, e.t) { + t.Errorf("%T trace should equal %T's, received %v, expected %v", e1, e, e1.t, e.t) + } + if e1.c != e.c { + t.Errorf("%T parent error should equal %T's, received %T[%s], expected %T[%s]", e1, e, e1.c, e1.c, e.c, e.c) + } + e2 := ¬Found{} + if !e.As(e2) { + t.Errorf("%T should be assertable as %T", e, e2) + } + if e2.m != e.m { + t.Errorf("%T message should equal %T's, received %s, expected %s", e2, e, e2.m, e.m) + } + if !reflect.DeepEqual(e2.t, e.t) { + t.Errorf("%T trace should equal %T's, received %v, expected %v", e2, e, e2.t, e.t) + } + if e2.c != e.c { + t.Errorf("%T parent error should equal %T's, received %T[%s], expected %T[%s]", e2, e, e2.c, e2.c, e.c, e.c) + } + e3 := e2 + if !e.As(&e3) { + t.Errorf("%T should be assertable as %T", e, e3) + } + if e3.m != e.m { + t.Errorf("%T message should equal %T's, received %s, expected %s", e3, e, e3.m, e.m) + } + if !reflect.DeepEqual(e3.t, e.t) { + t.Errorf("%T trace should equal %T's, received %v, expected %v", e3, e, e3.t, e.t) + } + if e3.c != e.c { + t.Errorf("%T parent error should equal %T's, received %T[%s], expected %T[%s]", e3, e, e3.c, e3.c, e.c, e.c) + } +} + +func TestNotFound_Is(t *testing.T) { + e := notFound{} + if e.Is(err) { + t.Errorf("%T should not be a valid %T", err, e) + } + e1 := &Err{} + if e.Is(e1) { + t.Errorf("%T should not be a valid %T", e1, e) + } + e2 := ¬Found{} + if !e.Is(e2) { + t.Errorf("%T should be a valid %T", e2, e) + } + e3 := notFound{} + if !e.Is(e3) { + t.Errorf("%T should be a valid %T", e3, e) + } +} + +func TestNotFound_Unwrap(t *testing.T) { + e := notFound{Err: Err{c: fmt.Errorf("ttt")}} + w := e.Unwrap() + if w != e.c { + t.Errorf("Unwrap() returned: %T[%s], expected: %T[%s]", w, w, e.c, e.c) + } +} + +func TestIsNotImplemented(t *testing.T) { + e := notImplemented{} + e1 := &Err{} + if IsNotImplemented(e1) { + t.Errorf("%T should not be a valid %T", e1, e) + } + e2 := ¬Implemented{} + if !IsNotImplemented(e2) { + t.Errorf("%T should be a valid %T", e2, e) + } + e3 := notImplemented{} + if !IsNotImplemented(e3) { + t.Errorf("%T should be a valid %T", e3, e) + } +} + +func TestNotImplemented_As(t *testing.T) { + e := notImplemented{Err: Err{m: "test", c: fmt.Errorf("ttt")}} + e0 := err + if e.As(e0) { + t.Errorf("%T should not be assertable as %T", e, e0) + } + e1 := Err{} + if !e.As(&e1) { + t.Errorf("%T should be assertable as %T", e, e1) + } + if e1.m != e.m { + t.Errorf("%T message should equal %T's, received %s, expected %s", e1, e, e1.m, e.m) + } + if !reflect.DeepEqual(e1.t, e.t) { + t.Errorf("%T trace should equal %T's, received %v, expected %v", e1, e, e1.t, e.t) + } + if e1.c != e.c { + t.Errorf("%T parent error should equal %T's, received %T[%s], expected %T[%s]", e1, e, e1.c, e1.c, e.c, e.c) + } + e2 := ¬Implemented{} + if !e.As(e2) { + t.Errorf("%T should be assertable as %T", e, e2) + } + if e2.m != e.m { + t.Errorf("%T message should equal %T's, received %s, expected %s", e2, e, e2.m, e.m) + } + if !reflect.DeepEqual(e2.t, e.t) { + t.Errorf("%T trace should equal %T's, received %v, expected %v", e2, e, e2.t, e.t) + } + if e2.c != e.c { + t.Errorf("%T parent error should equal %T's, received %T[%s], expected %T[%s]", e2, e, e2.c, e2.c, e.c, e.c) + } + e3 := e2 + if !e.As(&e3) { + t.Errorf("%T should be assertable as %T", e, e3) + } + if e3.m != e.m { + t.Errorf("%T message should equal %T's, received %s, expected %s", e3, e, e3.m, e.m) + } + if !reflect.DeepEqual(e3.t, e.t) { + t.Errorf("%T trace should equal %T's, received %v, expected %v", e3, e, e3.t, e.t) + } + if e3.c != e.c { + t.Errorf("%T parent error should equal %T's, received %T[%s], expected %T[%s]", e3, e, e3.c, e3.c, e.c, e.c) + } +} + +func TestNotImplemented_Is(t *testing.T) { + e := notImplemented{} + if e.Is(err) { + t.Errorf("%T should not be a valid %T", err, e) + } + e1 := &Err{} + if e.Is(e1) { + t.Errorf("%T should not be a valid %T", e1, e) + } + e2 := ¬Implemented{} + if !e.Is(e2) { + t.Errorf("%T should be a valid %T", e2, e) + } + e3 := notImplemented{} + if !e.Is(e3) { + t.Errorf("%T should be a valid %T", e3, e) + } +} + +func TestNotImplemented_Unwrap(t *testing.T) { + e := notImplemented{Err: Err{c: fmt.Errorf("ttt")}} + w := e.Unwrap() + if w != e.c { + t.Errorf("Unwrap() returned: %T[%s], expected: %T[%s]", w, w, e.c, e.c) + } +} + +func TestIsNotSupported(t *testing.T) { + e := notSupported{} + e1 := &Err{} + if IsNotSupported(e1) { + t.Errorf("%T should not be a valid %T", e1, e) + } + e2 := ¬Supported{} + if !IsNotSupported(e2) { + t.Errorf("%T should be a valid %T", e2, e) + } + e3 := notSupported{} + if !IsNotSupported(e3) { + t.Errorf("%T should be a valid %T", e3, e) + } +} + +func TestNotSupported_As(t *testing.T) { + e := notSupported{Err: Err{m: "test", c: fmt.Errorf("ttt")}} + e0 := err + if e.As(e0) { + t.Errorf("%T should not be assertable as %T", e, e0) + } + e1 := Err{} + if !e.As(&e1) { + t.Errorf("%T should be assertable as %T", e, e1) + } + if e1.m != e.m { + t.Errorf("%T message should equal %T's, received %s, expected %s", e1, e, e1.m, e.m) + } + if !reflect.DeepEqual(e1.t, e.t) { + t.Errorf("%T trace should equal %T's, received %v, expected %v", e1, e, e1.t, e.t) + } + if e1.c != e.c { + t.Errorf("%T parent error should equal %T's, received %T[%s], expected %T[%s]", e1, e, e1.c, e1.c, e.c, e.c) + } + e2 := ¬Supported{} + if !e.As(e2) { + t.Errorf("%T should be assertable as %T", e, e2) + } + if e2.m != e.m { + t.Errorf("%T message should equal %T's, received %s, expected %s", e2, e, e2.m, e.m) + } + if !reflect.DeepEqual(e2.t, e.t) { + t.Errorf("%T trace should equal %T's, received %v, expected %v", e2, e, e2.t, e.t) + } + if e2.c != e.c { + t.Errorf("%T parent error should equal %T's, received %T[%s], expected %T[%s]", e2, e, e2.c, e2.c, e.c, e.c) + } + e3 := e2 + if !e.As(&e3) { + t.Errorf("%T should be assertable as %T", e, e3) + } + if e3.m != e.m { + t.Errorf("%T message should equal %T's, received %s, expected %s", e3, e, e3.m, e.m) + } + if !reflect.DeepEqual(e3.t, e.t) { + t.Errorf("%T trace should equal %T's, received %v, expected %v", e3, e, e3.t, e.t) + } + if e3.c != e.c { + t.Errorf("%T parent error should equal %T's, received %T[%s], expected %T[%s]", e3, e, e3.c, e3.c, e.c, e.c) + } +} + +func TestNotSupported_Is(t *testing.T) { + e := notSupported{} + if e.Is(err) { + t.Errorf("%T should not be a valid %T", err, e) + } + e1 := &Err{} + if e.Is(e1) { + t.Errorf("%T should not be a valid %T", e1, e) + } + e2 := ¬Supported{} + if !e.Is(e2) { + t.Errorf("%T should be a valid %T", e2, e) + } + e3 := notSupported{} + if !e.Is(e3) { + t.Errorf("%T should be a valid %T", e3, e) + } +} + +func TestNotSupported_Unwrap(t *testing.T) { + e := notSupported{Err: Err{c: fmt.Errorf("ttt")}} + w := e.Unwrap() + if w != e.c { + t.Errorf("Unwrap() returned: %T[%s], expected: %T[%s]", w, w, e.c, e.c) + } +} + +func TestIsNotValid(t *testing.T) { + e := notValid{} + e1 := &Err{} + if IsNotValid(e1) { + t.Errorf("%T should not be a valid %T", e1, e) + } + e2 := ¬Valid{} + if !IsNotValid(e2) { + t.Errorf("%T should be a valid %T", e2, e) + } + e3 := notValid{} + if !IsNotValid(e3) { + t.Errorf("%T should be a valid %T", e3, e) + } +} + +func TestNotValid_As(t *testing.T) { + e := notValid{Err: Err{m: "test", c: fmt.Errorf("ttt")}} + e0 := err + if e.As(e0) { + t.Errorf("%T should not be assertable as %T", e, e0) + } + e1 := Err{} + if !e.As(&e1) { + t.Errorf("%T should be assertable as %T", e, e1) + } + if e1.m != e.m { + t.Errorf("%T message should equal %T's, received %s, expected %s", e1, e, e1.m, e.m) + } + if !reflect.DeepEqual(e1.t, e.t) { + t.Errorf("%T trace should equal %T's, received %v, expected %v", e1, e, e1.t, e.t) + } + if e1.c != e.c { + t.Errorf("%T parent error should equal %T's, received %T[%s], expected %T[%s]", e1, e, e1.c, e1.c, e.c, e.c) + } + e2 := ¬Valid{} + if !e.As(e2) { + t.Errorf("%T should be assertable as %T", e, e2) + } + if e2.m != e.m { + t.Errorf("%T message should equal %T's, received %s, expected %s", e2, e, e2.m, e.m) + } + if !reflect.DeepEqual(e2.t, e.t) { + t.Errorf("%T trace should equal %T's, received %v, expected %v", e2, e, e2.t, e.t) + } + if e2.c != e.c { + t.Errorf("%T parent error should equal %T's, received %T[%s], expected %T[%s]", e2, e, e2.c, e2.c, e.c, e.c) + } + e3 := e2 + if !e.As(&e3) { + t.Errorf("%T should be assertable as %T", e, e3) + } + if e3.m != e.m { + t.Errorf("%T message should equal %T's, received %s, expected %s", e3, e, e3.m, e.m) + } + if !reflect.DeepEqual(e3.t, e.t) { + t.Errorf("%T trace should equal %T's, received %v, expected %v", e3, e, e3.t, e.t) + } + if e3.c != e.c { + t.Errorf("%T parent error should equal %T's, received %T[%s], expected %T[%s]", e3, e, e3.c, e3.c, e.c, e.c) + } +} + +func TestNotValid_Is(t *testing.T) { + e := notValid{} + if e.Is(err) { + t.Errorf("%T should not be a valid %T", err, e) + } + e1 := &Err{} + if e.Is(e1) { + t.Errorf("%T should not be a valid %T", e1, e) + } + e2 := ¬Valid{} + if !e.Is(e2) { + t.Errorf("%T should be a valid %T", e2, e) + } + e3 := notValid{} + if !e.Is(e3) { + t.Errorf("%T should be a valid %T", e3, e) + } +} + +func TestNotValid_Unwrap(t *testing.T) { + e := notValid{Err: Err{c: fmt.Errorf("ttt")}} + w := e.Unwrap() + if w != e.c { + t.Errorf("Unwrap() returned: %T[%s], expected: %T[%s]", w, w, e.c, e.c) + } +} + +func TestIsTimeout(t *testing.T) { + e := timeout{} + e1 := &Err{} + if IsTimeout(e1) { + t.Errorf("%T should not be a valid %T", e1, e) + } + e2 := &timeout{} + if !IsTimeout(e2) { + t.Errorf("%T should be a valid %T", e2, e) + } + e3 := timeout{} + if !IsTimeout(e3) { + t.Errorf("%T should be a valid %T", e3, e) + } +} + +func TestTimeout_As(t *testing.T) { + e := timeout{Err: Err{m: "test", c: fmt.Errorf("ttt")}} + e0 := err + if e.As(e0) { + t.Errorf("%T should not be assertable as %T", e, e0) + } + e1 := Err{} + if !e.As(&e1) { + t.Errorf("%T should be assertable as %T", e, e1) + } + if e1.m != e.m { + t.Errorf("%T message should equal %T's, received %s, expected %s", e1, e, e1.m, e.m) + } + if !reflect.DeepEqual(e1.t, e.t) { + t.Errorf("%T trace should equal %T's, received %v, expected %v", e1, e, e1.t, e.t) + } + if e1.c != e.c { + t.Errorf("%T parent error should equal %T's, received %T[%s], expected %T[%s]", e1, e, e1.c, e1.c, e.c, e.c) + } + e2 := &timeout{} + if !e.As(e2) { + t.Errorf("%T should be assertable as %T", e, e2) + } + if e2.m != e.m { + t.Errorf("%T message should equal %T's, received %s, expected %s", e2, e, e2.m, e.m) + } + if !reflect.DeepEqual(e2.t, e.t) { + t.Errorf("%T trace should equal %T's, received %v, expected %v", e2, e, e2.t, e.t) + } + if e2.c != e.c { + t.Errorf("%T parent error should equal %T's, received %T[%s], expected %T[%s]", e2, e, e2.c, e2.c, e.c, e.c) + } + e3 := e2 + if !e.As(&e3) { + t.Errorf("%T should be assertable as %T", e, e3) + } + if e3.m != e.m { + t.Errorf("%T message should equal %T's, received %s, expected %s", e3, e, e3.m, e.m) + } + if !reflect.DeepEqual(e3.t, e.t) { + t.Errorf("%T trace should equal %T's, received %v, expected %v", e3, e, e3.t, e.t) + } + if e3.c != e.c { + t.Errorf("%T parent error should equal %T's, received %T[%s], expected %T[%s]", e3, e, e3.c, e3.c, e.c, e.c) + } +} + +func TestTimeout_Is(t *testing.T) { + e := timeout{} + if e.Is(err) { + t.Errorf("%T should not be a valid %T", err, e) + } + e1 := &Err{} + if e.Is(e1) { + t.Errorf("%T should not be a valid %T", e1, e) + } + e2 := &timeout{} + if !e.Is(e2) { + t.Errorf("%T should be a valid %T", e2, e) + } + e3 := timeout{} + if !e.Is(e3) { + t.Errorf("%T should be a valid %T", e3, e) + } +} + +func TestTimeout_Unwrap(t *testing.T) { + e := timeout{Err: Err{c: fmt.Errorf("ttt")}} + w := e.Unwrap() + if w != e.c { + t.Errorf("Unwrap() returned: %T[%s], expected: %T[%s]", w, w, e.c, e.c) + } +} + +func TestIsUnauthorized(t *testing.T) { + e := unauthorized{} + e1 := &Err{} + if IsUnauthorized(e1) { + t.Errorf("%T should not be a valid %T", e1, e) + } + e2 := &unauthorized{} + if !IsUnauthorized(e2) { + t.Errorf("%T should be a valid %T", e2, e) + } + e3 := unauthorized{} + if !IsUnauthorized(e3) { + t.Errorf("%T should be a valid %T", e3, e) + } +} + +func TestUnauthorized_As(t *testing.T) { + e := unauthorized{Err: Err{m: "test", c: fmt.Errorf("ttt")}} + e0 := err + if e.As(e0) { + t.Errorf("%T should not be assertable as %T", e, e0) + } + e1 := Err{} + if !e.As(&e1) { + t.Errorf("%T should be assertable as %T", e, e1) + } + if e1.m != e.m { + t.Errorf("%T message should equal %T's, received %s, expected %s", e1, e, e1.m, e.m) + } + if !reflect.DeepEqual(e1.t, e.t) { + t.Errorf("%T trace should equal %T's, received %v, expected %v", e1, e, e1.t, e.t) + } + if e1.c != e.c { + t.Errorf("%T parent error should equal %T's, received %T[%s], expected %T[%s]", e1, e, e1.c, e1.c, e.c, e.c) + } + e2 := &unauthorized{} + if !e.As(e2) { + t.Errorf("%T should be assertable as %T", e, e2) + } + if e2.m != e.m { + t.Errorf("%T message should equal %T's, received %s, expected %s", e2, e, e2.m, e.m) + } + if !reflect.DeepEqual(e2.t, e.t) { + t.Errorf("%T trace should equal %T's, received %v, expected %v", e2, e, e2.t, e.t) + } + if e2.c != e.c { + t.Errorf("%T parent error should equal %T's, received %T[%s], expected %T[%s]", e2, e, e2.c, e2.c, e.c, e.c) + } + e3 := e2 + if !e.As(&e3) { + t.Errorf("%T should be assertable as %T", e, e3) + } + if e3.m != e.m { + t.Errorf("%T message should equal %T's, received %s, expected %s", e3, e, e3.m, e.m) + } + if !reflect.DeepEqual(e3.t, e.t) { + t.Errorf("%T trace should equal %T's, received %v, expected %v", e3, e, e3.t, e.t) + } + if e3.c != e.c { + t.Errorf("%T parent error should equal %T's, received %T[%s], expected %T[%s]", e3, e, e3.c, e3.c, e.c, e.c) + } +} + +func TestUnauthorized_Is(t *testing.T) { + e := unauthorized{} + if e.Is(err) { + t.Errorf("%T should not be a valid %T", err, e) + } + e1 := &Err{} + if e.Is(e1) { + t.Errorf("%T should not be a valid %T", e1, e) + } + e2 := &unauthorized{} + if !e.Is(e2) { + t.Errorf("%T should be a valid %T", e2, e) + } + e3 := unauthorized{} + if !e.Is(e3) { + t.Errorf("%T should be a valid %T", e3, e) + } +} + +func TestUnauthorized_Unwrap(t *testing.T) { + e := unauthorized{Err: Err{c: fmt.Errorf("ttt")}} + w := e.Unwrap() + if w != e.c { + t.Errorf("Unwrap() returned: %T[%s], expected: %T[%s]", w, w, e.c, e.c) + } +} + +func TestUnauthorized_Challenge(t *testing.T) { + e := &unauthorized{Err: Err{c: fmt.Errorf("ttt")}} + msgChallenge := "test challenge" + e = e.Challenge(msgChallenge) + + if e.challenge != msgChallenge { + t.Errorf("Invalid challenge message for %T %s, expected %s", e, e.challenge, msgChallenge) + } +} + +func TestChallenge(t *testing.T) { + var errI error + msgChallenge := "test challenge" + e := unauthorized{ + Err: Err{c: fmt.Errorf("ttt")}, + challenge: msgChallenge, + } + errI = &e + if e.challenge != msgChallenge { + t.Errorf("Invalid challenge message for %T %s, expected %s", e, e.challenge, msgChallenge) + } + ch := Challenge(errI) + if ch != msgChallenge { + t.Errorf("Invalid challenge message for %T %s, expected %s", errI, ch, msgChallenge) + } +} + +func TestHttpErrors(t *testing.T) { + t.Skipf("TODO") +} + +func TestErrorHandlerFn_ServeHTTP(t *testing.T) { + t.Skipf("TODO") +} + +func TestHandleError(t *testing.T) { + t.Skipf("TODO") +} + +func TestRenderErrors(t *testing.T) { + t.Skipf("TODO") +} + +func TestWrapWithStatus(t *testing.T) { + t.Skipf("TODO") +} diff --git a/join.go b/join.go new file mode 100644 index 0000000..add3b6c --- /dev/null +++ b/join.go @@ -0,0 +1,44 @@ +package errors + +import "unsafe" + +func Join(errs ...error) error { + n := 0 + for _, err := range errs { + if err != nil { + n++ + } + } + if n == 0 { + return nil + } + e := &joinError{ + errs: make([]error, 0, n), + } + for _, err := range errs { + if err != nil { + e.errs = append(e.errs, err) + } + } + return e +} + +type joinError struct { + errs []error +} + +func (e *joinError) Error() string { + // Since Join returns nil if every value in errs is nil, + // e.errs cannot be empty. + if len(e.errs) == 1 { + return e.errs[0].Error() + } + + b := []byte(e.errs[0].Error()) + for _, err := range e.errs[1:] { + b = append(b, ':', ' ') + b = append(b, err.Error()...) + } + // At this point, b has at least one byte '\n'. + return unsafe.String(&b[0], len(b)) +} diff --git a/redirects.go b/redirects.go new file mode 100644 index 0000000..fd7da83 --- /dev/null +++ b/redirects.go @@ -0,0 +1,98 @@ +package errors + +import ( + "fmt" + "net/http" +) + +func SeeOther(u string) *redirect { + return &redirect{s: http.StatusSeeOther, u: u} +} +func NewSeeOther(e error, u string) *redirect { + return &redirect{c: e, s: http.StatusSeeOther, u: u} +} +func Found(u string) *redirect { + return &redirect{s: http.StatusFound, u: u} +} +func NewFound(e error, u string) *redirect { + return &redirect{c: e, s: http.StatusFound, u: u} +} +func MovedPermanently(u string) *redirect { + return &redirect{s: http.StatusMovedPermanently, u: u} +} +func NewMovedPermanently(e error, u string) *redirect { + return &redirect{c: e, s: http.StatusMovedPermanently, u: u} +} +func NotModified(u string) *redirect { + return &redirect{s: http.StatusNotModified, u: u} +} +func NewNotModified(e error, u string) *redirect { + return &redirect{c: e, s: http.StatusNotModified, u: u} +} +func TemporaryRedirect(u string) *redirect { + return &redirect{s: http.StatusTemporaryRedirect, u: u} +} +func NewTemporaryRedirect(e error, u string) *redirect { + return &redirect{c: e, s: http.StatusTemporaryRedirect, u: u} +} +func PermanentRedirect(u string) *redirect { + return &redirect{s: http.StatusPermanentRedirect, u: u} +} +func NewPermanentRedirect(e error, u string) *redirect { + return &redirect{c: e, s: http.StatusPermanentRedirect, u: u} +} + +type redirect struct { + c error + u string + s int +} + +func (r redirect) Error() string { + if r.c == nil { + return fmt.Sprintf("Redirect %d to %s", r.s, r.u) + } + return fmt.Sprintf("Redirect %d to %s: %s", r.s, r.u, r.c) +} + +// As is used by the errors.As() function to coerce the method's parameter to the one of the receiver +// +// if the underlying logic of the receiver's type can understand it. +// +// In this case we're converting a forbidden to its underlying type Err. +func (r *redirect) As(err interface{}) bool { + switch x := err.(type) { + case **redirect: + *x = r + case *redirect: + *x = *r + default: + return false + } + return true +} + +func (r redirect) Is(e error) bool { + rr := redirect{} + return As(e, &rr) && r.s == rr.s +} + +func IsRedirect(e error) bool { + _, okp := e.(*redirect) + _, oks := e.(redirect) + return okp || oks || As(e, &redirect{}) +} + +func IsNotModified(e error) bool { + ep, okp := e.(*redirect) + es, oks := e.(redirect) + + ae := redirect{} + return (okp && ep.s == http.StatusNotModified) || + (oks && es.s == http.StatusNotModified) || + (As(e, &ae) && ae.s == http.StatusNotModified) +} + +func (r redirect) Unwrap() error { + return r.Unwrap() +} diff --git a/stack.go b/stack.go new file mode 100644 index 0000000..d7e0070 --- /dev/null +++ b/stack.go @@ -0,0 +1,219 @@ +package errors + +import ( + "bytes" + "fmt" + "io" + "path" + "runtime" + "strconv" + "strings" +) + +// Frame represents a program counter inside a stack frame. +// For historical reasons if Frame is interpreted as a uintptr +// its value represents the program counter + 1. +type Frame uintptr + +// pc returns the program counter for this frame; +// multiple frames may have the same PC value. +func (f Frame) pc() uintptr { return uintptr(f) - 1 } + +// file returns the full path to the file that contains the +// function for this Frame's pc. +func (f Frame) file() string { + fn := runtime.FuncForPC(f.pc()) + if fn == nil { + return "unknown" + } + file, _ := fn.FileLine(f.pc()) + return file +} + +// line returns the line number of source code of the +// function for this Frame's pc. +func (f Frame) line() int { + fn := runtime.FuncForPC(f.pc()) + if fn == nil { + return 0 + } + _, line := fn.FileLine(f.pc()) + return line +} + +// name returns the name of this function, if known. +func (f Frame) name() string { + fn := runtime.FuncForPC(f.pc()) + if fn == nil { + return "unknown" + } + return fn.Name() +} + +// Format formats the frame according to the fmt.Formatter interface. +// +// %s source file +// %d source line +// %n function name +// %v equivalent to %s:%d +// +// Format accepts flags that alter the printing of some verbs, as follows: +// +// %+s function name and path of source file relative to the compile time +// GOPATH separated by \n\t (\n\t) +// %+v equivalent to %+s:%d +func (f Frame) Format(s fmt.State, verb rune) { + switch verb { + case 's': + switch { + case s.Flag('+'): + io.WriteString(s, f.name()) + io.WriteString(s, "\n\t") + io.WriteString(s, f.file()) + default: + io.WriteString(s, path.Base(f.file())) + } + case 'd': + io.WriteString(s, strconv.Itoa(f.line())) + case 'n': + io.WriteString(s, funcname(f.name())) + case 'v': + f.Format(s, 's') + io.WriteString(s, ":") + f.Format(s, 'd') + } +} + +// MarshalText formats a stacktrace Frame as a text string. The output is the +// same as that of fmt.Sprintf("%+v", f), but without newlines or tabs. +func (f Frame) MarshalText() ([]byte, error) { + name := f.name() + if name == "unknown" { + return []byte(name), nil + } + return []byte(fmt.Sprintf("%s %s:%d", name, f.file(), f.line())), nil +} + +func (f Frame) MarshalJSON() ([]byte, error) { + name := f.name() + w := bytes.NewBuffer(nil) + if name == "unknown" { + w.WriteByte('"') + w.WriteString(name) + w.WriteByte('"') + return w.Bytes(), nil + } + w.WriteByte('{') + w.WriteString("\"function\": ") + w.WriteByte('"') + w.WriteString(funcname(name)) + w.WriteByte('"') + w.WriteByte(',') + w.WriteString("\"file\": ") + w.WriteByte('"') + w.WriteString(f.file()) + w.WriteByte('"') + w.WriteByte(',') + w.WriteString("\"line\": ") + w.WriteString(fmt.Sprintf("%d", f.line())) + w.WriteByte('}') + return w.Bytes(), nil +} + +// StackTrace is stack of Frames from innermost (newest) to outermost (oldest). +type StackTrace []Frame + +// Format formats the stack of Frames according to the fmt.Formatter interface. +// +// %s lists source files for each Frame in the stack +// %v lists the source file and line number for each Frame in the stack +// +// Format accepts flags that alter the printing of some verbs, as follows: +// +// %+v Prints filename, function, and line number for each Frame in the stack. +func (st StackTrace) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + switch { + case s.Flag('+'): + for _, f := range st { + io.WriteString(s, "\n") + f.Format(s, verb) + } + case s.Flag('#'): + fmt.Fprintf(s, "%#v", []Frame(st)) + default: + st.formatSlice(s, verb) + } + case 's': + st.formatSlice(s, verb) + } +} + +func (st StackTrace) MarshalJSON() ([]byte, error) { + w := bytes.NewBuffer(nil) + w.WriteByte('[') + for i, f := range st { + b, _ := f.MarshalJSON() + w.Write(b) + if i == len(st)-1 { + break + } + w.WriteByte(',') + } + w.WriteByte(']') + return w.Bytes(), nil +} + +// formatSlice will format this StackTrace into the given buffer as a slice of +// Frame, only valid when called with '%s' or '%v'. +func (st StackTrace) formatSlice(s fmt.State, verb rune) { + io.WriteString(s, "[") + for i, f := range st { + if i > 0 { + io.WriteString(s, " ") + } + f.Format(s, verb) + } + io.WriteString(s, "]") +} + +// stack represents a stack of program counters. +type stack []uintptr + +func (s *stack) Format(st fmt.State, verb rune) { + switch verb { + case 'v': + switch { + case st.Flag('+'): + for _, pc := range *s { + f := Frame(pc) + fmt.Fprintf(st, "\n%+v", f) + } + } + } +} + +func (s *stack) StackTrace() StackTrace { + f := make([]Frame, len(*s)) + for i := 0; i < len(f); i++ { + f[i] = Frame((*s)[i]) + } + return f +} + +func callers(skip int) *stack { + const depth = 32 + var pcs [depth]uintptr + n := runtime.Callers(skip+3, pcs[:]) + var st stack = pcs[0:n] + return &st +} + +// funcname removes the path prefix component of a function's name reported by func.Name(). +func funcname(name string) string { + i := strings.LastIndex(name, "/") + name = name[i+1:] + i = strings.Index(name, ".") + return name[i+1:] +} diff --git a/stack_test.go b/stack_test.go new file mode 100644 index 0000000..f760944 --- /dev/null +++ b/stack_test.go @@ -0,0 +1,263 @@ +package errors + +import ( + "fmt" + "regexp" + "runtime" + "strings" + "testing" +) + +var initpc = caller() + +func testFormatRegexp(t *testing.T, n int, arg interface{}, format, want string) { + t.Helper() + got := fmt.Sprintf(format, arg) + gotLines := strings.SplitN(got, "\n", -1) + wantLines := strings.SplitN(want, "\n", -1) + + if len(wantLines) > len(gotLines) { + t.Errorf("test %d: wantLines(%d) > gotLines(%d):\n got: %q\nwant: %q", n+1, len(wantLines), len(gotLines), got, want) + return + } + + for i, w := range wantLines { + match, err := regexp.MatchString(w, gotLines[i]) + if err != nil { + t.Fatal(err) + } + if !match { + t.Errorf("test %d: line %d: fmt.Sprintf(%q, err):\n got: %q\nwant: %q", n+1, i+1, format, got, want) + } + } +} + +type X struct{} + +// val returns a Frame pointing to itself. +func (x X) val() Frame { + return caller() +} + +// ptr returns a Frame pointing to itself. +func (x *X) ptr() Frame { + return caller() +} + +func TestFrameFormat(t *testing.T) { + var tests = []struct { + Frame + format string + want string + }{{ + initpc, + "%s", + "stack_test.go", + }, { + initpc, + "%+s", + "github.com/go-ap/errors.init\n" + + "\t.+/errors/stack_test.go", + }, { + 0, + "%s", + "unknown", + }, { + 0, + "%+s", + "unknown", + }, { + initpc, + "%d", + "11", + }, { + 0, + "%d", + "0", + }, { + initpc, + "%n", + "init", + }, { + func() Frame { + var x X + return x.ptr() + }(), + "%n", + `\(\*X\).ptr`, + }, { + func() Frame { + var x X + return x.val() + }(), + "%n", + "X.val", + }, { + 0, + "%n", + "", + }, { + initpc, + "%v", + "stack_test.go:11", + }, { + initpc, + "%+v", + "github.com/go-ap/errors.init\n" + + "\t.+/errors/stack_test.go:11", + }, { + 0, + "%v", + "unknown:0", + }} + + for i, tt := range tests { + testFormatRegexp(t, i, tt.Frame, tt.format, tt.want) + } +} + +func TestFuncname(t *testing.T) { + tests := []struct { + name, want string + }{ + {"", ""}, + {"runtime.main", "main"}, + {"github.com/go-ap/errors.funcname", "funcname"}, + {"funcname", "funcname"}, + {"io.copyBuffer", "copyBuffer"}, + {"main.(*R).Write", "(*R).Write"}, + } + + for _, tt := range tests { + got := funcname(tt.name) + want := tt.want + if got != want { + t.Errorf("funcname(%q): want: %q, got %q", tt.name, want, got) + } + } +} + +func TestStackTrace(t *testing.T) { + tests := []struct { + err error + want []string + }{{ + Newf("ooh"), []string{ + "github.com/go-ap/errors.TestStackTrace\n" + + "\t.+/errors/stack_test.go:145", + }, + }, { + wrap(Newf("ooh"), "ahh"), []string{ + "github.com/go-ap/errors.TestStackTrace\n" + + "\t.+/errors/stack_test.go:150", // this is the stack of Wrap, not New + }, + }, { + func() error { return Newf("ooh") }(), []string{ + `github.com/go-ap/errors.TestStackTrace.func1` + + "\n\t.+/errors/stack_test.go:155", // this is the stack of New + "github.com/go-ap/errors.TestStackTrace\n" + + "\t.+/errors/stack_test.go:155", // this is the stack of New's caller + }, + }} + t.Skipf(`TODO(marius): This needs some more work +As going one level up the stack in stack.callers() removes meaningful information from the tests`) + for i, tt := range tests { + x, ok := tt.err.(interface { + StackTrace() StackTrace + }) + if !ok { + t.Errorf("expected %#v to implement StackTrace() StackTrace", tt.err) + continue + } + st := x.StackTrace() + if len(st) == 0 { + continue + } + for j, want := range tt.want { + testFormatRegexp(t, i, st[j], "%+v", want) + } + } +} + +func stackTrace() StackTrace { + const depth = 8 + var pcs [depth]uintptr + n := runtime.Callers(1, pcs[:]) + var st stack = pcs[0:n] + return st.StackTrace() +} + +func TestStackTraceFormat(t *testing.T) { + tests := []struct { + StackTrace + format string + want string + }{{ + nil, + "%s", + `\[\]`, + }, { + nil, + "%v", + `\[\]`, + }, { + nil, + "%+v", + "", + }, { + nil, + "%#v", + `\[\]errors.Frame\(nil\)`, + }, { + make(StackTrace, 0), + "%s", + `\[\]`, + }, { + make(StackTrace, 0), + "%v", + `\[\]`, + }, { + make(StackTrace, 0), + "%+v", + "", + }, { + make(StackTrace, 0), + "%#v", + `\[\]errors.Frame{}`, + }, { + stackTrace()[:2], + "%s", + `\[stack_test.go stack_test.go\]`, + }, { + stackTrace()[:2], + "%v", + `\[stack_test.go:183 stack_test.go:230\]`, + }, { + stackTrace()[:2], + "%+v", + "\n" + + "github.com/go-ap/errors.stackTrace\n" + + "\t.+/errors/stack_test.go:183\n" + + "github.com/go-ap/errors.TestStackTraceFormat\n" + + "\t.+/errors/stack_test.go:234", + }, { + stackTrace()[:2], + "%#v", + `\[\]errors.Frame{stack_test.go:183, stack_test.go:242}`, + }} + + t.Skipf(`TODO(marius): This needs some more work +As going one level up the stack in stack.callers() removes meaningful information from the tests`) + for i, tt := range tests { + testFormatRegexp(t, i, tt.StackTrace, tt.format, tt.want) + } +} + +// a version of runtime.Caller that returns a Frame, not a uintptr. +func caller() Frame { + var pcs [3]uintptr + n := runtime.Callers(2, pcs[:]) + frames := runtime.CallersFrames(pcs[:n]) + frame, _ := frames.Next() + return Frame(frame.PC) +}