fixing up dockerfile for build and added missing vendor
parent
3fe7bdb762
commit
e2ca326b66
@ -0,0 +1,20 @@
|
||||
Copyright (c) 2016-Present https://github.com/go-chi authors
|
||||
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
@ -0,0 +1,24 @@
|
||||
# render
|
||||
|
||||
The `render` package helps manage HTTP request / response payloads.
|
||||
|
||||
Every well-designed, robust and maintainable Web Service / REST API also needs
|
||||
well-*defined* request and response payloads. Together with the endpoint handlers,
|
||||
the request and response payloads make up the contract between your server and the
|
||||
clients calling on it.
|
||||
|
||||
Typically in a REST API application, you will have your data models (objects/structs)
|
||||
that hold lower-level runtime application state, and at times you need to assemble,
|
||||
decorate, hide or transform the representation before responding to a client. That
|
||||
server output (response payload) structure, is also likely the input structure to
|
||||
another handler on the server.
|
||||
|
||||
This is where `render` comes in - offering a few simple helpers and interfaces to
|
||||
provide a simple pattern for managing payload encoding and decoding.
|
||||
|
||||
We've also combined it with some helpers for responding to content types and parsing
|
||||
request bodies. Please have a look at the [rest](https://github.com/go-chi/chi/blob/master/_examples/rest/main.go)
|
||||
example which uses the latest chi/render sub-pkg.
|
||||
|
||||
All feedback is welcome, thank you!
|
||||
|
@ -0,0 +1,84 @@
|
||||
package render
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
ContentTypeCtxKey = &contextKey{"ContentType"}
|
||||
)
|
||||
|
||||
// ContentType is an enumeration of common HTTP content types.
|
||||
type ContentType int
|
||||
|
||||
// ContentTypes handled by this package.
|
||||
const (
|
||||
ContentTypeUnknown = iota
|
||||
ContentTypePlainText
|
||||
ContentTypeHTML
|
||||
ContentTypeJSON
|
||||
ContentTypeXML
|
||||
ContentTypeForm
|
||||
ContentTypeEventStream
|
||||
)
|
||||
|
||||
func GetContentType(s string) ContentType {
|
||||
s = strings.TrimSpace(strings.Split(s, ";")[0])
|
||||
switch s {
|
||||
case "text/plain":
|
||||
return ContentTypePlainText
|
||||
case "text/html", "application/xhtml+xml":
|
||||
return ContentTypeHTML
|
||||
case "application/json", "text/javascript":
|
||||
return ContentTypeJSON
|
||||
case "text/xml", "application/xml":
|
||||
return ContentTypeXML
|
||||
case "application/x-www-form-urlencoded":
|
||||
return ContentTypeForm
|
||||
case "text/event-stream":
|
||||
return ContentTypeEventStream
|
||||
default:
|
||||
return ContentTypeUnknown
|
||||
}
|
||||
}
|
||||
|
||||
// SetContentType is a middleware that forces response Content-Type.
|
||||
func SetContentType(contentType ContentType) func(next http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
r = r.WithContext(context.WithValue(r.Context(), ContentTypeCtxKey, contentType))
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
}
|
||||
|
||||
// GetRequestContentType is a helper function that returns ContentType based on
|
||||
// context or request headers.
|
||||
func GetRequestContentType(r *http.Request) ContentType {
|
||||
if contentType, ok := r.Context().Value(ContentTypeCtxKey).(ContentType); ok {
|
||||
return contentType
|
||||
}
|
||||
return GetContentType(r.Header.Get("Content-Type"))
|
||||
}
|
||||
|
||||
func GetAcceptedContentType(r *http.Request) ContentType {
|
||||
if contentType, ok := r.Context().Value(ContentTypeCtxKey).(ContentType); ok {
|
||||
return contentType
|
||||
}
|
||||
|
||||
var contentType ContentType
|
||||
|
||||
// Parse request Accept header.
|
||||
fields := strings.Split(r.Header.Get("Accept"), ",")
|
||||
if len(fields) > 0 {
|
||||
contentType = GetContentType(strings.TrimSpace(fields[0]))
|
||||
}
|
||||
|
||||
if contentType == ContentTypeUnknown {
|
||||
contentType = ContentTypePlainText
|
||||
}
|
||||
return contentType
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
package render
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Decode is a package-level variable set to our default Decoder. We do this
|
||||
// because it allows you to set render.Decode to another function with the
|
||||
// same function signature, while also utilizing the render.Decoder() function
|
||||
// itself. Effectively, allowing you to easily add your own logic to the package
|
||||
// defaults. For example, maybe you want to impose a limit on the number of
|
||||
// bytes allowed to be read from the request body.
|
||||
var Decode = DefaultDecoder
|
||||
|
||||
func DefaultDecoder(r *http.Request, v interface{}) error {
|
||||
var err error
|
||||
|
||||
switch GetRequestContentType(r) {
|
||||
case ContentTypeJSON:
|
||||
err = DecodeJSON(r.Body, v)
|
||||
case ContentTypeXML:
|
||||
err = DecodeXML(r.Body, v)
|
||||
// case ContentTypeForm: // TODO
|
||||
default:
|
||||
err = errors.New("render: unable to automatically decode the request content type")
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func DecodeJSON(r io.Reader, v interface{}) error {
|
||||
defer io.Copy(ioutil.Discard, r)
|
||||
return json.NewDecoder(r).Decode(v)
|
||||
}
|
||||
|
||||
func DecodeXML(r io.Reader, v interface{}) error {
|
||||
defer io.Copy(ioutil.Discard, r)
|
||||
return xml.NewDecoder(r).Decode(v)
|
||||
}
|
@ -0,0 +1,134 @@
|
||||
package render
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// Renderer interface for managing response payloads.
|
||||
type Renderer interface {
|
||||
Render(w http.ResponseWriter, r *http.Request) error
|
||||
}
|
||||
|
||||
// Binder interface for managing request payloads.
|
||||
type Binder interface {
|
||||
Bind(r *http.Request) error
|
||||
}
|
||||
|
||||
// Bind decodes a request body and executes the Binder method of the
|
||||
// payload structure.
|
||||
func Bind(r *http.Request, v Binder) error {
|
||||
if err := Decode(r, v); err != nil {
|
||||
return err
|
||||
}
|
||||
return binder(r, v)
|
||||
}
|
||||
|
||||
// Render renders a single payload and respond to the client request.
|
||||
func Render(w http.ResponseWriter, r *http.Request, v Renderer) error {
|
||||
if err := renderer(w, r, v); err != nil {
|
||||
return err
|
||||
}
|
||||
Respond(w, r, v)
|
||||
return nil
|
||||
}
|
||||
|
||||
// RenderList renders a slice of payloads and responds to the client request.
|
||||
func RenderList(w http.ResponseWriter, r *http.Request, l []Renderer) error {
|
||||
for _, v := range l {
|
||||
if err := renderer(w, r, v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
Respond(w, r, l)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Executed top-down
|
||||
func renderer(w http.ResponseWriter, r *http.Request, v Renderer) error {
|
||||
rv := reflect.ValueOf(v)
|
||||
if rv.Kind() == reflect.Ptr {
|
||||
rv = rv.Elem()
|
||||
}
|
||||
|
||||
// We call it top-down.
|
||||
if err := v.Render(w, r); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// We're done if the Renderer isn't a struct object
|
||||
if rv.Kind() != reflect.Struct {
|
||||
return nil
|
||||
}
|
||||
|
||||
// For structs, we call Render on each field that implements Renderer
|
||||
for i := 0; i < rv.NumField(); i++ {
|
||||
f := rv.Field(i)
|
||||
if f.Type().Implements(rendererType) {
|
||||
|
||||
if f.IsNil() {
|
||||
continue
|
||||
}
|
||||
|
||||
fv := f.Interface().(Renderer)
|
||||
if err := renderer(w, r, fv); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Executed bottom-up
|
||||
func binder(r *http.Request, v Binder) error {
|
||||
rv := reflect.ValueOf(v)
|
||||
if rv.Kind() == reflect.Ptr {
|
||||
rv = rv.Elem()
|
||||
}
|
||||
|
||||
// Call Binder on non-struct types right away
|
||||
if rv.Kind() != reflect.Struct {
|
||||
return v.Bind(r)
|
||||
}
|
||||
|
||||
// For structs, we call Bind on each field that implements Binder
|
||||
for i := 0; i < rv.NumField(); i++ {
|
||||
f := rv.Field(i)
|
||||
if f.Type().Implements(binderType) {
|
||||
|
||||
if f.IsNil() {
|
||||
continue
|
||||
}
|
||||
|
||||
fv := f.Interface().(Binder)
|
||||
if err := binder(r, fv); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We call it bottom-up
|
||||
if err := v.Bind(r); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
rendererType = reflect.TypeOf(new(Renderer)).Elem()
|
||||
binderType = reflect.TypeOf(new(Binder)).Elem()
|
||||
)
|
||||
|
||||
// contextKey is a value for use with context.WithValue. It's used as
|
||||
// a pointer so it fits in an interface{} without allocation. This technique
|
||||
// for defining context keys was copied from Go 1.7's new use of context in net/http.
|
||||
type contextKey struct {
|
||||
name string
|
||||
}
|
||||
|
||||
func (k *contextKey) String() string {
|
||||
return "chi render context value " + k.name
|
||||
}
|
@ -0,0 +1,228 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue