├── LICENSE ├── README.md └── jsonerror.go /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 PJ Engineering and Business Solutions Pty. Ltd. 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | JSONError for Golang [![GoDoc](http://godoc.org/github.com/pjebs/jsonerror?status.svg)](http://godoc.org/github.com/pjebs/jsonerror) 2 | ============= 3 | 4 | This package is for adding some structure to your error messages. This makes error-handling, debugging and diagnosis for all your Go projects a lot more elegant and simpler. Use it wherever `error` type is required. 5 | 6 | It utilizes the fact that built-in [`type error`](http://golang.org/pkg/builtin/#error) is actually an `interface.` 7 | 8 | 9 | The package also contains the **ErrorCollection** struct which allows for accumulation of multiple errors. 10 | It is safe to use from multiple concurrent goroutines unlike other comparable packages. 11 | 12 | Please **Star** this package so I can add it to [awesome-go](https://github.com/avelino/awesome-go). 13 | 14 | Refer to documentation on [GoDoc](http://godoc.org/github.com/pjebs/jsonerror) because the information below is a small subset of all the features. 15 | 16 | 17 | Install 18 | ------- 19 | 20 | ```shell 21 | go get -u github.com/pjebs/jsonerror 22 | ``` 23 | 24 | Optional - if you want to output JSON formatted error messages (e.g. for REST API): 25 | 26 | ```shell 27 | go get -u gopkg.in/unrolled/render.v1 28 | ``` 29 | 30 | Prehistoric Usage - Using [Go Standard Library](http://golang.org/pkg/errors/#example_New) 31 | ----- 32 | 33 | ```go 34 | import ( 35 | "errors" 36 | "fmt" 37 | ) 38 | 39 | func main() { 40 | err := errors.New("emit macho dwarf: elf header corrupted") 41 | if err != nil { 42 | fmt.Print(err) 43 | } 44 | } 45 | 46 | //Or alternatively 47 | 48 | panic(errors.New("failed")) 49 | 50 | ``` 51 | 52 | 53 | Using this package instead 54 | ----- 55 | 56 | ```go 57 | import ( 58 | errors "github.com/pjebs/jsonerror" //aliased for ease of usage 59 | "math" //For realSquareRoot() example function below 60 | ) 61 | 62 | //EXAMPLE 1 - Creating a JE Struct 63 | 64 | err := errors.New(1, "Square root of negative number is prohibited", "Please make number positive or zero") //Domain is optional and not included here 65 | 66 | //Or 67 | err := errors.New(1, "Square root of negative number is prohibited", "Please make number positive or zero", "com.github.pjebs.jsonerror") 68 | 69 | //EXAMPLE 2 - Practical Example 70 | 71 | //Custom function 72 | func realSquareRoot(n float64) (float64, error) { 73 | if n < 0 { 74 | return 0, errors.New(1, "Square root of negative number is prohibited", "Please make number positive or zero") 75 | } else { 76 | return math.Sqrt(n), nil 77 | } 78 | } 79 | 80 | //A function that uses realSquareRoot 81 | func main() { 82 | 83 | s, err := realSquareRoot(12.0) 84 | if err != nil { 85 | if err.(errors.JE).Code == 1 { 86 | //Square root of negative number 87 | } else { 88 | //Unknown error 89 | } 90 | return 91 | } 92 | 93 | //s is Valid answer 94 | } 95 | 96 | 97 | ``` 98 | 99 | Methods 100 | -------- 101 | 102 | ```go 103 | func New(code int, error string, message string, domain ...string) JE 104 | ``` 105 | 106 | `code int` - Error code. Arbitrary and set by *fiat*. Different types of errors should have an unique `error code` in your project. 107 | 108 | `error string` - A standard description of the `error code.` 109 | 110 | `message string` - A more detailed description that may be customized for the particular circumstances. May also provide extra information. 111 | 112 | `domain ...string` - *Optional* It allows you to distinguish between same `error codes.` Only 1 `domain` string is allowed. 113 | 114 | 115 | ```go 116 | func (this JE) Render() map[string]string { 117 | ``` 118 | 119 | Formats `JE` (JSONError) struct so it can be used by [gopkg.in/unrolled/render.v1](https://github.com/unrolled/render) package to generate JSON output. 120 | 121 | 122 | Output JSON formatted error message (i.e. REST API Server response) 123 | ---------- 124 | 125 | ```go 126 | import ( 127 | "github.com/codegangsta/negroni" //Using Negroni (https://github.com/codegangsta/negroni) 128 | errors "github.com/pjebs/jsonerror" 129 | "gopkg.in/unrolled/render.v1" 130 | "net/http" 131 | ) 132 | 133 | func main() { 134 | mux := http.NewServeMux() 135 | mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { 136 | 137 | err := errors.New(12, "Unauthorized Access", "Please log in first to access this site") 138 | 139 | r := render.New(render.Options{}) 140 | r.JSON(w, http.StatusUnauthorized, err.Render()) 141 | return 142 | 143 | }) 144 | 145 | n := negroni.Classic() 146 | n.UseHandler(mux) 147 | n.Run(":3000") 148 | } 149 | 150 | ``` 151 | 152 | For the above example, the web server will respond with a HTTP Status Code of 401 (Status Unauthorized), Content-Type as application/json and a JSON response: 153 | 154 | ```json 155 | {"code":"12","error":"Unauthorized Access","message":"Please log in first to access this site"} 156 | ``` 157 | 158 | FAQ 159 | -------- 160 | 161 | **What is the domain parameter?** 162 | 163 | The domain parameter is optional. It allows you to distinguish between same error codes. That way different packages (or different parts of your own project) can use the same error codes (for different purposes) and still be differentiated by the domain identifier. 164 | 165 | NB: The domain parameter is not outputted by `Render()` (for generating JSON formatted output) 166 | 167 | **How do I use this package?** 168 | 169 | When you want to return an error (e.g. from a function), just return a `JE` struct. See the example code above. 170 | 171 | Or you can use it with `panic()`. 172 | 173 | ```go 174 | panic(jsonerror.New(1, "error", "message")) 175 | ``` 176 | 177 | **What are the error codes?** 178 | 179 | You make them up for your particular project! By *fiat*, you arbitrarily set each error code to mean a different *type* of error. 180 | 181 | 182 | Final Notes 183 | -------- 184 | 185 | If you found this package useful, please **Star** it on github. Feel free to fork or provide pull requests. Any bug reports will be warmly received. 186 | 187 | 188 | [PJ Engineering and Business Solutions Pty. Ltd.](http://www.pjebs.com.au) 189 | -------------------------------------------------------------------------------- /jsonerror.go: -------------------------------------------------------------------------------- 1 | package jsonerror 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "sync" 7 | "time" 8 | ) 9 | 10 | // ErrorCollection can be configured to allow duplicates. 11 | type DuplicatationOptions int 12 | 13 | const ( 14 | AllowDuplicates DuplicatationOptions = 0 15 | RejectDuplicatesIgnoreTimestamp DuplicatationOptions = 1 //Ignore timestamp information in JE struct 16 | RejectDuplicates DuplicatationOptions = 2 17 | ) 18 | 19 | // DefaultErrorFormatter represents the default formatter for displaying the collection 20 | // of errors. 21 | var DefaultErrorFormatter = func(i int, err error, str *string) { 22 | *str = fmt.Sprintf("%s\n%d:%s", *str, i, err.Error()) 23 | } 24 | 25 | // ErrorCollection allows multiple errors to be accumulated and then returned as a single error. 26 | // ErrorCollection can be safely used by concurrent goroutines. 27 | type ErrorCollection struct { 28 | DuplicatationOptions DuplicatationOptions 29 | Errors []error 30 | Formatter func(i int, err error, str *string) 31 | lock sync.RWMutex 32 | } 33 | 34 | // Creates a new empty ErrorCollection. 35 | // When `dup` is set, any duplicate error message is discarded 36 | // and not appended to the collection 37 | func NewErrorCollection(dup ...DuplicatationOptions) *ErrorCollection { 38 | ec := &ErrorCollection{} 39 | ec.Errors = []error{} 40 | ec.Formatter = DefaultErrorFormatter 41 | if len(dup) != 0 { 42 | ec.DuplicatationOptions = dup[0] 43 | } 44 | return ec 45 | } 46 | 47 | // Append an error to the error collection without locking 48 | func (ec *ErrorCollection) addError(err error) { 49 | 50 | if err == nil { 51 | return 52 | } 53 | 54 | if ec.DuplicatationOptions != AllowDuplicates { 55 | //Don't append if err is a duplicate 56 | for i, containedErr := range ec.Errors { 57 | 58 | var je1 *JE 59 | var je2 *JE 60 | 61 | s, ok := err.(JE) 62 | if ok { 63 | je1 = &s 64 | } else { 65 | s, ok := err.(*JE) 66 | if ok { 67 | je1 = s 68 | } 69 | } 70 | 71 | _, ok = containedErr.(JE) 72 | if ok { 73 | t := (ec.Errors[i]).(JE) 74 | je2 = &t 75 | } else { 76 | _, ok := containedErr.(*JE) 77 | if ok { 78 | je2 = (ec.Errors[i]).(*JE) 79 | } 80 | } 81 | 82 | if je1 != nil && je2 != nil { 83 | //Don't use Reflection since both are JE structs 84 | if (*je1).Code == (*je2).Code && (*je1).Domain == (*je2).Domain && (*je1).error == (*je2).error && (*je1).message == (*je2).message { 85 | if ec.DuplicatationOptions == RejectDuplicates { 86 | if (*je1).time.Equal((*je2).time) { 87 | //Both JE structs are 100% identical including timestamp 88 | return 89 | } 90 | } else { 91 | //We don't care about timestamps 92 | return 93 | } 94 | } 95 | } else { 96 | //Use Reflection 97 | if reflect.DeepEqual(containedErr, err) { 98 | return 99 | } 100 | } 101 | } 102 | } 103 | ec.Errors = append(ec.Errors, err) 104 | } 105 | 106 | // AddError appends an error to the error collection. 107 | // It is safe to use from multiple concurrent goroutines. 108 | func (ec *ErrorCollection) AddError(err error) { 109 | ec.lock.Lock() 110 | defer ec.lock.Unlock() 111 | 112 | ec.addError(err) 113 | } 114 | 115 | // AddErrors appends multiple errors to the error collection. 116 | // It is safe to use from multiple concurrent goroutines. 117 | func (ec *ErrorCollection) AddErrors(errs ...error) { 118 | ec.lock.Lock() 119 | defer ec.lock.Unlock() 120 | 121 | for _, err := range errs { 122 | ec.addError(err) 123 | } 124 | } 125 | 126 | // AddErrorCollection appends an entire ErrorCollection to the receiver error collection. 127 | // It is safe to use from multiple concurrent goroutines. 128 | func (ec *ErrorCollection) AddErrorCollection(errs *ErrorCollection) { 129 | ec.lock.Lock() 130 | defer ec.lock.Unlock() 131 | 132 | for _, err := range errs.Errors { 133 | ec.addError(err) 134 | } 135 | } 136 | 137 | // Error return a list of all contained errors. 138 | // The output can be formatted by setting a custom Formatter. 139 | func (ec *ErrorCollection) Error() string { 140 | if ec.Formatter == nil { 141 | return "" 142 | } 143 | 144 | ec.lock.RLock() 145 | defer ec.lock.RUnlock() 146 | str := "" 147 | for i, err := range ec.Errors { 148 | if ec.Formatter != nil { 149 | ec.Formatter(i, err, &str) 150 | } 151 | } 152 | return str 153 | } 154 | 155 | // IsNil returns whether an error is nil or not. 156 | // It can be used with ErrorCollection or generic errors 157 | func IsNil(err error) bool { 158 | switch v := err.(type) { 159 | case *ErrorCollection: 160 | if len(v.Errors) == 0 { 161 | return true 162 | } else { 163 | return false 164 | } 165 | default: 166 | if err == nil { 167 | return true 168 | } else { 169 | return false 170 | } 171 | } 172 | } 173 | 174 | // JE allows errors to contain Code, Domain, Error and Message information. 175 | // Only Code and Domain are exported so that once a JE struct is created, the key elements are static. 176 | type JE struct { 177 | Code int 178 | Domain string 179 | error string 180 | message string 181 | time time.Time //Displayed as Unix timestamp (number of nanoseconds elapsed since January 1, 1970 UTC) 182 | DisplayTime bool 183 | } 184 | 185 | // New creates a new JE struct. 186 | // Domain is optional but can be at most 1 string. 187 | func New(code int, error string, message string, domain ...string) JE { 188 | j := JE{Code: code, error: error, message: message, time: time.Now().UTC()} 189 | if len(domain) != 0 { 190 | j.Domain = domain[0] 191 | } 192 | return j 193 | } 194 | 195 | // NewAndDisplayTime creates a new JE struct and configures it to display the timestamp. 196 | // Domain is optional but can be at most 1 string. 197 | func NewAndDisplayTime(code int, error string, message string, domain ...string) JE { 198 | j := JE{Code: code, error: error, message: message, time: time.Now().UTC(), DisplayTime: true} 199 | if len(domain) != 0 { 200 | j.Domain = domain[0] 201 | } 202 | return j 203 | } 204 | 205 | // Error generates a string that neatly formats the contents of the JE struct. 206 | // JSONError satisfies the error interface. Useful with panic(). 207 | func (j JE) Error() string { 208 | finalString := fmt.Sprintf("[code]: %d", j.Code) 209 | 210 | if j.error != "" { 211 | finalString = finalString + fmt.Sprintf(" [error]: %s", j.error) 212 | } 213 | 214 | if j.message != "" { 215 | finalString = finalString + fmt.Sprintf(" [message]: %s", j.message) 216 | } 217 | 218 | if j.Domain != "" { 219 | finalString = finalString + fmt.Sprintf(" [domain]: %s", j.Domain) 220 | } 221 | 222 | if j.DisplayTime { 223 | finalString = finalString + fmt.Sprintf(" [time]: %d", j.time.UnixNano()) 224 | } 225 | 226 | return finalString 227 | } 228 | 229 | //Return the time the JE struct was created 230 | func (j JE) Time() time.Time { 231 | return j.time 232 | } 233 | 234 | //For use with package: "gopkg.in/unrolled/render.v1". 235 | //Can easily output properly formatted JSON error messages for REST API services. 236 | func (j JE) Render() map[string]string { 237 | 238 | if j.error == "" { 239 | if j.message == "" { 240 | return map[string]string{"code": fmt.Sprintf("%d", j.Code)} 241 | } else { 242 | return map[string]string{"code": fmt.Sprintf("%d", j.Code), "message": j.message} 243 | } 244 | } else { 245 | if j.message == "" { 246 | return map[string]string{"code": fmt.Sprintf("%d", j.Code), "error": j.error} 247 | } else { 248 | return map[string]string{"code": fmt.Sprintf("%d", j.Code), "error": j.error, "message": j.message} 249 | } 250 | } 251 | } 252 | --------------------------------------------------------------------------------