You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
229 lines
6.5 KiB
Go
229 lines
6.5 KiB
Go
package render
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"encoding/xml"
|
|
"fmt"
|
|
"net/http"
|
|
"reflect"
|
|
)
|
|
|
|
// M is a convenience alias for quickly building a map structure that is going
|
|
// out to a responder. Just a short-hand.
|
|
type M map[string]interface{}
|
|
|
|
// Respond is a package-level variable set to our default Responder. We do this
|
|
// because it allows you to set render.Respond to another function with the
|
|
// same function signature, while also utilizing the render.Responder() function
|
|
// itself. Effectively, allowing you to easily add your own logic to the package
|
|
// defaults. For example, maybe you want to test if v is an error and respond
|
|
// differently, or log something before you respond.
|
|
var Respond = DefaultResponder
|
|
|
|
// StatusCtxKey is a context key to record a future HTTP response status code.
|
|
var StatusCtxKey = &contextKey{"Status"}
|
|
|
|
// Status sets a HTTP response status code hint into request context at any point
|
|
// during the request life-cycle. Before the Responder sends its response header
|
|
// it will check the StatusCtxKey
|
|
func Status(r *http.Request, status int) {
|
|
*r = *r.WithContext(context.WithValue(r.Context(), StatusCtxKey, status))
|
|
}
|
|
|
|
// Respond handles streaming JSON and XML responses, automatically setting the
|
|
// Content-Type based on request headers. It will default to a JSON response.
|
|
func DefaultResponder(w http.ResponseWriter, r *http.Request, v interface{}) {
|
|
if v != nil {
|
|
switch reflect.TypeOf(v).Kind() {
|
|
case reflect.Chan:
|
|
switch GetAcceptedContentType(r) {
|
|
case ContentTypeEventStream:
|
|
channelEventStream(w, r, v)
|
|
return
|
|
default:
|
|
v = channelIntoSlice(w, r, v)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Format response based on request Accept header.
|
|
switch GetAcceptedContentType(r) {
|
|
case ContentTypeJSON:
|
|
JSON(w, r, v)
|
|
case ContentTypeXML:
|
|
XML(w, r, v)
|
|
default:
|
|
JSON(w, r, v)
|
|
}
|
|
}
|
|
|
|
// PlainText writes a string to the response, setting the Content-Type as
|
|
// text/plain.
|
|
func PlainText(w http.ResponseWriter, r *http.Request, v string) {
|
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
|
if status, ok := r.Context().Value(StatusCtxKey).(int); ok {
|
|
w.WriteHeader(status)
|
|
}
|
|
w.Write([]byte(v))
|
|
}
|
|
|
|
// Data writes raw bytes to the response, setting the Content-Type as
|
|
// application/octet-stream.
|
|
func Data(w http.ResponseWriter, r *http.Request, v []byte) {
|
|
w.Header().Set("Content-Type", "application/octet-stream")
|
|
if status, ok := r.Context().Value(StatusCtxKey).(int); ok {
|
|
w.WriteHeader(status)
|
|
}
|
|
w.Write(v)
|
|
}
|
|
|
|
// HTML writes a string to the response, setting the Content-Type as text/html.
|
|
func HTML(w http.ResponseWriter, r *http.Request, v string) {
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
if status, ok := r.Context().Value(StatusCtxKey).(int); ok {
|
|
w.WriteHeader(status)
|
|
}
|
|
w.Write([]byte(v))
|
|
}
|
|
|
|
// JSON marshals 'v' to JSON, automatically escaping HTML and setting the
|
|
// Content-Type as application/json.
|
|
func JSON(w http.ResponseWriter, r *http.Request, v interface{}) {
|
|
buf := &bytes.Buffer{}
|
|
enc := json.NewEncoder(buf)
|
|
enc.SetEscapeHTML(true)
|
|
if err := enc.Encode(v); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if status, ok := r.Context().Value(StatusCtxKey).(int); ok {
|
|
w.WriteHeader(status)
|
|
}
|
|
w.Write(buf.Bytes())
|
|
}
|
|
|
|
// XML marshals 'v' to JSON, setting the Content-Type as application/xml. It
|
|
// will automatically prepend a generic XML header (see encoding/xml.Header) if
|
|
// one is not found in the first 100 bytes of 'v'.
|
|
func XML(w http.ResponseWriter, r *http.Request, v interface{}) {
|
|
b, err := xml.Marshal(v)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/xml; charset=utf-8")
|
|
if status, ok := r.Context().Value(StatusCtxKey).(int); ok {
|
|
w.WriteHeader(status)
|
|
}
|
|
|
|
// Try to find <?xml header in first 100 bytes (just in case there're some XML comments).
|
|
findHeaderUntil := len(b)
|
|
if findHeaderUntil > 100 {
|
|
findHeaderUntil = 100
|
|
}
|
|
if !bytes.Contains(b[:findHeaderUntil], []byte("<?xml")) {
|
|
// No header found. Print it out first.
|
|
w.Write([]byte(xml.Header))
|
|
}
|
|
|
|
w.Write(b)
|
|
}
|
|
|
|
// NoContent returns a HTTP 204 "No Content" response.
|
|
func NoContent(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(204)
|
|
}
|
|
|
|
func channelEventStream(w http.ResponseWriter, r *http.Request, v interface{}) {
|
|
if reflect.TypeOf(v).Kind() != reflect.Chan {
|
|
panic(fmt.Sprintf("render: event stream expects a channel, not %v", reflect.TypeOf(v).Kind()))
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "text/event-stream; charset=utf-8")
|
|
w.Header().Set("Cache-Control", "no-cache")
|
|
w.Header().Set("Connection", "keep-alive")
|
|
w.WriteHeader(200)
|
|
|
|
ctx := r.Context()
|
|
for {
|
|
switch chosen, recv, ok := reflect.Select([]reflect.SelectCase{
|
|
{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(ctx.Done())},
|
|
{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(v)},
|
|
}); chosen {
|
|
case 0: // equivalent to: case <-ctx.Done()
|
|
w.Write([]byte("event: error\ndata: {\"error\":\"Server Timeout\"}\n\n"))
|
|
return
|
|
|
|
default: // equivalent to: case v, ok := <-stream
|
|
if !ok {
|
|
w.Write([]byte("event: EOF\n\n"))
|
|
return
|
|
}
|
|
v := recv.Interface()
|
|
|
|
// Build each channel item.
|
|
if rv, ok := v.(Renderer); ok {
|
|
err := renderer(w, r, rv)
|
|
if err != nil {
|
|
v = err
|
|
} else {
|
|
v = rv
|
|
}
|
|
}
|
|
|
|
bytes, err := json.Marshal(v)
|
|
if err != nil {
|
|
w.Write([]byte(fmt.Sprintf("event: error\ndata: {\"error\":\"%v\"}\n\n", err)))
|
|
if f, ok := w.(http.Flusher); ok {
|
|
f.Flush()
|
|
}
|
|
continue
|
|
}
|
|
w.Write([]byte(fmt.Sprintf("event: data\ndata: %s\n\n", bytes)))
|
|
if f, ok := w.(http.Flusher); ok {
|
|
f.Flush()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// channelIntoSlice buffers channel data into a slice.
|
|
func channelIntoSlice(w http.ResponseWriter, r *http.Request, from interface{}) interface{} {
|
|
ctx := r.Context()
|
|
|
|
var to []interface{}
|
|
for {
|
|
switch chosen, recv, ok := reflect.Select([]reflect.SelectCase{
|
|
{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(ctx.Done())},
|
|
{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(from)},
|
|
}); chosen {
|
|
case 0: // equivalent to: case <-ctx.Done()
|
|
http.Error(w, "Server Timeout", 504)
|
|
return nil
|
|
|
|
default: // equivalent to: case v, ok := <-stream
|
|
if !ok {
|
|
return to
|
|
}
|
|
v := recv.Interface()
|
|
|
|
// Render each channel item.
|
|
if rv, ok := v.(Renderer); ok {
|
|
err := renderer(w, r, rv)
|
|
if err != nil {
|
|
v = err
|
|
} else {
|
|
v = rv
|
|
}
|
|
}
|
|
|
|
to = append(to, v)
|
|
}
|
|
}
|
|
}
|