From 79a3b8a6b149f603a3284b38483e841307fe665e Mon Sep 17 00:00:00 2001 From: Maksym Kryvchun Date: Tue, 7 Mar 2023 06:34:44 +0200 Subject: [PATCH] feat: support errors.Join [#15] (#16) --- .github/workflows/check.yml | 2 +- README.md | 5 +- README.md.tmpl | 5 +- go.mod | 2 +- go.sum | 22 +++---- internal/cmd/generator/generator.go | 4 +- makefile | 2 +- pkg/v1/grpcerr/grpcerr_generated.go | 58 ++++++++++++------- pkg/v1/grpcerr/grpcerr_generated.go.tmpl | 34 +++++++---- pkg/v1/grpcerr/grpcerr_generated_test.go | 16 +++++ pkg/v1/grpcerr/grpcerr_generated_test.go.tmpl | 16 +++++ pkg/v1/httperr/httperr_generated.go | 39 ++++++------- pkg/v1/httperr/httperr_generated.go.tmpl | 17 ++---- pkg/v1/httperr/httperr_generated_test.go | 16 +++++ pkg/v1/httperr/httperr_generated_test.go.tmpl | 18 +++++- pkg/v1/semerr/multi.go | 2 + vendor/modules.txt | 2 +- 17 files changed, 176 insertions(+), 84 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index a551dc3..9068864 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -14,7 +14,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v3 with: - go-version: '1.19.3' + go-version: '1.20.1' id: go - name: Check out code into the Go module directory diff --git a/README.md b/README.md index 119d015..f2698f8 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ [![Coverage Status](https://coveralls.io/repos/github/hedhyw/semerr/badge.svg?branch=main)](https://coveralls.io/github/hedhyw/semerr?branch=main) [![PkgGoDev](https://pkg.go.dev/badge/github.com/hedhyw/semerr)](https://pkg.go.dev/github.com/hedhyw/semerr?tab=doc) -Package `semerr` helps to work with errors in Golang. +Package `semerr` helps to work with errors in Golang. It supports go 1.20 [errors.Join](https://pkg.go.dev/errors#Join). @@ -88,10 +88,13 @@ func (s *Server) handleCreateOrder(w http.ResponseWriter, r *http.Request) { ```go errOriginal := errors.New("some error") errWrapped := semerr.NewBadRequestError(errOriginal) // The text will be the same. +errJoined := errors.Join(errOriginal, errWrapped) // It supports joined errors. fmt.Println(errWrapped) // "some error" fmt.Println(httperr.Code(errWrapped)) // http.StatusBadRequest +fmt.Println(httperr.Code(errJoined)) // http.StatusBadRequest fmt.Println(grpcerr.Code(errWrapped)) // codes.InvalidArgument +fmt.Println(grpcerr.Code(errJoined)) // codes.InvalidArgument fmt.Println(errors.Is(err, errOriginal)) // true fmt.Println(semerr.NewBadRequestError(nil)) // nil fmt.Println(httperr.Wrap(errOriginal, http.StatusBadRequest)) // = semerr.NewBadRequestError(errOriginal) diff --git a/README.md.tmpl b/README.md.tmpl index fe2e686..4dd5e9e 100644 --- a/README.md.tmpl +++ b/README.md.tmpl @@ -8,7 +8,7 @@ [![Coverage Status](https://coveralls.io/repos/github/hedhyw/semerr/badge.svg?branch=main)](https://coveralls.io/github/hedhyw/semerr?branch=main) [![PkgGoDev](https://pkg.go.dev/badge/github.com/hedhyw/semerr)](https://pkg.go.dev/github.com/hedhyw/semerr?tab=doc) -Package `semerr` helps to work with errors in Golang. +Package `semerr` helps to work with errors in Golang. It supports go 1.20 [errors.Join](https://pkg.go.dev/errors#Join). @@ -88,10 +88,13 @@ func (s *Server) handleCreateOrder(w http.ResponseWriter, r *http.Request) { ```go errOriginal := errors.New("some error") errWrapped := semerr.NewBadRequestError(errOriginal) // The text will be the same. +errJoined := errors.Join(errOriginal, errWrapped) // It supports joined errors. fmt.Println(errWrapped) // "some error" fmt.Println(httperr.Code(errWrapped)) // http.StatusBadRequest +fmt.Println(httperr.Code(errJoined)) // http.StatusBadRequest fmt.Println(grpcerr.Code(errWrapped)) // codes.InvalidArgument +fmt.Println(grpcerr.Code(errJoined)) // codes.InvalidArgument fmt.Println(errors.Is(err, errOriginal)) // true fmt.Println(semerr.NewBadRequestError(nil)) // nil fmt.Println(httperr.Wrap(errOriginal, http.StatusBadRequest)) // = semerr.NewBadRequestError(errOriginal) diff --git a/go.mod b/go.mod index 1698785..d89b49e 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/golang/protobuf v1.5.2 // indirect github.com/kr/pretty v0.3.0 // indirect github.com/rogpeppe/go-internal v1.9.0 // indirect - google.golang.org/genproto v0.0.0-20230227214838-9b19f0bdc514 // indirect + google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) diff --git a/go.sum b/go.sum index d660c0b..94f59af 100644 --- a/go.sum +++ b/go.sum @@ -48,7 +48,7 @@ cloud.google.com/go/aiplatform v1.27.0/go.mod h1:Bvxqtl40l0WImSb04d0hXFU7gDOiq9j cloud.google.com/go/aiplatform v1.35.0/go.mod h1:7MFT/vCaOyZT/4IIFfxH4ErVg/4ku6lKv3w0+tFTgXQ= cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI= cloud.google.com/go/analytics v0.12.0/go.mod h1:gkfj9h6XRf9+TS4bmuhPEShsh3hH8PAZzm/41OOhQd4= -cloud.google.com/go/analytics v0.17.0/go.mod h1:WXFa3WSym4IZ+JiKmavYdJwGG/CvpqiqczmL59bTD9M= +cloud.google.com/go/analytics v0.18.0/go.mod h1:ZkeHGQlcIPkw0R/GW+boWHhCOR43xz9RN/jn7WcqfIE= cloud.google.com/go/apigateway v1.3.0/go.mod h1:89Z8Bhpmxu6AmUxuVRg/ECRGReEdiP3vQtk4Z1J9rJk= cloud.google.com/go/apigateway v1.4.0/go.mod h1:pHVY9MKGaH9PQ3pJ4YLzoj6U5FUDeDFBllIz7WmzJoc= cloud.google.com/go/apigateway v1.5.0/go.mod h1:GpnZR3Q4rR7LVu5951qfXPJCHquZt02jf7xQx7kpqN8= @@ -64,12 +64,12 @@ cloud.google.com/go/appengine v1.5.0/go.mod h1:TfasSozdkFI0zeoxW3PTBLiNqRmzraodC cloud.google.com/go/appengine v1.6.0/go.mod h1:hg6i0J/BD2cKmDJbaFSYHFyZkgBEfQrDg/X0V5fJn84= cloud.google.com/go/area120 v0.5.0/go.mod h1:DE/n4mp+iqVyvxHN41Vf1CR602GiHQjFPusMFW6bGR4= cloud.google.com/go/area120 v0.6.0/go.mod h1:39yFJqWVgm0UZqWTOdqkLhjoC7uFfgXRC8g/ZegeAh0= -cloud.google.com/go/area120 v0.7.0/go.mod h1:a3+8EUD1SX5RUcCs3MY5YasiO1z6yLiNLRiFrykbynY= +cloud.google.com/go/area120 v0.7.1/go.mod h1:j84i4E1RboTWjKtZVWXPqvK5VHQFJRF2c1Nm69pWm9k= cloud.google.com/go/artifactregistry v1.6.0/go.mod h1:IYt0oBPSAGYj/kprzsBjZ/4LnG/zOcHyFHjWPCi6SAQ= cloud.google.com/go/artifactregistry v1.7.0/go.mod h1:mqTOFOnGZx8EtSqK/ZWcsm/4U8B77rbcLP6ruDU2Ixk= cloud.google.com/go/artifactregistry v1.8.0/go.mod h1:w3GQXkJX8hiKN0v+at4b0qotwijQbYUqF2GWkZzAhC0= cloud.google.com/go/artifactregistry v1.9.0/go.mod h1:2K2RqvA2CYvAeARHRkLDhMDJ3OXy26h3XW+3/Jh2uYc= -cloud.google.com/go/artifactregistry v1.11.1/go.mod h1:lLYghw+Itq9SONbCa1YWBoWs1nOucMH0pwXN1rOBZFI= +cloud.google.com/go/artifactregistry v1.11.2/go.mod h1:nLZns771ZGAwVLzTX/7Al6R9ehma4WUEhZGWV6CeQNQ= cloud.google.com/go/asset v1.5.0/go.mod h1:5mfs8UvcM5wHhqtSv8J1CtxxaQq3AdBxxQi2jGW/K4o= cloud.google.com/go/asset v1.7.0/go.mod h1:YbENsRK4+xTiL+Ofoj5Ckf+O17kJtgp3Y3nn4uzZz5s= cloud.google.com/go/asset v1.8.0/go.mod h1:mUNGKhiqIdbr8X7KNayoYvyc4HbbFO9URsjbytpUaW0= @@ -105,7 +105,7 @@ cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM7 cloud.google.com/go/bigquery v1.42.0/go.mod h1:8dRTJxhtG+vwBKzE5OseQn/hiydoQN3EedCaOdYmxRA= cloud.google.com/go/bigquery v1.43.0/go.mod h1:ZMQcXHsl+xmU1z36G2jNGZmKp9zNY5BUua5wDgmNCfw= cloud.google.com/go/bigquery v1.44.0/go.mod h1:0Y33VqXTEsbamHJvJHdFmtqHvMIY28aK1+dFsvaChGc= -cloud.google.com/go/bigquery v1.47.0/go.mod h1:sA9XOgy0A8vQK9+MWhEQTY6Tix87M/ZurWFIxmF9I/E= +cloud.google.com/go/bigquery v1.48.0/go.mod h1:QAwSz+ipNgfL5jxiaK7weyOhzdoAy1zFm0Nf1fysJac= cloud.google.com/go/billing v1.4.0/go.mod h1:g9IdKBEFlItS8bTtlrZdVLWSSdSyFUZKXNS02zKMOZY= cloud.google.com/go/billing v1.5.0/go.mod h1:mztb1tBc3QekhjSgmpf/CV4LzWXLzCArwpLmP2Gm88s= cloud.google.com/go/billing v1.6.0/go.mod h1:WoXzguj+BeHXPbKfNWkqVtDdzORazmCjraY+vrxcyvI= @@ -124,7 +124,7 @@ cloud.google.com/go/channel v1.9.0/go.mod h1:jcu05W0my9Vx4mt3/rEHpfxc9eKi9XwsdDL cloud.google.com/go/channel v1.11.0/go.mod h1:IdtI0uWGqhEeatSB62VOoJ8FSUhJ9/+iGkJVqp74CGE= cloud.google.com/go/cloudbuild v1.3.0/go.mod h1:WequR4ULxlqvMsjDEEEFnOG5ZSRSgWOywXYDb1vPE6U= cloud.google.com/go/cloudbuild v1.4.0/go.mod h1:5Qwa40LHiOXmz3386FrjrYM93rM/hdRr7b53sySrTqA= -cloud.google.com/go/cloudbuild v1.6.0/go.mod h1:UIbc/w9QCbH12xX+ezUsgblrWv+Cv4Tw83GiSMHOn9M= +cloud.google.com/go/cloudbuild v1.7.0/go.mod h1:zb5tWh2XI6lR9zQmsm1VRA+7OCuve5d8S+zJUul8KTg= cloud.google.com/go/clouddms v1.3.0/go.mod h1:oK6XsCDdW4Ib3jCCBugx+gVjevp2TMXFtgxvPSee3OM= cloud.google.com/go/clouddms v1.4.0/go.mod h1:Eh7sUGCC+aKry14O1NRljhjyrr0NFC0G2cjwX0cByRk= cloud.google.com/go/clouddms v1.5.0/go.mod h1:QSxQnhikCLUw13iAbffF2CZxAER3xDGNHjsTAkQJcQA= @@ -277,7 +277,7 @@ cloud.google.com/go/iot v1.5.0/go.mod h1:mpz5259PDl3XJthEmh9+ap0affn/MqNSP4My77Q cloud.google.com/go/kms v1.4.0/go.mod h1:fajBHndQ+6ubNw6Ss2sSd+SWvjL26RNo/dr7uxsnnOA= cloud.google.com/go/kms v1.5.0/go.mod h1:QJS2YY0eJGBg3mnDfuaCyLauWwBJiHRboYxJ++1xJNg= cloud.google.com/go/kms v1.6.0/go.mod h1:Jjy850yySiasBUDi6KFUwUv2n1+o7QZFyuUJg6OgjA0= -cloud.google.com/go/kms v1.8.0/go.mod h1:4xFEhYFqvW+4VMELtZyxomGSYtSQKzM178ylFW4jMAg= +cloud.google.com/go/kms v1.9.0/go.mod h1:qb1tPTgfF9RQP8e1wq4cLFErVuTJv7UsSC915J8dh3w= cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic= cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI= cloud.google.com/go/language v1.7.0/go.mod h1:DJ6dYN/W+SQOjF8e1hLQXMF21AkH2w9wiPzPCJa2MIE= @@ -421,7 +421,7 @@ cloud.google.com/go/securitycenter v1.16.0/go.mod h1:Q9GMaLQFUD+5ZTabrbujNWLtSLZ cloud.google.com/go/securitycenter v1.18.1/go.mod h1:0/25gAzCM/9OL9vVx4ChPeM/+DlfGQJDwBy/UC8AKK0= cloud.google.com/go/servicecontrol v1.4.0/go.mod h1:o0hUSJ1TXJAmi/7fLJAedOovnujSEvjKCAFNXPQ1RaU= cloud.google.com/go/servicecontrol v1.5.0/go.mod h1:qM0CnXHhyqKVuiZnGKrIurvVImCs8gmqWsDoqe9sU1s= -cloud.google.com/go/servicecontrol v1.10.0/go.mod h1:pQvyvSRh7YzUF2efw7H87V92mxU8FnFDawMClGCNuAA= +cloud.google.com/go/servicecontrol v1.11.0/go.mod h1:kFmTzYzTUIuZs0ycVqRHNaNhgR+UMUpw9n02l/pY+mc= cloud.google.com/go/servicedirectory v1.4.0/go.mod h1:gH1MUaZCgtP7qQiI+F+A+OpeKF/HQWgtAddhTbhL2bs= cloud.google.com/go/servicedirectory v1.5.0/go.mod h1:QMKFL0NUySbpZJ1UZs3oFAmdvVxhhxB6eJ/Vlp73dfg= cloud.google.com/go/servicedirectory v1.6.0/go.mod h1:pUlbnWsLH9c13yGkxCmfumWEPjsRs1RlmJ4pqiNjVL4= @@ -472,10 +472,10 @@ cloud.google.com/go/trace v1.4.0/go.mod h1:UG0v8UBqzusp+z63o7FK74SdFE+AXpCLdFb1r cloud.google.com/go/trace v1.8.0/go.mod h1:zH7vcsbAhklH8hWFig58HvxcxyQbaIqMarMg9hn5ECA= cloud.google.com/go/translate v1.3.0/go.mod h1:gzMUwRjvOqj5i69y/LYLd8RrNQk+hOmIXTi9+nb3Djs= cloud.google.com/go/translate v1.4.0/go.mod h1:06Dn/ppvLD6WvA5Rhdp029IX2Mi3Mn7fpMRLPvXT5Wg= -cloud.google.com/go/translate v1.5.0/go.mod h1:29YDSYveqqpA1CQFD7NQuP49xymq17RXNaUDdc0mNu0= +cloud.google.com/go/translate v1.6.0/go.mod h1:lMGRudH1pu7I3n3PETiOB2507gf3HnfLV8qlkHZEyos= cloud.google.com/go/video v1.8.0/go.mod h1:sTzKFc0bUSByE8Yoh8X0mn8bMymItVGPfTuUBUyRgxk= cloud.google.com/go/video v1.9.0/go.mod h1:0RhNKFRF5v92f8dQt0yhaHrEuH95m068JYOvLZYnJSw= -cloud.google.com/go/video v1.12.0/go.mod h1:MLQew95eTuaNDEGriQdcYn0dTwf9oWiA4uYebxM5kdg= +cloud.google.com/go/video v1.13.0/go.mod h1:ulzkYlYgCp15N2AokzKjy7MQ9ejuynOJdf1tR5lGthk= cloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU= cloud.google.com/go/videointelligence v1.7.0/go.mod h1:k8pI/1wAhjznARtVT9U1llUaFNPh7muw8QyOUpavru4= cloud.google.com/go/videointelligence v1.8.0/go.mod h1:dIcCn4gVDdS7yte/w+koiXn5dWVplOZkE+xwG9FgK+M= @@ -1285,8 +1285,8 @@ google.golang.org/genproto v0.0.0-20230127162408-596548ed4efa/go.mod h1:RGgjbofJ google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44/go.mod h1:8B0gmkoRebU8ukX6HP+4wrVQUY1+6PkQ44BSyIlflHA= google.golang.org/genproto v0.0.0-20230222225845-10f96fb3dbec/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw= -google.golang.org/genproto v0.0.0-20230227214838-9b19f0bdc514 h1:rtNKfB++wz5mtDY2t5C8TXlU5y52ojSu7tZo0z7u8eQ= -google.golang.org/genproto v0.0.0-20230227214838-9b19f0bdc514/go.mod h1:TvhZT5f700eVlTNwND1xoEZQeWTB2RY/65kplwl/bFA= +google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 h1:DdoeryqhaXp1LtT/emMP1BRJPHHKFi5akj/nbx/zNTA= +google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= diff --git a/internal/cmd/generator/generator.go b/internal/cmd/generator/generator.go index e91f099..f0750b4 100644 --- a/internal/cmd/generator/generator.go +++ b/internal/cmd/generator/generator.go @@ -4,6 +4,7 @@ import ( "bufio" "bytes" "context" + "errors" "fmt" "go/format" "io/fs" @@ -18,7 +19,6 @@ import ( _ "embed" - "github.com/hedhyw/semerr/pkg/v1/semerr" "google.golang.org/grpc/codes" "gopkg.in/yaml.v2" ) @@ -106,7 +106,7 @@ func walkFn(errDefs []errorDefinition) fs.WalkDirFunc { return fmt.Errorf("opening out file: %w", err) } - defer func() { err = semerr.NewMultiError(err, f.Close()) }() + defer func() { err = errors.Join(err, f.Close()) }() var buf bytes.Buffer err = tmpl.Execute(&buf, errDefs) diff --git a/makefile b/makefile index fae4e70..71f3974 100644 --- a/makefile +++ b/makefile @@ -1,5 +1,5 @@ FILES_DIR?=$(PWD)/pkg/v1 -GOLANGCI_LINT_VER:=v1.50.1 +GOLANGCI_LINT_VER:=v1.51.2 all: generate lint test .PHONY: all diff --git a/pkg/v1/grpcerr/grpcerr_generated.go b/pkg/v1/grpcerr/grpcerr_generated.go index 05b35a2..9eb34f2 100755 --- a/pkg/v1/grpcerr/grpcerr_generated.go +++ b/pkg/v1/grpcerr/grpcerr_generated.go @@ -11,49 +11,63 @@ import ( "google.golang.org/grpc/status" ) -// Code returns grpc status code for err. +// Code returns grpc status code for err. In case of joined errors +// it returns the first code found in the chain. func Code(err error) codes.Code { - switch err.(type) { - case nil: + switch { + case err == nil: return codes.OK - case semerr.StatusRequestTimeoutError: + case errors.As(err, &semerr.StatusRequestTimeoutError{}): return 1 - case semerr.InternalServerError: + case errors.As(err, &semerr.InternalServerError{}): return 2 - case semerr.BadRequestError: + case errors.As(err, &semerr.BadRequestError{}): return 3 - case semerr.UnsupportedMediaTypeError: + case errors.As(err, &semerr.UnsupportedMediaTypeError{}): return 3 - case semerr.StatusGatewayTimeoutError: + case errors.As(err, &semerr.StatusGatewayTimeoutError{}): return 4 - case semerr.NotFoundError: + case errors.As(err, &semerr.NotFoundError{}): return 5 - case semerr.ConflictError: + case errors.As(err, &semerr.ConflictError{}): return 6 - case semerr.ForbiddenError: + case errors.As(err, &semerr.ForbiddenError{}): return 7 - case semerr.TooManyRequestsError: + case errors.As(err, &semerr.TooManyRequestsError{}): return 8 - case semerr.RequestEntityTooLargeError: + case errors.As(err, &semerr.RequestEntityTooLargeError{}): return 11 - case semerr.UnimplementedError: + case errors.As(err, &semerr.UnimplementedError{}): return 12 - case semerr.ServiceUnavailableError: + case errors.As(err, &semerr.ServiceUnavailableError{}): return 14 - case semerr.UnauthorizedError: + case errors.As(err, &semerr.UnauthorizedError{}): return 16 + default: + return getGRPCErrorCode(err) } +} - code := status.Code(err) - if code != codes.OK && code != codes.Unknown { - return code +func getGRPCErrorCode(err error) codes.Code { + var errGRPC interface { + GRPCStatus() *status.Status + error } - if err = errors.Unwrap(err); err == nil { - return codes.Unknown + if errors.As(err, &errGRPC) { + status := errGRPC.GRPCStatus() + + if status == nil { + return codes.Unknown + } + + code := status.Code() + if code != codes.OK && code != codes.Unknown { + return code + } } - return Code(err) + return codes.Unknown } // Wrap wraps the `err` with an error corresponding to the `code`. diff --git a/pkg/v1/grpcerr/grpcerr_generated.go.tmpl b/pkg/v1/grpcerr/grpcerr_generated.go.tmpl index 197be84..afa03b8 100644 --- a/pkg/v1/grpcerr/grpcerr_generated.go.tmpl +++ b/pkg/v1/grpcerr/grpcerr_generated.go.tmpl @@ -11,27 +11,41 @@ import ( "google.golang.org/grpc/status" ) -// Code returns grpc status code for err. +// Code returns grpc status code for err. In case of joined errors +// it returns the first code found in the chain. func Code(err error) codes.Code { - switch err.(type) { - case nil: + switch { + case err == nil: return codes.OK {{- range $errorDef := . }} - case semerr.{{ $errorDef.Name }}: + case errors.As(err, &semerr.{{ $errorDef.Name }}{}): return {{ $errorDef.GRPCStatus }} {{- end }} + default: + return getGRPCErrorCode(err) } +} - code := status.Code(err) - if code != codes.OK && code != codes.Unknown { - return code +func getGRPCErrorCode(err error) codes.Code { + var errGRPC interface { + GRPCStatus() *status.Status + error } - if err = errors.Unwrap(err); err == nil { - return codes.Unknown + if errors.As(err, &errGRPC) { + status := errGRPC.GRPCStatus() + + if status == nil { + return codes.Unknown + } + + code := status.Code() + if code != codes.OK && code != codes.Unknown { + return code + } } - return Code(err) + return codes.Unknown } // Wrap wraps the `err` with an error corresponding to the `code`. diff --git a/pkg/v1/grpcerr/grpcerr_generated_test.go b/pkg/v1/grpcerr/grpcerr_generated_test.go index 04ab2d8..e854b10 100755 --- a/pkg/v1/grpcerr/grpcerr_generated_test.go +++ b/pkg/v1/grpcerr/grpcerr_generated_test.go @@ -214,3 +214,19 @@ func TestWrap(t *testing.T) { }) } } + +func TestJoin(t *testing.T) { + t.Parallel() + + const err = semerr.Error("some error") + + gotCode := grpcerr.Code(errors.Join( + fmt.Errorf("regular: %w", err), + fmt.Errorf("bad request: %w", semerr.NewBadRequestError(err)), + semerr.NewNotFoundError(fmt.Errorf("not found: %w", err)), + )) + + if gotCode != codes.InvalidArgument { + t.Fatal("exp", codes.InvalidArgument, "got", gotCode) + } +} diff --git a/pkg/v1/grpcerr/grpcerr_generated_test.go.tmpl b/pkg/v1/grpcerr/grpcerr_generated_test.go.tmpl index 6ae186e..3d582df 100644 --- a/pkg/v1/grpcerr/grpcerr_generated_test.go.tmpl +++ b/pkg/v1/grpcerr/grpcerr_generated_test.go.tmpl @@ -106,3 +106,19 @@ func TestWrap(t *testing.T) { }) } } + +func TestJoin(t *testing.T) { + t.Parallel() + + const err = semerr.Error("some error") + + gotCode := grpcerr.Code(errors.Join( + fmt.Errorf("regular: %w", err), + fmt.Errorf("bad request: %w", semerr.NewBadRequestError(err)), + semerr.NewNotFoundError(fmt.Errorf("not found: %w", err)), + )) + + if gotCode != codes.InvalidArgument { + t.Fatal("exp", codes.InvalidArgument, "got", gotCode) + } +} diff --git a/pkg/v1/httperr/httperr_generated.go b/pkg/v1/httperr/httperr_generated.go index cbbb1c0..5481b30 100755 --- a/pkg/v1/httperr/httperr_generated.go +++ b/pkg/v1/httperr/httperr_generated.go @@ -9,44 +9,41 @@ import ( "github.com/hedhyw/semerr/pkg/v1/semerr" ) -// Code returns http status code for err. +// Code returns http status code for err. In case of joined errors +// it returns the first code found in the chain. func Code(err error) int { - switch err.(type) { - case nil: + switch { + case err == nil: return http.StatusOK - case semerr.StatusRequestTimeoutError: + case errors.As(err, &semerr.StatusRequestTimeoutError{}): return 408 - case semerr.InternalServerError: + case errors.As(err, &semerr.InternalServerError{}): return 500 - case semerr.BadRequestError: + case errors.As(err, &semerr.BadRequestError{}): return 400 - case semerr.UnsupportedMediaTypeError: + case errors.As(err, &semerr.UnsupportedMediaTypeError{}): return 415 - case semerr.StatusGatewayTimeoutError: + case errors.As(err, &semerr.StatusGatewayTimeoutError{}): return 504 - case semerr.NotFoundError: + case errors.As(err, &semerr.NotFoundError{}): return 404 - case semerr.ConflictError: + case errors.As(err, &semerr.ConflictError{}): return 409 - case semerr.ForbiddenError: + case errors.As(err, &semerr.ForbiddenError{}): return 403 - case semerr.TooManyRequestsError: + case errors.As(err, &semerr.TooManyRequestsError{}): return 429 - case semerr.RequestEntityTooLargeError: + case errors.As(err, &semerr.RequestEntityTooLargeError{}): return 413 - case semerr.UnimplementedError: + case errors.As(err, &semerr.UnimplementedError{}): return 501 - case semerr.ServiceUnavailableError: + case errors.As(err, &semerr.ServiceUnavailableError{}): return 503 - case semerr.UnauthorizedError: + case errors.As(err, &semerr.UnauthorizedError{}): return 401 - } - - if err = errors.Unwrap(err); err == nil { + default: return http.StatusInternalServerError } - - return Code(err) } // Wrap wraps the `err` with an error corresponding to the `code`. diff --git a/pkg/v1/httperr/httperr_generated.go.tmpl b/pkg/v1/httperr/httperr_generated.go.tmpl index 0cea779..c1570b2 100644 --- a/pkg/v1/httperr/httperr_generated.go.tmpl +++ b/pkg/v1/httperr/httperr_generated.go.tmpl @@ -9,26 +9,21 @@ import ( "github.com/hedhyw/semerr/pkg/v1/semerr" ) -// Code returns http status code for err. +// Code returns http status code for err. In case of joined errors +// it returns the first code found in the chain. func Code(err error) int { - switch err.(type) { - case nil: + switch { + case err == nil: return http.StatusOK {{- range $errorDef := . }} - case semerr.{{ $errorDef.Name }}: + case errors.As(err, &semerr.{{ $errorDef.Name }}{}): return {{ $errorDef.HTTPStatus }} {{- end }} - } - - if err = errors.Unwrap(err); err == nil { + default: return http.StatusInternalServerError } - - return Code(err) } - - // Wrap wraps the `err` with an error corresponding to the `code`. // If there is no `err` for this code then the `err` will be returned // without wrapping. diff --git a/pkg/v1/httperr/httperr_generated_test.go b/pkg/v1/httperr/httperr_generated_test.go index 7f5726c..107b3b9 100755 --- a/pkg/v1/httperr/httperr_generated_test.go +++ b/pkg/v1/httperr/httperr_generated_test.go @@ -208,3 +208,19 @@ func TestWrap(t *testing.T) { }) } } + +func TestJoin(t *testing.T) { + t.Parallel() + + const err = semerr.Error("some error") + + gotCode := httperr.Code(errors.Join( + fmt.Errorf("regular: %w", err), + fmt.Errorf("bad request: %w", semerr.NewBadRequestError(err)), + semerr.NewNotFoundError(fmt.Errorf("not found: %w", err)), + )) + + if gotCode != http.StatusBadRequest { + t.Fatal("exp", http.StatusBadRequest, "got", gotCode) + } +} diff --git a/pkg/v1/httperr/httperr_generated_test.go.tmpl b/pkg/v1/httperr/httperr_generated_test.go.tmpl index 16be805..69c8185 100644 --- a/pkg/v1/httperr/httperr_generated_test.go.tmpl +++ b/pkg/v1/httperr/httperr_generated_test.go.tmpl @@ -99,4 +99,20 @@ func TestWrap(t *testing.T) { } }) } -} \ No newline at end of file +} + +func TestJoin(t *testing.T) { + t.Parallel() + + const err = semerr.Error("some error") + + gotCode := httperr.Code(errors.Join( + fmt.Errorf("regular: %w", err), + fmt.Errorf("bad request: %w", semerr.NewBadRequestError(err)), + semerr.NewNotFoundError(fmt.Errorf("not found: %w", err)), + )) + + if gotCode != http.StatusBadRequest { + t.Fatal("exp", http.StatusBadRequest, "got", gotCode) + } +} diff --git a/pkg/v1/semerr/multi.go b/pkg/v1/semerr/multi.go index 7a00141..0a42dfb 100644 --- a/pkg/v1/semerr/multi.go +++ b/pkg/v1/semerr/multi.go @@ -25,6 +25,8 @@ func (m MultiErr) Error() string { // NewMultiError creates a error that can hold multiple errors. // It skips or nil values. If count of errors is 1, it returns the // original value. The main error is the first. +// +// Deprecated: use errors.Join. func NewMultiError(errs ...error) error { if len(errs) == 0 { return nil diff --git a/vendor/modules.txt b/vendor/modules.txt index b336cbb..b092eb9 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -9,7 +9,7 @@ github.com/golang/protobuf/ptypes/timestamp ## explicit; go 1.12 # github.com/rogpeppe/go-internal v1.9.0 ## explicit; go 1.17 -# google.golang.org/genproto v0.0.0-20230227214838-9b19f0bdc514 +# google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 ## explicit; go 1.19 google.golang.org/genproto/googleapis/rpc/status # google.golang.org/grpc v1.53.0