Skip to content

Commit 06ba8c7

Browse files
author
Peter Kieltyka
committed
Move chi/render sub-pkg as its own package within the go-chi org
0 parents  commit 06ba8c7

File tree

5 files changed

+514
-0
lines changed

5 files changed

+514
-0
lines changed

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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+

content_type.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
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+
}

decoder.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package render
2+
3+
import (
4+
"encoding/json"
5+
"encoding/xml"
6+
"errors"
7+
"io"
8+
"io/ioutil"
9+
"net/http"
10+
)
11+
12+
// Decode is a package-level variable set to our default Decoder. We do this
13+
// because it allows you to set render.Decode to another function with the
14+
// same function signature, while also utilizing the render.Decoder() function
15+
// itself. Effectively, allowing you to easily add your own logic to the package
16+
// defaults. For example, maybe you want to impose a limit on the number of
17+
// bytes allowed to be read from the request body.
18+
var Decode = DefaultDecoder
19+
20+
func DefaultDecoder(r *http.Request, v interface{}) error {
21+
var err error
22+
23+
switch GetRequestContentType(r) {
24+
case ContentTypeJSON:
25+
err = DecodeJSON(r.Body, v)
26+
case ContentTypeXML:
27+
err = DecodeXML(r.Body, v)
28+
// case ContentTypeForm: // TODO
29+
default:
30+
err = errors.New("render: unable to automatically decode the request content type")
31+
}
32+
33+
return err
34+
}
35+
36+
func DecodeJSON(r io.Reader, v interface{}) error {
37+
defer io.Copy(ioutil.Discard, r)
38+
return json.NewDecoder(r).Decode(v)
39+
}
40+
41+
func DecodeXML(r io.Reader, v interface{}) error {
42+
defer io.Copy(ioutil.Discard, r)
43+
return xml.NewDecoder(r).Decode(v)
44+
}

render.go

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
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+
// Executed top-down
48+
func renderer(w http.ResponseWriter, r *http.Request, v Renderer) error {
49+
rv := reflect.ValueOf(v)
50+
if rv.Kind() == reflect.Ptr {
51+
rv = rv.Elem()
52+
}
53+
54+
// We call it top-down.
55+
if err := v.Render(w, r); err != nil {
56+
return err
57+
}
58+
59+
// We're done if the Renderer isn't a struct object
60+
if rv.Kind() != reflect.Struct {
61+
return nil
62+
}
63+
64+
// For structs, we call Render on each field that implements Renderer
65+
for i := 0; i < rv.NumField(); i++ {
66+
f := rv.Field(i)
67+
if f.Type().Implements(rendererType) {
68+
69+
if f.IsNil() {
70+
continue
71+
}
72+
73+
fv := f.Interface().(Renderer)
74+
if err := renderer(w, r, fv); err != nil {
75+
return err
76+
}
77+
78+
}
79+
}
80+
81+
return nil
82+
}
83+
84+
// Executed bottom-up
85+
func binder(r *http.Request, v Binder) error {
86+
rv := reflect.ValueOf(v)
87+
if rv.Kind() == reflect.Ptr {
88+
rv = rv.Elem()
89+
}
90+
91+
// Call Binder on non-struct types right away
92+
if rv.Kind() != reflect.Struct {
93+
return v.Bind(r)
94+
}
95+
96+
// For structs, we call Bind on each field that implements Binder
97+
for i := 0; i < rv.NumField(); i++ {
98+
f := rv.Field(i)
99+
if f.Type().Implements(binderType) {
100+
101+
if f.IsNil() {
102+
continue
103+
}
104+
105+
fv := f.Interface().(Binder)
106+
if err := binder(r, fv); err != nil {
107+
return err
108+
}
109+
}
110+
}
111+
112+
// We call it bottom-up
113+
if err := v.Bind(r); err != nil {
114+
return err
115+
}
116+
117+
return nil
118+
}
119+
120+
var (
121+
rendererType = reflect.TypeOf(new(Renderer)).Elem()
122+
binderType = reflect.TypeOf(new(Binder)).Elem()
123+
)
124+
125+
// contextKey is a value for use with context.WithValue. It's used as
126+
// a pointer so it fits in an interface{} without allocation. This technique
127+
// for defining context keys was copied from Go 1.7's new use of context in net/http.
128+
type contextKey struct {
129+
name string
130+
}
131+
132+
func (k *contextKey) String() string {
133+
return "chi render context value " + k.name
134+
}

0 commit comments

Comments
 (0)