diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..21b3252 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,21 @@ +# http://editorconfig.org +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 2 +tab_width = 2 +insert_final_newline = true +trim_trailing_whitespace = true +max_line_length = 100 + +[*.go] +indent_style = tab + +[*.{yml, yaml}] +indent_style = tab + +[Makefile] +indent_style = tab diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8b22763 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +# Test binary, build with `go test -c` +*.test + +# Coverage +coverage.txt +profile.cov + +*.out + +#Debug +__debug_bin* +debug + +# IDE +.idea +.vscode +.DS_STORE diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..e2d18a4 --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,105 @@ +run: + concurrency: 4 + timeout: 10m + go: '1.22' + +linters-settings: + gocyclo: + min-complexity: 25 + dupl: + threshold: 100 + goconst: + min-len: 2 + min-occurrences: 3 + misspell: + locale: US + goimports: + local-prefixes: github.com/htquangg/avault + gocritic: + enabled-tags: + - diagnostic + - experimental + - opinionated + - performance + - style + disabled-checks: + - wrapperFunc + - ifElseChain + funlen: + lines: 350 + statements: 200 + revive: + # default rules derived from upstream revive repo + # https://github.com/walles/revive/blob/f417cbd57c6d90b43bdb7f113c222e5aeef117e5/defaults.toml + rules: + - name: blank-imports + - name: context-as-argument + - name: context-keys-type + - name: dot-imports + - name: error-return + - name: error-strings + - name: error-naming + - name: exported + # - name: if-return + - name: increment-decrement + - name: var-naming + - name: var-declaration + - name: package-comments + - name: range + - name: receiver-naming + - name: time-naming + - name: unexported-return + - name: indent-error-flow + - name: errorf + - name: empty-block + - name: superfluous-else + # - name: unused-parameter + - name: unreachable-code + - name: redefines-builtin-id + +linters: + disable-all: true + enable: + - bodyclose + # - deadcode + # - depguard + - dogsled + # - dupl + # - errcheck + - funlen + - gocritic + - gocyclo + # - gofmt + - goimports + - revive + # - gosec + - gosimple + - govet + - ineffassign + - misspell + - nakedret + - exportloopref + - staticcheck + # - structcheck + - stylecheck + - typecheck + - unconvert + - unused + # - varcheck + - whitespace + - goconst + - unused + # - gochecknoinits + +issues: + exclude-dirs: + - .artifacts + - .codecov + - .github + - build + - deploy + - proto + - tools + exclude: + - consider giving a name to these results + - include an explanation for nolint directive diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0becda3 --- /dev/null +++ b/Makefile @@ -0,0 +1,22 @@ +.PHONY: generate-proto +generate-proto: ## generate proto + buf generate + +.PHONY: lint +lint: ## lint + @echo "Linting" + golangci-lint run \ + --timeout 10m \ + --config ./.golangci.yaml \ + --out-format=github-actions \ + --concurrency=$$(getconf _NPROCESSORS_ONLN) + +.PHONY: test +test: ## run the go tests + go test -coverprofile cover.out ./... + +.PHONY: help +help: ## print help + @awk 'BEGIN {FS = ":.*##"; printf "Usage:\n make \033[36m\033[0m\n"} /^[$$()% 0-9a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + +.DEFAULT_GOAL := help diff --git a/README.md b/README.md new file mode 100644 index 0000000..e7e69fd --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# AErrors + +Builds on Go 1.22 errors by adding HTTP statuses and GRPC codes to them. + +## Installation + + go get -u github.com/htquangg/aerrors + +## Prerequisites + +Go 1.22 diff --git a/aerrors.go b/aerrors.go new file mode 100644 index 0000000..9a53a89 --- /dev/null +++ b/aerrors.go @@ -0,0 +1,148 @@ +package aerrors + +import ( + "bytes" + "errors" + "fmt" + "reflect" +) + +type TypeCoder interface { + TypeCode() string +} + +type ErrorCode string + +func (err ErrorCode) TypeCode() string { + if err == ErrOK { + return "" + } + return string(err) +} + +func (err ErrorCode) Error() string { + if err == ErrOK { + return "" + } + + return string(err) +} + +type AError struct { + code ErrorCode + parent error + reason string + message string + stack string +} + +func New(code ErrorCode, reason string) *AError { + return &AError{code: code, reason: reason} +} + +func (err *AError) Code() ErrorCode { + return err.code +} + +func (err *AError) Parent() error { + return err.parent +} + +func (err *AError) Reason() string { + return err.reason +} + +func (err *AError) Message() string { + return err.message +} + +func (err *AError) Stack() string { + return err.stack +} + +func (err *AError) WithCode(code ErrorCode) *AError { + if err == nil { + return nil + } + err.code = code + return err +} + +func (err *AError) WithParent(parent error) *AError { + if err == nil { + return nil + } + err.parent = parent + return err +} + +func (err *AError) WithReason(reason string) *AError { + if err == nil { + return nil + } + err.reason = reason + return err +} + +func (err *AError) WithMessage(message string) *AError { + if err == nil { + return nil + } + err.message = message + return err +} + +func (err *AError) WithStack() *AError { + if err == nil { + return nil + } + err.message = LogStack(2, 0) + return err +} + +// nolint:gocritic +func (err AError) Error() string { + str := bytes.NewBuffer([]byte{}) + fmt.Fprintf(str, "code: %s, ", err.code.Error()) + str.WriteString("reason: ") + str.WriteString(err.reason + ", ") + str.WriteString("message: ") + str.WriteString(err.message) + if err.parent != nil { + str.WriteString(", error: ") + str.WriteString(err.parent.Error()) + } + if err.stack != "" { + str.WriteString("\n") + str.WriteString(err.stack) + } + + return str.String() +} + +func (err *AError) Is(target error) bool { + t, ok := target.(*AError) + if !ok { + return false + } + if t.code != ErrOK && t.code != err.code { + return false + } + if t.message != "" && t.message != err.message { + return false + } + if t.parent != nil && !errors.Is(err.parent, t.parent) { + return false + } + + return true +} + +func (err *AError) As(target interface{}) bool { + _, ok := target.(**AError) + if !ok { + return false + } + reflect.Indirect(reflect.ValueOf(target)).Set(reflect.ValueOf(err)) + return true +} diff --git a/aerrors_test.go b/aerrors_test.go new file mode 100644 index 0000000..69f5837 --- /dev/null +++ b/aerrors_test.go @@ -0,0 +1,26 @@ +package aerrors + +import ( + "fmt" + "testing" +) + +func TestNew(t *testing.T) { + e := A() + fmt.Printf("%s\n\n", e) + fmt.Printf("%v\n\n", e) +} + +func A() error { + return B() +} + +func B() error { + return C() +} + +func C() error { + return Internal("internal server error"). + WithParent(fmt.Errorf("db connection error")). + WithStack() +} diff --git a/buf.gen.yaml b/buf.gen.yaml new file mode 100644 index 0000000..f987d81 --- /dev/null +++ b/buf.gen.yaml @@ -0,0 +1,12 @@ +version: v1 +managed: + enabled: true + go_package_prefix: + default: github.com/htquang/aerrors;aerrors + except: + - buf.build/googleapis/googleapis +plugins: + - name: go + out: . + opt: + - paths=source_relative diff --git a/buf.yaml b/buf.yaml new file mode 100644 index 0000000..dddd980 --- /dev/null +++ b/buf.yaml @@ -0,0 +1,9 @@ +version: v1 +lint: + enum_zero_value_suffix: _UNKNOWN + except: + - PACKAGE_VERSION_SUFFIX + - PACKAGE_DIRECTORY_MATCH +breaking: + use: + - FILE diff --git a/errorspb.pb.go b/errorspb.pb.go new file mode 100644 index 0000000..24dac80 --- /dev/null +++ b/errorspb.pb.go @@ -0,0 +1,188 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.34.2 +// protoc (unknown) +// source: errorspb.proto + +package aerrors + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type ErrorDetail struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Reason string `protobuf:"bytes,1,opt,name=Reason,proto3" json:"Reason,omitempty"` + Message string `protobuf:"bytes,2,opt,name=Message,proto3" json:"Message,omitempty"` + TypeCode string `protobuf:"bytes,3,opt,name=TypeCode,proto3" json:"TypeCode,omitempty"` + HTTPCode int64 `protobuf:"varint,4,opt,name=HTTPCode,proto3" json:"HTTPCode,omitempty"` + GRPCCode int64 `protobuf:"varint,5,opt,name=GRPCCode,proto3" json:"GRPCCode,omitempty"` +} + +func (x *ErrorDetail) Reset() { + *x = ErrorDetail{} + if protoimpl.UnsafeEnabled { + mi := &file_errorspb_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ErrorDetail) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ErrorDetail) ProtoMessage() {} + +func (x *ErrorDetail) ProtoReflect() protoreflect.Message { + mi := &file_errorspb_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ErrorDetail.ProtoReflect.Descriptor instead. +func (*ErrorDetail) Descriptor() ([]byte, []int) { + return file_errorspb_proto_rawDescGZIP(), []int{0} +} + +func (x *ErrorDetail) GetReason() string { + if x != nil { + return x.Reason + } + return "" +} + +func (x *ErrorDetail) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *ErrorDetail) GetTypeCode() string { + if x != nil { + return x.TypeCode + } + return "" +} + +func (x *ErrorDetail) GetHTTPCode() int64 { + if x != nil { + return x.HTTPCode + } + return 0 +} + +func (x *ErrorDetail) GetGRPCCode() int64 { + if x != nil { + return x.GRPCCode + } + return 0 +} + +var File_errorspb_proto protoreflect.FileDescriptor + +var file_errorspb_proto_rawDesc = []byte{ + 0x0a, 0x0e, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x70, 0x62, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x12, 0x07, 0x61, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x22, 0x93, 0x01, 0x0a, 0x0b, 0x45, 0x72, + 0x72, 0x6f, 0x72, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x12, 0x16, 0x0a, 0x06, 0x52, 0x65, 0x61, + 0x73, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x52, 0x65, 0x61, 0x73, 0x6f, + 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x07, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x54, + 0x79, 0x70, 0x65, 0x43, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x54, + 0x79, 0x70, 0x65, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x48, 0x54, 0x54, 0x50, 0x43, + 0x6f, 0x64, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x48, 0x54, 0x54, 0x50, 0x43, + 0x6f, 0x64, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x47, 0x52, 0x50, 0x43, 0x43, 0x6f, 0x64, 0x65, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x47, 0x52, 0x50, 0x43, 0x43, 0x6f, 0x64, 0x65, 0x42, + 0x7c, 0x0a, 0x0b, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x42, 0x0d, + 0x45, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x70, 0x62, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, + 0x22, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x74, 0x71, 0x75, + 0x61, 0x6e, 0x67, 0x2f, 0x61, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x3b, 0x61, 0x65, 0x72, 0x72, + 0x6f, 0x72, 0x73, 0xa2, 0x02, 0x03, 0x41, 0x58, 0x58, 0xaa, 0x02, 0x07, 0x41, 0x65, 0x72, 0x72, + 0x6f, 0x72, 0x73, 0xca, 0x02, 0x07, 0x41, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0xe2, 0x02, 0x13, + 0x41, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0xea, 0x02, 0x07, 0x41, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_errorspb_proto_rawDescOnce sync.Once + file_errorspb_proto_rawDescData = file_errorspb_proto_rawDesc +) + +func file_errorspb_proto_rawDescGZIP() []byte { + file_errorspb_proto_rawDescOnce.Do(func() { + file_errorspb_proto_rawDescData = protoimpl.X.CompressGZIP(file_errorspb_proto_rawDescData) + }) + return file_errorspb_proto_rawDescData +} + +var file_errorspb_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_errorspb_proto_goTypes = []any{ + (*ErrorDetail)(nil), // 0: aerrors.ErrorDetail +} +var file_errorspb_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_errorspb_proto_init() } +func file_errorspb_proto_init() { + if File_errorspb_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_errorspb_proto_msgTypes[0].Exporter = func(v any, i int) any { + switch v := v.(*ErrorDetail); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_errorspb_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_errorspb_proto_goTypes, + DependencyIndexes: file_errorspb_proto_depIdxs, + MessageInfos: file_errorspb_proto_msgTypes, + }.Build() + File_errorspb_proto = out.File + file_errorspb_proto_rawDesc = nil + file_errorspb_proto_goTypes = nil + file_errorspb_proto_depIdxs = nil +} diff --git a/errorspb.proto b/errorspb.proto new file mode 100644 index 0000000..6b125d9 --- /dev/null +++ b/errorspb.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + +package aerrors; + +message ErrorDetail { + string Reason = 1; + string Message = 2; + string TypeCode = 3; + int64 HTTPCode = 4; + int64 GRPCCode = 5; +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..64fe5b3 --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +module github.com/htquangg/aerrors + +go 1.22.2 + +require ( + google.golang.org/grpc v1.65.0 + google.golang.org/protobuf v1.34.2 +) + +require ( + golang.org/x/net v0.27.0 // indirect + golang.org/x/sys v0.22.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240722135656-d784300faade // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..2f53a5d --- /dev/null +++ b/go.sum @@ -0,0 +1,14 @@ +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240722135656-d784300faade h1:oCRSWfwGXQsqlVdErcyTt4A93Y8fo0/9D4b1gnI++qo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240722135656-d784300faade/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= +google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= +google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= diff --git a/grpc.go b/grpc.go new file mode 100644 index 0000000..5971486 --- /dev/null +++ b/grpc.go @@ -0,0 +1,310 @@ +package aerrors + +import ( + "errors" + "fmt" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +type GRPCCoder interface { + GRPCCode() codes.Code +} + +// nolint:gocyclo +func (err ErrorCode) GRPCCode() codes.Code { + switch err { + // GRPC Errors + case ErrOK: + return codes.OK + case ErrCanceled: + return codes.Canceled + case ErrUnknown: + return codes.Unknown + case ErrInvalidArgument: + return codes.InvalidArgument + case ErrDeadlineExceeded: + return codes.DeadlineExceeded + case ErrNotFound: + return codes.NotFound + case ErrAlreadyExists: + return codes.AlreadyExists + case ErrPermissionDenied: + return codes.PermissionDenied + case ErrResourceExhausted: + return codes.ResourceExhausted + case ErrFailedPrecondition: + return codes.FailedPrecondition + case ErrAborted: + return codes.Aborted + case ErrOutOfRange: + return codes.OutOfRange + case ErrUnimplemented: + return codes.Unimplemented + case ErrInternal: + return codes.Internal + case ErrUnavailable: + return codes.Unavailable + case ErrDataLoss: + return codes.DataLoss + case ErrUnauthenticated: + return codes.Unauthenticated + + // HTTP Errors + case ErrBadRequest: + return codes.InvalidArgument + case ErrUnauthorized: + return codes.Unauthenticated + case ErrForbidden: + return codes.PermissionDenied + case ErrMethodNotAllowed: + return codes.Unimplemented + case ErrRequestTimeout: + return codes.DeadlineExceeded + case ErrConflict: + return codes.AlreadyExists + case ErrImATeapot: + return codes.Unknown + case ErrUnprocessableEntity: + return codes.InvalidArgument + case ErrTooManyRequests: + return codes.ResourceExhausted + case ErrUnavailableForLegalReasons: + return codes.Unavailable + case ErrInternalServerError: + return codes.Internal + case ErrNotImplemented: + return codes.Unimplemented + case ErrBadGateway: + return codes.Aborted + case ErrServiceUnavailable: + return codes.Unavailable + case ErrGatewayTimeout: + return codes.DeadlineExceeded + default: + return codes.Internal + } +} + +func (err ErrorCode) GRPCStatus() *status.Status { + return errToStatus(err) +} + +// nolint:gocritic +func (err AError) GRPCStatus() *status.Status { + return errToStatus(err) +} + +type grpcError struct { + status *status.Status + grpcCode codes.Code + httpCode int + code string + reason string + message string +} + +func (err grpcError) Error() string { + return fmt.Sprintf("Code=%s Reason=%s Message=(%v)", err.code, err.reason, err.message) +} + +func (err grpcError) GRPCStatus() *status.Status { + return err.status +} + +func (err grpcError) HTTPCode() int { + return err.httpCode +} + +func (err grpcError) GRPCCode() codes.Code { + return err.grpcCode +} + +func (err grpcError) TypeCode() string { + return err.code +} + +// Is returns true if any of TypeCoder, HTTPCoder, GRPCCoder are a match between the error and target +func (err grpcError) Is(target error) bool { + if t, ok := target.(GRPCCoder); ok && err.grpcCode == t.GRPCCode() { + return true + } + if t, ok := target.(HTTPCoder); ok && err.httpCode == t.HTTPCode() { + return true + } + if t, ok := target.(TypeCoder); ok && err.code == t.TypeCode() { + return true + } + return false +} + +// GRPCCode returns the GRPC code for the given error or codes.OK when nil or codes.Unknown otherwise +func GRPCCode(err error) codes.Code { + if err == nil { + return ErrOK.GRPCCode() + } + var e GRPCCoder + if errors.As(err, &e) { + return e.GRPCCode() + } + return ErrUnknown.GRPCCode() +} + +// SendGRPCError ensures that the error being used is sent with the correct code applied +// +// Use in the server when sending errors. +// If err is nil then SendGRPCError returns nil. +func SendGRPCError(err error) error { + if err == nil { + return nil + } + + // Already setup with a grpcCode + if _, ok := status.FromError(err); ok { + return err + } + + s := errToStatus(err) + + return s.Err() +} + +// ReceiveGRPCError recreates the error with the coded Error reapplied +// +// Non-nil results can be used as both Error and *status.Status. Methods +// errors.Is()/errors.As(), and status.Convert()/status.FromError() will +// continue to work. +// +// Use in the clients when receiving errors. +// If err is nil then ReceiveGRPCError returns nil. +func ReceiveGRPCError(err error) error { + if err == nil { + return nil + } + + s, ok := status.FromError(err) + if !ok { + return &grpcError{ + status: s, + grpcCode: ErrUnknown.GRPCCode(), + httpCode: ErrUnknown.HTTPCode(), + code: ErrUnknown.TypeCode(), + reason: ErrUnknown.Error(), + message: err.Error(), + } + } + + grpcCode := s.Code() + httpCode := ErrUnknown.HTTPCode() + embedType := codeToError(grpcCode).TypeCode() + reason := ErrUnknown.Error() + + for _, detail := range s.Details() { + switch d := detail.(type) { + case *ErrorDetail: + grpcCode = codes.Code(d.GRPCCode) + httpCode = int(d.HTTPCode) + embedType = d.TypeCode + reason = d.Reason + default: + } + } + + return &grpcError{ + status: s, + grpcCode: grpcCode, + httpCode: httpCode, + code: embedType, + reason: reason, + message: s.Message(), + } +} + +// convert a code to a known Error type; +func codeToError(code codes.Code) ErrorCode { + switch code { + case codes.OK: + return ErrOK + case codes.Canceled: + return ErrCanceled + case codes.Unknown: + return ErrUnknown + case codes.InvalidArgument: + return ErrInvalidArgument + case codes.DeadlineExceeded: + return ErrDeadlineExceeded + case codes.NotFound: + return ErrNotFound + case codes.AlreadyExists: + return ErrAlreadyExists + case codes.PermissionDenied: + return ErrPermissionDenied + case codes.ResourceExhausted: + return ErrResourceExhausted + case codes.FailedPrecondition: + return ErrFailedPrecondition + case codes.Aborted: + return ErrAborted + case codes.OutOfRange: + return ErrOutOfRange + case codes.Unimplemented: + return ErrUnimplemented + case codes.Internal: + return ErrInternal + case codes.Unavailable: + return ErrUnavailable + case codes.DataLoss: + return ErrDataLoss + case codes.Unauthenticated: + return ErrUnauthenticated + default: + return ErrInternal + } +} + +// convert an error into a gRPC *status.Status +func errToStatus(err error) *status.Status { + grpcCode := ErrUnknown.GRPCCode() + httpCode := ErrUnknown.HTTPCode() + typeCode := ErrUnknown.TypeCode() + + // Set the grpcCode based on GRPCCoder output; otherwise leave as Unknown + var grpcCoder GRPCCoder + if errors.As(err, &grpcCoder) { + grpcCode = grpcCoder.GRPCCode() + } + + // short circuit building detailed errors if the code is OK + if grpcCode == codes.OK { + return status.New(codes.OK, "") + } + + // Set the httpCode based on HTTPCoder output; otherwise leave as Unknown + var httpCoder HTTPCoder + if errors.As(err, &httpCoder) { + httpCode = httpCoder.HTTPCode() + } + + // Embed the specific error "type"; otherwise leave as "UNKNOWN" + var typeCoder TypeCoder + if errors.As(err, &typeCoder) { + typeCode = typeCoder.TypeCode() + } + + errInfo := &ErrorDetail{ + TypeCode: typeCode, + GRPCCode: int64(grpcCode), + HTTPCode: int64(httpCode), + } + + var aErr AError + if ok := errors.As(err, &aErr); ok { + errInfo.Reason = aErr.reason + errInfo.Message = aErr.message + } + + s, _ := status.New(grpcCode, err.Error()).WithDetails(errInfo) + + return s +} diff --git a/http.go b/http.go new file mode 100644 index 0000000..ce11031 --- /dev/null +++ b/http.go @@ -0,0 +1,98 @@ +package aerrors + +import ( + "errors" + "net/http" +) + +type HTTPCoder interface { + HTTPCode() int +} + +// nolint:gocyclo +func (err ErrorCode) HTTPCode() int { + switch err { + // GRPC Errors + case ErrOK: + return http.StatusOK + case ErrCanceled: + return http.StatusRequestTimeout + case ErrUnknown: + return http.StatusNotExtended + case ErrInvalidArgument: + return http.StatusBadRequest + case ErrDeadlineExceeded: + return http.StatusGatewayTimeout + case ErrNotFound: + return http.StatusNotFound + case ErrAlreadyExists: + return http.StatusConflict + case ErrPermissionDenied: + return http.StatusForbidden + case ErrResourceExhausted: + return http.StatusTooManyRequests + case ErrFailedPrecondition: + return http.StatusBadRequest + case ErrAborted: + return http.StatusConflict + case ErrOutOfRange: + return http.StatusUnprocessableEntity + case ErrUnimplemented: + return http.StatusNotImplemented + case ErrInternal: + return http.StatusInternalServerError + case ErrUnavailable: + return http.StatusServiceUnavailable + case ErrDataLoss: + return http.StatusInternalServerError + case ErrUnauthenticated: + return http.StatusUnauthorized + + // HTTP Errors + case ErrBadRequest: + return http.StatusBadRequest + case ErrUnauthorized: + return http.StatusUnauthorized + case ErrForbidden: + return http.StatusForbidden + case ErrMethodNotAllowed: + return http.StatusMethodNotAllowed + case ErrRequestTimeout: + return http.StatusRequestTimeout + case ErrConflict: + return http.StatusConflict + case ErrImATeapot: + return 418 // teapot support + case ErrUnprocessableEntity: + return http.StatusUnprocessableEntity + case ErrTooManyRequests: + return http.StatusTooManyRequests + case ErrUnavailableForLegalReasons: + return http.StatusUnavailableForLegalReasons + case ErrInternalServerError: + return http.StatusInternalServerError + case ErrNotImplemented: + return http.StatusNotImplemented + case ErrBadGateway: + return http.StatusBadGateway + case ErrServiceUnavailable: + return http.StatusServiceUnavailable + case ErrGatewayTimeout: + return http.StatusGatewayTimeout + default: + return http.StatusInternalServerError + } +} + +// HTTPCode returns the HTTP status for the given error or http.StatusOK when nil or http.StatusNotExtended otherwise +func HTTPCode(err error) int { + if err == nil { + return ErrOK.HTTPCode() + } + + var e HTTPCoder + if errors.As(err, &e) { + return e.HTTPCode() + } + return ErrUnknown.HTTPCode() +} diff --git a/stack.go b/stack.go new file mode 100644 index 0000000..e3f0250 --- /dev/null +++ b/stack.go @@ -0,0 +1,21 @@ +package aerrors + +import ( + "bytes" + "fmt" + "runtime" +) + +// LogStack return call function stack info from start stack to end stack. +// if end is a positive number, return all call function stack. +func LogStack(start, end int) string { + stack := bytes.Buffer{} + for i := start; i < end || end <= 0; i++ { + pc, str, line, _ := runtime.Caller(i) + if line == 0 { + break + } + stack.WriteString(fmt.Sprintf("%s:%d %s\n", str, line, runtime.FuncForPC(pc).Name())) + } + return stack.String() +} diff --git a/types.go b/types.go new file mode 100644 index 0000000..b203ee3 --- /dev/null +++ b/types.go @@ -0,0 +1,121 @@ +package aerrors + +// Errors named in line with GRPC codes and some that overlap with HTTP statuses +const ( + ErrOK ErrorCode = "OK" // HTTP: 200 GRPC: codes.OK + ErrCanceled ErrorCode = "CANCELED" // HTTP: 408 GRPC: codes.Canceled + ErrUnknown ErrorCode = "UNKNOWN" // HTTP: 510 GRPC: codes.Unknown + ErrInvalidArgument ErrorCode = "INVALID_ARGUMENT" // HTTP: 400 GRPC: codes.InvalidArgument + ErrDeadlineExceeded ErrorCode = "DEADLINE_EXCEEDED" // HTTP: 504 GRPC: codes.DeadlineExceeded + ErrNotFound ErrorCode = "NOT_FOUND" // HTTP: 404 GRPC: codes.NotFound + ErrAlreadyExists ErrorCode = "ALREADY_EXISTS" // HTTP: 409 GRPC: codes.AlreadyExists + ErrPermissionDenied ErrorCode = "PERMISSION_DENIED" // HTTP: 403 GRPC: codes.PermissionDenied + ErrResourceExhausted ErrorCode = "RESOURCE_EXHAUSTED" // HTTP: 429 GRPC: codes.ResourceExhausted + ErrFailedPrecondition ErrorCode = "FAILED_PRECONDITION" // HTTP: 400 GRPC: codes.FailedPrecondition + ErrAborted ErrorCode = "ABORTED" // HTTP: 409 GRPC: codes.Aborted + ErrOutOfRange ErrorCode = "OUT_OF_RANGE" // HTTP: 422 GRPC: codes.OutOfRange + ErrUnimplemented ErrorCode = "UNIMPLEMENTED" // HTTP: 501 GRPC: codes.Unimplemented + ErrInternal ErrorCode = "INTERNAL" // HTTP: 500 GRPC: codes.Internal + ErrUnavailable ErrorCode = "UNAVAILABLE" // HTTP: 503 GRPC: codes.Unavailable + ErrDataLoss ErrorCode = "DATA_LOSS" // HTTP: 500 GRPC: codes.DataLoss + ErrUnauthenticated ErrorCode = "UNAUTHENTICATED" // HTTP: 401 GRPC: codes.Unauthenticated +) + +// Errors named in line with HTTP statuses +const ( + ErrBadRequest ErrorCode = "BAD_REQUEST" // HTTP: 400 GRPC: codes.InvalidArgument + ErrUnauthorized ErrorCode = "UNAUTHORIZED" // HTTP: 401 GRPC: codes.Unauthenticated + ErrForbidden ErrorCode = "FORBIDDEN" // HTTP: 403 GRPC: codes.PermissionDenied + ErrMethodNotAllowed ErrorCode = "METHOD_NOT_ALLOWED" // HTTP: 405 GRPC: codes.Unimplemented + ErrRequestTimeout ErrorCode = "REQUEST_TIMEOUT" // HTTP: 408 GRPC: codes.DeadlineExceeded + ErrConflict ErrorCode = "CONFLICT" // HTTP: 409 GRPC: codes.AlreadyExists + ErrImATeapot ErrorCode = "IM_A_TEAPOT" // HTTP: 418 GRPC: codes.Unknown + ErrUnprocessableEntity ErrorCode = "UNPROCESSABLE_ENTITY" // HTTP: 422 GRPC: codes.InvalidArgument + ErrTooManyRequests ErrorCode = "TOO_MANY_REQUESTS" // HTTP: 429 GRPC: codes.ResourceExhausted + ErrUnavailableForLegalReasons ErrorCode = "UNAVAILABLE_FOR_LEGAL_REASONS" // HTTP: 451 GRPC: codes.Unavailable + ErrInternalServerError ErrorCode = "INTERNAL_SERVER_ERROR" // HTTP: 500 GRPC: codes.Internal + ErrNotImplemented ErrorCode = "NOT_IMPLEMENTED" // HTTP: 501 GRPC: codes.Unimplemented + ErrBadGateway ErrorCode = "BAD_GATEWAY" // HTTP: 502 GRPC: codes.Aborted + ErrServiceUnavailable ErrorCode = "SERVICE_UNAVAILABLE" // HTTP: 503 GRPC: codes.Unavailable + ErrGatewayTimeout ErrorCode = "GATEWAY_TIMEOUT" // HTTP: 504 GRPC: codes.DeadlineExceeded +) + +func InvalidArgument(reason string) *AError { + return New(ErrInvalidArgument, reason) +} + +func IsValidArgument(err *AError) bool { + return err.code == ErrInvalidArgument +} + +func FailedPrecondition(reason string) *AError { + return New(ErrFailedPrecondition, reason) +} + +func IsFailedPrecondition(err *AError) bool { + return err.code == ErrFailedPrecondition +} + +func Unauthentication(reason string) *AError { + return New(ErrUnauthenticated, reason) +} + +func IsUnauthentication(err *AError) bool { + return err.code == ErrUnauthenticated || err.code == ErrUnauthorized +} + +func PermissionDenied(reason string) *AError { + return New(ErrPermissionDenied, reason) +} + +func IsPermissionDenied(err *AError) bool { + return err.code == ErrPermissionDenied || err.code == ErrForbidden +} + +func NotFound(reason string) *AError { + return New(ErrNotFound, reason) +} + +func IsNotFound(err *AError) bool { + return err.code == ErrNotFound +} + +func AlreadyExists(reason string) *AError { + return New(ErrConflict, reason) +} + +func IsAlreadyExists(err *AError) bool { + return err.code == ErrAlreadyExists || err.code == ErrConflict || err.code == ErrAborted +} + +func Internal(reason string) *AError { + return New(ErrInternal, reason) +} + +func IsInternal(err *AError) bool { + return err.code == ErrInternal || err.code == ErrInternalServerError || err.code == ErrDataLoss +} + +func Unimplemented(reason string) *AError { + return New(ErrUnimplemented, reason) +} + +func IsUnimplemented(err *AError) bool { + return err.code == ErrUnimplemented || err.code == ErrMethodNotAllowed || err.code == ErrNotImplemented +} + +func Unavailable(reason string) *AError { + return New(ErrUnavailable, reason) +} + +func IsUnavailable(err *AError) bool { + return err.code == ErrUnavailable || err.code == ErrServiceUnavailable +} + +func DeadlineExceeded(reason string) *AError { + return New(ErrGatewayTimeout, reason) +} + +func IsDeadlineExceeded(err *AError) bool { + return err.code == ErrDeadlineExceeded || err.code == ErrGatewayTimeout +}