gin/context.go

364 lines
9.8 KiB
Go
Raw Normal View History

2014-08-29 17:49:50 +00:00
// Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
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 {
writermem responseWriter
Request *http.Request
Writer ResponseWriter
Keys map[string]interface{}
Errors errorMsgs
Params httprouter.Params
Engine *Engine
handlers []HandlerFunc
index int8
accepted []string
}
/************************************/
2014-10-08 19:37:26 +00:00
/********** CONTEXT CREATION ********/
/************************************/
func (engine *Engine) createContext(w http.ResponseWriter, req *http.Request, params httprouter.Params, handlers []HandlerFunc) *Context {
2014-10-08 19:37:26 +00:00
c := engine.pool.Get().(*Context)
c.writermem.reset(w)
c.Request = req
c.Params = params
c.handlers = handlers
c.Keys = nil
c.index = -1
2014-08-31 16:28:18 +00:00
c.accepted = nil
2014-07-17 22:29:20 +00:00
c.Errors = c.Errors[0:0]
return c
}
2014-10-08 19:37:26 +00:00
func (engine *Engine) reuseContext(c *Context) {
engine.pool.Put(c)
}
func (c *Context) Copy() *Context {
var cp Context = *c
cp.index = AbortIndex
cp.handlers = nil
return &cp
}
2014-10-08 19:37:26 +00:00
/************************************/
/*************** FLOW ***************/
/************************************/
// 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)
}
}
2014-10-08 19:37:26 +00:00
// Forces the system to do not continue calling the pending handlers in the chain.
func (c *Context) Abort() {
c.index = AbortIndex
}
2014-10-08 19:37:26 +00:00
// Same than AbortWithStatus() but also writes the specified response status code.
// For example, the first handler checks if the request is authorized. If it's not, context.AbortWithStatus(401) should be called.
func (c *Context) AbortWithStatus(code int) {
c.Writer.WriteHeader(code)
c.Abort()
}
/************************************/
/********* 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)
2014-10-08 19:37:26 +00:00
// context.AbortWithStatus(500)
// ```
func (c *Context) Fail(code int, err error) {
c.Error(err, "Operation aborted")
2014-10-08 19:37:26 +00:00
c.AbortWithStatus(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 {
2014-10-08 19:37:26 +00:00
nuErrors := len(c.Errors)
if nuErrors > 0 {
return errors.New(c.Errors[nuErrors-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 {
2014-10-08 19:37:26 +00:00
value, ok := c.Keys[key]
if ok {
2014-10-08 19:37:26 +00:00
return value, 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 {
2014-10-08 19:37:26 +00:00
log.Panicf("Key %s doesn't exist", value)
}
return value
}
2014-10-08 23:40:42 +00:00
func (c *Context) ClientIP() string {
clientIP := c.Request.Header.Get("X-Real-IP")
if len(clientIP) == 0 {
clientIP = c.Request.Header.Get("X-Forwarded-For")
}
if len(clientIP) == 0 {
clientIP = c.Request.RemoteAddr
}
return clientIP
}
/************************************/
2014-10-08 19:37:26 +00:00
/********* PARSING REQUEST **********/
/************************************/
// 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
}
2014-10-08 19:37:26 +00:00
/************************************/
/******** RESPONSE RENDERING ********/
/************************************/
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)
2014-10-08 19:37:26 +00:00
c.AbortWithStatus(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)
}
2014-07-28 22:51:34 +00:00
// Returns a HTTP redirect to the specific location.
func (c *Context) Redirect(code int, location string) {
if code >= 300 && code <= 308 {
c.Render(code, render.Redirect, location)
} else {
panic(fmt.Sprintf("Cannot send a redirect with status code %d", code))
}
2014-07-28 22:48:02 +00:00
}
// 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)
}
2014-10-08 19:37:26 +00:00
c.Writer.WriteHeader(code)
c.Writer.Write(data)
}
2014-07-17 00:01:42 +00:00
// Writes the specified file into the body stream
func (c *Context) File(filepath string) {
http.ServeFile(c.Writer, c.Request, filepath)
}
/************************************/
/******** CONTENT NEGOTIATION *******/
/************************************/
2014-08-31 16:28:18 +00:00
type Negotiate struct {
Offered []string
HTMLPath string
2014-08-31 16:28:18 +00:00
HTMLData interface{}
JSONData interface{}
XMLData interface{}
Data interface{}
}
2014-08-31 16:28:18 +00:00
func (c *Context) Negotiate(code int, config Negotiate) {
2014-08-31 16:41:11 +00:00
switch c.NegotiateFormat(config.Offered...) {
case MIMEJSON:
2014-08-31 16:28:18 +00:00
data := chooseData(config.JSONData, config.Data)
c.JSON(code, data)
case MIMEHTML:
2014-08-31 16:28:18 +00:00
data := chooseData(config.HTMLData, config.Data)
if len(config.HTMLPath) == 0 {
panic("negotiate config is wrong. html path is needed")
}
c.HTML(code, config.HTMLPath, data)
case MIMEXML:
2014-08-31 16:28:18 +00:00
data := chooseData(config.XMLData, config.Data)
c.XML(code, data)
2014-08-31 16:28:18 +00:00
default:
2014-08-31 16:41:11 +00:00
c.Fail(http.StatusNotAcceptable, errors.New("the accepted formats are not offered by the server"))
}
}
func (c *Context) NegotiateFormat(offered ...string) string {
2014-08-31 16:41:11 +00:00
if len(offered) == 0 {
panic("you must provide at least one offer")
}
if c.accepted == nil {
c.accepted = parseAccept(c.Request.Header.Get("Accept"))
}
if len(c.accepted) == 0 {
return offered[0]
} else {
for _, accepted := range c.accepted {
for _, offert := range offered {
if accepted == offert {
return offert
}
}
}
return ""
}
}
func (c *Context) SetAccepted(formats ...string) {
c.accepted = formats
}