Skip to content

Commit

Permalink
New errors API!!
Browse files Browse the repository at this point in the history
  • Loading branch information
manucorporat committed May 22, 2015
1 parent 5f76ba2 commit e94247f
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 101 deletions.
133 changes: 63 additions & 70 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,11 @@ func (c *Context) AbortWithStatus(code int) {
c.Abort()
}

func (c *Context) AbortWithError(code int, err error) *errorMsg {
c.AbortWithStatus(code)
return c.Error(err)
}

func (c *Context) IsAborted() bool {
return c.index == AbortIndex
}
Expand All @@ -134,30 +139,16 @@ func (c *Context) IsAborted() bool {
/********* ERROR MANAGEMENT *********/
/************************************/

// Fail is the same as Abort plus an error message.
// Calling `context.Fail(500, err)` is equivalent to:
// ```
// context.Error("Operation aborted", err)
// context.AbortWithStatus(500)
// ```
func (c *Context) Fail(code int, err error) {
c.Error(err, "Operation aborted")
c.AbortWithStatus(code)
}

func (c *Context) ErrorTyped(err error, typ int, meta interface{}) {
c.Errors = append(c.Errors, errorMsg{
Error: err,
Flags: typ,
Meta: meta,
})
}

// Attaches an error to the current context. The error is pushed to a list of errors.
// It's a good idea to call Error for each error that occurred during the resolution of a request.
// A middleware can be used to collect all the errors and push them to a database together, print a log, or append it in the HTTP response.
func (c *Context) Error(err error, meta interface{}) {
c.ErrorTyped(err, ErrorTypeExternal, meta)
func (c *Context) Error(err error) *errorMsg {
newError := &errorMsg{
Error: err,
Flags: ErrorTypePrivate,
}
c.Errors = append(c.Errors, newError)
return newError
}

func (c *Context) LastError() error {
Expand All @@ -168,6 +159,35 @@ func (c *Context) LastError() error {
return nil
}

/************************************/
/******** METADATA MANAGEMENT********/
/************************************/

// Sets a new pair key/value just for the specified context.
// It also lazy initializes the hashmap.
func (c *Context) Set(key string, value interface{}) {
if c.Keys == nil {
c.Keys = make(map[string]interface{})
}
c.Keys[key] = value
}

// Get returns the value for the given key or an error if the key does not exist.
func (c *Context) Get(key string) (value interface{}, exists bool) {
if c.Keys != nil {
value, exists = c.Keys[key]
}
return
}

// MustGet returns the value for the given key or panics if the value doesn't exist.
func (c *Context) MustGet(key string) interface{} {
if value, exists := c.Get(key); exists {
return value
}
panic("Key \"" + key + "\" does not exist")
}

/************************************/
/************ INPUT DATA ************/
/************************************/
Expand Down Expand Up @@ -233,40 +253,29 @@ func (c *Context) postFormValue(key string) (string, bool) {
return "", false
}

/************************************/
/******** METADATA MANAGEMENT********/
/************************************/

// Sets a new pair key/value just for the specified context.
// It also lazy initializes the hashmap.
func (c *Context) Set(key string, value interface{}) {
if c.Keys == nil {
c.Keys = make(map[string]interface{})
}
c.Keys[key] = value
// This function checks the Content-Type to select a binding engine automatically,
// Depending the "Content-Type" header different bindings are used:
// "application/json" --> JSON binding
// "application/xml" --> XML binding
// else --> returns an error
// if Parses the request's body as JSON if Content-Type == "application/json" using JSON or XML as a JSON input. It decodes the json payload into the struct specified as a pointer.Like ParseBody() but this method also writes a 400 error if the json is not valid.
func (c *Context) Bind(obj interface{}) error {
b := binding.Default(c.Request.Method, c.ContentType())
return c.BindWith(obj, b)
}

// Get returns the value for the given key or an error if the key does not exist.
func (c *Context) Get(key string) (value interface{}, exists bool) {
if c.Keys != nil {
value, exists = c.Keys[key]
}
return
func (c *Context) BindJSON(obj interface{}) error {
return c.BindWith(obj, binding.JSON)
}

// MustGet returns the value for the given key or panics if the value doesn't exist.
func (c *Context) MustGet(key string) interface{} {
if value, exists := c.Get(key); exists {
return value
} else {
panic("Key \"" + key + "\" does not exist")
func (c *Context) BindWith(obj interface{}, b binding.Binding) error {
if err := b.Bind(c.Request, obj); err != nil {
c.AbortWithError(400, err).Type(ErrorTypeBind)
return err
}
return nil
}

/************************************/
/********* PARSING REQUEST **********/
/************************************/

func (c *Context) ClientIP() string {
clientIP := c.Request.Header.Get("X-Real-IP")
if len(clientIP) > 0 {
Expand All @@ -284,25 +293,6 @@ func (c *Context) ContentType() string {
return filterFlags(c.Request.Header.Get("Content-Type"))
}

// This function checks the Content-Type to select a binding engine automatically,
// Depending the "Content-Type" header different bindings are used:
// "application/json" --> JSON binding
// "application/xml" --> XML binding
// else --> returns an error
// if Parses the request's body as JSON if Content-Type == "application/json" using JSON or XML as a JSON input. It decodes the json payload into the struct specified as a pointer.Like ParseBody() but this method also writes a 400 error if the json is not valid.
func (c *Context) Bind(obj interface{}) bool {
b := binding.Default(c.Request.Method, c.ContentType())
return c.BindWith(obj, b)
}

func (c *Context) BindWith(obj interface{}, b binding.Binding) bool {
if err := b.Bind(c.Request, obj); err != nil {
c.Fail(400, err)
return false
}
return true
}

/************************************/
/******** RESPONSE RENDERING ********/
/************************************/
Expand All @@ -319,8 +309,7 @@ func (c *Context) Render(code int, r render.Render) {
c.Writer.WriteHeader(code)
if err := r.Write(c.Writer); err != nil {
debugPrintError(err)
c.ErrorTyped(err, ErrorTypeInternal, nil)
c.AbortWithStatus(500)
c.AbortWithError(500, err).Type(ErrorTypeRender)
}
}

Expand Down Expand Up @@ -430,7 +419,7 @@ func (c *Context) Negotiate(code int, config Negotiate) {
c.XML(code, data)

default:
c.Fail(http.StatusNotAcceptable, errors.New("the accepted formats are not offered by the server"))
c.AbortWithError(http.StatusNotAcceptable, errors.New("the accepted formats are not offered by the server"))
}
}

Expand Down Expand Up @@ -458,6 +447,10 @@ func (c *Context) SetAccepted(formats ...string) {
c.Accepted = formats
}

/************************************/
/******** CONTENT NEGOTIATION *******/
/************************************/

func (c *Context) Deadline() (deadline time.Time, ok bool) {
return
}
Expand Down
44 changes: 22 additions & 22 deletions context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func TestContextReset(t *testing.T) {
c.index = 2
c.Writer = &responseWriter{ResponseWriter: httptest.NewRecorder()}
c.Params = Params{Param{}}
c.Error(errors.New("test"), nil)
c.Error(errors.New("test"))
c.Set("foo", "bar")
c.reset()

Expand Down Expand Up @@ -352,49 +352,49 @@ func TestContextError(t *testing.T) {
assert.Nil(t, c.LastError())
assert.Empty(t, c.Errors.String())

c.Error(errors.New("first error"), "some data")
c.Error(errors.New("first error")).Meta("some data")
assert.Equal(t, c.LastError().Error(), "first error")
assert.Len(t, c.Errors, 1)
assert.Equal(t, c.Errors.String(), "Error #01: first error\n Meta: some data\n")

c.Error(errors.New("second error"), "some data 2")
c.Error(errors.New("second error")).Meta("some data 2")
assert.Equal(t, c.LastError().Error(), "second error")
assert.Len(t, c.Errors, 2)
assert.Equal(t, c.Errors.String(), "Error #01: first error\n Meta: some data\n"+
"Error #02: second error\n Meta: some data 2\n")

assert.Equal(t, c.Errors[0].Error, errors.New("first error"))
assert.Equal(t, c.Errors[0].Meta, "some data")
assert.Equal(t, c.Errors[0].Flags, ErrorTypeExternal)
assert.Equal(t, c.Errors[0].Metadata, "some data")
assert.Equal(t, c.Errors[0].Flags, ErrorTypePrivate)

assert.Equal(t, c.Errors[1].Error, errors.New("second error"))
assert.Equal(t, c.Errors[1].Meta, "some data 2")
assert.Equal(t, c.Errors[1].Flags, ErrorTypeExternal)
assert.Equal(t, c.Errors[1].Metadata, "some data 2")
assert.Equal(t, c.Errors[1].Flags, ErrorTypePrivate)
}

func TestContextTypedError(t *testing.T) {
c, _, _ := createTestContext()
c.ErrorTyped(errors.New("externo 0"), ErrorTypeExternal, nil)
c.ErrorTyped(errors.New("externo 1"), ErrorTypeExternal, nil)
c.ErrorTyped(errors.New("interno 0"), ErrorTypeInternal, nil)
c.ErrorTyped(errors.New("externo 2"), ErrorTypeExternal, nil)
c.ErrorTyped(errors.New("interno 1"), ErrorTypeInternal, nil)
c.ErrorTyped(errors.New("interno 2"), ErrorTypeInternal, nil)

for _, err := range c.Errors.ByType(ErrorTypeExternal) {
assert.Equal(t, err.Flags, ErrorTypeExternal)
c.Error(errors.New("externo 0")).Type(ErrorTypePublic)
c.Error(errors.New("externo 1")).Type(ErrorTypePublic)
c.Error(errors.New("interno 0")).Type(ErrorTypePrivate)
c.Error(errors.New("externo 2")).Type(ErrorTypePublic)
c.Error(errors.New("interno 1")).Type(ErrorTypePrivate)
c.Error(errors.New("interno 2")).Type(ErrorTypePrivate)

for _, err := range c.Errors.ByType(ErrorTypePublic) {
assert.Equal(t, err.Flags, ErrorTypePublic)
}

for _, err := range c.Errors.ByType(ErrorTypeInternal) {
assert.Equal(t, err.Flags, ErrorTypeInternal)
for _, err := range c.Errors.ByType(ErrorTypePrivate) {
assert.Equal(t, err.Flags, ErrorTypePrivate)
}

assert.Equal(t, c.Errors.Errors(), []string{"externo 0", "externo 1", "interno 0", "externo 2", "interno 1", "interno 2"})
}

func TestContextFail(t *testing.T) {
c, w, _ := createTestContext()
c.Fail(401, errors.New("bad input"))
c.AbortWithError(401, errors.New("bad input"))
c.Writer.WriteHeaderNow()

assert.Equal(t, w.Code, 401)
Expand Down Expand Up @@ -434,7 +434,7 @@ func TestContextAutoBind(t *testing.T) {
Foo string `json:"foo"`
Bar string `json:"bar"`
}
assert.True(t, c.Bind(&obj))
assert.NoError(t, c.Bind(&obj))
assert.Equal(t, obj.Bar, "foo")
assert.Equal(t, obj.Foo, "bar")
assert.Equal(t, w.Body.Len(), 0)
Expand All @@ -450,7 +450,7 @@ func TestContextBadAutoBind(t *testing.T) {
}

assert.False(t, c.IsAborted())
assert.False(t, c.Bind(&obj))
assert.Error(t, c.Bind(&obj))
c.Writer.WriteHeaderNow()

assert.Empty(t, obj.Bar)
Expand All @@ -467,7 +467,7 @@ func TestContextBindWith(t *testing.T) {
Foo string `json:"foo"`
Bar string `json:"bar"`
}
assert.True(t, c.BindWith(&obj, binding.JSON))
assert.NoError(t, c.BindWith(&obj, binding.JSON))
assert.Equal(t, obj.Bar, "foo")
assert.Equal(t, obj.Foo, "bar")
assert.Equal(t, w.Body.Len(), 0)
Expand Down
30 changes: 22 additions & 8 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,33 @@ import (
)

const (
ErrorTypeInternal = 1 << iota
ErrorTypeExternal = 1 << iota
ErrorTypeAny = 0xffffffff
ErrorTypeBind = 1 << 31
ErrorTypeRender = 1 << 30
ErrorTypePrivate = 1 << 0
ErrorTypePublic = 1 << 1

ErrorTypeAny = 0xffffffff
ErrorTypeNu = 2
)

// Used internally to collect errors that occurred during an http request.
type errorMsg struct {
Error error `json:"error"`
Flags int `json:"-"`
Meta interface{} `json:"meta"`
Error error `json:"error"`
Flags int `json:"-"`
Metadata interface{} `json:"meta"`
}

func (msg *errorMsg) Type(flags int) *errorMsg {
msg.Flags = flags
return msg
}

func (msg *errorMsg) Meta(data interface{}) *errorMsg {
msg.Metadata = data
return msg
}

type errorMsgs []errorMsg
type errorMsgs []*errorMsg

func (a errorMsgs) ByType(typ int) errorMsgs {
if len(a) == 0 {
Expand Down Expand Up @@ -54,7 +68,7 @@ func (a errorMsgs) String() string {
}
var buffer bytes.Buffer
for i, msg := range a {
fmt.Fprintf(&buffer, "Error #%02d: %s\n Meta: %v\n", (i + 1), msg.Error, msg.Meta)
fmt.Fprintf(&buffer, "Error #%02d: %s\n Meta: %v\n", (i + 1), msg.Error, msg.Metadata)
}
return buffer.String()
}
2 changes: 1 addition & 1 deletion middleware_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ func TestMiddlewareFailHandlersChain(t *testing.T) {
router := New()
router.Use(func(context *Context) {
signature += "A"
context.Fail(500, errors.New("foo"))
context.AbortWithError(500, errors.New("foo"))
})
router.Use(func(context *Context) {
signature += "B"
Expand Down

0 comments on commit e94247f

Please sign in to comment.