├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── errors.go ├── errors_test.go └── vendor └── vendor.json /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/*/ 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: false 3 | go: 4 | - 1.4 5 | - 1.5 6 | - 1.6 7 | - 1.7 8 | - tip 9 | 10 | script: 11 | - go get golang.org/x/tools/cmd/cover 12 | - go get github.com/mattn/goveralls 13 | - go test -v -covermode=count -coverprofile=coverage.out 14 | 15 | after_success: 16 | - goveralls -coverprofile=coverage.out -service=travis-ci -repotoken ujDHE2bWzb8zOE54D1OcC5gXxmX6sO8yt 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Danniel Magno Pereira 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 | # jsonapi-errors [![GoDoc](https://godoc.org/github.com/golang/gddo?status.svg)](http://godoc.org/github.com/AmuzaTkts/jsonapi-errors) [![Build Status](https://travis-ci.org/AmuzaTkts/jsonapi-errors.svg?branch=master)](https://travis-ci.org/AmuzaTkts/jsonapi-errors) [![Coverage Status](https://coveralls.io/repos/github/AmuzaTkts/jsonapi-errors/badge.svg?branch=master)](https://coveralls.io/github/AmuzaTkts/jsonapi-errors?branch=master) [![Go ReportCard](https://goreportcard.com/badge/github.com/AmuzaTkts/jsonapi-errors)](https://goreportcard.com/report/github.com/AmuzaTkts/jsonapi-errors) 2 | 3 | This package provides error bindings based on the [JSON API](http://jsonapi.org/format/#errors) 4 | reference. 5 | 6 | The package provides two main structs that you can use on your application, the 7 | `Error` and `Bag` structs. When returning errors from your API you should return 8 | a `Bag` containing one or more errors. 9 | 10 | ```Go 11 | bag := NewBagWithError(502, "Oops =(") 12 | jsonStr, _ := json.Marshal(bag) 13 | ``` 14 | 15 | The above code will return the following JSON structure: 16 | 17 | ```JSON 18 | { 19 | "errors": [ 20 | { 21 | "detail": "Oops =(", 22 | "status": "502" 23 | } 24 | ], 25 | "status": "502" 26 | } 27 | ``` 28 | 29 | ## Multiple errors 30 | This package adds the possibility to add multiple errors with different status 31 | codes. The package will check for the range of the errors and will set the main 32 | status key to the lower bound of the error class. 33 | 34 | Eg: If add an error `501` and `502`, the main status key will be `500`. 35 | 36 | ```Go 37 | bag := NewBag() 38 | bag.AddError(501, "Server Error 1") 39 | bag.AddError(502, "Server Error 2") 40 | 41 | jsonStr, _ := json.Marshal(bag) 42 | ``` 43 | 44 | Will return: 45 | ```JSON 46 | { 47 | "errors": [ 48 | { 49 | "detail": "Server Error 1", 50 | "status": "501" 51 | }, 52 | { 53 | "detail": "Server Error 2", 54 | "status": "502" 55 | } 56 | ], 57 | "status": "500" 58 | } 59 | ``` 60 | 61 | It's also possible to have errors of different classes(`400` and `500` for example), 62 | in this case the package will silently return `400` as the main status. 63 | 64 | ```Go 65 | bag := NewBag() 66 | bag.AddError(401, "Client Error 1") 67 | bag.AddError(502, "Server Error 1") 68 | 69 | jsonStr, _ := json.Marshal(bag) 70 | ``` 71 | 72 | ```JSON 73 | { 74 | "errors": [ 75 | { 76 | "detail": "Client Error 1", 77 | "status": "401" 78 | }, 79 | { 80 | "detail": "Server Error 1", 81 | "status": "502" 82 | } 83 | ], 84 | "status": "400" 85 | } 86 | ``` 87 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | // Errors package implements the JSON API v1.0 format for errors. 2 | // Ref: http://jsonapi.org/format/#errors 3 | package errors 4 | 5 | import "fmt" 6 | 7 | // Source contain references to the source of the error 8 | type Source struct { 9 | Pointer string `json:"pointer,omitempty"` 10 | Parameter string `json:"parameter,omitempty"` 11 | } 12 | 13 | // Link should lead to further details about this error 14 | type Link struct { 15 | About string `json:"about,omitempty"` 16 | } 17 | 18 | // Error provides additional information about problems encountered while 19 | // performing an operation. 20 | type Error struct { 21 | Code int `json:"code,string,omitempty"` 22 | Debug map[string]interface{} `json:"debug,omitempty"` 23 | Detail string `json:"detail"` 24 | ID string `json:"id,omitempty"` 25 | Links *Link `json:"links,omitempty"` 26 | Meta map[string]interface{} `json:"meta,omitempty"` 27 | Source *Source `json:"source,omitempty"` 28 | Status int `json:"status,string"` 29 | Title string `json:"title,omitempty"` 30 | } 31 | 32 | func (e *Error) Error() string { 33 | return fmt.Sprintf("%d: %s", e.Status, e.Detail) 34 | } 35 | 36 | func (e *Error) SetAboutLink(link string) { 37 | l := &Link{link} 38 | e.Links = l 39 | } 40 | 41 | func (e *Error) AddSourceNode() *Source { 42 | s := &Source{} 43 | e.Source = s 44 | 45 | return s 46 | } 47 | 48 | func (e *Error) SetPointer(pointer string) *Error { 49 | if e.Source == nil { 50 | e.AddSourceNode() 51 | } 52 | 53 | e.Source.Pointer = pointer 54 | 55 | return e 56 | } 57 | 58 | func (e *Error) SetParameter(parameter string) *Error { 59 | if e.Source == nil { 60 | e.AddSourceNode() 61 | } 62 | 63 | e.Source.Parameter = parameter 64 | 65 | return e 66 | } 67 | 68 | func NewError(status int, detail string) *Error { 69 | return &Error{ 70 | Status: status, 71 | Detail: detail, 72 | } 73 | } 74 | 75 | type Bag struct { 76 | Errors []*Error `json:"errors"` 77 | Status int `json:"status,string,omitempty"` 78 | } 79 | 80 | func (b *Bag) Add(err *Error) *Error { 81 | b.Errors = append(b.Errors, err) 82 | 83 | if len(b.Errors) == 1 { 84 | b.Status = err.Status 85 | 86 | return err 87 | } 88 | 89 | if b.Status != err.Status { 90 | if statusClass := GetErrorClass(b.Status); statusClass == GetErrorClass(err.Status) { 91 | b.Status = statusClass 92 | } else { 93 | b.Status = 400 94 | } 95 | } 96 | 97 | return err 98 | } 99 | 100 | func (b *Bag) AddError(status int, detail string) *Error { 101 | return b.Add(NewError(status, detail)) 102 | } 103 | 104 | // NewBag returns a new Bag 105 | func NewBag() *Bag { 106 | return &Bag{} 107 | } 108 | 109 | // NewBagWithError is a shorthand to `errors.NewBag().Add(status, detail)` 110 | func NewBagWithError(status int, detail string) *Bag { 111 | bag := &Bag{} 112 | bag.AddError(status, detail) 113 | 114 | return bag 115 | } 116 | 117 | // GetErrorClass returns 400 or 500 depending of the error code passed. 118 | // If the error code is less than 400 or greater or equal to 600 it returns 0. 119 | func GetErrorClass(err int) int { 120 | if err < 400 || err >= 600 { 121 | return 0 122 | } 123 | 124 | if err >= 400 && err < 500 { 125 | return 400 126 | } 127 | 128 | return 500 129 | } 130 | -------------------------------------------------------------------------------- /errors_test.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestGetErrorClass(t *testing.T) { 11 | var cases = []struct { 12 | code int 13 | expected int 14 | }{ 15 | {301, 0}, 16 | {302, 0}, 17 | {600, 0}, 18 | {900, 0}, 19 | 20 | {401, 400}, 21 | {402, 400}, 22 | {403, 400}, 23 | 24 | {500, 500}, 25 | {501, 500}, 26 | {502, 500}, 27 | } 28 | 29 | for _, tc := range cases { 30 | assert.Equal(t, tc.expected, GetErrorClass(tc.code)) 31 | } 32 | } 33 | 34 | func TestNewBag(t *testing.T) { 35 | bag := NewBag() 36 | 37 | assert.IsType(t, &Bag{}, bag) 38 | assert.Equal(t, 0, len(bag.Errors)) 39 | } 40 | 41 | func TestNewBagWithError(t *testing.T) { 42 | bag := NewBagWithError(500, "Oops") 43 | 44 | assert.IsType(t, &Bag{}, bag) 45 | assert.Equal(t, 1, len(bag.Errors)) 46 | assert.Equal(t, 500, bag.Status) 47 | } 48 | 49 | func TestBagStatusWithMultipleErrorsWithSameStatus(t *testing.T) { 50 | bag := NewBag() 51 | 52 | bag.AddError(502, "Server Error 1") 53 | bag.AddError(502, "Server Error 2") 54 | 55 | jsonString := `{"errors":[{"detail":"Server Error 1","status":"502"},{"detail":"Server Error 2","status":"502"}],"status":"502"}` 56 | jsonBytes, _ := json.Marshal(bag) 57 | 58 | assert.Equal(t, 502, bag.Status) 59 | assert.Equal(t, jsonString, string(jsonBytes)) 60 | } 61 | 62 | func TestBagStatusWithMultipleErrorsWithSameClass(t *testing.T) { 63 | bag := NewBag() 64 | 65 | bag.AddError(501, "Server Error 1") 66 | bag.AddError(502, "Server Error 2") 67 | 68 | jsonString := `{"errors":[{"detail":"Server Error 1","status":"501"},{"detail":"Server Error 2","status":"502"}],"status":"500"}` 69 | jsonBytes, _ := json.Marshal(bag) 70 | 71 | assert.Equal(t, 500, bag.Status) 72 | assert.Equal(t, jsonString, string(jsonBytes)) 73 | } 74 | 75 | func TestBagStatusWithMultipleErrorsWithDifferentClass(t *testing.T) { 76 | bag := NewBag() 77 | 78 | bag.AddError(401, "Client Error 1") 79 | bag.AddError(502, "Server Error 1") 80 | 81 | jsonString := `{"errors":[{"detail":"Client Error 1","status":"401"},{"detail":"Server Error 1","status":"502"}],"status":"400"}` 82 | jsonBytes, _ := json.Marshal(bag) 83 | 84 | assert.Equal(t, 400, bag.Status) 85 | assert.Equal(t, jsonString, string(jsonBytes)) 86 | } 87 | 88 | func TestError(t *testing.T) { 89 | err := NewError(400, "Test error") 90 | 91 | assert.Error(t, err) 92 | assert.Equal(t, "400: Test error", err.Error()) 93 | } 94 | 95 | func TestErrorSetAboutLink(t *testing.T) { 96 | link := "http://google.com" 97 | err := NewError(400, "Test error") 98 | err.SetAboutLink(link) 99 | 100 | jsonString := `{"detail":"Test error","links":{"about":"http://google.com"},"status":"400"}` 101 | jsonBytes, _ := json.Marshal(err) 102 | 103 | assert.Equal(t, link, err.Links.About) 104 | assert.Equal(t, jsonString, string(jsonBytes)) 105 | } 106 | 107 | func TestSetPointer(t *testing.T) { 108 | pointer := "/data/attributes/password" 109 | err := NewError(400, "Test error") 110 | err.SetPointer(pointer) 111 | 112 | jsonString := `{"detail":"Test error","source":{"pointer":"/data/attributes/password"},"status":"400"}` 113 | jsonBytes, _ := json.Marshal(err) 114 | 115 | assert.Equal(t, pointer, err.Source.Pointer) 116 | assert.Equal(t, jsonString, string(jsonBytes)) 117 | } 118 | 119 | func TestSetParameter(t *testing.T) { 120 | parameter := "order" 121 | err := NewError(400, "Test error") 122 | err.SetParameter(parameter) 123 | 124 | jsonString := `{"detail":"Test error","source":{"parameter":"order"},"status":"400"}` 125 | jsonBytes, _ := json.Marshal(err) 126 | 127 | assert.Equal(t, parameter, err.Source.Parameter) 128 | assert.Equal(t, jsonString, string(jsonBytes)) 129 | } 130 | -------------------------------------------------------------------------------- /vendor/vendor.json: -------------------------------------------------------------------------------- 1 | { 2 | "comment": "", 3 | "ignore": "test", 4 | "package": [ 5 | { 6 | "checksumSHA1": "Bn333k9lTndxU3D6n/G5c+GMcYY=", 7 | "path": "github.com/stretchr/testify/assert", 8 | "revision": "bcd9e3389dd03b0b668d11f4d462a6af6c2dfd60", 9 | "revisionTime": "2016-04-13T09:04:24Z" 10 | } 11 | ], 12 | "rootPath": "github.com/AmuzaTkts/jsonapi-errors" 13 | } 14 | --------------------------------------------------------------------------------