├── .github └── workflows │ ├── codecov.yml │ ├── go-pkg-proxy.yml │ └── go.yml ├── .gitignore ├── LICENSE ├── README.md ├── doc.go ├── error.go ├── error_test.go ├── examples ├── advanced │ └── main.go ├── http-sample │ └── main.go └── simple │ └── main.go ├── go.mod ├── go.sum ├── http_util.go ├── http_util_test.go ├── string_utils.go └── string_utils_test.go /.github/workflows/codecov.yml: -------------------------------------------------------------------------------- 1 | name: Codecov 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | name: Build 13 | runs-on: ubuntu-latest 14 | steps: 15 | 16 | - name: Set up Go 1.x 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: ^1.13 20 | 21 | - name: Check out code into the Go module directory 22 | uses: actions/checkout@v2 23 | with: 24 | fetch-depth: '0' 25 | 26 | - name: Get dependencies 27 | run: | 28 | go get -v -t -d ./... 29 | if [ -f Gopkg.toml ]; then 30 | curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh 31 | dep ensure 32 | fi 33 | - name: Build 34 | run: go build -v ./... 35 | 36 | - name: Generate coverage report 37 | run: | 38 | go test `go list ./... | grep -v examples` -coverprofile=coverage.txt -covermode=atomic 39 | - name: Upload coverage report 40 | uses: codecov/codecov-action@v1 41 | with: 42 | # token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos 43 | file: ./coverage.txt 44 | flags: unittests 45 | name: codecov-umbrella 46 | -------------------------------------------------------------------------------- /.github/workflows/go-pkg-proxy.yml: -------------------------------------------------------------------------------- 1 | name: Go Pkg Proxy 2 | 3 | on: 4 | release: 5 | types: 6 | - created 7 | tags: 8 | - 'v[0-9]+.[0-9]+.[0-9]+' 9 | - '**/v[0-9]+.[0-9]+.[0-9]+' 10 | 11 | jobs: 12 | build: 13 | name: Renew documentation 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Pull new module version 17 | uses: andrewslotin/go-proxy-pull-action@master 18 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | name: Build 13 | runs-on: ubuntu-latest 14 | steps: 15 | 16 | - name: Set up Go 1.x 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: ^1.13 20 | 21 | - name: Check out code into the Go module directory 22 | uses: actions/checkout@v2 23 | 24 | - name: Get dependencies 25 | run: | 26 | go get -v -t -d ./... 27 | if [ -f Gopkg.toml ]; then 28 | curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh 29 | dep ensure 30 | fi 31 | 32 | - name: Test 33 | run: go test -v ./... 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | # IDEA files 18 | .idea/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 The Neutrino Corporation 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # :japanese_goblin: DDD Error 2 | 3 | ![Go Build](https://github.com/neutrinocorp/ddderr/workflows/Go/badge.svg?branch=master) 4 | [![GoDoc](https://pkg.go.dev/badge/github.com/neutrinocorp/ddderr/v3)][godocs] 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/neutrinocorp/ddderr)](https://goreportcard.com/report/github.com/neutrinocorp/ddderr) 6 | [![codebeat badge](https://codebeat.co/badges/22d865b6-c99a-469a-bb85-6b2d6f44a6fe)](https://codebeat.co/projects/github-com-neutrinocorp-ddderr-master) 7 | [![Coverage Status][cov-img]][cov] 8 | [![Go Version][go-img]][go] 9 | 10 | `DDD Error` is a _reflection-free_ Domain-Driven error wrapper made for _Go_. 11 | 12 | Using existing _validators_ such as playground's implementation is _overwhelming because tag validation_ and the need to rewrite descriptions. 13 | With `DDD Error`, _you may still use 3rd-party validators_ or _make your own validations_ in your value objects, entities or aggregates. 14 | 15 | In addition, infrastructure exceptions were added so you may be able to _catch specific kind of infrastructure errors._ 16 | 17 | Exceptions _descriptions are based on the [Google Cloud API Design Guidelines](https://cloud.google.com/apis/design/errors)_. 18 | 19 | `DDD Error` is compatible with popular error-handling packages such as [Hashicorp's go-multierror](https://github.com/hashicorp/go-multierror) 20 | 21 | In conclusion, `DDD Error` aims to _ease the lack of exception handling_ in The Go Programming Language by defining a _wide selection of common exceptions_ 22 | which happen inside the _domain and/or infrastructure_ layer(s). 23 | 24 | _Note: `DDD Error` is dependency-free, it complies with Go's built-in error interface and avoids reflection to increase overall performance._ 25 | 26 | ## Installation 27 | Install `DDD Error` by running the command 28 | 29 | go get github.com/neutrinocorp/ddderr/v3 30 | 31 | Full documentation is available 32 | [here](https://pkg.go.dev/github.com/neutrinocorp/ddderr) 33 | 34 | 35 | ## Common Use Cases 36 | - Implement retry strategy and circuit breaker resiliency patterns by adding Network exception to the whitelist. 37 | - Not Acknowledging messages from an event bus if got a Network or Infrastructure generic exception. 38 | - Get an HTTP/gRPC/OpenCensus status code from an error. 39 | - Implement multiple strategies when an specific (or generic) type of error was thrown in. 40 | - Fine-grained exception logging on infrastructure layer by using GetParentDescription() function. 41 | 42 | ## Usage 43 | 44 | **HTTP status codes** 45 | 46 | Set an HTTP error code depending on the exception. 47 | 48 | ```go 49 | err := ddderr.NewNotFound("foo") 50 | log.Print(err) // prints: "The resource foo was not found" 51 | 52 | if err.IsNotFound() { 53 | log.Print(http.StatusNotFound) // prints: 404 54 | return 55 | } 56 | 57 | log.Print(http.StatusInternalServerError) // prints: 500 58 | ``` 59 | 60 | Or use the _builtin HTTP utils:_ 61 | 62 | ```go 63 | err := ddderr.NewNotFound("foo") 64 | log.Print(err) // prints: "The resource foo was not found" 65 | 66 | // HttpError struct is ready to be marshaled using JSON encoding libs 67 | // 68 | // Function accepts the following optional params (specified on the RFC spec): 69 | // - Type 70 | // - Instance 71 | httpErr := ddderr.NewHttpError("", "", err) 72 | // Will output -> If errType param is empty, then HTTP status text is used as type 73 | // (e.g. Not Found, Internal Server Error) 74 | log.Print(httpErr.Type) 75 | // Will output -> 404 as we got a NotFound error type 76 | log.Print(httpErr.Status) 77 | // Will output -> The resource foo was not found 78 | log.Print(httpErr.Detail) 79 | ``` 80 | 81 | **Domain generic exceptions** 82 | 83 | Create a generic domain exception when other domain errors don't fulfill your requirements. 84 | 85 | ```go 86 | err := ddderr.NewDomain("generic error title", "foo has returned a generic domain error") 87 | log.Print(err) // prints: "foo has returned a generic domain error" 88 | 89 | if err.IsDomain() { 90 | log.Print(http.StatusBadRequest) // prints: 400 91 | return 92 | } 93 | 94 | log.Print(http.StatusInternalServerError) // prints: 500 95 | ``` 96 | 97 | **Infrastructure generic exceptions** 98 | 99 | Create a generic infrastructure exception when other infrastructure exceptions don't fulfill your requirements. 100 | 101 | ```go 102 | msgErr := errors.New("sarama: Apache kafka consumer error") 103 | err := ddderr.NewInfrastructure("generic error title", "error while consuming message from queue"). 104 | AttachParent(msgErr) 105 | log.Print(err) // prints: "error while consuming message from queue" 106 | log.Print(err.Parent()) // prints: "sarama: Apache kafka consumer error" 107 | ``` 108 | 109 | **Implement multiple strategies depending on exception kind** 110 | 111 | Take an specific action depending on the exception kind. 112 | 113 | ```go 114 | esErr := errors.New("failed to connect to Elasticsearch host http://127.0.0.1:9300") 115 | 116 | err := ddderr.NewRemoteCall("http://127.0.0.1:9300"). 117 | AttachParent(esErr) 118 | log.Print("infrastructure error: ", err) // prints "failed to call external resource [http://127.0.0.1:9300]" 119 | log.Print("infrastructure error resource: ", err.Property()) // http://127.0.0.1:9300 120 | log.Print("is domain error: ", err.IsDomain()) // false 121 | log.Print("is infrastructure error: ", err.IsInfrastructure()) // true 122 | log.Print("infrastructure error parent: ", err.Parent()) // prints "failed to connect to Elasticsearch host http://127.0.0.1:9300" 123 | 124 | if err.IsRemoteCall() { 125 | // implement retry and/or circuit breaker pattern(s) 126 | } 127 | ``` 128 | 129 | See [examples][examples] for more details. 130 | 131 | ## Requirements 132 | - Go version >= 1.13 133 | 134 | [actions]: https://github.com/neutrinocorp/ddderr/workflows/Go/badge.svg?branch=master 135 | [godocs]: https://pkg.go.dev/github.com/neutrinocorp/ddderr/v3 136 | [cov-img]: https://codecov.io/gh/NeutrinoCorp/ddderr/branch/master/graph/badge.svg 137 | [cov]: https://codecov.io/gh/NeutrinoCorp/ddderr 138 | [go-img]: https://img.shields.io/github/go-mod/go-version/NeutrinoCorp/ddderr?style=square 139 | [go]: https://github.com/NeutrinoCorp/ddderr/blob/master/go.mod 140 | [examples]: https://github.com/neutrinocorp/ddderr/tree/master/examples 141 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package ddderr -or DDD Error- is a generic domain-driven exception wrapper made for Go. 2 | // 3 | // DDD Error aims to ease the lack of exception handling in The Go Programming Language by 4 | // defining a wide selection of common exceptions which happen inside the domain and/or infrastructure layer(s). 5 | // 6 | // DDD Error is dependency-free, it complies with Go's built-in error interface and avoids reflection to increase overall performance. 7 | package ddderr 8 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package ddderr 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | ) 7 | 8 | const ( 9 | // error types groups 10 | domain = "Domain" 11 | infrastructure = "Infrastructure" 12 | // specific error types 13 | notFound = "NotFound" 14 | alreadyExists = "AlreadyExists" 15 | outOfRange = "OutOfRange" 16 | invalidFormat = "InvalidFormat" 17 | required = "Required" 18 | remoteCall = "FailedRemoteCall" 19 | 20 | unknownDomain = "UnknownDomain" 21 | unknownInfrastructure = "UnknownInfrastructure" 22 | ) 23 | 24 | // Error contains specific mechanisms useful for further error mapping and other 25 | // specific use cases 26 | type Error struct { 27 | parent error 28 | group string 29 | kind string 30 | property string 31 | title string 32 | description string 33 | statusName string 34 | 35 | dynamicDescription bool 36 | dynamicStatus bool 37 | limitA, limitB int 38 | formats []string 39 | } 40 | 41 | var _ error = Error{} 42 | 43 | // Error returns the error description 44 | func (e Error) Error() string { 45 | return e.Description() 46 | } 47 | 48 | // Kind retrieves the error type (e.g. NotFound, AlreadyExists) 49 | func (e Error) Kind() string { 50 | return e.kind 51 | } 52 | 53 | // SetKind sets the specific error type (e.g. NotFound, AlreadyExists) 54 | func (e Error) SetKind(kind string) Error { 55 | e.kind = kind 56 | return e 57 | } 58 | 59 | // Property returns the resource or field which contains the error 60 | func (e Error) Property() string { 61 | return e.property 62 | } 63 | 64 | // SetProperty sets the field or resource for an error 65 | func (e Error) SetProperty(property string) Error { 66 | e.property = property 67 | e.dynamicDescription = true 68 | e.dynamicStatus = true 69 | return e 70 | } 71 | 72 | // Title retrieves a generic error message 73 | func (e Error) Title() string { 74 | return e.title 75 | } 76 | 77 | // SetTitle sets a generic error message 78 | func (e Error) SetTitle(title string) Error { 79 | e.title = title 80 | return e 81 | } 82 | 83 | // Description retrieves a specific and detailed error message 84 | func (e Error) Description() string { 85 | if !e.dynamicDescription { 86 | return e.description 87 | } 88 | 89 | switch e.kind { 90 | case alreadyExists: 91 | return newAlreadyExistsDescription(e.property) 92 | case invalidFormat: 93 | if e.formats == nil { 94 | return e.description 95 | } 96 | return newInvalidFormatDescription(e.property, e.formats...) 97 | case remoteCall: 98 | return newRemoteCallDescription(e.property) 99 | case notFound: 100 | return newNotFoundDescription(e.property) 101 | case outOfRange: 102 | return newOutOfRangeDescription(e.property, e.limitA, e.limitB) 103 | case required: 104 | return newRequiredDescription(e.property) 105 | default: 106 | return e.description 107 | } 108 | } 109 | 110 | // SetDescription sets a specific and detailed error message 111 | func (e Error) SetDescription(description string) Error { 112 | e.description = description 113 | e.dynamicDescription = false 114 | return e 115 | } 116 | 117 | // SetStatus sets a system-owned status for the Error 118 | func (e Error) SetStatus(status string) Error { 119 | e.statusName = status 120 | e.dynamicStatus = false 121 | return e 122 | } 123 | 124 | // Status retrieves the status name of the Error owned by the system 125 | func (e Error) Status() string { 126 | if !e.dynamicStatus { 127 | return e.statusName 128 | } 129 | return getSanitizedStatusName(e.property, e.kind) 130 | } 131 | 132 | // Parent returns the error parent 133 | // 134 | // Note: Might return nil if parent was not specified 135 | func (e Error) Parent() error { 136 | return e.parent 137 | } 138 | 139 | // SetParent sets a parent error to the given DDD error 140 | func (e Error) SetParent(err error) Error { 141 | e.parent = err 142 | return e 143 | } 144 | 145 | // IsDomain checks if the error belongs to Domain error group 146 | func (e Error) IsDomain() bool { 147 | return e.group == domain 148 | } 149 | 150 | // IsInfrastructure checks if the error belongs to Infrastructure error group 151 | func (e Error) IsInfrastructure() bool { 152 | return e.group == infrastructure 153 | } 154 | 155 | // IsRemoteCall checks if the error belongs to Failed Remote Call error types 156 | func (e Error) IsRemoteCall() bool { 157 | return e.kind == remoteCall 158 | } 159 | 160 | // IsNotFound checks if the error belongs to Not Found error types 161 | func (e Error) IsNotFound() bool { 162 | return e.kind == notFound 163 | } 164 | 165 | // IsAlreadyExists checks if the error belongs to Already Exists error types 166 | func (e Error) IsAlreadyExists() bool { 167 | return e.kind == alreadyExists 168 | } 169 | 170 | // IsOutOfRange checks if the error belongs to Out of Range error types 171 | func (e Error) IsOutOfRange() bool { 172 | return e.kind == outOfRange 173 | } 174 | 175 | // IsInvalidFormat checks if the error belongs to Invalid Format error types 176 | func (e Error) IsInvalidFormat() bool { 177 | return e.kind == invalidFormat 178 | } 179 | 180 | // IsRequired checks if the error belongs to Required error types 181 | func (e Error) IsRequired() bool { 182 | return e.kind == required 183 | } 184 | 185 | // NewDomain creates an Error for Domain generic use cases 186 | func NewDomain(title, description string) Error { 187 | return Error{ 188 | parent: nil, 189 | group: domain, 190 | kind: unknownDomain, 191 | property: "", 192 | title: title, 193 | description: description, 194 | } 195 | } 196 | 197 | // NewInfrastructure creates an Error for Infrastructure generic use cases 198 | func NewInfrastructure(title, description string) Error { 199 | return Error{ 200 | parent: nil, 201 | group: infrastructure, 202 | kind: unknownInfrastructure, 203 | property: "", 204 | title: title, 205 | description: description, 206 | } 207 | } 208 | 209 | // NewRemoteCall creates an Error for network remote calls failing scenarios 210 | // 211 | // (e.g. database connection failed, sync inter-service transaction failed over a networking problem) 212 | func NewRemoteCall(externalResource string) Error { 213 | return Error{ 214 | parent: nil, 215 | group: infrastructure, 216 | kind: remoteCall, 217 | property: externalResource, 218 | title: "Remote call failed", 219 | description: newRemoteCallDescription(externalResource), 220 | statusName: "FailedRemoteCall", 221 | } 222 | } 223 | 224 | func newRemoteCallDescription(resource string) string { 225 | desc := "Failed to call external resource" 226 | if resource != "" { 227 | desc = desc + " [" + resource + "]" 228 | } 229 | return desc 230 | } 231 | 232 | // NewNotFound creates an Error for Not Found use cases 233 | // 234 | // (description e.g. The resource foo was not found) 235 | func NewNotFound(resource string) Error { 236 | return Error{ 237 | parent: nil, 238 | group: domain, 239 | kind: notFound, 240 | property: resource, 241 | title: "Resource not found", 242 | description: newNotFoundDescription(resource), 243 | statusName: getSanitizedStatusName(resource, "NotFound"), 244 | } 245 | } 246 | 247 | func newNotFoundDescription(resource string) string { 248 | desc := "not found" 249 | if resource != "" { 250 | desc = "The resource " + resource + " was not found" 251 | } 252 | return desc 253 | } 254 | 255 | // NewAlreadyExists creates an Error for Already Exists use cases 256 | // 257 | // (description e.g. The resource foo was already created) 258 | func NewAlreadyExists(resource string) Error { 259 | return Error{ 260 | parent: nil, 261 | group: domain, 262 | kind: alreadyExists, 263 | property: resource, 264 | title: "Resource already exists", 265 | description: newAlreadyExistsDescription(resource), 266 | statusName: getSanitizedStatusName(resource, "AlreadyExists"), 267 | } 268 | } 269 | 270 | func newAlreadyExistsDescription(resource string) string { 271 | desc := "already exists" 272 | if resource != "" { 273 | desc = "The resource " + resource + " already exists" 274 | } 275 | return desc 276 | } 277 | 278 | // NewOutOfRange creates an Error for Out of Range use cases 279 | // 280 | // (description e.g. The property foo is out of range [A, B)) 281 | func NewOutOfRange(property string, a, b int) Error { 282 | return Error{ 283 | parent: nil, 284 | group: domain, 285 | kind: outOfRange, 286 | property: property, 287 | title: "Property is out of the specified range", 288 | description: newOutOfRangeDescription(property, a, b), 289 | statusName: getSanitizedStatusName(property, "OutOfRange"), 290 | limitA: a, 291 | limitB: b, 292 | } 293 | } 294 | 295 | func newOutOfRangeDescription(property string, a, b int) string { 296 | desc := "out of range [" + strconv.Itoa(a) + "," + strconv.Itoa(b) + ")" 297 | if property != "" { 298 | desc = "The property " + property + " is " + desc 299 | } 300 | return desc 301 | } 302 | 303 | // NewInvalidFormat creates an Error for Invalid Format use cases 304 | // 305 | // (description e.g. The property foo has an invalid format, expected [x1, x2, xN]) 306 | func NewInvalidFormat(property string, formats ...string) Error { 307 | return Error{ 308 | parent: nil, 309 | group: domain, 310 | kind: invalidFormat, 311 | property: property, 312 | title: "Property is not a valid format", 313 | description: newInvalidFormatDescription(property, formats...), 314 | statusName: getSanitizedStatusName(property, "InvalidFormat"), 315 | formats: formats, 316 | } 317 | } 318 | 319 | func newInvalidFormatDescription(property string, formats ...string) string { 320 | desc := "invalid format, expected [" + strings.Join(formats, ",") + "]" 321 | if property != "" { 322 | desc = "The property " + property + " has an " + desc 323 | } 324 | return desc 325 | } 326 | 327 | // NewRequired creates an Error for Required use cases 328 | // 329 | // (description e.g. The property foo is required) 330 | func NewRequired(property string) Error { 331 | return Error{ 332 | parent: nil, 333 | group: domain, 334 | kind: required, 335 | property: property, 336 | title: "Missing property", 337 | description: newRequiredDescription(property), 338 | statusName: getSanitizedStatusName(property, "IsRequired"), 339 | } 340 | } 341 | 342 | func newRequiredDescription(property string) string { 343 | desc := "required" 344 | if property != "" { 345 | desc = "The property " + property + " is " + desc 346 | } 347 | return desc 348 | } 349 | -------------------------------------------------------------------------------- /error_test.go: -------------------------------------------------------------------------------- 1 | package ddderr 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestError_Setters(t *testing.T) { 11 | pqMockedErr := errors.New("pq: Generic PostgreSQL driver error") 12 | err := NewDomain("", ""). 13 | SetParent(pqMockedErr). 14 | SetKind("CustomKind"). 15 | SetTitle("generic title"). 16 | SetDescription("specific description"). 17 | SetProperty("foo"). 18 | SetStatus("FooIsBad") 19 | 20 | assert.EqualValues(t, pqMockedErr, err.Parent()) 21 | assert.Equal(t, "CustomKind", err.Kind()) 22 | assert.Equal(t, "generic title", err.Title()) 23 | assert.Equal(t, "specific description", err.Description()) 24 | assert.Equal(t, "foo", err.Property()) 25 | assert.Equal(t, "specific description", err.Error()) 26 | assert.Equal(t, "FooIsBad", err.Status()) 27 | 28 | err = NewOutOfRange("fooBar", 8, 16) 29 | err = err.SetProperty("barBaz") 30 | assert.Equal(t, "The property barBaz is out of range [8,16)", err.Error()) 31 | 32 | } 33 | 34 | var dynamicFieldTests = []struct { 35 | In Error 36 | InDynamicField string 37 | ExpDesc string 38 | ExpStatus string 39 | }{ 40 | { 41 | In: NewOutOfRange("bar", 0, 3), 42 | InDynamicField: "foo", 43 | ExpDesc: "The property foo is out of range [0,3)", 44 | ExpStatus: "FooOutOfRange", 45 | }, 46 | { 47 | In: NewAlreadyExists("bar"), 48 | InDynamicField: "foo", 49 | ExpDesc: "The resource foo already exists", 50 | ExpStatus: "FooAlreadyExists", 51 | }, 52 | { 53 | In: NewInvalidFormat("bar", "jpeg", "gif"), 54 | InDynamicField: "foo", 55 | ExpDesc: "The property foo has an invalid format, expected [jpeg,gif]", 56 | ExpStatus: "FooInvalidFormat", 57 | }, 58 | { 59 | In: Error{ 60 | kind: invalidFormat, 61 | }, 62 | InDynamicField: "foo", 63 | ExpDesc: "", 64 | ExpStatus: "FooInvalidFormat", 65 | }, 66 | { 67 | In: NewNotFound("bar"), 68 | InDynamicField: "foo", 69 | ExpDesc: "The resource foo was not found", 70 | ExpStatus: "FooNotFound", 71 | }, 72 | { 73 | In: NewRemoteCall("bar.com"), 74 | InDynamicField: "foo.org", 75 | ExpDesc: "Failed to call external resource [foo.org]", 76 | ExpStatus: "FooOrgFailedRemoteCall", 77 | }, 78 | { 79 | In: NewRequired("bar"), 80 | InDynamicField: "foo", 81 | ExpDesc: "The property foo is required", 82 | ExpStatus: "FooRequired", 83 | }, 84 | { 85 | In: Error{}, 86 | InDynamicField: "", 87 | ExpDesc: "", 88 | ExpStatus: "", 89 | }, 90 | } 91 | 92 | func TestDynamicFields(t *testing.T) { 93 | for _, tt := range dynamicFieldTests { 94 | t.Run("", func(t *testing.T) { 95 | err := tt.In.SetProperty(tt.InDynamicField) 96 | assert.Equal(t, tt.ExpDesc, err.Description()) 97 | assert.Equal(t, tt.ExpStatus, err.Status()) 98 | }) 99 | } 100 | } 101 | 102 | var newDomainTestSuite = []struct { 103 | InTitle string 104 | InDesc string 105 | Exp Error 106 | }{ 107 | { 108 | InTitle: "", 109 | InDesc: "", 110 | Exp: Error{ 111 | parent: nil, 112 | group: domain, 113 | kind: unknownDomain, 114 | property: "", 115 | title: "", 116 | description: "", 117 | }, 118 | }, 119 | { 120 | InTitle: "generic title", 121 | InDesc: "foo description", 122 | Exp: Error{ 123 | parent: nil, 124 | group: domain, 125 | kind: unknownDomain, 126 | property: "", 127 | title: "generic title", 128 | description: "foo description", 129 | }, 130 | }, 131 | } 132 | 133 | func TestNewDomain(t *testing.T) { 134 | for _, tt := range newDomainTestSuite { 135 | t.Run("", func(t *testing.T) { 136 | err := NewDomain(tt.InTitle, tt.InDesc) 137 | assert.EqualValues(t, tt.Exp, err) 138 | assert.Equal(t, tt.InTitle, err.Title()) 139 | assert.Equal(t, tt.InDesc, err.Description()) 140 | assert.Equal(t, tt.Exp.Kind(), err.Kind()) 141 | assert.Empty(t, err.Property()) 142 | assert.True(t, err.IsDomain()) 143 | assert.False(t, err.IsInfrastructure()) 144 | assert.False(t, err.IsRequired()) 145 | assert.False(t, err.IsOutOfRange()) 146 | assert.False(t, err.IsInvalidFormat()) 147 | assert.False(t, err.IsAlreadyExists()) 148 | assert.False(t, err.IsRemoteCall()) 149 | assert.False(t, err.IsNotFound()) 150 | }) 151 | } 152 | } 153 | 154 | var newInfraTestSuite = []struct { 155 | InTitle string 156 | InDesc string 157 | Exp Error 158 | }{ 159 | { 160 | InTitle: "", 161 | InDesc: "", 162 | Exp: Error{ 163 | parent: nil, 164 | group: infrastructure, 165 | kind: unknownInfrastructure, 166 | property: "", 167 | title: "", 168 | description: "", 169 | }, 170 | }, 171 | { 172 | InTitle: "generic title", 173 | InDesc: "foo description", 174 | Exp: Error{ 175 | parent: nil, 176 | group: infrastructure, 177 | kind: unknownInfrastructure, 178 | property: "", 179 | title: "generic title", 180 | description: "foo description", 181 | }, 182 | }, 183 | } 184 | 185 | func TestNewInfrastructure(t *testing.T) { 186 | for _, tt := range newInfraTestSuite { 187 | t.Run("", func(t *testing.T) { 188 | err := NewInfrastructure(tt.InTitle, tt.InDesc) 189 | assert.EqualValues(t, tt.Exp, err) 190 | assert.Equal(t, tt.InTitle, err.Title()) 191 | assert.Equal(t, tt.InDesc, err.Description()) 192 | assert.Equal(t, tt.Exp.Kind(), err.Kind()) 193 | assert.Empty(t, err.Property()) 194 | assert.True(t, err.IsInfrastructure()) 195 | assert.False(t, err.IsDomain()) 196 | assert.False(t, err.IsRequired()) 197 | assert.False(t, err.IsOutOfRange()) 198 | assert.False(t, err.IsInvalidFormat()) 199 | assert.False(t, err.IsAlreadyExists()) 200 | assert.False(t, err.IsRemoteCall()) 201 | assert.False(t, err.IsNotFound()) 202 | }) 203 | } 204 | } 205 | 206 | var newRemoteCallTestSuite = []struct { 207 | InExternalResource string 208 | Exp Error 209 | }{ 210 | { 211 | InExternalResource: "", 212 | Exp: Error{ 213 | parent: nil, 214 | group: infrastructure, 215 | kind: remoteCall, 216 | property: "", 217 | title: "Remote call failed", 218 | description: "Failed to call external resource", 219 | statusName: "FailedRemoteCall", 220 | }, 221 | }, 222 | { 223 | InExternalResource: "https://foo.com", 224 | Exp: Error{ 225 | parent: nil, 226 | group: infrastructure, 227 | kind: remoteCall, 228 | property: "https://foo.com", 229 | title: "Remote call failed", 230 | description: "Failed to call external resource [https://foo.com]", 231 | statusName: "FailedRemoteCall", 232 | }, 233 | }, 234 | } 235 | 236 | func TestNewRemoteCall(t *testing.T) { 237 | for _, tt := range newRemoteCallTestSuite { 238 | t.Run("", func(t *testing.T) { 239 | err := NewRemoteCall(tt.InExternalResource) 240 | assert.EqualValues(t, tt.Exp, err) 241 | assert.Equal(t, tt.Exp.Title(), err.Title()) 242 | assert.Equal(t, tt.Exp.Description(), err.Description()) 243 | assert.Equal(t, tt.Exp.Property(), err.Property()) 244 | assert.Equal(t, tt.Exp.Kind(), err.Kind()) 245 | assert.True(t, err.IsRemoteCall()) 246 | assert.True(t, err.IsInfrastructure()) 247 | assert.False(t, err.IsDomain()) 248 | assert.False(t, err.IsAlreadyExists()) 249 | assert.False(t, err.IsRequired()) 250 | assert.False(t, err.IsOutOfRange()) 251 | assert.False(t, err.IsInvalidFormat()) 252 | assert.False(t, err.IsNotFound()) 253 | }) 254 | } 255 | } 256 | 257 | var newNotFoundTestSuite = []struct { 258 | InResource string 259 | Exp Error 260 | }{ 261 | { 262 | InResource: "", 263 | Exp: Error{ 264 | parent: nil, 265 | group: domain, 266 | kind: notFound, 267 | property: "", 268 | title: "Resource not found", 269 | description: "not found", 270 | statusName: "NotFound", 271 | }, 272 | }, 273 | { 274 | InResource: "foo", 275 | Exp: Error{ 276 | parent: nil, 277 | group: domain, 278 | kind: notFound, 279 | property: "foo", 280 | title: "Resource not found", 281 | description: "The resource foo was not found", 282 | statusName: "FooNotFound", 283 | }, 284 | }, 285 | } 286 | 287 | func TestNewNotFound(t *testing.T) { 288 | for _, tt := range newNotFoundTestSuite { 289 | t.Run("", func(t *testing.T) { 290 | err := NewNotFound(tt.InResource) 291 | assert.EqualValues(t, tt.Exp, err) 292 | assert.Equal(t, tt.Exp.Title(), err.Title()) 293 | assert.Equal(t, tt.Exp.Description(), err.Description()) 294 | assert.Equal(t, tt.Exp.Property(), err.Property()) 295 | assert.Equal(t, tt.Exp.Kind(), err.Kind()) 296 | assert.True(t, err.IsNotFound()) 297 | assert.True(t, err.IsDomain()) 298 | assert.False(t, err.IsInfrastructure()) 299 | assert.False(t, err.IsRemoteCall()) 300 | assert.False(t, err.IsAlreadyExists()) 301 | assert.False(t, err.IsRequired()) 302 | assert.False(t, err.IsOutOfRange()) 303 | assert.False(t, err.IsInvalidFormat()) 304 | }) 305 | } 306 | } 307 | 308 | var newAlreadyExistsTestSuite = []struct { 309 | InResource string 310 | Exp Error 311 | }{ 312 | { 313 | InResource: "", 314 | Exp: Error{ 315 | parent: nil, 316 | group: domain, 317 | kind: alreadyExists, 318 | property: "", 319 | title: "Resource already exists", 320 | description: "already exists", 321 | statusName: "AlreadyExists", 322 | }, 323 | }, 324 | { 325 | InResource: "foo", 326 | Exp: Error{ 327 | parent: nil, 328 | group: domain, 329 | kind: alreadyExists, 330 | property: "foo", 331 | title: "Resource already exists", 332 | description: "The resource foo already exists", 333 | statusName: "FooAlreadyExists", 334 | }, 335 | }, 336 | } 337 | 338 | func TestNewAlreadyExists(t *testing.T) { 339 | for _, tt := range newAlreadyExistsTestSuite { 340 | t.Run("", func(t *testing.T) { 341 | err := NewAlreadyExists(tt.InResource) 342 | assert.EqualValues(t, tt.Exp, err) 343 | assert.Equal(t, tt.Exp.Title(), err.Title()) 344 | assert.Equal(t, tt.Exp.Description(), err.Description()) 345 | assert.Equal(t, tt.Exp.Property(), err.Property()) 346 | assert.Equal(t, tt.Exp.Kind(), err.Kind()) 347 | assert.True(t, err.IsAlreadyExists()) 348 | assert.True(t, err.IsDomain()) 349 | assert.False(t, err.IsInfrastructure()) 350 | assert.False(t, err.IsRequired()) 351 | assert.False(t, err.IsOutOfRange()) 352 | assert.False(t, err.IsInvalidFormat()) 353 | assert.False(t, err.IsRemoteCall()) 354 | assert.False(t, err.IsNotFound()) 355 | }) 356 | } 357 | } 358 | 359 | var newOutOfRangeTestSuite = []struct { 360 | InProp string 361 | InLimA int 362 | InLimB int 363 | Exp Error 364 | }{ 365 | { 366 | InProp: "", 367 | InLimA: 0, 368 | InLimB: 0, 369 | Exp: Error{ 370 | parent: nil, 371 | group: domain, 372 | kind: outOfRange, 373 | property: "", 374 | title: "Property is out of the specified range", 375 | description: "out of range [0,0)", 376 | statusName: "OutOfRange", 377 | }, 378 | }, 379 | { 380 | InProp: "foo", 381 | InLimA: 0, 382 | InLimB: 0, 383 | Exp: Error{ 384 | parent: nil, 385 | group: domain, 386 | kind: outOfRange, 387 | property: "foo", 388 | title: "Property is out of the specified range", 389 | description: "The property foo is out of range [0,0)", 390 | statusName: "FooOutOfRange", 391 | }, 392 | }, 393 | { 394 | InProp: "", 395 | InLimA: 8, 396 | InLimB: 256, 397 | Exp: Error{ 398 | parent: nil, 399 | group: domain, 400 | kind: outOfRange, 401 | property: "", 402 | title: "Property is out of the specified range", 403 | description: "out of range [8,256)", 404 | statusName: "OutOfRange", 405 | }, 406 | }, 407 | { 408 | InProp: "foo", 409 | InLimA: 8, 410 | InLimB: 256, 411 | Exp: Error{ 412 | parent: nil, 413 | group: domain, 414 | kind: outOfRange, 415 | property: "foo", 416 | title: "Property is out of the specified range", 417 | description: "The property foo is out of range [8,256)", 418 | statusName: "FooOutOfRange", 419 | }, 420 | }, 421 | } 422 | 423 | func TestNewOutOfRange(t *testing.T) { 424 | for _, tt := range newOutOfRangeTestSuite { 425 | t.Run("", func(t *testing.T) { 426 | err := NewOutOfRange(tt.InProp, tt.InLimA, tt.InLimB) 427 | assert.Equal(t, tt.Exp.Title(), err.Title()) 428 | assert.Equal(t, tt.Exp.Description(), err.Description()) 429 | assert.Equal(t, tt.Exp.Property(), err.Property()) 430 | assert.Equal(t, tt.Exp.Kind(), err.Kind()) 431 | assert.True(t, err.IsOutOfRange()) 432 | assert.True(t, err.IsDomain()) 433 | assert.False(t, err.IsInfrastructure()) 434 | assert.False(t, err.IsRequired()) 435 | assert.False(t, err.IsNotFound()) 436 | assert.False(t, err.IsRemoteCall()) 437 | assert.False(t, err.IsAlreadyExists()) 438 | assert.False(t, err.IsInvalidFormat()) 439 | }) 440 | } 441 | } 442 | 443 | var newInvalidFormatTestSuite = []struct { 444 | InProperty string 445 | InFormats []string 446 | Exp Error 447 | }{ 448 | { 449 | InProperty: "", 450 | InFormats: nil, 451 | Exp: Error{ 452 | parent: nil, 453 | group: domain, 454 | kind: invalidFormat, 455 | property: "", 456 | title: "Property is not a valid format", 457 | description: "invalid format, expected []", 458 | statusName: "InvalidFormat", 459 | }, 460 | }, 461 | { 462 | InProperty: "", 463 | InFormats: []string{"foo"}, 464 | Exp: Error{ 465 | parent: nil, 466 | group: domain, 467 | kind: invalidFormat, 468 | property: "", 469 | title: "Property is not a valid format", 470 | description: "invalid format, expected [foo]", 471 | statusName: "InvalidFormat", 472 | }, 473 | }, 474 | { 475 | InProperty: "foo", 476 | InFormats: []string{"bar"}, 477 | Exp: Error{ 478 | parent: nil, 479 | group: domain, 480 | kind: invalidFormat, 481 | property: "foo", 482 | title: "Property is not a valid format", 483 | description: "The property foo has an invalid format, expected [bar]", 484 | statusName: "FooInvalidFormat", 485 | }, 486 | }, 487 | { 488 | InProperty: "foo", 489 | InFormats: []string{"bar", "baz"}, 490 | Exp: Error{ 491 | parent: nil, 492 | group: domain, 493 | kind: invalidFormat, 494 | property: "foo", 495 | title: "Property is not a valid format", 496 | description: "The property foo has an invalid format, expected [bar,baz]", 497 | statusName: "FooInvalidFormat", 498 | }, 499 | }, 500 | } 501 | 502 | func TestNewInvalidFormat(t *testing.T) { 503 | for _, tt := range newInvalidFormatTestSuite { 504 | t.Run("", func(t *testing.T) { 505 | err := NewInvalidFormat(tt.InProperty, tt.InFormats...) 506 | assert.Equal(t, tt.Exp.Title(), err.Title()) 507 | assert.Equal(t, tt.Exp.Description(), err.Description()) 508 | assert.Equal(t, tt.Exp.Property(), err.Property()) 509 | assert.Equal(t, tt.Exp.Kind(), err.Kind()) 510 | assert.True(t, err.IsInvalidFormat()) 511 | assert.True(t, err.IsDomain()) 512 | assert.False(t, err.IsInfrastructure()) 513 | assert.False(t, err.IsAlreadyExists()) 514 | assert.False(t, err.IsRequired()) 515 | assert.False(t, err.IsOutOfRange()) 516 | assert.False(t, err.IsRemoteCall()) 517 | assert.False(t, err.IsNotFound()) 518 | }) 519 | } 520 | } 521 | 522 | var newRequiredTestSuite = []struct { 523 | InProp string 524 | Exp Error 525 | }{ 526 | { 527 | InProp: "", 528 | Exp: Error{ 529 | parent: nil, 530 | group: domain, 531 | kind: required, 532 | property: "", 533 | title: "Missing property", 534 | description: "required", 535 | statusName: "IsRequired", 536 | }, 537 | }, 538 | { 539 | InProp: "foo", 540 | Exp: Error{ 541 | parent: nil, 542 | group: domain, 543 | kind: required, 544 | property: "foo", 545 | title: "Missing property", 546 | description: "The property foo is required", 547 | statusName: "FooIsRequired", 548 | }, 549 | }, 550 | } 551 | 552 | func TestNewRequired(t *testing.T) { 553 | for _, tt := range newRequiredTestSuite { 554 | t.Run("", func(t *testing.T) { 555 | err := NewRequired(tt.InProp) 556 | assert.EqualValues(t, tt.Exp, err) 557 | assert.Equal(t, tt.Exp.Title(), err.Title()) 558 | assert.Equal(t, tt.Exp.Description(), err.Description()) 559 | assert.Equal(t, tt.Exp.Property(), err.Property()) 560 | assert.Equal(t, tt.Exp.Kind(), err.Kind()) 561 | assert.True(t, err.IsRequired()) 562 | assert.True(t, err.IsDomain()) 563 | assert.False(t, err.IsInfrastructure()) 564 | assert.False(t, err.IsNotFound()) 565 | assert.False(t, err.IsRemoteCall()) 566 | assert.False(t, err.IsAlreadyExists()) 567 | assert.False(t, err.IsOutOfRange()) 568 | assert.False(t, err.IsInvalidFormat()) 569 | }) 570 | } 571 | } 572 | -------------------------------------------------------------------------------- /examples/advanced/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | 7 | "github.com/neutrinocorp/ddderr" 8 | ) 9 | 10 | func main() { 11 | _, err := getFooByID("123") 12 | if err != nil { 13 | log.Print(err) 14 | 15 | if customErr, ok := err.(ddderr.Error); ok { 16 | // Will output -> lib/pq mocked error 17 | log.Print(customErr.Parent()) 18 | // Will output -> true 19 | log.Print(customErr.IsRemoteCall()) 20 | // Will output -> true 21 | log.Print(customErr.IsInfrastructure()) 22 | } 23 | } 24 | } 25 | 26 | func getFooByID(_ string) (interface{}, error) { 27 | return nil, ddderr.NewRemoteCall("localhost:5432"). 28 | AttachParent(errors.New("pq: Failed to connect to PostgreSQL host")) 29 | } 30 | -------------------------------------------------------------------------------- /examples/http-sample/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/neutrinocorp/ddderr" 7 | ) 8 | 9 | func main() { 10 | _, err := getFooByID("123") 11 | if err != nil { 12 | // HttpError struct is ready to be marshaled using JSON encoding libs 13 | httpErr := ddderr.NewHttpError("", "", err) 14 | // Will output -> If errType param is empty, then HTTP status text is used as type 15 | // (e.g. Not Found, Internal Server Error) 16 | log.Print(httpErr.Type) 17 | // Will output -> 404 as we got a NotFound error type 18 | log.Print(httpErr.Status) 19 | // Will output -> The resource foo was not found 20 | log.Print(httpErr.Detail) 21 | } 22 | } 23 | 24 | func getFooByID(_ string) (interface{}, error) { 25 | return nil, ddderr.NewNotFound("foo") 26 | } 27 | -------------------------------------------------------------------------------- /examples/simple/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/neutrinocorp/ddderr" 7 | ) 8 | 9 | func main() { 10 | _, err := getFooByID("123") 11 | if err != nil { 12 | // Will output -> The resource foo was not found 13 | log.Print(err) 14 | } 15 | } 16 | 17 | func getFooByID(_ string) (interface{}, error) { 18 | return nil, ddderr.NewNotFound("foo") 19 | } 20 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/neutrinocorp/ddderr/v3 2 | 3 | go 1.13 4 | 5 | require github.com/stretchr/testify v1.7.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 6 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 7 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 9 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 10 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 11 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 12 | -------------------------------------------------------------------------------- /http_util.go: -------------------------------------------------------------------------------- 1 | package ddderr 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | // HttpError is an RFC-compliant HTTP protocol problem object. 8 | // 9 | // For more information about the fields, please go to: https://datatracker.ietf.org/doc/html/rfc7807 10 | type HttpError struct { 11 | Type string `json:"type,omitempty"` 12 | Title string `json:"title,omitempty"` 13 | Status string `json:"status,omitempty"` 14 | StatusCode int `json:"status_code,omitempty"` 15 | Detail string `json:"detail,omitempty"` 16 | Instance string `json:"instance,omitempty"` 17 | } 18 | 19 | // NewHttpError builds an HttpError from the given DDD error 20 | func NewHttpError(errType, instance string, err error) HttpError { 21 | if err == nil { 22 | return HttpError{} 23 | } 24 | 25 | code := http.StatusInternalServerError 26 | errHttpType := getHttpErrorType(errType, code) 27 | 28 | customErr, ok := err.(Error) 29 | if !ok { 30 | return HttpError{ 31 | Type: errHttpType, 32 | Title: err.Error(), 33 | Status: http.StatusText(code), 34 | StatusCode: code, 35 | Detail: err.Error(), 36 | Instance: instance, 37 | } 38 | } 39 | 40 | code = GetHttpStatusCode(customErr) 41 | errHttpType = getHttpErrorType(errType, code) 42 | return HttpError{ 43 | Type: errHttpType, 44 | Title: customErr.Title(), 45 | Status: getHttpDddErrorStatus(customErr, code), 46 | StatusCode: code, 47 | Detail: customErr.Description(), 48 | Instance: instance, 49 | } 50 | } 51 | 52 | // retrieves a generic HTTP problem object type. 53 | // 54 | // For more information, go to: https://datatracker.ietf.org/doc/html/rfc7807#section-4.2 55 | func getHttpErrorType(rootType string, status int) string { 56 | if rootType != "" { 57 | return rootType 58 | } 59 | return http.StatusText(status) 60 | } 61 | 62 | // retrieves a status name from an Error or a generic HTTP problem object type. 63 | // 64 | // For more information, go to: https://datatracker.ietf.org/doc/html/rfc7807#section-4.2 65 | func getHttpDddErrorStatus(err Error, status int) string { 66 | if statusName := err.Status(); statusName != "" { 67 | return statusName 68 | } 69 | return http.StatusText(status) 70 | } 71 | 72 | // GetHttpStatusCode retrieves an HTTP status code from the given error 73 | func GetHttpStatusCode(err Error) int { 74 | switch { 75 | case err.IsAlreadyExists(): 76 | return http.StatusConflict 77 | case err.IsNotFound(): 78 | return http.StatusNotFound 79 | case err.IsInvalidFormat() || err.IsRequired() || err.IsOutOfRange() || err.IsDomain(): 80 | return http.StatusBadRequest 81 | case err.IsRemoteCall(): 82 | return http.StatusBadGateway 83 | default: 84 | return http.StatusInternalServerError 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /http_util_test.go: -------------------------------------------------------------------------------- 1 | package ddderr 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | var getCodeHttpTestSuite = []struct { 12 | InErr Error 13 | ExpCode int 14 | }{ 15 | { 16 | InErr: Error{}, 17 | ExpCode: http.StatusInternalServerError, 18 | }, 19 | { 20 | InErr: NewInfrastructure("generic title", "specific description"), 21 | ExpCode: http.StatusInternalServerError, 22 | }, 23 | { 24 | InErr: NewRemoteCall("tcp:172.16.52.1"), 25 | ExpCode: http.StatusBadGateway, 26 | }, 27 | { 28 | InErr: NewDomain("generic title", "specific description"), 29 | ExpCode: http.StatusBadRequest, 30 | }, 31 | { 32 | InErr: NewInvalidFormat("foo", "1", "5", "9"), 33 | ExpCode: http.StatusBadRequest, 34 | }, 35 | { 36 | InErr: NewOutOfRange("foo", 8, 256), 37 | ExpCode: http.StatusBadRequest, 38 | }, 39 | { 40 | InErr: NewRequired("foo"), 41 | ExpCode: http.StatusBadRequest, 42 | }, 43 | { 44 | InErr: NewAlreadyExists("foo"), 45 | ExpCode: http.StatusConflict, 46 | }, 47 | { 48 | InErr: NewNotFound("foo"), 49 | ExpCode: http.StatusNotFound, 50 | }, 51 | } 52 | 53 | func TestGetHttpStatusCode(t *testing.T) { 54 | for _, tt := range getCodeHttpTestSuite { 55 | t.Run("", func(t *testing.T) { 56 | code := GetHttpStatusCode(tt.InErr) 57 | assert.Equal(t, tt.ExpCode, code) 58 | }) 59 | } 60 | } 61 | 62 | var newHttpErrorTestSuite = []struct { 63 | InErrType string 64 | InInstance string 65 | InErr error 66 | ExpHttpErr HttpError 67 | }{ 68 | { 69 | InErrType: "", 70 | InInstance: "", 71 | InErr: nil, 72 | ExpHttpErr: HttpError{ 73 | Type: "", 74 | Title: "", 75 | Status: "", 76 | StatusCode: 0, 77 | Detail: "", 78 | Instance: "", 79 | }, 80 | }, 81 | { 82 | InErrType: "https://neutrinocorp.org/iam/probs/generic-error", 83 | InInstance: "/users/12345/msg/abc", 84 | InErr: nil, 85 | ExpHttpErr: HttpError{ 86 | Type: "", 87 | Title: "", 88 | Status: "", 89 | StatusCode: 0, 90 | Detail: "", 91 | Instance: "", 92 | }, 93 | }, 94 | { 95 | InErrType: "", 96 | InInstance: "/users/12345/msg/abc", 97 | InErr: errors.New("generic error"), 98 | ExpHttpErr: HttpError{ 99 | Type: "Internal Server Error", 100 | Title: "generic error", 101 | Status: "Internal Server Error", 102 | StatusCode: http.StatusInternalServerError, 103 | Detail: "generic error", 104 | Instance: "/users/12345/msg/abc", 105 | }, 106 | }, 107 | { 108 | InErrType: "https://neutrinocorp.org/iam/probs/generic-error", 109 | InInstance: "/users/12345/msg/abc", 110 | InErr: errors.New("generic error"), 111 | ExpHttpErr: HttpError{ 112 | Type: "https://neutrinocorp.org/iam/probs/generic-error", 113 | Title: "generic error", 114 | Status: "Internal Server Error", 115 | StatusCode: http.StatusInternalServerError, 116 | Detail: "generic error", 117 | Instance: "/users/12345/msg/abc", 118 | }, 119 | }, 120 | { 121 | InErrType: "https://neutrinocorp.org/iam/probs/generic-error", 122 | InInstance: "/users/12345/msg/abc", 123 | InErr: errors.New("generic error"), 124 | ExpHttpErr: HttpError{ 125 | Type: "https://neutrinocorp.org/iam/probs/generic-error", 126 | Title: "generic error", 127 | Status: "Internal Server Error", 128 | StatusCode: http.StatusInternalServerError, 129 | Detail: "generic error", 130 | Instance: "/users/12345/msg/abc", 131 | }, 132 | }, 133 | { 134 | InErrType: "https://neutrinocorp.org/iam/probs/not-found", 135 | InInstance: "/users/12345/msg/abc", 136 | InErr: NewNotFound("foo").SetStatus("FooNotFound"), 137 | ExpHttpErr: HttpError{ 138 | Type: "https://neutrinocorp.org/iam/probs/not-found", 139 | Title: "Resource not found", 140 | Status: "FooNotFound", 141 | StatusCode: http.StatusNotFound, 142 | Detail: "The resource foo was not found", 143 | Instance: "/users/12345/msg/abc", 144 | }, 145 | }, 146 | { 147 | InErrType: "", 148 | InInstance: "/users/12345/msg/abc", 149 | InErr: NewNotFound("foo").SetStatus("FooNotFound"), 150 | ExpHttpErr: HttpError{ 151 | Type: "Not Found", 152 | Title: "Resource not found", 153 | Status: "FooNotFound", 154 | StatusCode: http.StatusNotFound, 155 | Detail: "The resource foo was not found", 156 | Instance: "/users/12345/msg/abc", 157 | }, 158 | }, 159 | { 160 | InErrType: "https://neutrinocorp.org/iam/probs/required", 161 | InInstance: "/users/12345/msg/abc", 162 | InErr: NewRequired("foo"), 163 | ExpHttpErr: HttpError{ 164 | Type: "https://neutrinocorp.org/iam/probs/required", 165 | Title: "Missing property", 166 | Status: "FooIsRequired", 167 | StatusCode: http.StatusBadRequest, 168 | Detail: "The property foo is required", 169 | Instance: "/users/12345/msg/abc", 170 | }, 171 | }, 172 | { 173 | InErrType: "https://neutrinocorp.org/iam/probs/generic-domain", 174 | InInstance: "", 175 | InErr: NewDomain("generic title", "specific description"), 176 | ExpHttpErr: HttpError{ 177 | Type: "https://neutrinocorp.org/iam/probs/generic-domain", 178 | Title: "generic title", 179 | Status: "Bad Request", 180 | StatusCode: http.StatusBadRequest, 181 | Detail: "specific description", 182 | Instance: "", 183 | }, 184 | }, 185 | { 186 | InErrType: "https://neutrinocorp.org/iam/probs/generic-domain", 187 | InInstance: "", 188 | InErr: NewDomain("generic title", "specific description").SetStatus("GenericError"), 189 | ExpHttpErr: HttpError{ 190 | Type: "https://neutrinocorp.org/iam/probs/generic-domain", 191 | Title: "generic title", 192 | Status: "GenericError", 193 | StatusCode: http.StatusBadRequest, 194 | Detail: "specific description", 195 | Instance: "", 196 | }, 197 | }, 198 | { 199 | InErrType: "https://neutrinocorp.org/iam/probs/generic-infra", 200 | InInstance: "", 201 | InErr: NewInfrastructure("generic title", "specific description"), 202 | ExpHttpErr: HttpError{ 203 | Type: "https://neutrinocorp.org/iam/probs/generic-infra", 204 | Title: "generic title", 205 | Status: "Internal Server Error", 206 | StatusCode: http.StatusInternalServerError, 207 | Detail: "specific description", 208 | Instance: "", 209 | }, 210 | }, 211 | } 212 | 213 | func TestNewHttpError(t *testing.T) { 214 | for _, tt := range newHttpErrorTestSuite { 215 | t.Run("", func(t *testing.T) { 216 | err := NewHttpError(tt.InErrType, tt.InInstance, tt.InErr) 217 | assert.EqualValues(t, tt.ExpHttpErr, err) 218 | }) 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /string_utils.go: -------------------------------------------------------------------------------- 1 | package ddderr 2 | 3 | import ( 4 | "strings" 5 | "unicode" 6 | ) 7 | 8 | func getSanitizedStatusName(attribute, operation string) string { 9 | if attribute == "" { 10 | return operation 11 | } 12 | return replaceUnderscoreFromString(attribute) + operation 13 | } 14 | 15 | func replaceUnderscoreFromString(str string) string { 16 | var b strings.Builder 17 | b.Grow(len(str)) 18 | isTitle := false 19 | for i, ch := range str { 20 | if unicode.IsLetter(ch) { 21 | if i == 0 || isTitle { 22 | isTitle = false 23 | ch = unicode.ToTitle(ch) 24 | } 25 | b.WriteRune(ch) 26 | continue 27 | } 28 | isTitle = true 29 | } 30 | return b.String() 31 | } 32 | -------------------------------------------------------------------------------- /string_utils_test.go: -------------------------------------------------------------------------------- 1 | package ddderr 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | var getSanitizedStatusNameTestSuite = []struct { 9 | InAttr string 10 | InOp string 11 | Exp string 12 | }{ 13 | { 14 | InAttr: "", 15 | InOp: "", 16 | Exp: "", 17 | }, 18 | { 19 | InAttr: "-", 20 | InOp: "", 21 | Exp: "", 22 | }, 23 | { 24 | InAttr: "-e", 25 | InOp: "", 26 | Exp: "E", 27 | }, 28 | { 29 | InAttr: "foo-bar-baz", 30 | InOp: "", 31 | Exp: "FooBarBaz", 32 | }, 33 | { 34 | InAttr: "foo-bar-baz", 35 | InOp: "NotFound", 36 | Exp: "FooBarBazNotFound", 37 | }, 38 | { 39 | InAttr: "foo_bar_baz", 40 | InOp: "NotFound", 41 | Exp: "FooBarBazNotFound", 42 | }, 43 | { 44 | InAttr: "_foo_bar_baz", 45 | InOp: "NotFound", 46 | Exp: "FooBarBazNotFound", 47 | }, 48 | { 49 | InAttr: "foo#bar#baz", 50 | InOp: "NotFound", 51 | Exp: "FooBarBazNotFound", 52 | }, 53 | } 54 | 55 | func TestGetSanitizedStatusName(t *testing.T) { 56 | for _, tt := range getSanitizedStatusNameTestSuite { 57 | t.Run("", func(t *testing.T) { 58 | status := getSanitizedStatusName(tt.InAttr, tt.InOp) 59 | assert.Equal(t, tt.Exp, status) 60 | }) 61 | } 62 | } 63 | 64 | func BenchmarkGetSanitizedStatusName(b *testing.B) { 65 | for i := 0; i < b.N; i++ { 66 | b.ReportAllocs() 67 | getSanitizedStatusName("foo_bar_baz", "NotFound") 68 | } 69 | } 70 | --------------------------------------------------------------------------------