diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index f93ba87..0000000 --- a/.gitattributes +++ /dev/null @@ -1,22 +0,0 @@ -# Set the default behavior, in case people don't have core.autocrlf set. -* text=auto - -# convert to lf on checkout. -*.go text eol=lf -*.mod text eol=lf -*.sum text eol=lf -*.md text eol=lf -*.svg text eol=lf -*.http text eol=lf -*.ini text eol=lf -*.conf text eol=lf -*.sh text eol=lf -*.yaml text eol=lf -*.proto text eol=lf -*.json text eol=lf -Dockerfile text eol=lf - -# Denote all files that are truly binary and should not be modified. -*.png binary -*.jpg binary -*.jpeg binary \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5ea9152..9c7943f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,8 +9,14 @@ jobs: test-project: runs-on: ubuntu-20.04 steps: + - name: Setup + uses: actions/setup-go@v4 + with: + go-version: "1.21" + - run: go version + - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Test - run: make test \ No newline at end of file + run: make test diff --git a/HISTORY.md b/HISTORY.md index f7464e3..7dc09f2 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,11 @@ ## ✒ 历史版本的特性介绍 (Features in old versions) +### v0.6.0-alpha + +> 此版本发布于 2024-08-09 + +* 重构,调整使用姿势 + ### v0.5.2 > 此版本发布于 2023-07-27 diff --git a/LICENSE b/LICENSE index e4875a0..0b0d54b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2022 FishGoddess +Copyright (c) 2024 FishGoddess Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), diff --git a/Makefile b/Makefile index e028a99..f4f3057 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,9 @@ -.PHONY: test fmt +.PHONY: fmt test -all: test fmt - -test: - go test -cover ./... +all: fmt test fmt: - go fmt ./... \ No newline at end of file + go fmt ./... + +test: + go test -cover -count=1 -test.cpu=1 ./... \ No newline at end of file diff --git a/README.en.md b/README.en.md index e4d841d..00cadbc 100644 --- a/README.en.md +++ b/README.en.md @@ -2,7 +2,7 @@ [![Go Doc](_icons/godoc.svg)](https://pkg.go.dev/github.com/FishGoddess/errors) [![License](_icons/license.svg)](https://opensource.org/licenses/MIT) -[![License](_icons/coverage.svg)](_icons/coverage.svg) +[![Coverage](_icons/coverage.svg)](_icons/coverage.svg) ![Test](https://github.com/FishGoddess/errors/actions/workflows/test.yml/badge.svg) **Errors** is a lib for handling error gracefully in Go. @@ -25,50 +25,27 @@ import ( "github.com/FishGoddess/errors" ) -const ( - codeTestError = 10000 // Your error's code -) +func main() { + // Use wrap function to create an *Error error which has code and message. + err := errors.Wrap(1000, "need login") + fmt.Println(err) -// TestError returns a test error. -func TestError(err error) error { - return errors.Wrap(err, codeTestError) -} + // You can get code and message of err anytime. + fmt.Println(err.Code(), err.Message()) -// IsTestError if err is test error. -func IsTestError(err error) bool { - return errors.Is(err, codeTestError) -} + // Try these ways to get code and message! + // You will get default code or message if err doesn't have a code or message. + fmt.Println(errors.Code(err, 6699), errors.Message(err, "default message")) + fmt.Println(errors.Code(io.EOF, 6699), errors.Message(io.EOF, "default message")) -func main() { - // We provide three graceful ways to handle error in Go: Wrap() and Unwrap() and Is(). - // Wrap wraps error with a code and Unwrap returns one error with this code. - // Is returns one error is with the same code or not. - // As you can see, we define two functions above, and this is the basic way to use this lib. - err := TestError(errors.New("something wrong")) - if IsTestError(err) { - fmt.Println("I got a test error") - } - - // Also, we provide some basic errors for you: - err = errors.BadRequest(nil) // Classic enough! Ah :) - err = errors.Forbidden(nil) // Classic enough! Ah :) - err = errors.NotFound(nil) // Classic enough! Ah :) - err = errors.RequestTimeout(nil) // Classic enough! Ah :) - err = errors.InternalServerError(nil) // Classic enough! Ah :) - err = errors.DBError(nil) - err = errors.PageTokenInvalid(nil) - - // Use WithMsg to carry a message. - err = errors.Wrap(io.EOF, codeTestError, errors.WithMsg("test")) - fmt.Println(err.Error()) - fmt.Println(errors.Msg(err)) - fmt.Println(errors.MsgOrDefault(io.EOF, "default error message")) + // Also, we provide some useful information carrier for you. + err = errors.Wrap(9999, "io timeout").With(io.EOF).WithCaller() + fmt.Println(err) } ``` * [basic](_examples/basic.go) -* [status](_examples/status.go) ### 👥 Contributing diff --git a/README.md b/README.md index 7b43a1a..ec2122e 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Go Doc](_icons/godoc.svg)](https://pkg.go.dev/github.com/FishGoddess/errors) [![License](_icons/license.svg)](https://opensource.org/licenses/MIT) -[![License](_icons/coverage.svg)](_icons/coverage.svg) +[![Coverage](_icons/coverage.svg)](_icons/coverage.svg) ![Test](https://github.com/FishGoddess/errors/actions/workflows/test.yml/badge.svg) **Errors** 是一个用于优雅地处理 Go 中错误的库。 @@ -25,50 +25,27 @@ import ( "github.com/FishGoddess/errors" ) -const ( - codeTestError = 10000 // Your error's code -) +func main() { + // Use wrap function to create an *Error error which has code and message. + err := errors.Wrap(1000, "need login") + fmt.Println(err) -// TestError returns a test error. -func TestError(err error) error { - return errors.Wrap(err, codeTestError) -} + // You can get code and message of err anytime. + fmt.Println(err.Code(), err.Message()) -// IsTestError if err is test error. -func IsTestError(err error) bool { - return errors.Is(err, codeTestError) -} + // Try these ways to get code and message! + // You will get default code or message if err doesn't have a code or message. + fmt.Println(errors.Code(err, 6699), errors.Message(err, "default message")) + fmt.Println(errors.Code(io.EOF, 6699), errors.Message(io.EOF, "default message")) -func main() { - // We provide three graceful ways to handle error in Go: Wrap() and Unwrap() and Is(). - // Wrap wraps error with a code and Unwrap returns one error with this code. - // Is returns one error is with the same code or not. - // As you can see, we define two functions above, and this is the basic way to use this lib. - err := TestError(errors.New("something wrong")) - if IsTestError(err) { - fmt.Println("I got a test error") - } - - // Also, we provide some basic errors for you: - err = errors.BadRequest(nil) // Classic enough! Ah :) - err = errors.Forbidden(nil) // Classic enough! Ah :) - err = errors.NotFound(nil) // Classic enough! Ah :) - err = errors.RequestTimeout(nil) // Classic enough! Ah :) - err = errors.InternalServerError(nil) // Classic enough! Ah :) - err = errors.DBError(nil) - err = errors.PageTokenInvalid(nil) - - // Use WithMsg to carry a message. - err = errors.Wrap(io.EOF, codeTestError, errors.WithMsg("test")) - fmt.Println(err.Error()) - fmt.Println(errors.Msg(err)) - fmt.Println(errors.MsgOrDefault(io.EOF, "default error message")) + // Also, we provide some useful information carrier for you. + err = errors.Wrap(9999, "io timeout").With(io.EOF).WithCaller() + fmt.Println(err) } ``` * [basic](_examples/basic.go) -* [status](_examples/status.go) ### 👥 贡献者 diff --git a/_examples/basic.go b/_examples/basic.go index 7014bf7..c4f0f78 100644 --- a/_examples/basic.go +++ b/_examples/basic.go @@ -1,4 +1,4 @@ -// Copyright 2022 FishGoddess. All rights reserved. +// Copyright 2024 FishGoddess. All rights reserved. // Use of this source code is governed by a MIT style // license that can be found in the LICENSE file. @@ -11,42 +11,20 @@ import ( "github.com/FishGoddess/errors" ) -const ( - codeTestError = 10000 // Your error's code -) - -// TestError returns a test error. -func TestError(err error) error { - return errors.Wrap(err, codeTestError) -} - -// IsTestError if err is test error. -func IsTestError(err error) bool { - return errors.Is(err, codeTestError) -} - func main() { - // We provide three graceful ways to handle error in Go: Wrap() and Unwrap() and Is(). - // Wrap wraps error with a code and Unwrap returns one error with this code. - // Is returns one error is with the same code or not. - // As you can see, we define two functions above, and this is the basic way to use this lib. - err := TestError(errors.New("something wrong")) - if IsTestError(err) { - fmt.Println("I got a test error") - } + // Use wrap function to create an *Error error which has code and message. + err := errors.Wrap(1000, "need login") + fmt.Println(err) + + // You can get code and message of err anytime. + fmt.Println(err.Code(), err.Message()) - // Also, we provide some basic errors for you: - err = errors.BadRequest(nil) // Classic enough! Ah :) - err = errors.Forbidden(nil) // Classic enough! Ah :) - err = errors.NotFound(nil) // Classic enough! Ah :) - err = errors.RequestTimeout(nil) // Classic enough! Ah :) - err = errors.InternalServerError(nil) // Classic enough! Ah :) - err = errors.DBError(nil) - err = errors.PageTokenInvalid(nil) + // Try these ways to get code and message! + // You will get default code or message if err doesn't have a code or message. + fmt.Println(errors.Code(err, 6699), errors.Message(err, "default message")) + fmt.Println(errors.Code(io.EOF, 6699), errors.Message(io.EOF, "default message")) - // Use WithMsg to carry a message. - err = errors.Wrap(io.EOF, codeTestError, errors.WithMsg("test")) - fmt.Println(err.Error()) - fmt.Println(errors.Msg(err)) - fmt.Println(errors.MsgOrDefault(io.EOF, "default error message")) + // Also, we provide some useful information carrier for you. + err = errors.Wrap(9999, "io timeout").With(io.EOF).WithCaller() + fmt.Println(err) } diff --git a/_examples/status.go b/_examples/status.go deleted file mode 100644 index 1bb482c..0000000 --- a/_examples/status.go +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright 2023 FishGoddess. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -package main - -import ( - "fmt" - "io" - - "github.com/FishGoddess/errors" - "github.com/FishGoddess/errors/status" -) - -var ( - errAuthFailed = errors.New("auth failed") -) - -func isAuthFailed(err error) bool { - return err == errAuthFailed -} - -func main() { - // We provide a way to transfer errors to status. - // A status can be used by a server like grpc server with google.status. - // For example, we have an auth failed error which will be returned by service. - // However, we usually return a status code represents it in server, - // and we usually return a human-being readable msg instead of the error msg. - // So it has a gap between service error and server status. - // You can register a status with server code and msg, then use Parse to restore them. - authFailedStatus := status.New(1000, "you should check auth", isAuthFailed) - status.RegisterStatus(authFailedStatus) - - // Get code and msg from error. - code, msg := status.Parse(errAuthFailed) - fmt.Println(code, msg) - - // Of course, you can use errors.Wrap to get an error with msg. - // Then register a status about it. - errCode := int32(123456) - err := errors.Wrap(io.EOF, errCode, errors.WithMsg("hello")) - - isError := func(err error) bool { - return errors.Is(err, errCode) - } - - // As you can see, the error code and status code don't need to be the same. - statusCode := int32(654321) - status.RegisterStatus(status.New(statusCode, "i am status", isError)) - - // The msg will be the one set to error using WithMsg. - // The registered msg would be used only if the error doesn't have a set msg. - // Try to remove errors.WithMsg("hello") above and run again. - code, msg = status.Parse(err) - fmt.Println(code, msg) - - // Also, you can register many statuses at one time by RegisterStatuses. - status.RegisterStatuses( - status.New(123, "abc", nil), - status.New(666, "xxx", nil), - ) -} diff --git a/_icons/coverage.svg b/_icons/coverage.svg index 417699f..64d796b 100644 --- a/_icons/coverage.svg +++ b/_icons/coverage.svg @@ -10,7 +10,7 @@ coverage coverage - 85% - 85% + 79% + 79% \ No newline at end of file diff --git a/errors.go b/errors.go index 8a8bd4c..8b35dad 100644 --- a/errors.go +++ b/errors.go @@ -1,4 +1,4 @@ -// Copyright 2023 FishGoddess. All rights reserved. +// Copyright 2024 FishGoddess. All rights reserved. // Use of this source code is governed by a MIT style // license that can be found in the LICENSE file. @@ -6,15 +6,28 @@ package errors import ( "errors" - "fmt" ) -// New returns a string error. func New(text string) error { return errors.New(text) } -// Newf returns a string error formatted with params. -func Newf(text string, params ...interface{}) error { - return fmt.Errorf(text, params...) +// Is is a shortcut of errors.Is. +func Is(err, target error) bool { + return errors.Is(err, target) +} + +// As is a shortcut of errors.As. +func As(err error, target any) bool { + return errors.As(err, target) +} + +// Unwrap is a shortcut of errors.Unwrap. +func Unwrap(err error) error { + return errors.Unwrap(err) +} + +// Join is a shortcut of errors.Join. +func Join(errs ...error) error { + return errors.Join(errs...) } diff --git a/errors_test.go b/errors_test.go index 7123ee4..ec64686 100644 --- a/errors_test.go +++ b/errors_test.go @@ -1,25 +1,164 @@ -// Copyright 2023 FishGoddess. All rights reserved. +// Copyright 2024 FishGoddess. All rights reserved. // Use of this source code is governed by a MIT style // license that can be found in the LICENSE file. package errors import ( + "errors" + "io" + "os" "testing" ) -// go test -v -cover -run=^TestNew$ -func TestNew(t *testing.T) { - err := New("test") - if err.Error() != "test" { - t.Errorf("err.Error() %s != 'test'", err.Error()) +type testError struct { + reason string +} + +func (te *testError) Code() int32 { + return 500 +} + +func (te *testError) Error() string { + return te.reason +} + +// go test -v -cover -count=1 -test.cpu=1 -run=^TestIs$ +func TestIs(t *testing.T) { + testErr := &testError{reason: "test error"} + wrapErr := Wrap(-1000, "wrap test error").With(testErr) + + testCases := []struct { + err error + cause error + }{ + { + err: io.EOF, + cause: io.EOF, + }, + { + err: Wrap(1000, "wow").With(testErr), + cause: testErr, + }, + { + err: Wrap(1000, "wow too").With(wrapErr), + cause: testErr, + }, + } + + for _, testCase := range testCases { + if ok := Is(testCase.err, testCase.cause); !ok { + t.Errorf("testCase.err %+v isn't testCase.cause %+v", testCase.err, testCase.cause) + } } } -// go test -v -cover -run=^TestNewf$ -func TestNewf(t *testing.T) { - err := Newf("test %d %.2f", 123, 3.14) - if err.Error() != "test 123 3.14" { - t.Errorf("err.Error() %s != 'test 123 3.14'", err.Error()) +// go test -v -cover -count=1 -test.cpu=1 -run=^TestAs$ +func TestAs(t *testing.T) { + testErr := &testError{reason: "test error"} + wrapErr := Wrap(-1000, "wrap test error").With(testErr) + + targetTestErr := &testError{} + targetTestErr2 := &testError{} + targetPathErr := &os.PathError{} + + testCases := []struct { + err error + target any + ok bool + want any + }{ + { + err: Wrap(1000, "wow").With(testErr), + target: &targetTestErr, + ok: true, + want: &testErr, + }, + { + err: Wrap(1000, "wow too").With(wrapErr), + target: &targetTestErr2, + ok: true, + want: &testErr, + }, + { + err: Wrap(1000, "no"), + target: &targetPathErr, + ok: false, + want: nil, + }, + } + + for _, testCase := range testCases { + ok := As(testCase.err, testCase.target) + if ok != testCase.ok { + t.Errorf("err %+v ok %+v != err %+v testCase.ok %+v", testCase.err, testCase.target, ok, testCase.ok) + } + } +} + +// go test -v -cover -count=1 -test.cpu=1 -run=^TestUnwrap$ +func TestUnwrap(t *testing.T) { + testErr := &testError{reason: "test error"} + wrapErr := Wrap(-1000, "wrap test error").With(testErr) + + testCases := []struct { + err error + cause error + }{ + { + err: testErr, + cause: nil, + }, + { + err: Wrap(1000, "wow").With(testErr), + cause: testErr, + }, + { + err: Wrap(1000, "wow too").With(wrapErr), + cause: wrapErr, + }, + } + + for _, testCase := range testCases { + if Unwrap(testCase.err) != testCase.cause { + t.Errorf("Unwrap(testCase.err) %+v != testCase.cause %+v", Unwrap(testCase.err), testCase.cause) + } + } +} + +// go test -v -cover -count=1 -test.cpu=1 -run=^TestJoin$ +func TestJoin(t *testing.T) { + testCases := []struct { + errs []error + }{ + { + errs: nil, + }, + { + errs: []error{nil, nil}, + }, + { + errs: []error{io.EOF, &testError{}}, + }, + { + errs: []error{io.EOF, nil}, + }, + } + + for _, testCase := range testCases { + got := Join(testCase.errs...) + want := errors.Join(testCase.errs...) + + if got != nil && want != nil { + if got.Error() != want.Error() { + t.Errorf("got %+v != want %+v", got, want) + } + + continue + } + + if got != want { + t.Errorf("got %+v != want %+v", got, want) + } } } diff --git a/extension.go b/extension.go deleted file mode 100644 index fe41b24..0000000 --- a/extension.go +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2023 FishGoddess. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -package errors - -const ( - codeDBError = 1100 - codePageTokenInvalid = 1200 -) - -// DBError returns a db error. -func DBError(err error, opts ...Option) error { - return Wrap(err, codeDBError, opts...) -} - -// IsDBError if err is db error. -func IsDBError(err error) bool { - return Is(err, codeDBError) -} - -// UnwrapDBError if err is db error. -func UnwrapDBError(err error) (error, bool) { - return Unwrap(err, codeDBError) -} - -// PageTokenInvalid returns a page token invalid error. -func PageTokenInvalid(err error, opts ...Option) error { - return Wrap(err, codePageTokenInvalid, opts...) -} - -// IsPageTokenInvalid if err is page token invalid. -func IsPageTokenInvalid(err error) bool { - return Is(err, codePageTokenInvalid) -} - -// UnwrapPageTokenInvalid if err is page token invalid. -func UnwrapPageTokenInvalid(err error) (error, bool) { - return Unwrap(err, codePageTokenInvalid) -} diff --git a/extension_test.go b/extension_test.go deleted file mode 100644 index 5d3988e..0000000 --- a/extension_test.go +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright 2022 FishGoddess. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -package errors - -import ( - "errors" - "testing" -) - -// go test -v -cover -run=^TestDBError$ -func TestDBError(t *testing.T) { - err := DBError(nil) - if err != nil { - t.Error("DBError is wrong", err) - } - - err = DBError(errors.New("db error")) - if !IsDBError(err) { - t.Error("IsDBError is wrong", err) - } - - if e, ok := UnwrapDBError(err); !ok || e.Error() != "db error" { - t.Error("DBError or UnwrapDBError is wrong", err) - } -} - -// go test -v -cover -run=^TestPageTokenInvalid$ -func TestPageTokenInvalid(t *testing.T) { - err := PageTokenInvalid(nil) - if err != nil { - t.Error("PageTokenInvalid is wrong", err) - } - - err = PageTokenInvalid(errors.New("page token invalid")) - if !IsPageTokenInvalid(err) { - t.Error("IsPageTokenInvalid is wrong", err) - } - - if e, ok := UnwrapPageTokenInvalid(err); !ok || e.Error() != "page token invalid" { - t.Error("PageTokenInvalid or UnwrapPageTokenInvalid is wrong", err) - } -} diff --git a/option.go b/option.go deleted file mode 100644 index f0f0710..0000000 --- a/option.go +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright 2023 FishGoddess. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -package errors - -import "fmt" - -type Option func(e *Error) - -func (o Option) Apply(e *Error) { - o(e) -} - -func applyOptions(e *Error, opts []Option) { - for _, opt := range opts { - opt.Apply(e) - } -} - -// WithMsg sets msg to e. -func WithMsg(msg string, params ...interface{}) Option { - return func(e *Error) { - if len(params) > 0 { - msg = fmt.Sprintf(msg, params...) - } - - e.msg = msg - } -} diff --git a/option_test.go b/option_test.go deleted file mode 100644 index c2e2966..0000000 --- a/option_test.go +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright 2023 FishGoddess. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -package errors - -import ( - "net/http" - "testing" -) - -// go test -v -cover -run=^TestOptionApply$ -func TestOptionApply(t *testing.T) { - opt := Option(func(e *Error) { - e.code = http.StatusOK - }) - - got := &Error{code: 0} - expect := &Error{code: http.StatusOK} - - opt.Apply(got) - if got.String() != expect.String() { - t.Errorf("got %s != expect %s", got.String(), expect.String()) - } -} - -// go test -v -cover -run=^TestWithMsg$ -func TestWithMsg(t *testing.T) { - got := &Error{msg: ""} - expect := &Error{msg: "ok"} - - WithMsg("ok")(got) - if got.String() != expect.String() { - t.Errorf("got %s != expect %s", got.String(), expect.String()) - } - - got = &Error{msg: ""} - expect = &Error{msg: "ok123"} - - WithMsg("%s%d", "ok", 123)(got) - if got.String() != expect.String() { - t.Errorf("got %s != expect %s", got.String(), expect.String()) - } -} diff --git a/x/runtime.go b/runtime.go similarity index 89% rename from x/runtime.go rename to runtime.go index 64f9be6..d2ef404 100644 --- a/x/runtime.go +++ b/runtime.go @@ -10,7 +10,7 @@ import ( ) func Caller() string { - _, file, line, ok := runtime.Caller(1) + _, file, line, ok := runtime.Caller(2) if !ok { return "" } @@ -20,7 +20,7 @@ func Caller() string { func Callers() []string { var pcs [16]uintptr - n := runtime.Callers(2, pcs[:]) + n := runtime.Callers(3, pcs[:]) frames := runtime.CallersFrames(pcs[:n]) var callers []string diff --git a/x/runtime_test.go b/runtime_test.go similarity index 100% rename from x/runtime_test.go rename to runtime_test.go diff --git a/standard.go b/standard.go deleted file mode 100644 index c9b2660..0000000 --- a/standard.go +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright 2022 FishGoddess. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -package errors - -import ( - "net/http" -) - -const ( - _ = http.StatusTeapot // :) -) - -const ( - codeBadRequest = http.StatusBadRequest // Classic... - codeForbidden = http.StatusForbidden // Classic... - codeNotFound = http.StatusNotFound // Classic... - codeRequestTimeout = http.StatusRequestTimeout // Classic... - codeInternalServerError = http.StatusInternalServerError // Classic... -) - -// BadRequest returns a bad request error. -func BadRequest(err error, opts ...Option) error { - return Wrap(err, codeBadRequest, opts...) -} - -// IsBadRequest if err is bad request. -func IsBadRequest(err error) bool { - return Is(err, codeBadRequest) -} - -// UnwrapBadRequest if err is bad request. -func UnwrapBadRequest(err error) (error, bool) { - return Unwrap(err, codeBadRequest) -} - -// Forbidden returns a forbidden error. -func Forbidden(err error, opts ...Option) error { - return Wrap(err, codeForbidden, opts...) -} - -// IsForbidden if err is forbidden. -func IsForbidden(err error) bool { - return Is(err, codeForbidden) -} - -// UnwrapForbidden if err is forbidden. -func UnwrapForbidden(err error) (error, bool) { - return Unwrap(err, codeForbidden) -} - -// NotFound returns a not found error. -func NotFound(err error, opts ...Option) error { - return Wrap(err, codeNotFound, opts...) -} - -// IsNotFound if err is not found. -func IsNotFound(err error) bool { - return Is(err, codeNotFound) -} - -// UnwrapNotFound if err is not found. -func UnwrapNotFound(err error) (error, bool) { - return Unwrap(err, codeNotFound) -} - -// RequestTimeout returns a request timeout error. -func RequestTimeout(err error, opts ...Option) error { - return Wrap(err, codeRequestTimeout, opts...) -} - -// IsRequestTimeout if err is request timeout. -func IsRequestTimeout(err error) bool { - return Is(err, codeRequestTimeout) -} - -// UnwrapRequestTimeout if err is request timeout. -func UnwrapRequestTimeout(err error) (error, bool) { - return Unwrap(err, codeRequestTimeout) -} - -// InternalServerError returns an internal server error. -func InternalServerError(err error, opts ...Option) error { - return Wrap(err, codeInternalServerError, opts...) -} - -// IsInternalServerError if err is an internal server. -func IsInternalServerError(err error) bool { - return Is(err, codeInternalServerError) -} - -// UnwrapInternalServerError if err is an internal server. -func UnwrapInternalServerError(err error) (error, bool) { - return Unwrap(err, codeInternalServerError) -} diff --git a/standard_test.go b/standard_test.go deleted file mode 100644 index 47f47a7..0000000 --- a/standard_test.go +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright 2022 FishGoddess. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -package errors - -import ( - "errors" - "testing" -) - -// go test -v -cover -run=^TestBadRequest$ -func TestBadRequest(t *testing.T) { - err := BadRequest(nil) - if err != nil { - t.Error("BadRequest is wrong", err) - } - - err = BadRequest(errors.New("bad request")) - if !IsBadRequest(err) { - t.Error("IsBadRequest is wrong", err) - } - - if e, ok := UnwrapBadRequest(err); !ok || e.Error() != "bad request" { - t.Error("BadRequest or UnwrapBadRequest is wrong", err) - } -} - -// go test -v -cover -run=^TestForbidden$ -func TestForbidden(t *testing.T) { - err := Forbidden(nil) - if err != nil { - t.Error("Forbidden is wrong", err) - } - - err = Forbidden(errors.New("forbidden")) - if !IsForbidden(err) { - t.Error("IsForbidden is wrong", err) - } - - if e, ok := UnwrapForbidden(err); !ok || e.Error() != "forbidden" { - t.Error("Forbidden or UnwrapForbidden is wrong", err) - } -} - -// go test -v -cover -run=^TestNotFound$ -func TestNotFound(t *testing.T) { - err := NotFound(nil) - if err != nil { - t.Error("NotFound is wrong", err) - } - - err = NotFound(errors.New("not found")) - if !IsNotFound(err) { - t.Error("IsNotFound is wrong", err) - } - - if e, ok := UnwrapNotFound(err); !ok || e.Error() != "not found" { - t.Error("NotFound or UnwrapNotFound is wrong", err) - } -} - -// go test -v -cover -run=^TestRequestTimeout$ -func TestRequestTimeout(t *testing.T) { - err := RequestTimeout(nil) - if err != nil { - t.Error("RequestTimeout is wrong", err) - } - - err = RequestTimeout(errors.New("request timeout")) - if !IsRequestTimeout(err) { - t.Error("IsRequestTimeout is wrong", err) - } - - if e, ok := UnwrapRequestTimeout(err); !ok || e.Error() != "request timeout" { - t.Error("RequestTimeout or UnwrapRequestTimeout is wrong", err) - } -} - -// go test -v -cover -run=^TestInternalServerError$ -func TestInternalServerError(t *testing.T) { - err := InternalServerError(nil) - if err != nil { - t.Error("InternalServerError is wrong", err) - } - - err = InternalServerError(errors.New("internal server error")) - if !IsInternalServerError(err) { - t.Error("IsInternalServerError is wrong", err) - } - - if e, ok := UnwrapInternalServerError(err); !ok || e.Error() != "internal server error" { - t.Error("InternalServerError or UnwrapInternalServerError is wrong", err) - } -} diff --git a/status/status.go b/status/status.go deleted file mode 100644 index 5171600..0000000 --- a/status/status.go +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright 2023 FishGoddess. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -package status - -import ( - "net/http" - "sync" - - "github.com/FishGoddess/errors" -) - -var ( - CodeOK int32 = 0 - CodeUnknown int32 = http.StatusInternalServerError -) - -var ( - // MsgUnknown is the default msg returned from Parse if err passed to Parse doesn't have a msg. - MsgUnknown = "Server is too busy, please try again later." -) - -var ( - statuses = make([]*Status, 0, 8) - statusesLock sync.RWMutex -) - -// Status wraps code and msg for returning. -type Status struct { - code int32 - msg string - isError func(err error) bool -} - -// New returns a new with given params. -// isError returns if err belongs to status. -func New(code int32, msg string, isError func(err error) bool) *Status { - return &Status{ - code: code, - msg: msg, - isError: isError, - } -} - -func (s *Status) Code() int32 { - if s == nil { - return CodeOK - } - - return s.code -} - -func (s *Status) Msg() string { - if s == nil { - return "" - } - - return s.msg -} - -func (s *Status) IsError(err error) bool { - return s.isError != nil && s.isError(err) -} - -// RegisterStatus registers a new status to local statuses so that we can use it in Parse. -func RegisterStatus(status *Status) { - statusesLock.Lock() - statuses = append(statuses, status) - statusesLock.Unlock() -} - -// RegisterStatuses registers some statuses to local statuses so that we can use it in Parse. -func RegisterStatuses(registerStatus ...*Status) { - statusesLock.Lock() - defer statusesLock.Unlock() - - for _, status := range registerStatus { - statuses = append(statuses, status) - } -} - -// Parse parses err and returns its code and error created from msg. -func Parse(err error) (int32, string) { - return ParseOrDefault(err, CodeUnknown, MsgUnknown) -} - -// ParseOrDefault parses err and returns its code and error created from msg. -func ParseOrDefault(err error, defaultCode int32, defaultMsg string) (int32, string) { - if err == nil { - return CodeOK, "" - } - - statusesLock.RLock() - defer statusesLock.RUnlock() - - for _, status := range statuses { - if status != nil && status.IsError(err) { - code := status.Code() - msg := errors.MsgOrDefault(err, status.Msg()) - return code, msg - } - } - - msg := errors.MsgOrDefault(err, defaultMsg) - return defaultCode, msg -} diff --git a/status/status_test.go b/status/status_test.go deleted file mode 100644 index 5c57b98..0000000 --- a/status/status_test.go +++ /dev/null @@ -1,111 +0,0 @@ -package status - -import ( - "io" - "net/http" - "testing" - - "github.com/FishGoddess/errors" -) - -// go test -v -cover -run=^TestRegisterStatus$ -func TestRegisterStatus(t *testing.T) { - registered := New(666, "xxx", nil) - RegisterStatus(registered) - - found := false - for _, status := range statuses { - if status == registered { - found = true - break - } - } - - if !found { - t.Error("registered status not found", statuses) - } -} - -// go test -v -cover -run=^TestRegisterStatuses$ -func TestRegisterStatuses(t *testing.T) { - registered := []*Status{ - New(123, "abc", nil), - New(666, "xxx", nil), - } - - RegisterStatuses(registered...) - - for i := range registered { - found := false - for _, status := range statuses { - if status == registered[i] { - found = true - break - } - } - - if !found { - t.Error("registered status not found", statuses) - } - } -} - -// go test -v -cover -run=^TestParse$ -func TestParse(t *testing.T) { - codeBadRequest := int32(http.StatusBadRequest) - codeForbidden := int32(http.StatusForbidden) - codeNotFound := int32(http.StatusNotFound) - codeRequestTimeout := int32(http.StatusRequestTimeout) - codeDBError := int32(1100) - codePageTokenInvalid := int32(1200) - - RegisterStatus(New(codeBadRequest, "bad request", errors.IsBadRequest)) - RegisterStatus(New(codeForbidden, "forbidden", errors.IsForbidden)) - RegisterStatus(New(codeNotFound, "not found", errors.IsNotFound)) - RegisterStatus(New(codeRequestTimeout, "request timeout", errors.IsRequestTimeout)) - RegisterStatus(New(codeDBError, "db error", errors.IsDBError)) - RegisterStatus(New(codePageTokenInvalid, "page token invalid", errors.IsPageTokenInvalid)) - - type parseTestCase struct { - target error - code int32 - msg string - } - - parseTestCases := []*parseTestCase{ - {target: errors.BadRequest(nil), code: 0, msg: ""}, - {target: errors.Forbidden(nil), code: 0, msg: ""}, - {target: errors.NotFound(nil), code: 0, msg: ""}, - {target: errors.RequestTimeout(nil), code: 0, msg: ""}, - {target: errors.InternalServerError(nil), code: 0, msg: ""}, - {target: errors.DBError(nil), code: 0, msg: ""}, - {target: errors.PageTokenInvalid(nil), code: 0, msg: ""}, - - {target: errors.BadRequest(io.EOF), code: codeBadRequest, msg: "bad request"}, - {target: errors.Forbidden(io.EOF), code: codeForbidden, msg: "forbidden"}, - {target: errors.NotFound(io.EOF), code: codeNotFound, msg: "not found"}, - {target: errors.RequestTimeout(io.EOF), code: codeRequestTimeout, msg: "request timeout"}, - {target: errors.InternalServerError(io.EOF), code: CodeUnknown, msg: "Server is too busy, please try again later."}, - {target: errors.DBError(io.EOF), code: codeDBError, msg: "db error"}, - {target: errors.PageTokenInvalid(io.EOF), code: codePageTokenInvalid, msg: "page token invalid"}, - - {target: errors.BadRequest(io.EOF, errors.WithMsg("BadRequest")), code: codeBadRequest, msg: "BadRequest"}, - {target: errors.Forbidden(io.EOF, errors.WithMsg("Forbidden")), code: codeForbidden, msg: "Forbidden"}, - {target: errors.NotFound(io.EOF, errors.WithMsg("NotFound")), code: codeNotFound, msg: "NotFound"}, - {target: errors.RequestTimeout(io.EOF, errors.WithMsg("RequestTimeout")), code: codeRequestTimeout, msg: "RequestTimeout"}, - {target: errors.InternalServerError(io.EOF, errors.WithMsg("InternalServerError")), code: CodeUnknown, msg: "InternalServerError"}, - {target: errors.DBError(io.EOF, errors.WithMsg("DBError")), code: codeDBError, msg: "DBError"}, - {target: errors.PageTokenInvalid(io.EOF, errors.WithMsg("PageTokenInvalid")), code: codePageTokenInvalid, msg: "PageTokenInvalid"}, - } - - for _, parseTestCase := range parseTestCases { - code, msg := Parse(parseTestCase.target) - if code != parseTestCase.code { - t.Errorf("code %d != parseTestCase.code %d", code, parseTestCase.code) - } - - if msg != parseTestCase.msg { - t.Errorf("msg %s != parseTestCase.msg %s", msg, parseTestCase.msg) - } - } -} diff --git a/types.go b/types.go deleted file mode 100644 index 00899e2..0000000 --- a/types.go +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright 2022 FishGoddess. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -package errors - -import ( - "errors" - "fmt" -) - -const ( - nilString = "" -) - -var ( - // FormatError formats e and returns a string in Error(). - FormatError = func(e *Error) string { - return e.err.Error() - } - - // FormatString formats e and returns a string in String(). - FormatString = func(e *Error) string { - return fmt.Sprintf("%d (%s)", e.code, e.msg) - } -) - -// Error wraps err with some information. -type Error struct { - err error - - code int32 - msg string -} - -func (e *Error) Error() string { - if e == nil || e.err == nil { - return nilString - } - - return FormatError(e) -} - -func (e *Error) String() string { - if e == nil || e.err == nil { - return nilString - } - - return FormatString(e) -} - -// Is returns if e has the same type of target. -func (e *Error) Is(target error) bool { - if e == nil { - return e == target - } - - err, ok := target.(*Error) - if !ok { - return e.err == target - } - - return e.code == err.code -} - -// Unwrap returns err inside. -func (e *Error) Unwrap() error { - if e == nil { - return nil - } - - return e.err -} - -// Wrap wraps err with code -func Wrap(err error, code int32, opts ...Option) error { - if err == nil { - return nil - } - - e := &Error{ - err: err, - code: code, - msg: "", - } - - applyOptions(e, opts) - return e -} - -// Unwrap returns if err is Error and its code == code, and the original error will be returned, too. -func Unwrap(err error, code int32) (error, bool) { - for { - if err == nil { - return nil, false - } - - e, ok := err.(*Error) - if !ok { - return err, false - } - - if e.code == code { - return err, true - } - - err = errors.Unwrap(err) - } -} - -// Is returns if err is Error and its code == code. -func Is(err error, code int32) bool { - _, ok := Unwrap(err, code) - return ok -} - -// Msg returns the msg of err and false if err doesn't have a msg. -func Msg(err error) (string, bool) { - if err == nil { - return "", false - } - - e, ok := err.(*Error) - if !ok { - return err.Error(), false - } - - return e.msg, e.msg != "" -} - -// MsgOrDefault returns the msg of err or defaultMsg if err doesn't have a msg. -func MsgOrDefault(err error, defaultMsg string) string { - if msg, ok := Msg(err); ok { - return msg - } - - return defaultMsg -} diff --git a/types_test.go b/types_test.go deleted file mode 100644 index 4097861..0000000 --- a/types_test.go +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright 2022 FishGoddess. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -package errors - -import ( - "errors" - "testing" -) - -// go test -v -cover -run=^TestWrapAndUnwrap$ -func TestWrapAndUnwrap(t *testing.T) { - code := int32(500) - - err := Wrap(nil, code) - if err != nil { - t.Errorf("err %+v is wrong", err) - } - - err = Wrap(errors.New("500"), code) - if e, ok := Unwrap(err, code); !ok || e.Error() != "500" { - t.Errorf("err %+v is wrong", err) - } -} - -// go test -v -cover -run=^TestMsg$ -func TestMsg(t *testing.T) { - code := int32(500) - - msg, ok := Msg(Wrap(nil, code)) - if ok { - t.Error("msg should be not ok") - } - - if msg != "" { - t.Errorf("msg %s is wrong", msg) - } - - msg, ok = Msg(Wrap(errors.New("500"), code)) - if ok { - t.Error("msg should be not ok") - } - - if msg != "" { - t.Errorf("msg %s is wrong", msg) - } - - msg, ok = Msg(Wrap(errors.New("500"), code, WithMsg("internal"))) - if !ok { - t.Error("msg should be ok") - } - - if msg != "internal" { - t.Errorf("msg %s is wrong", msg) - } -} - -// go test -v -cover -run=^TestMsgOrDefault$ -func TestMsgOrDefault(t *testing.T) { - code := int32(500) - - msg := MsgOrDefault(Wrap(nil, code), "xxx") - if msg != "xxx" { - t.Errorf("msg %s is wrong", msg) - } - - msg = MsgOrDefault(Wrap(errors.New("500"), code), "abc") - if msg != "abc" { - t.Errorf("msg %s is wrong", msg) - } - - msg = MsgOrDefault(Wrap(errors.New("500"), code, WithMsg("internal")), "ignore") - if msg != "internal" { - t.Errorf("msg %s is wrong", msg) - } -} diff --git a/x/unwrap.go b/unwrap.go similarity index 100% rename from x/unwrap.go rename to unwrap.go diff --git a/x/unwrap_test.go b/unwrap_test.go similarity index 100% rename from x/unwrap_test.go rename to unwrap_test.go diff --git a/x/wrap.go b/wrap.go similarity index 83% rename from x/wrap.go rename to wrap.go index 118a893..a0c378a 100644 --- a/x/wrap.go +++ b/wrap.go @@ -5,6 +5,7 @@ package errors import ( + "bytes" "fmt" "strings" ) @@ -14,7 +15,6 @@ type Error struct { message string cause error caller string - args []any } // Wrap returns *Error with code and message formatted with args. @@ -47,11 +47,6 @@ func (e *Error) WithCallers() *Error { return e } -func (e *Error) WithArgs(args ...any) *Error { - e.args = append(e.args, args...) - return e -} - // Code returns the code of *Error. func (e *Error) Code() int32 { return e.code @@ -74,9 +69,16 @@ func (e *Error) Error() string { // String returns *Error as string. func (e *Error) String() string { - if e.cause == nil { - return fmt.Sprintf("%d: %s", e.code, e.message) + var buff bytes.Buffer + fmt.Fprintf(&buff, "%d: %s", e.code, e.message) + + if e.caller != "" { + fmt.Fprintf(&buff, " [%s]", e.caller) + } + + if e.cause != nil { + fmt.Fprintf(&buff, " (%+v)", e.cause) } - return fmt.Sprintf("%d: %s (%+v)", e.code, e.message, e.cause) + return buff.String() } diff --git a/x/wrap_test.go b/wrap_test.go similarity index 100% rename from x/wrap_test.go rename to wrap_test.go diff --git a/x/errors.go b/x/errors.go deleted file mode 100644 index 8b35dad..0000000 --- a/x/errors.go +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2024 FishGoddess. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -package errors - -import ( - "errors" -) - -func New(text string) error { - return errors.New(text) -} - -// Is is a shortcut of errors.Is. -func Is(err, target error) bool { - return errors.Is(err, target) -} - -// As is a shortcut of errors.As. -func As(err error, target any) bool { - return errors.As(err, target) -} - -// Unwrap is a shortcut of errors.Unwrap. -func Unwrap(err error) error { - return errors.Unwrap(err) -} - -// Join is a shortcut of errors.Join. -func Join(errs ...error) error { - return errors.Join(errs...) -} diff --git a/x/errors_test.go b/x/errors_test.go deleted file mode 100644 index 53c644a..0000000 --- a/x/errors_test.go +++ /dev/null @@ -1,126 +0,0 @@ -// Copyright 2024 FishGoddess. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -package errors - -import ( - "io" - "os" - "testing" -) - -type testError struct { - reason string -} - -func (te *testError) Code() int32 { - return 500 -} - -func (te *testError) Error() string { - return te.reason -} - -// go test -v -cover -count=1 -test.cpu=1 -run=^TestIs$ -func TestIs(t *testing.T) { - testErr := &testError{reason: "test error"} - wrapErr := Wrap(-1000, "wrap test error").With(testErr) - - testCases := []struct { - err error - cause error - }{ - { - err: io.EOF, - cause: io.EOF, - }, - { - err: Wrap(1000, "wow").With(testErr), - cause: testErr, - }, - { - err: Wrap(1000, "wow too").With(wrapErr), - cause: testErr, - }, - } - - for _, testCase := range testCases { - if ok := Is(testCase.err, testCase.cause); !ok { - t.Errorf("testCase.err %+v isn't testCase.cause %+v", testCase.err, testCase.cause) - } - } -} - -// go test -v -cover -count=1 -test.cpu=1 -run=^TestAs$ -func TestAs(t *testing.T) { - testErr := &testError{reason: "test error"} - wrapErr := Wrap(-1000, "wrap test error").With(testErr) - - targetTestErr := &testError{} - targetTestErr2 := &testError{} - targetPathErr := &os.PathError{} - - testCases := []struct { - err error - target any - ok bool - want any - }{ - { - err: Wrap(1000, "wow").With(testErr), - target: &targetTestErr, - ok: true, - want: &testErr, - }, - { - err: Wrap(1000, "wow too").With(wrapErr), - target: &targetTestErr2, - ok: true, - want: &testErr, - }, - { - err: Wrap(1000, "no"), - target: &targetPathErr, - ok: false, - want: nil, - }, - } - - for _, testCase := range testCases { - ok := As(testCase.err, testCase.target) - if ok != testCase.ok { - t.Errorf("err %+v ok %+v != err %+v testCase.ok %+v", testCase.err, testCase.target, ok, testCase.ok) - } - } -} - -// go test -v -cover -count=1 -test.cpu=1 -run=^TestUnwrap$ -func TestUnwrap(t *testing.T) { - testErr := &testError{reason: "test error"} - wrapErr := Wrap(-1000, "wrap test error").With(testErr) - - testCases := []struct { - err error - cause error - }{ - { - err: testErr, - cause: nil, - }, - { - err: Wrap(1000, "wow").With(testErr), - cause: testErr, - }, - { - err: Wrap(1000, "wow too").With(wrapErr), - cause: wrapErr, - }, - } - - for _, testCase := range testCases { - if Unwrap(testCase.err) != testCase.cause { - t.Errorf("Unwrap(testCase.err) %+v != testCase.cause %+v", Unwrap(testCase.err), testCase.cause) - } - } -}