├── .travis.yml ├── LICENSE ├── README.md ├── decoder.go ├── content_type.go ├── render.go └── responder.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.7.x 5 | - 1.8.x 6 | - tip 7 | 8 | install: 9 | - go get -u golang.org/x/tools/cmd/goimports 10 | 11 | script: 12 | - go get -d -t ./... 13 | - go test ./... 14 | - > 15 | goimports -d -e ./ | grep '.*' && { echo; echo "Aborting due to non-empty goimports output."; exit 1; } || : 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016-Present https://github.com/go-chi authors 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # render 2 | 3 | The `render` package helps manage HTTP request / response payloads. 4 | 5 | Every well-designed, robust and maintainable Web Service / REST API also needs 6 | well-*defined* request and response payloads. Together with the endpoint handlers, 7 | the request and response payloads make up the contract between your server and the 8 | clients calling on it. 9 | 10 | Typically in a REST API application, you will have your data models (objects/structs) 11 | that hold lower-level runtime application state, and at times you need to assemble, 12 | decorate, hide or transform the representation before responding to a client. That 13 | server output (response payload) structure, is also likely the input structure to 14 | another handler on the server. 15 | 16 | This is where `render` comes in - offering a few simple helpers and interfaces to 17 | provide a simple pattern for managing payload encoding and decoding. 18 | 19 | We've also combined it with some helpers for responding to content types and parsing 20 | request bodies. Please have a look at the [rest](https://github.com/go-chi/chi/blob/master/_examples/rest/main.go) 21 | example which uses the latest chi/render sub-pkg. 22 | 23 | All feedback is welcome, thank you! 24 | 25 | -------------------------------------------------------------------------------- /decoder.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "encoding/xml" 5 | "errors" 6 | "io" 7 | "io/ioutil" 8 | "net/http" 9 | 10 | jsoniter "github.com/json-iterator/go" 11 | ) 12 | 13 | // Decode is a package-level variable set to our default Decoder. We do this 14 | // because it allows you to set render.Decode to another function with the 15 | // same function signature, while also utilizing the render.Decoder() function 16 | // itself. Effectively, allowing you to easily add your own logic to the package 17 | // defaults. For example, maybe you want to impose a limit on the number of 18 | // bytes allowed to be read from the request body. 19 | var Decode = DefaultDecoder 20 | 21 | func DefaultDecoder(r *http.Request, v interface{}) error { 22 | var err error 23 | 24 | switch GetRequestContentType(r) { 25 | case ContentTypeJSON: 26 | err = DecodeJSON(r.Body, v) 27 | case ContentTypeXML: 28 | err = DecodeXML(r.Body, v) 29 | // case ContentTypeForm: // TODO 30 | default: 31 | err = errors.New("render: unable to automatically decode the request content type") 32 | } 33 | 34 | return err 35 | } 36 | 37 | func DecodeJSON(r io.Reader, v interface{}) error { 38 | defer io.Copy(ioutil.Discard, r) 39 | return jsoniter.NewDecoder(r).Decode(v) 40 | } 41 | 42 | func DecodeXML(r io.Reader, v interface{}) error { 43 | defer io.Copy(ioutil.Discard, r) 44 | return xml.NewDecoder(r).Decode(v) 45 | } 46 | -------------------------------------------------------------------------------- /content_type.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "strings" 7 | ) 8 | 9 | var ( 10 | ContentTypeCtxKey = &contextKey{"ContentType"} 11 | ) 12 | 13 | // ContentType is an enumeration of common HTTP content types. 14 | type ContentType int 15 | 16 | // ContentTypes handled by this package. 17 | const ( 18 | ContentTypeUnknown = iota 19 | ContentTypePlainText 20 | ContentTypeHTML 21 | ContentTypeJSON 22 | ContentTypeXML 23 | ContentTypeForm 24 | ContentTypeEventStream 25 | ) 26 | 27 | func GetContentType(s string) ContentType { 28 | s = strings.TrimSpace(strings.Split(s, ";")[0]) 29 | switch s { 30 | case "text/plain": 31 | return ContentTypePlainText 32 | case "text/html", "application/xhtml+xml": 33 | return ContentTypeHTML 34 | case "application/json", "text/javascript": 35 | return ContentTypeJSON 36 | case "text/xml", "application/xml": 37 | return ContentTypeXML 38 | case "application/x-www-form-urlencoded": 39 | return ContentTypeForm 40 | case "text/event-stream": 41 | return ContentTypeEventStream 42 | default: 43 | return ContentTypeUnknown 44 | } 45 | } 46 | 47 | // SetContentType is a middleware that forces response Content-Type. 48 | func SetContentType(contentType ContentType) func(next http.Handler) http.Handler { 49 | return func(next http.Handler) http.Handler { 50 | fn := func(w http.ResponseWriter, r *http.Request) { 51 | r = r.WithContext(context.WithValue(r.Context(), ContentTypeCtxKey, contentType)) 52 | next.ServeHTTP(w, r) 53 | } 54 | return http.HandlerFunc(fn) 55 | } 56 | } 57 | 58 | // GetRequestContentType is a helper function that returns ContentType based on 59 | // context or request headers. 60 | func GetRequestContentType(r *http.Request) ContentType { 61 | if contentType, ok := r.Context().Value(ContentTypeCtxKey).(ContentType); ok { 62 | return contentType 63 | } 64 | return GetContentType(r.Header.Get("Content-Type")) 65 | } 66 | 67 | func GetAcceptedContentType(r *http.Request) ContentType { 68 | if contentType, ok := r.Context().Value(ContentTypeCtxKey).(ContentType); ok { 69 | return contentType 70 | } 71 | 72 | var contentType ContentType 73 | 74 | // Parse request Accept header. 75 | fields := strings.Split(r.Header.Get("Accept"), ",") 76 | if len(fields) > 0 { 77 | contentType = GetContentType(strings.TrimSpace(fields[0])) 78 | } 79 | 80 | if contentType == ContentTypeUnknown { 81 | contentType = ContentTypePlainText 82 | } 83 | return contentType 84 | } 85 | -------------------------------------------------------------------------------- /render.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "net/http" 5 | "reflect" 6 | ) 7 | 8 | // Renderer interface for managing response payloads. 9 | type Renderer interface { 10 | Render(w http.ResponseWriter, r *http.Request) error 11 | } 12 | 13 | // Binder interface for managing request payloads. 14 | type Binder interface { 15 | Bind(r *http.Request) error 16 | } 17 | 18 | // Bind decodes a request body and executes the Binder method of the 19 | // payload structure. 20 | func Bind(r *http.Request, v Binder) error { 21 | if err := Decode(r, v); err != nil { 22 | return err 23 | } 24 | return binder(r, v) 25 | } 26 | 27 | // Render renders a single payload and respond to the client request. 28 | func Render(w http.ResponseWriter, r *http.Request, v Renderer) error { 29 | if err := renderer(w, r, v); err != nil { 30 | return err 31 | } 32 | Respond(w, r, v) 33 | return nil 34 | } 35 | 36 | // RenderList renders a slice of payloads and responds to the client request. 37 | func RenderList(w http.ResponseWriter, r *http.Request, l []Renderer) error { 38 | for _, v := range l { 39 | if err := renderer(w, r, v); err != nil { 40 | return err 41 | } 42 | } 43 | Respond(w, r, l) 44 | return nil 45 | } 46 | 47 | func isNil(f reflect.Value) bool { 48 | switch f.Kind() { 49 | case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: 50 | return f.IsNil() 51 | default: 52 | return false 53 | } 54 | } 55 | 56 | // Executed top-down 57 | func renderer(w http.ResponseWriter, r *http.Request, v Renderer) error { 58 | rv := reflect.ValueOf(v) 59 | if rv.Kind() == reflect.Ptr { 60 | rv = rv.Elem() 61 | } 62 | 63 | // We call it top-down. 64 | if err := v.Render(w, r); err != nil { 65 | return err 66 | } 67 | 68 | // We're done if the Renderer isn't a struct object 69 | if rv.Kind() != reflect.Struct { 70 | return nil 71 | } 72 | 73 | // For structs, we call Render on each field that implements Renderer 74 | for i := 0; i < rv.NumField(); i++ { 75 | f := rv.Field(i) 76 | if f.Type().Implements(rendererType) { 77 | 78 | if isNil(f) { 79 | continue 80 | } 81 | 82 | fv := f.Interface().(Renderer) 83 | if err := renderer(w, r, fv); err != nil { 84 | return err 85 | } 86 | 87 | } 88 | } 89 | 90 | return nil 91 | } 92 | 93 | // Executed bottom-up 94 | func binder(r *http.Request, v Binder) error { 95 | rv := reflect.ValueOf(v) 96 | if rv.Kind() == reflect.Ptr { 97 | rv = rv.Elem() 98 | } 99 | 100 | // Call Binder on non-struct types right away 101 | if rv.Kind() != reflect.Struct { 102 | return v.Bind(r) 103 | } 104 | 105 | // For structs, we call Bind on each field that implements Binder 106 | for i := 0; i < rv.NumField(); i++ { 107 | f := rv.Field(i) 108 | if f.Type().Implements(binderType) { 109 | 110 | if isNil(f) { 111 | continue 112 | } 113 | 114 | fv := f.Interface().(Binder) 115 | if err := binder(r, fv); err != nil { 116 | return err 117 | } 118 | } 119 | } 120 | 121 | // We call it bottom-up 122 | if err := v.Bind(r); err != nil { 123 | return err 124 | } 125 | 126 | return nil 127 | } 128 | 129 | var ( 130 | rendererType = reflect.TypeOf(new(Renderer)).Elem() 131 | binderType = reflect.TypeOf(new(Binder)).Elem() 132 | ) 133 | 134 | // contextKey is a value for use with context.WithValue. It's used as 135 | // a pointer so it fits in an interface{} without allocation. This technique 136 | // for defining context keys was copied from Go 1.7's new use of context in net/http. 137 | type contextKey struct { 138 | name string 139 | } 140 | 141 | func (k *contextKey) String() string { 142 | return "chi render context value " + k.name 143 | } 144 | -------------------------------------------------------------------------------- /responder.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/xml" 7 | "fmt" 8 | "net/http" 9 | "reflect" 10 | 11 | jsoniter "github.com/json-iterator/go" 12 | ) 13 | 14 | // M is a convenience alias for quickly building a map structure that is going 15 | // out to a responder. Just a short-hand. 16 | type M map[string]interface{} 17 | 18 | // Respond is a package-level variable set to our default Responder. We do this 19 | // because it allows you to set render.Respond to another function with the 20 | // same function signature, while also utilizing the render.Responder() function 21 | // itself. Effectively, allowing you to easily add your own logic to the package 22 | // defaults. For example, maybe you want to test if v is an error and respond 23 | // differently, or log something before you respond. 24 | var Respond = DefaultResponder 25 | 26 | // StatusCtxKey is a context key to record a future HTTP response status code. 27 | var StatusCtxKey = &contextKey{"Status"} 28 | 29 | // Status sets a HTTP response status code hint into request context at any point 30 | // during the request life-cycle. Before the Responder sends its response header 31 | // it will check the StatusCtxKey 32 | func Status(r *http.Request, status int) { 33 | *r = *r.WithContext(context.WithValue(r.Context(), StatusCtxKey, status)) 34 | } 35 | 36 | // Respond handles streaming JSON and XML responses, automatically setting the 37 | // Content-Type based on request headers. It will default to a JSON response. 38 | func DefaultResponder(w http.ResponseWriter, r *http.Request, v interface{}) { 39 | if v != nil { 40 | switch reflect.TypeOf(v).Kind() { 41 | case reflect.Chan: 42 | switch GetAcceptedContentType(r) { 43 | case ContentTypeEventStream: 44 | channelEventStream(w, r, v) 45 | return 46 | default: 47 | v = channelIntoSlice(w, r, v) 48 | } 49 | } 50 | } 51 | 52 | // Format response based on request Accept header. 53 | switch GetAcceptedContentType(r) { 54 | case ContentTypeJSON: 55 | JSON(w, r, v) 56 | case ContentTypeXML: 57 | XML(w, r, v) 58 | default: 59 | JSON(w, r, v) 60 | } 61 | } 62 | 63 | // PlainText writes a string to the response, setting the Content-Type as 64 | // text/plain. 65 | func PlainText(w http.ResponseWriter, r *http.Request, v string) { 66 | w.Header().Set("Content-Type", "text/plain; charset=utf-8") 67 | if status, ok := r.Context().Value(StatusCtxKey).(int); ok { 68 | w.WriteHeader(status) 69 | } 70 | w.Write([]byte(v)) 71 | } 72 | 73 | // Data writes raw bytes to the response, setting the Content-Type as 74 | // application/octet-stream. 75 | func Data(w http.ResponseWriter, r *http.Request, v []byte) { 76 | w.Header().Set("Content-Type", "application/octet-stream") 77 | if status, ok := r.Context().Value(StatusCtxKey).(int); ok { 78 | w.WriteHeader(status) 79 | } 80 | w.Write(v) 81 | } 82 | 83 | // HTML writes a string to the response, setting the Content-Type as text/html. 84 | func HTML(w http.ResponseWriter, r *http.Request, v string) { 85 | w.Header().Set("Content-Type", "text/html; charset=utf-8") 86 | if status, ok := r.Context().Value(StatusCtxKey).(int); ok { 87 | w.WriteHeader(status) 88 | } 89 | w.Write([]byte(v)) 90 | } 91 | 92 | // JSON marshals 'v' to JSON, automatically escaping HTML and setting the 93 | // Content-Type as application/json. 94 | func JSON(w http.ResponseWriter, r *http.Request, v interface{}) { 95 | buf := &bytes.Buffer{} 96 | enc := jsoniter.NewEncoder(buf) 97 | enc.SetEscapeHTML(true) 98 | if err := enc.Encode(v); err != nil { 99 | http.Error(w, err.Error(), http.StatusInternalServerError) 100 | return 101 | } 102 | 103 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 104 | if status, ok := r.Context().Value(StatusCtxKey).(int); ok { 105 | w.WriteHeader(status) 106 | } 107 | w.Write(buf.Bytes()) 108 | } 109 | 110 | // XML marshals 'v' to JSON, setting the Content-Type as application/xml. It 111 | // will automatically prepend a generic XML header (see encoding/xml.Header) if 112 | // one is not found in the first 100 bytes of 'v'. 113 | func XML(w http.ResponseWriter, r *http.Request, v interface{}) { 114 | b, err := xml.Marshal(v) 115 | if err != nil { 116 | http.Error(w, err.Error(), http.StatusInternalServerError) 117 | return 118 | } 119 | 120 | w.Header().Set("Content-Type", "application/xml; charset=utf-8") 121 | if status, ok := r.Context().Value(StatusCtxKey).(int); ok { 122 | w.WriteHeader(status) 123 | } 124 | 125 | // Try to find 100 { 128 | findHeaderUntil = 100 129 | } 130 | if !bytes.Contains(b[:findHeaderUntil], []byte("