diff --git a/context.go b/context.go new file mode 100644 index 00000000..004bee2c --- /dev/null +++ b/context.go @@ -0,0 +1,257 @@ +package gin + +import ( + "bytes" + "errors" + "fmt" + "github.com/gin-gonic/gin/binding" + "github.com/gin-gonic/gin/render" + "github.com/julienschmidt/httprouter" + "log" + "net/http" +) + +const ( + ErrorTypeInternal = 1 << iota + ErrorTypeExternal = 1 << iota + ErrorTypeAll = 0xffffffff +) + +// Used internally to collect errors that occurred during an http request. +type errorMsg struct { + Err string `json:"error"` + Type uint32 `json:"-"` + Meta interface{} `json:"meta"` +} + +type errorMsgs []errorMsg + +func (a errorMsgs) ByType(typ uint32) errorMsgs { + if len(a) == 0 { + return a + } + result := make(errorMsgs, 0, len(a)) + for _, msg := range a { + if msg.Type&typ > 0 { + result = append(result, msg) + } + } + return result +} + +func (a errorMsgs) String() string { + if len(a) == 0 { + return "" + } + var buffer bytes.Buffer + for i, msg := range a { + text := fmt.Sprintf("Error #%02d: %s \n Meta: %v\n", (i + 1), msg.Err, msg.Meta) + buffer.WriteString(text) + } + return buffer.String() +} + +// Context is the most important part of gin. It allows us to pass variables between middleware, +// manage the flow, validate the JSON of a request and render a JSON response for example. +type Context struct { + Request *http.Request + Writer ResponseWriter + Keys map[string]interface{} + Errors errorMsgs + Params httprouter.Params + Engine *Engine + handlers []HandlerFunc + index int8 +} + +/************************************/ +/********** ROUTES GROUPING *********/ +/************************************/ + +func (engine *Engine) createContext(w http.ResponseWriter, req *http.Request, params httprouter.Params, handlers []HandlerFunc) *Context { + c := engine.cache.Get().(*Context) + c.Writer.reset(w) + c.Request = req + c.Params = params + c.handlers = handlers + c.Keys = nil + c.index = -1 + return c +} + +/************************************/ +/****** FLOW AND ERROR MANAGEMENT****/ +/************************************/ + +func (c *Context) Copy() *Context { + var cp Context = *c + cp.index = AbortIndex + cp.handlers = nil + return &cp +} + +// Next should be used only in the middlewares. +// It executes the pending handlers in the chain inside the calling handler. +// See example in github. +func (c *Context) Next() { + c.index++ + s := int8(len(c.handlers)) + for ; c.index < s; c.index++ { + c.handlers[c.index](c) + } +} + +// Forces the system to do not continue calling the pending handlers. +// For example, the first handler checks if the request is authorized. If it's not, context.Abort(401) should be called. +// The rest of pending handlers would never be called for that request. +func (c *Context) Abort(code int) { + if code >= 0 { + c.Writer.WriteHeader(code) + } + c.index = AbortIndex +} + +// Fail is the same as Abort plus an error message. +// Calling `context.Fail(500, err)` is equivalent to: +// ``` +// context.Error("Operation aborted", err) +// context.Abort(500) +// ``` +func (c *Context) Fail(code int, err error) { + c.Error(err, "Operation aborted") + c.Abort(code) +} + +func (c *Context) ErrorTyped(err error, typ uint32, meta interface{}) { + c.Errors = append(c.Errors, errorMsg{ + Err: err.Error(), + Type: 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) LastError() error { + s := len(c.Errors) + if s > 0 { + return errors.New(c.Errors[s-1].Err) + } else { + 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, item interface{}) { + if c.Keys == nil { + c.Keys = make(map[string]interface{}) + } + c.Keys[key] = item +} + +// Get returns the value for the given key or an error if the key does not exist. +func (c *Context) Get(key string) (interface{}, error) { + if c.Keys != nil { + item, ok := c.Keys[key] + if ok { + return item, nil + } + } + return nil, errors.New("Key does not exist.") +} + +// MustGet returns the value for the given key or panics if the value doesn't exist. +func (c *Context) MustGet(key string) interface{} { + value, err := c.Get(key) + if err != nil || value == nil { + log.Panicf("Key %s doesn't exist", key) + } + return value +} + +/************************************/ +/******** ENCOGING MANAGEMENT********/ +/************************************/ + +// 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 { + var b binding.Binding + ctype := filterFlags(c.Request.Header.Get("Content-Type")) + switch { + case c.Request.Method == "GET" || ctype == MIMEPOSTForm: + b = binding.Form + case ctype == MIMEJSON: + b = binding.JSON + case ctype == MIMEXML || ctype == MIMEXML2: + b = binding.XML + default: + c.Fail(400, errors.New("unknown content-type: "+ctype)) + return false + } + 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 +} + +func (c *Context) Render(code int, render render.Render, obj ...interface{}) { + if err := render.Render(c.Writer, code, obj...); err != nil { + c.ErrorTyped(err, ErrorTypeInternal, obj) + c.Abort(500) + } +} + +// Serializes the given struct as JSON into the response body in a fast and efficient way. +// It also sets the Content-Type as "application/json". +func (c *Context) JSON(code int, obj interface{}) { + c.Render(code, render.JSON, obj) +} + +// Serializes the given struct as XML into the response body in a fast and efficient way. +// It also sets the Content-Type as "application/xml". +func (c *Context) XML(code int, obj interface{}) { + c.Render(code, render.XML, obj) +} + +// Renders the HTTP template specified by its file name. +// It also updates the HTTP code and sets the Content-Type as "text/html". +// See http://golang.org/doc/articles/wiki/ +func (c *Context) HTML(code int, name string, obj interface{}) { + c.Render(code, c.Engine.HTMLRender, name, obj) +} + +// Writes the given string into the response body and sets the Content-Type to "text/plain". +func (c *Context) String(code int, format string, values ...interface{}) { + c.Render(code, render.Plain, format, values) +} + +// Writes some data into the body stream and updates the HTTP code. +func (c *Context) Data(code int, contentType string, data []byte) { + if len(contentType) > 0 { + c.Writer.Header().Set("Content-Type", contentType) + } + if code >= 0 { + c.Writer.WriteHeader(code) + } + c.Writer.Write(data) +} diff --git a/gin.go b/gin.go index 4d768723..de7f86d1 100644 --- a/gin.go +++ b/gin.go @@ -1,15 +1,9 @@ package gin import ( - "bytes" - "encoding/xml" - "errors" - "fmt" - "github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/render" "github.com/julienschmidt/httprouter" "html/template" - "log" "math" "net/http" "path" @@ -26,39 +20,9 @@ const ( MIMEPOSTForm = "application/x-www-form-urlencoded" ) -const ( - ErrorTypeInternal = 1 << iota - ErrorTypeExternal = 1 << iota - ErrorTypeAll = 0xffffffff -) - type ( HandlerFunc func(*Context) - H map[string]interface{} - - // Used internally to collect errors that occurred during an http request. - errorMsg struct { - Err string `json:"error"` - Type uint32 `json:"-"` - Meta interface{} `json:"meta"` - } - - errorMsgs []errorMsg - - // Context is the most important part of gin. It allows us to pass variables between middleware, - // manage the flow, validate the JSON of a request and render a JSON response for example. - Context struct { - Request *http.Request - Writer ResponseWriter - Keys map[string]interface{} - Errors errorMsgs - Params httprouter.Params - Engine *Engine - handlers []HandlerFunc - index int8 - } - // Used internally to configure router, a RouterGroup is associated with a prefix // and an array of handlers (middlewares) RouterGroup struct { @@ -78,55 +42,6 @@ type ( } ) -// Allows type H to be used with xml.Marshal -func (h H) MarshalXML(e *xml.Encoder, start xml.StartElement) error { - start.Name = xml.Name{ - Space: "", - Local: "map", - } - if err := e.EncodeToken(start); err != nil { - return err - } - for key, value := range h { - elem := xml.StartElement{ - Name: xml.Name{Space: "", Local: key}, - Attr: []xml.Attr{}, - } - if err := e.EncodeElement(value, elem); err != nil { - return err - } - } - if err := e.EncodeToken(xml.EndElement{Name: start.Name}); err != nil { - return err - } - return nil -} - -func (a errorMsgs) ByType(typ uint32) errorMsgs { - if len(a) == 0 { - return a - } - result := make(errorMsgs, 0, len(a)) - for _, msg := range a { - if msg.Type&typ > 0 { - result = append(result, msg) - } - } - return result -} - -func (a errorMsgs) String() string { - if len(a) == 0 { - return "" - } - var buffer bytes.Buffer - for i, msg := range a { - text := fmt.Sprintf("Error #%02d: %s \n Meta: %v\n", (i + 1), msg.Err, msg.Meta) - buffer.WriteString(text) - } - return buffer.String() -} - // Returns a new blank Engine instance without any middleware attached. // The most basic configuration func New() *Engine { @@ -200,32 +115,11 @@ func (engine *Engine) RunTLS(addr string, cert string, key string) { /********** ROUTES GROUPING *********/ /************************************/ -func (engine *Engine) createContext(w http.ResponseWriter, req *http.Request, params httprouter.Params, handlers []HandlerFunc) *Context { - c := engine.cache.Get().(*Context) - c.Writer.reset(w) - c.Request = req - c.Params = params - c.handlers = handlers - c.Keys = nil - c.index = -1 - return c -} - // Adds middlewares to the group, see example code in github. func (group *RouterGroup) Use(middlewares ...HandlerFunc) { group.Handlers = append(group.Handlers, middlewares...) } -func joinGroupPath(elems ...string) string { - joined := path.Join(elems...) - lastComponent := elems[len(elems)-1] - // Append a '/' if the last component had one, but only if it's not there already - if len(lastComponent) > 0 && lastComponent[len(lastComponent)-1] == '/' && joined[len(joined)-1] != '/' { - return joined + "/" - } - return joined -} - // Creates a new router group. You should add all the routes that have common middlwares or the same path prefix. // For example, all the routes that use a common middlware for authorization could be grouped. func (group *RouterGroup) Group(component string, handlers ...HandlerFunc) *RouterGroup { @@ -318,189 +212,3 @@ func (group *RouterGroup) combineHandlers(handlers []HandlerFunc) []HandlerFunc h = append(h, handlers...) return h } - -/************************************/ -/****** FLOW AND ERROR MANAGEMENT****/ -/************************************/ - -func (c *Context) Copy() *Context { - var cp Context = *c - cp.index = AbortIndex - cp.handlers = nil - return &cp -} - -// Next should be used only in the middlewares. -// It executes the pending handlers in the chain inside the calling handler. -// See example in github. -func (c *Context) Next() { - c.index++ - s := int8(len(c.handlers)) - for ; c.index < s; c.index++ { - c.handlers[c.index](c) - } -} - -// Forces the system to do not continue calling the pending handlers. -// For example, the first handler checks if the request is authorized. If it's not, context.Abort(401) should be called. -// The rest of pending handlers would never be called for that request. -func (c *Context) Abort(code int) { - if code >= 0 { - c.Writer.WriteHeader(code) - } - c.index = AbortIndex -} - -// Fail is the same as Abort plus an error message. -// Calling `context.Fail(500, err)` is equivalent to: -// ``` -// context.Error("Operation aborted", err) -// context.Abort(500) -// ``` -func (c *Context) Fail(code int, err error) { - c.Error(err, "Operation aborted") - c.Abort(code) -} - -func (c *Context) ErrorTyped(err error, typ uint32, meta interface{}) { - c.Errors = append(c.Errors, errorMsg{ - Err: err.Error(), - Type: 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) LastError() error { - s := len(c.Errors) - if s > 0 { - return errors.New(c.Errors[s-1].Err) - } else { - 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, item interface{}) { - if c.Keys == nil { - c.Keys = make(map[string]interface{}) - } - c.Keys[key] = item -} - -// Get returns the value for the given key or an error if the key does not exist. -func (c *Context) Get(key string) (interface{}, error) { - if c.Keys != nil { - item, ok := c.Keys[key] - if ok { - return item, nil - } - } - return nil, errors.New("Key does not exist.") -} - -// MustGet returns the value for the given key or panics if the value doesn't exist. -func (c *Context) MustGet(key string) interface{} { - value, err := c.Get(key) - if err != nil || value == nil { - log.Panicf("Key %s doesn't exist", key) - } - return value -} - -/************************************/ -/******** ENCOGING MANAGEMENT********/ -/************************************/ - -func filterFlags(content string) string { - for i, a := range content { - if a == ' ' || a == ';' { - return content[:i] - } - } - return content -} - -// 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 { - var b binding.Binding - ctype := filterFlags(c.Request.Header.Get("Content-Type")) - switch { - case c.Request.Method == "GET" || ctype == MIMEPOSTForm: - b = binding.Form - case ctype == MIMEJSON: - b = binding.JSON - case ctype == MIMEXML || ctype == MIMEXML2: - b = binding.XML - default: - c.Fail(400, errors.New("unknown content-type: "+ctype)) - return false - } - 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 -} - -func (c *Context) Render(code int, render render.Render, obj ...interface{}) { - if err := render.Render(c.Writer, code, obj...); err != nil { - c.ErrorTyped(err, ErrorTypeInternal, obj) - c.Abort(500) - } -} - -// Serializes the given struct as JSON into the response body in a fast and efficient way. -// It also sets the Content-Type as "application/json". -func (c *Context) JSON(code int, obj interface{}) { - c.Render(code, render.JSON, obj) -} - -// Serializes the given struct as XML into the response body in a fast and efficient way. -// It also sets the Content-Type as "application/xml". -func (c *Context) XML(code int, obj interface{}) { - c.Render(code, render.XML, obj) -} - -// Renders the HTTP template specified by its file name. -// It also updates the HTTP code and sets the Content-Type as "text/html". -// See http://golang.org/doc/articles/wiki/ -func (c *Context) HTML(code int, name string, obj interface{}) { - c.Render(code, c.Engine.HTMLRender, name, obj) -} - -// Writes the given string into the response body and sets the Content-Type to "text/plain". -func (c *Context) String(code int, format string, values ...interface{}) { - c.Render(code, render.Plain, format, values) -} - -// Writes some data into the body stream and updates the HTTP code. -func (c *Context) Data(code int, contentType string, data []byte) { - if len(contentType) > 0 { - c.Writer.Header().Set("Content-Type", contentType) - } - if code >= 0 { - c.Writer.WriteHeader(code) - } - c.Writer.Write(data) -} diff --git a/utils.go b/utils.go new file mode 100644 index 00000000..f6e30462 --- /dev/null +++ b/utils.go @@ -0,0 +1,51 @@ +package gin + +import ( + "encoding/xml" + "path" +) + +type H map[string]interface{} + +// Allows type H to be used with xml.Marshal +func (h H) MarshalXML(e *xml.Encoder, start xml.StartElement) error { + start.Name = xml.Name{ + Space: "", + Local: "map", + } + if err := e.EncodeToken(start); err != nil { + return err + } + for key, value := range h { + elem := xml.StartElement{ + Name: xml.Name{Space: "", Local: key}, + Attr: []xml.Attr{}, + } + if err := e.EncodeElement(value, elem); err != nil { + return err + } + } + if err := e.EncodeToken(xml.EndElement{Name: start.Name}); err != nil { + return err + } + return nil +} + +func joinGroupPath(elems ...string) string { + joined := path.Join(elems...) + lastComponent := elems[len(elems)-1] + // Append a '/' if the last component had one, but only if it's not there already + if len(lastComponent) > 0 && lastComponent[len(lastComponent)-1] == '/' && joined[len(joined)-1] != '/' { + return joined + "/" + } + return joined +} + +func filterFlags(content string) string { + for i, a := range content { + if a == ' ' || a == ';' { + return content[:i] + } + } + return content +}