You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
306 lines
7.7 KiB
306 lines
7.7 KiB
package blademaster |
|
|
|
import ( |
|
"context" |
|
"math" |
|
"net/http" |
|
"strconv" |
|
|
|
"go-common/library/ecode" |
|
"go-common/library/net/http/blademaster/binding" |
|
"go-common/library/net/http/blademaster/render" |
|
|
|
"github.com/gogo/protobuf/proto" |
|
"github.com/gogo/protobuf/types" |
|
"github.com/pkg/errors" |
|
) |
|
|
|
const ( |
|
_abortIndex int8 = math.MaxInt8 / 2 |
|
) |
|
|
|
var ( |
|
_openParen = []byte("(") |
|
_closeParen = []byte(")") |
|
) |
|
|
|
// Context is the most important part. 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 { |
|
context.Context |
|
|
|
Request *http.Request |
|
Writer http.ResponseWriter |
|
|
|
// flow control |
|
index int8 |
|
handlers []HandlerFunc |
|
|
|
// Keys is a key/value pair exclusively for the context of each request. |
|
Keys map[string]interface{} |
|
|
|
Error error |
|
|
|
method string |
|
engine *Engine |
|
} |
|
|
|
/************************************/ |
|
/*********** FLOW CONTROL ***********/ |
|
/************************************/ |
|
|
|
// Next should be used only inside middleware. |
|
// It executes the pending handlers in the chain inside the calling handler. |
|
// See example in godoc. |
|
func (c *Context) Next() { |
|
c.index++ |
|
s := int8(len(c.handlers)) |
|
for ; c.index < s; c.index++ { |
|
// only check method on last handler, otherwise middlewares |
|
// will never be effected if request method is not matched |
|
if c.index == s-1 && c.method != c.Request.Method { |
|
code := http.StatusMethodNotAllowed |
|
c.Error = ecode.MethodNotAllowed |
|
http.Error(c.Writer, http.StatusText(code), code) |
|
return |
|
} |
|
|
|
c.handlers[c.index](c) |
|
} |
|
} |
|
|
|
// Abort prevents pending handlers from being called. Note that this will not stop the current handler. |
|
// Let's say you have an authorization middleware that validates that the current request is authorized. |
|
// If the authorization fails (ex: the password does not match), call Abort to ensure the remaining handlers |
|
// for this request are not called. |
|
func (c *Context) Abort() { |
|
c.index = _abortIndex |
|
} |
|
|
|
// AbortWithStatus calls `Abort()` and writes the headers with the specified status code. |
|
// For example, a failed attempt to authenticate a request could use: context.AbortWithStatus(401). |
|
func (c *Context) AbortWithStatus(code int) { |
|
c.Status(code) |
|
c.Abort() |
|
} |
|
|
|
// IsAborted returns true if the current context was aborted. |
|
func (c *Context) IsAborted() bool { |
|
return c.index >= _abortIndex |
|
} |
|
|
|
/************************************/ |
|
/******** METADATA MANAGEMENT********/ |
|
/************************************/ |
|
|
|
// Set is used to store a new key/value pair exclusively for this context. |
|
// It also lazy initializes c.Keys if it was not used previously. |
|
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, ie: (value, true). |
|
// If the value does not exists it returns (nil, false) |
|
func (c *Context) Get(key string) (value interface{}, exists bool) { |
|
value, exists = c.Keys[key] |
|
return |
|
} |
|
|
|
/************************************/ |
|
/******** RESPONSE RENDERING ********/ |
|
/************************************/ |
|
|
|
// bodyAllowedForStatus is a copy of http.bodyAllowedForStatus non-exported function. |
|
func bodyAllowedForStatus(status int) bool { |
|
switch { |
|
case status >= 100 && status <= 199: |
|
return false |
|
case status == 204: |
|
return false |
|
case status == 304: |
|
return false |
|
} |
|
return true |
|
} |
|
|
|
// Status sets the HTTP response code. |
|
func (c *Context) Status(code int) { |
|
c.Writer.WriteHeader(code) |
|
} |
|
|
|
// Render http response with http code by a render instance. |
|
func (c *Context) Render(code int, r render.Render) { |
|
r.WriteContentType(c.Writer) |
|
if code > 0 { |
|
c.Status(code) |
|
} |
|
|
|
if !bodyAllowedForStatus(code) { |
|
return |
|
} |
|
|
|
params := c.Request.Form |
|
|
|
cb := params.Get("callback") |
|
jsonp := cb != "" && params.Get("jsonp") == "jsonp" |
|
if jsonp { |
|
c.Writer.Write([]byte(cb)) |
|
c.Writer.Write(_openParen) |
|
} |
|
|
|
if err := r.Render(c.Writer); err != nil { |
|
c.Error = err |
|
return |
|
} |
|
|
|
if jsonp { |
|
if _, err := c.Writer.Write(_closeParen); err != nil { |
|
c.Error = errors.WithStack(err) |
|
} |
|
} |
|
} |
|
|
|
// JSON serializes the given struct as JSON into the response body. |
|
// It also sets the Content-Type as "application/json". |
|
func (c *Context) JSON(data interface{}, err error) { |
|
code := http.StatusOK |
|
c.Error = err |
|
bcode := ecode.Cause(err) |
|
// TODO app allow 5xx? |
|
/* |
|
if bcode.Code() == -500 { |
|
code = http.StatusServiceUnavailable |
|
} |
|
*/ |
|
writeStatusCode(c.Writer, bcode.Code()) |
|
c.Render(code, render.JSON{ |
|
Code: bcode.Code(), |
|
Message: bcode.Message(), |
|
Data: data, |
|
}) |
|
} |
|
|
|
// JSONMap serializes the given map as map JSON into the response body. |
|
// It also sets the Content-Type as "application/json". |
|
func (c *Context) JSONMap(data map[string]interface{}, err error) { |
|
code := http.StatusOK |
|
c.Error = err |
|
bcode := ecode.Cause(err) |
|
// TODO app allow 5xx? |
|
/* |
|
if bcode.Code() == -500 { |
|
code = http.StatusServiceUnavailable |
|
} |
|
*/ |
|
writeStatusCode(c.Writer, bcode.Code()) |
|
data["code"] = bcode.Code() |
|
if _, ok := data["message"]; !ok { |
|
data["message"] = bcode.Message() |
|
} |
|
c.Render(code, render.MapJSON(data)) |
|
} |
|
|
|
// XML serializes the given struct as XML into the response body. |
|
// It also sets the Content-Type as "application/xml". |
|
func (c *Context) XML(data interface{}, err error) { |
|
code := http.StatusOK |
|
c.Error = err |
|
bcode := ecode.Cause(err) |
|
// TODO app allow 5xx? |
|
/* |
|
if bcode.Code() == -500 { |
|
code = http.StatusServiceUnavailable |
|
} |
|
*/ |
|
writeStatusCode(c.Writer, bcode.Code()) |
|
c.Render(code, render.XML{ |
|
Code: bcode.Code(), |
|
Message: bcode.Message(), |
|
Data: data, |
|
}) |
|
} |
|
|
|
// Protobuf serializes the given struct as PB into the response body. |
|
// It also sets the ContentType as "application/x-protobuf". |
|
func (c *Context) Protobuf(data proto.Message, err error) { |
|
var ( |
|
bytes []byte |
|
) |
|
|
|
code := http.StatusOK |
|
c.Error = err |
|
bcode := ecode.Cause(err) |
|
|
|
any := new(types.Any) |
|
if data != nil { |
|
if bytes, err = proto.Marshal(data); err != nil { |
|
c.Error = errors.WithStack(err) |
|
return |
|
} |
|
any.TypeUrl = "type.googleapis.com/" + proto.MessageName(data) |
|
any.Value = bytes |
|
} |
|
writeStatusCode(c.Writer, bcode.Code()) |
|
c.Render(code, render.PB{ |
|
Code: int64(bcode.Code()), |
|
Message: bcode.Message(), |
|
Data: any, |
|
}) |
|
} |
|
|
|
// Bytes writes some data into the body stream and updates the HTTP code. |
|
func (c *Context) Bytes(code int, contentType string, data ...[]byte) { |
|
c.Render(code, render.Data{ |
|
ContentType: contentType, |
|
Data: data, |
|
}) |
|
} |
|
|
|
// String writes the given string into the response body. |
|
func (c *Context) String(code int, format string, values ...interface{}) { |
|
c.Render(code, render.String{Format: format, Data: values}) |
|
} |
|
|
|
// Redirect returns a HTTP redirect to the specific location. |
|
func (c *Context) Redirect(code int, location string) { |
|
c.Render(-1, render.Redirect{ |
|
Code: code, |
|
Location: location, |
|
Request: c.Request, |
|
}) |
|
} |
|
|
|
// BindWith bind req arg with parser. |
|
func (c *Context) BindWith(obj interface{}, b binding.Binding) error { |
|
return c.mustBindWith(obj, b) |
|
} |
|
|
|
// Bind bind req arg with defult form binding. |
|
func (c *Context) Bind(obj interface{}) error { |
|
return c.mustBindWith(obj, binding.Form) |
|
} |
|
|
|
// mustBindWith binds the passed struct pointer using the specified binding engine. |
|
// It will abort the request with HTTP 400 if any error ocurrs. |
|
// See the binding package. |
|
func (c *Context) mustBindWith(obj interface{}, b binding.Binding) (err error) { |
|
if err = b.Bind(c.Request, obj); err != nil { |
|
c.Error = ecode.RequestErr |
|
c.Render(http.StatusOK, render.JSON{ |
|
Code: ecode.RequestErr.Code(), |
|
Message: err.Error(), |
|
Data: nil, |
|
}) |
|
c.Abort() |
|
} |
|
return |
|
} |
|
|
|
func writeStatusCode(w http.ResponseWriter, ecode int) { |
|
header := w.Header() |
|
header.Set("bili-status-code", strconv.FormatInt(int64(ecode), 10)) |
|
}
|
|
|