├── wercker.yml
├── fixtures
├── basic
│ ├── content.tmpl
│ ├── delims.tmpl
│ ├── hypertext.html
│ ├── hello.tmpl
│ ├── admin
│ │ └── index.tmpl
│ ├── layout.tmpl
│ ├── another_layout.tmpl
│ └── current_layout.tmpl
└── custom_funcs
│ └── index.tmpl
├── LICENSE
├── README.md
├── render.go
└── render_test.go
/wercker.yml:
--------------------------------------------------------------------------------
1 | box: wercker/golang@1.1.1
--------------------------------------------------------------------------------
/fixtures/basic/content.tmpl:
--------------------------------------------------------------------------------
1 |
{{ . }}
2 |
--------------------------------------------------------------------------------
/fixtures/basic/delims.tmpl:
--------------------------------------------------------------------------------
1 | Hello {[{.}]}
--------------------------------------------------------------------------------
/fixtures/basic/hypertext.html:
--------------------------------------------------------------------------------
1 | Hypertext!
2 |
--------------------------------------------------------------------------------
/fixtures/basic/hello.tmpl:
--------------------------------------------------------------------------------
1 | Hello {{.}}
2 |
--------------------------------------------------------------------------------
/fixtures/basic/admin/index.tmpl:
--------------------------------------------------------------------------------
1 | Admin {{.}}
2 |
--------------------------------------------------------------------------------
/fixtures/custom_funcs/index.tmpl:
--------------------------------------------------------------------------------
1 | {{ myCustomFunc }}
2 |
--------------------------------------------------------------------------------
/fixtures/basic/layout.tmpl:
--------------------------------------------------------------------------------
1 | head
2 | {{ yield }}
3 | foot
4 |
--------------------------------------------------------------------------------
/fixtures/basic/another_layout.tmpl:
--------------------------------------------------------------------------------
1 | another head
2 | {{ yield }}
3 | another foot
4 |
--------------------------------------------------------------------------------
/fixtures/basic/current_layout.tmpl:
--------------------------------------------------------------------------------
1 | {{ current }} head
2 | {{ yield }}
3 | {{ current }} foot
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2013 Jeremy Saenz
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 [](https://app.wercker.com/project/bykey/fcf6b26a1b41f53540200b1949b48dec)
2 | Martini middleware/handler for easily rendering serialized JSON, XML, and HTML template responses.
3 |
4 | [API Reference](http://godoc.org/github.com/martini-contrib/render)
5 |
6 | ## Usage
7 | render uses Go's [html/template](http://golang.org/pkg/html/template/) package to render html templates.
8 |
9 | ~~~ go
10 | // main.go
11 | package main
12 |
13 | import (
14 | "github.com/go-martini/martini"
15 | "github.com/martini-contrib/render"
16 | )
17 |
18 | func main() {
19 | m := martini.Classic()
20 | // render html templates from templates directory
21 | m.Use(render.Renderer())
22 |
23 | m.Get("/", func(r render.Render) {
24 | r.HTML(200, "hello", "jeremy")
25 | })
26 |
27 | m.Run()
28 | }
29 |
30 | ~~~
31 |
32 | ~~~ html
33 |
34 | Hello {{.}}!
35 | ~~~
36 |
37 | ### Options
38 | `render.Renderer` comes with a variety of configuration options:
39 |
40 | ~~~ go
41 | // ...
42 | m.Use(render.Renderer(render.Options{
43 | Directory: "templates", // Specify what path to load the templates from.
44 | Layout: "layout", // Specify a layout template. Layouts can call {{ yield }} to render the current template.
45 | Extensions: []string{".tmpl", ".html"}, // Specify extensions to load for templates.
46 | Funcs: []template.FuncMap{AppHelpers}, // Specify helper function maps for templates to access.
47 | Delims: render.Delims{"{[{", "}]}"}, // Sets delimiters to the specified strings.
48 | Charset: "UTF-8", // Sets encoding for json and html content-types. Default is "UTF-8".
49 | IndentJSON: true, // Output human readable JSON
50 | IndentXML: true, // Output human readable XML
51 | HTMLContentType: "application/xhtml+xml", // Output XHTML content type instead of default "text/html"
52 | }))
53 | // ...
54 | ~~~
55 |
56 | ### Loading Templates
57 | By default the `render.Renderer` middleware will attempt to load templates with a '.tmpl' extension from the "templates" directory. Templates are found by traversing the templates directory and are named by path and basename. For instance, the following directory structure:
58 |
59 | ~~~
60 | templates/
61 | |
62 | |__ admin/
63 | | |
64 | | |__ index.tmpl
65 | | |
66 | | |__ edit.tmpl
67 | |
68 | |__ home.tmpl
69 | ~~~
70 |
71 | Will provide the following templates:
72 | ~~~
73 | admin/index
74 | admin/edit
75 | home
76 | ~~~
77 | ### Layouts
78 | `render.Renderer` provides a `yield` function for layouts to access:
79 | ~~~ go
80 | // ...
81 | m.Use(render.Renderer(render.Options{
82 | Layout: "layout",
83 | }))
84 | // ...
85 | ~~~
86 |
87 | ~~~ html
88 |
89 |
90 |
91 | Martini Plz
92 |
93 |
94 |
95 | {{ yield }}
96 |
97 |
98 | ~~~
99 |
100 | `current` can also be called to get the current template being rendered.
101 | ~~~ html
102 |
103 |
104 |
105 | Martini Plz
106 |
107 |
108 | This is the {{ current }} page.
109 |
110 |
111 | ~~~
112 |
113 | ### Character Encodings
114 | The `render.Renderer` middleware will automatically set the proper Content-Type header based on which function you call. See below for an example of what the default settings would output (note that UTF-8 is the default):
115 | ~~~ go
116 | // main.go
117 | package main
118 |
119 | import (
120 | "encoding/xml"
121 |
122 | "github.com/go-martini/martini"
123 | "github.com/martini-contrib/render"
124 | )
125 |
126 | type Greeting struct {
127 | XMLName xml.Name `xml:"greeting"`
128 | One string `xml:"one,attr"`
129 | Two string `xml:"two,attr"`
130 | }
131 |
132 | func main() {
133 | m := martini.Classic()
134 | m.Use(render.Renderer())
135 |
136 | // This will set the Content-Type header to "text/html; charset=UTF-8"
137 | m.Get("/", func(r render.Render) {
138 | r.HTML(200, "hello", "world")
139 | })
140 |
141 | // This will set the Content-Type header to "application/json; charset=UTF-8"
142 | m.Get("/api", func(r render.Render) {
143 | r.JSON(200, map[string]interface{}{"hello": "world"})
144 | })
145 |
146 | // This will set the Content-Type header to "text/xml; charset=UTF-8"
147 | m.Get("/xml", func(r render.Render) {
148 | r.XML(200, Greeting{One: "hello", Two: "world"})
149 | })
150 |
151 | // This will set the Content-Type header to "text/plain; charset=UTF-8"
152 | m.Get("/text", func(r render.Render) {
153 | r.Text(200, "hello, world")
154 | })
155 |
156 | m.Run()
157 | }
158 |
159 | ~~~
160 |
161 | In order to change the charset, you can set the `Charset` within the `render.Options` to your encoding value:
162 | ~~~ go
163 | // main.go
164 | package main
165 |
166 | import (
167 | "encoding/xml"
168 |
169 | "github.com/go-martini/martini"
170 | "github.com/martini-contrib/render"
171 | )
172 |
173 | type Greeting struct {
174 | XMLName xml.Name `xml:"greeting"`
175 | One string `xml:"one,attr"`
176 | Two string `xml:"two,attr"`
177 | }
178 |
179 | func main() {
180 | m := martini.Classic()
181 | m.Use(render.Renderer(render.Options{
182 | Charset: "ISO-8859-1",
183 | }))
184 |
185 | // This will set the Content-Type header to "text/html; charset=ISO-8859-1"
186 | m.Get("/", func(r render.Render) {
187 | r.HTML(200, "hello", "world")
188 | })
189 |
190 | // This will set the Content-Type header to "application/json; charset=ISO-8859-1"
191 | m.Get("/api", func(r render.Render) {
192 | r.JSON(200, map[string]interface{}{"hello": "world"})
193 | })
194 |
195 | // This will set the Content-Type header to "text/xml; charset=ISO-8859-1"
196 | m.Get("/xml", func(r render.Render) {
197 | r.XML(200, Greeting{One: "hello", Two: "world"})
198 | })
199 |
200 | // This will set the Content-Type header to "text/plain; charset=ISO-8859-1"
201 | m.Get("/text", func(r render.Render) {
202 | r.Text(200, "hello, world")
203 | })
204 |
205 | m.Run()
206 | }
207 |
208 | ~~~
209 |
210 | ## Authors
211 | * [Jeremy Saenz](http://github.com/codegangsta)
212 | * [Cory Jacobsen](http://github.com/unrolled)
213 |
--------------------------------------------------------------------------------
/render.go:
--------------------------------------------------------------------------------
1 | // Package render is a middleware for Martini that provides easy JSON serialization and HTML template rendering.
2 | //
3 | // package main
4 | //
5 | // import (
6 | // "encoding/xml"
7 | //
8 | // "github.com/go-martini/martini"
9 | // "github.com/martini-contrib/render"
10 | // )
11 | //
12 | // type Greeting struct {
13 | // XMLName xml.Name `xml:"greeting"`
14 | // One string `xml:"one,attr"`
15 | // Two string `xml:"two,attr"`
16 | // }
17 | //
18 | // func main() {
19 | // m := martini.Classic()
20 | // m.Use(render.Renderer()) // reads "templates" directory by default
21 | //
22 | // m.Get("/html", func(r render.Render) {
23 | // r.HTML(200, "mytemplate", nil)
24 | // })
25 | //
26 | // m.Get("/json", func(r render.Render) {
27 | // r.JSON(200, "hello world")
28 | // })
29 | //
30 | // m.Get("/xml", func(r render.Render) {
31 | // r.XML(200, Greeting{One: "hello", Two: "world"})
32 | // })
33 | //
34 | // m.Run()
35 | // }
36 | package render
37 |
38 | import (
39 | "bytes"
40 | "encoding/json"
41 | "encoding/xml"
42 | "fmt"
43 | "html/template"
44 | "io"
45 | "io/ioutil"
46 | "net/http"
47 | "os"
48 | "path/filepath"
49 | "strings"
50 |
51 | "github.com/oxtoacart/bpool"
52 |
53 | "github.com/go-martini/martini"
54 | )
55 |
56 | const (
57 | ContentType = "Content-Type"
58 | ContentLength = "Content-Length"
59 | ContentBinary = "application/octet-stream"
60 | ContentText = "text/plain"
61 | ContentJSON = "application/json"
62 | ContentHTML = "text/html"
63 | ContentXHTML = "application/xhtml+xml"
64 | ContentXML = "text/xml"
65 | defaultCharset = "UTF-8"
66 | )
67 |
68 | // Provides a temporary buffer to execute templates into and catch errors.
69 | var bufpool *bpool.BufferPool
70 |
71 | // Included helper functions for use when rendering html
72 | var helperFuncs = template.FuncMap{
73 | "yield": func() (string, error) {
74 | return "", fmt.Errorf("yield called with no layout defined")
75 | },
76 | "current": func() (string, error) {
77 | return "", nil
78 | },
79 | }
80 |
81 | // Render is a service that can be injected into a Martini handler. Render provides functions for easily writing JSON and
82 | // HTML templates out to a http Response.
83 | type Render interface {
84 | // JSON writes the given status and JSON serialized version of the given value to the http.ResponseWriter.
85 | JSON(status int, v interface{})
86 | // HTML renders a html template specified by the name and writes the result and given status to the http.ResponseWriter.
87 | HTML(status int, name string, v interface{}, htmlOpt ...HTMLOptions)
88 | // XML writes the given status and XML serialized version of the given value to the http.ResponseWriter.
89 | XML(status int, v interface{})
90 | // Data writes the raw byte array to the http.ResponseWriter.
91 | Data(status int, v []byte)
92 | // Text writes the given status and plain text to the http.ResponseWriter.
93 | Text(status int, v string)
94 | // Error is a convenience function that writes an http status to the http.ResponseWriter.
95 | Error(status int)
96 | // Status is an alias for Error (writes an http status to the http.ResponseWriter)
97 | Status(status int)
98 | // Redirect is a convienience function that sends an HTTP redirect. If status is omitted, uses 302 (Found)
99 | Redirect(location string, status ...int)
100 | // Template returns the internal *template.Template used to render the HTML
101 | Template() *template.Template
102 | // Header exposes the header struct from http.ResponseWriter.
103 | Header() http.Header
104 | }
105 |
106 | // Delims represents a set of Left and Right delimiters for HTML template rendering
107 | type Delims struct {
108 | // Left delimiter, defaults to {{
109 | Left string
110 | // Right delimiter, defaults to }}
111 | Right string
112 | }
113 |
114 | // Options is a struct for specifying configuration options for the render.Renderer middleware
115 | type Options struct {
116 | // Directory to load templates. Default is "templates"
117 | Directory string
118 | // Layout template name. Will not render a layout if "". Defaults to "".
119 | Layout string
120 | // Extensions to parse template files from. Defaults to [".tmpl"]
121 | Extensions []string
122 | // Funcs is a slice of FuncMaps to apply to the template upon compilation. This is useful for helper functions. Defaults to [].
123 | Funcs []template.FuncMap
124 | // Delims sets the action delimiters to the specified strings in the Delims struct.
125 | Delims Delims
126 | // Appends the given charset to the Content-Type header. Default is "UTF-8".
127 | Charset string
128 | // Outputs human readable JSON
129 | IndentJSON bool
130 | // Outputs human readable XML
131 | IndentXML bool
132 | // Prefixes the JSON output with the given bytes.
133 | PrefixJSON []byte
134 | // Prefixes the XML output with the given bytes.
135 | PrefixXML []byte
136 | // Allows changing of output to XHTML instead of HTML. Default is "text/html"
137 | HTMLContentType string
138 | }
139 |
140 | // HTMLOptions is a struct for overriding some rendering Options for specific HTML call
141 | type HTMLOptions struct {
142 | // Layout template name. Overrides Options.Layout.
143 | Layout string
144 | }
145 |
146 | // Renderer is a Middleware that maps a render.Render service into the Martini handler chain. An single variadic render.Options
147 | // struct can be optionally provided to configure HTML rendering. The default directory for templates is "templates" and the default
148 | // file extension is ".tmpl".
149 | //
150 | // If MARTINI_ENV is set to "" or "development" then templates will be recompiled on every request. For more performance, set the
151 | // MARTINI_ENV environment variable to "production"
152 | func Renderer(options ...Options) martini.Handler {
153 | opt := prepareOptions(options)
154 | cs := prepareCharset(opt.Charset)
155 | t := compile(opt)
156 | bufpool = bpool.NewBufferPool(64)
157 | return func(res http.ResponseWriter, req *http.Request, c martini.Context) {
158 | var tc *template.Template
159 | if martini.Env == martini.Dev {
160 | // recompile for easy development
161 | tc = compile(opt)
162 | } else {
163 | // use a clone of the initial template
164 | tc, _ = t.Clone()
165 | }
166 | c.MapTo(&renderer{res, req, tc, opt, cs}, (*Render)(nil))
167 | }
168 | }
169 |
170 | func prepareCharset(charset string) string {
171 | if len(charset) != 0 {
172 | return "; charset=" + charset
173 | }
174 |
175 | return "; charset=" + defaultCharset
176 | }
177 |
178 | func prepareOptions(options []Options) Options {
179 | var opt Options
180 | if len(options) > 0 {
181 | opt = options[0]
182 | }
183 |
184 | // Defaults
185 | if len(opt.Directory) == 0 {
186 | opt.Directory = "templates"
187 | }
188 | if len(opt.Extensions) == 0 {
189 | opt.Extensions = []string{".tmpl"}
190 | }
191 | if len(opt.HTMLContentType) == 0 {
192 | opt.HTMLContentType = ContentHTML
193 | }
194 |
195 | return opt
196 | }
197 |
198 | func compile(options Options) *template.Template {
199 | dir := options.Directory
200 | t := template.New(dir)
201 | t.Delims(options.Delims.Left, options.Delims.Right)
202 | // parse an initial template in case we don't have any
203 | template.Must(t.Parse("Martini"))
204 |
205 | filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
206 | r, err := filepath.Rel(dir, path)
207 | if err != nil {
208 | return err
209 | }
210 |
211 | ext := getExt(r)
212 |
213 | for _, extension := range options.Extensions {
214 | if ext == extension {
215 |
216 | buf, err := ioutil.ReadFile(path)
217 | if err != nil {
218 | panic(err)
219 | }
220 |
221 | name := (r[0 : len(r)-len(ext)])
222 | tmpl := t.New(filepath.ToSlash(name))
223 |
224 | // add our funcmaps
225 | for _, funcs := range options.Funcs {
226 | tmpl.Funcs(funcs)
227 | }
228 |
229 | // Bomb out if parse fails. We don't want any silent server starts.
230 | template.Must(tmpl.Funcs(helperFuncs).Parse(string(buf)))
231 | break
232 | }
233 | }
234 |
235 | return nil
236 | })
237 |
238 | return t
239 | }
240 |
241 | func getExt(s string) string {
242 | if strings.Index(s, ".") == -1 {
243 | return ""
244 | }
245 | return "." + strings.Join(strings.Split(s, ".")[1:], ".")
246 | }
247 |
248 | type renderer struct {
249 | http.ResponseWriter
250 | req *http.Request
251 | t *template.Template
252 | opt Options
253 | compiledCharset string
254 | }
255 |
256 | func (r *renderer) JSON(status int, v interface{}) {
257 | var result []byte
258 | var err error
259 | if r.opt.IndentJSON {
260 | result, err = json.MarshalIndent(v, "", " ")
261 | } else {
262 | result, err = json.Marshal(v)
263 | }
264 | if err != nil {
265 | http.Error(r, err.Error(), 500)
266 | return
267 | }
268 |
269 | // json rendered fine, write out the result
270 | r.Header().Set(ContentType, ContentJSON+r.compiledCharset)
271 | r.WriteHeader(status)
272 | if len(r.opt.PrefixJSON) > 0 {
273 | r.Write(r.opt.PrefixJSON)
274 | }
275 | r.Write(result)
276 | }
277 |
278 | func (r *renderer) HTML(status int, name string, binding interface{}, htmlOpt ...HTMLOptions) {
279 | opt := r.prepareHTMLOptions(htmlOpt)
280 | // assign a layout if there is one
281 | if len(opt.Layout) > 0 {
282 | r.addYield(name, binding)
283 | name = opt.Layout
284 | }
285 |
286 | buf, err := r.execute(name, binding)
287 | if err != nil {
288 | http.Error(r, err.Error(), http.StatusInternalServerError)
289 | return
290 | }
291 |
292 | // template rendered fine, write out the result
293 | r.Header().Set(ContentType, r.opt.HTMLContentType+r.compiledCharset)
294 | r.WriteHeader(status)
295 | io.Copy(r, buf)
296 | bufpool.Put(buf)
297 | }
298 |
299 | func (r *renderer) XML(status int, v interface{}) {
300 | var result []byte
301 | var err error
302 | if r.opt.IndentXML {
303 | result, err = xml.MarshalIndent(v, "", " ")
304 | } else {
305 | result, err = xml.Marshal(v)
306 | }
307 | if err != nil {
308 | http.Error(r, err.Error(), 500)
309 | return
310 | }
311 |
312 | // XML rendered fine, write out the result
313 | r.Header().Set(ContentType, ContentXML+r.compiledCharset)
314 | r.WriteHeader(status)
315 | if len(r.opt.PrefixXML) > 0 {
316 | r.Write(r.opt.PrefixXML)
317 | }
318 | r.Write(result)
319 | }
320 |
321 | func (r *renderer) Data(status int, v []byte) {
322 | if r.Header().Get(ContentType) == "" {
323 | r.Header().Set(ContentType, ContentBinary)
324 | }
325 | r.WriteHeader(status)
326 | r.Write(v)
327 | }
328 |
329 | func (r *renderer) Text(status int, v string) {
330 | if r.Header().Get(ContentType) == "" {
331 | r.Header().Set(ContentType, ContentText+r.compiledCharset)
332 | }
333 | r.WriteHeader(status)
334 | r.Write([]byte(v))
335 | }
336 |
337 | // Error writes the given HTTP status to the current ResponseWriter
338 | func (r *renderer) Error(status int) {
339 | r.WriteHeader(status)
340 | }
341 |
342 | func (r *renderer) Status(status int) {
343 | r.WriteHeader(status)
344 | }
345 |
346 | func (r *renderer) Redirect(location string, status ...int) {
347 | code := http.StatusFound
348 | if len(status) == 1 {
349 | code = status[0]
350 | }
351 |
352 | http.Redirect(r, r.req, location, code)
353 | }
354 |
355 | func (r *renderer) Template() *template.Template {
356 | return r.t
357 | }
358 |
359 | func (r *renderer) execute(name string, binding interface{}) (*bytes.Buffer, error) {
360 | buf := bufpool.Get()
361 | return buf, r.t.ExecuteTemplate(buf, name, binding)
362 | }
363 |
364 | func (r *renderer) addYield(name string, binding interface{}) {
365 | funcs := template.FuncMap{
366 | "yield": func() (template.HTML, error) {
367 | buf, err := r.execute(name, binding)
368 | // return safe html here since we are rendering our own template
369 | return template.HTML(buf.String()), err
370 | },
371 | "current": func() (string, error) {
372 | return name, nil
373 | },
374 | }
375 | r.t.Funcs(funcs)
376 | }
377 |
378 | func (r *renderer) prepareHTMLOptions(htmlOpt []HTMLOptions) HTMLOptions {
379 | if len(htmlOpt) > 0 {
380 | return htmlOpt[0]
381 | }
382 |
383 | return HTMLOptions{
384 | Layout: r.opt.Layout,
385 | }
386 | }
387 |
--------------------------------------------------------------------------------
/render_test.go:
--------------------------------------------------------------------------------
1 | package render
2 |
3 | import (
4 | "encoding/xml"
5 | "html/template"
6 | "net/http"
7 | "net/http/httptest"
8 | "net/url"
9 | "reflect"
10 | "testing"
11 |
12 | "github.com/go-martini/martini"
13 | )
14 |
15 | type Greeting struct {
16 | One string `json:"one"`
17 | Two string `json:"two"`
18 | }
19 |
20 | type GreetingXML struct {
21 | XMLName xml.Name `xml:"greeting"`
22 | One string `xml:"one,attr"`
23 | Two string `xml:"two,attr"`
24 | }
25 |
26 | func Test_Render_JSON(t *testing.T) {
27 | m := martini.Classic()
28 | m.Use(Renderer(Options{
29 | // nothing here to configure
30 | }))
31 |
32 | // routing
33 | m.Get("/foobar", func(r Render) {
34 | r.JSON(300, Greeting{"hello", "world"})
35 | })
36 |
37 | res := httptest.NewRecorder()
38 | req, _ := http.NewRequest("GET", "/foobar", nil)
39 |
40 | m.ServeHTTP(res, req)
41 |
42 | expect(t, res.Code, 300)
43 | expect(t, res.Header().Get(ContentType), ContentJSON+"; charset=UTF-8")
44 | expect(t, res.Body.String(), `{"one":"hello","two":"world"}`)
45 | }
46 |
47 | func Test_Render_JSON_Prefix(t *testing.T) {
48 | m := martini.Classic()
49 | prefix := ")]}',\n"
50 | m.Use(Renderer(Options{
51 | PrefixJSON: []byte(prefix),
52 | }))
53 |
54 | // routing
55 | m.Get("/foobar", func(r Render) {
56 | r.JSON(300, Greeting{"hello", "world"})
57 | })
58 |
59 | res := httptest.NewRecorder()
60 | req, _ := http.NewRequest("GET", "/foobar", nil)
61 |
62 | m.ServeHTTP(res, req)
63 |
64 | expect(t, res.Code, 300)
65 | expect(t, res.Header().Get(ContentType), ContentJSON+"; charset=UTF-8")
66 | expect(t, res.Body.String(), prefix+`{"one":"hello","two":"world"}`)
67 | }
68 |
69 | func Test_Render_Indented_JSON(t *testing.T) {
70 | m := martini.Classic()
71 | m.Use(Renderer(Options{
72 | IndentJSON: true,
73 | }))
74 |
75 | // routing
76 | m.Get("/foobar", func(r Render) {
77 | r.JSON(300, Greeting{"hello", "world"})
78 | })
79 |
80 | res := httptest.NewRecorder()
81 | req, _ := http.NewRequest("GET", "/foobar", nil)
82 |
83 | m.ServeHTTP(res, req)
84 |
85 | expect(t, res.Code, 300)
86 | expect(t, res.Header().Get(ContentType), ContentJSON+"; charset=UTF-8")
87 | expect(t, res.Body.String(), `{
88 | "one": "hello",
89 | "two": "world"
90 | }`)
91 | }
92 |
93 | func Test_Render_XML(t *testing.T) {
94 | m := martini.Classic()
95 | m.Use(Renderer(Options{
96 | // nothing here to configure
97 | }))
98 |
99 | // routing
100 | m.Get("/foobar", func(r Render) {
101 | r.XML(300, GreetingXML{One: "hello", Two: "world"})
102 | })
103 |
104 | res := httptest.NewRecorder()
105 | req, _ := http.NewRequest("GET", "/foobar", nil)
106 |
107 | m.ServeHTTP(res, req)
108 |
109 | expect(t, res.Code, 300)
110 | expect(t, res.Header().Get(ContentType), ContentXML+"; charset=UTF-8")
111 | expect(t, res.Body.String(), ``)
112 | }
113 |
114 | func Test_Render_XML_Prefix(t *testing.T) {
115 | m := martini.Classic()
116 | prefix := ")]}',\n"
117 | m.Use(Renderer(Options{
118 | PrefixXML: []byte(prefix),
119 | }))
120 |
121 | // routing
122 | m.Get("/foobar", func(r Render) {
123 | r.XML(300, GreetingXML{One: "hello", Two: "world"})
124 | })
125 |
126 | res := httptest.NewRecorder()
127 | req, _ := http.NewRequest("GET", "/foobar", nil)
128 |
129 | m.ServeHTTP(res, req)
130 |
131 | expect(t, res.Code, 300)
132 | expect(t, res.Header().Get(ContentType), ContentXML+"; charset=UTF-8")
133 | expect(t, res.Body.String(), prefix+``)
134 | }
135 |
136 | func Test_Render_Indented_XML(t *testing.T) {
137 | m := martini.Classic()
138 | m.Use(Renderer(Options{
139 | IndentXML: true,
140 | }))
141 |
142 | // routing
143 | m.Get("/foobar", func(r Render) {
144 | r.XML(300, GreetingXML{One: "hello", Two: "world"})
145 | })
146 |
147 | res := httptest.NewRecorder()
148 | req, _ := http.NewRequest("GET", "/foobar", nil)
149 |
150 | m.ServeHTTP(res, req)
151 |
152 | expect(t, res.Code, 300)
153 | expect(t, res.Header().Get(ContentType), ContentXML+"; charset=UTF-8")
154 | expect(t, res.Body.String(), ``)
155 | }
156 |
157 | func Test_Render_Bad_HTML(t *testing.T) {
158 | m := martini.Classic()
159 | m.Use(Renderer(Options{
160 | Directory: "fixtures/basic",
161 | }))
162 |
163 | // routing
164 | m.Get("/foobar", func(r Render) {
165 | r.HTML(200, "nope", nil)
166 | })
167 |
168 | res := httptest.NewRecorder()
169 | req, _ := http.NewRequest("GET", "/foobar", nil)
170 |
171 | m.ServeHTTP(res, req)
172 |
173 | expect(t, res.Code, 500)
174 | expect(t, res.Body.String(), "html/template: \"nope\" is undefined\n")
175 | }
176 |
177 | func Test_Render_HTML(t *testing.T) {
178 | m := martini.Classic()
179 | m.Use(Renderer(Options{
180 | Directory: "fixtures/basic",
181 | }))
182 |
183 | // routing
184 | m.Get("/foobar", func(r Render) {
185 | r.HTML(200, "hello", "jeremy")
186 | })
187 |
188 | res := httptest.NewRecorder()
189 | req, _ := http.NewRequest("GET", "/foobar", nil)
190 |
191 | m.ServeHTTP(res, req)
192 |
193 | expect(t, res.Code, 200)
194 | expect(t, res.Header().Get(ContentType), ContentHTML+"; charset=UTF-8")
195 | expect(t, res.Body.String(), "Hello jeremy
\n")
196 | }
197 |
198 | func Test_Render_XHTML(t *testing.T) {
199 | m := martini.Classic()
200 | m.Use(Renderer(Options{
201 | Directory: "fixtures/basic",
202 | HTMLContentType: ContentXHTML,
203 | }))
204 |
205 | m.Get("/foobar", func(r Render) {
206 | r.HTML(200, "hello", "jeremy")
207 | })
208 |
209 | res := httptest.NewRecorder()
210 | req, _ := http.NewRequest("GET", "/foobar", nil)
211 |
212 | m.ServeHTTP(res, req)
213 |
214 | expect(t, res.Code, 200)
215 | expect(t, res.Header().Get(ContentType), ContentXHTML+"; charset=UTF-8")
216 | expect(t, res.Body.String(), "Hello jeremy
\n")
217 | }
218 |
219 | func Test_Render_Extensions(t *testing.T) {
220 | m := martini.Classic()
221 | m.Use(Renderer(Options{
222 | Directory: "fixtures/basic",
223 | Extensions: []string{".tmpl", ".html"},
224 | }))
225 |
226 | // routing
227 | m.Get("/foobar", func(r Render) {
228 | r.HTML(200, "hypertext", nil)
229 | })
230 |
231 | res := httptest.NewRecorder()
232 | req, _ := http.NewRequest("GET", "/foobar", nil)
233 |
234 | m.ServeHTTP(res, req)
235 |
236 | expect(t, res.Code, 200)
237 | expect(t, res.Header().Get(ContentType), ContentHTML+"; charset=UTF-8")
238 | expect(t, res.Body.String(), "Hypertext!\n")
239 | }
240 |
241 | func Test_Render_Funcs(t *testing.T) {
242 |
243 | m := martini.Classic()
244 | m.Use(Renderer(Options{
245 | Directory: "fixtures/custom_funcs",
246 | Funcs: []template.FuncMap{
247 | {
248 | "myCustomFunc": func() string {
249 | return "My custom function"
250 | },
251 | },
252 | },
253 | }))
254 |
255 | // routing
256 | m.Get("/foobar", func(r Render) {
257 | r.HTML(200, "index", "jeremy")
258 | })
259 |
260 | res := httptest.NewRecorder()
261 | req, _ := http.NewRequest("GET", "/foobar", nil)
262 |
263 | m.ServeHTTP(res, req)
264 |
265 | expect(t, res.Body.String(), "My custom function\n")
266 | }
267 |
268 | func Test_Render_Layout(t *testing.T) {
269 | m := martini.Classic()
270 | m.Use(Renderer(Options{
271 | Directory: "fixtures/basic",
272 | Layout: "layout",
273 | }))
274 |
275 | // routing
276 | m.Get("/foobar", func(r Render) {
277 | r.HTML(200, "content", "jeremy")
278 | })
279 |
280 | res := httptest.NewRecorder()
281 | req, _ := http.NewRequest("GET", "/foobar", nil)
282 |
283 | m.ServeHTTP(res, req)
284 |
285 | expect(t, res.Body.String(), "head\njeremy
\n\nfoot\n")
286 | }
287 |
288 | func Test_Render_Layout_Current(t *testing.T) {
289 | m := martini.Classic()
290 | m.Use(Renderer(Options{
291 | Directory: "fixtures/basic",
292 | Layout: "current_layout",
293 | }))
294 |
295 | // routing
296 | m.Get("/foobar", func(r Render) {
297 | r.HTML(200, "content", "jeremy")
298 | })
299 |
300 | res := httptest.NewRecorder()
301 | req, _ := http.NewRequest("GET", "/foobar", nil)
302 |
303 | m.ServeHTTP(res, req)
304 |
305 | expect(t, res.Body.String(), "content head\njeremy
\n\ncontent foot\n")
306 | }
307 |
308 | func Test_Render_Nested_HTML(t *testing.T) {
309 | m := martini.Classic()
310 | m.Use(Renderer(Options{
311 | Directory: "fixtures/basic",
312 | }))
313 |
314 | // routing
315 | m.Get("/foobar", func(r Render) {
316 | r.HTML(200, "admin/index", "jeremy")
317 | })
318 |
319 | res := httptest.NewRecorder()
320 | req, _ := http.NewRequest("GET", "/foobar", nil)
321 |
322 | m.ServeHTTP(res, req)
323 |
324 | expect(t, res.Code, 200)
325 | expect(t, res.Header().Get(ContentType), ContentHTML+"; charset=UTF-8")
326 | expect(t, res.Body.String(), "Admin jeremy
\n")
327 | }
328 |
329 | func Test_Render_Delimiters(t *testing.T) {
330 | m := martini.Classic()
331 | m.Use(Renderer(Options{
332 | Delims: Delims{"{[{", "}]}"},
333 | Directory: "fixtures/basic",
334 | }))
335 |
336 | // routing
337 | m.Get("/foobar", func(r Render) {
338 | r.HTML(200, "delims", "jeremy")
339 | })
340 |
341 | res := httptest.NewRecorder()
342 | req, _ := http.NewRequest("GET", "/foobar", nil)
343 |
344 | m.ServeHTTP(res, req)
345 |
346 | expect(t, res.Code, 200)
347 | expect(t, res.Header().Get(ContentType), ContentHTML+"; charset=UTF-8")
348 | expect(t, res.Body.String(), "Hello jeremy
")
349 | }
350 |
351 | func Test_Render_BinaryData(t *testing.T) {
352 | m := martini.Classic()
353 | m.Use(Renderer(Options{
354 | // nothing here to configure
355 | }))
356 |
357 | // routing
358 | m.Get("/foobar", func(r Render) {
359 | r.Data(200, []byte("hello there"))
360 | })
361 |
362 | res := httptest.NewRecorder()
363 | req, _ := http.NewRequest("GET", "/foobar", nil)
364 |
365 | m.ServeHTTP(res, req)
366 |
367 | expect(t, res.Code, 200)
368 | expect(t, res.Header().Get(ContentType), ContentBinary)
369 | expect(t, res.Body.String(), "hello there")
370 | }
371 |
372 | func Test_Render_BinaryData_CustomMimeType(t *testing.T) {
373 | m := martini.Classic()
374 | m.Use(Renderer(Options{
375 | // nothing here to configure
376 | }))
377 |
378 | // routing
379 | m.Get("/foobar", func(r Render) {
380 | r.Header().Set(ContentType, "image/jpeg")
381 | r.Data(200, []byte("..jpeg data.."))
382 | })
383 |
384 | res := httptest.NewRecorder()
385 | req, _ := http.NewRequest("GET", "/foobar", nil)
386 |
387 | m.ServeHTTP(res, req)
388 |
389 | expect(t, res.Code, 200)
390 | expect(t, res.Header().Get(ContentType), "image/jpeg")
391 | expect(t, res.Body.String(), "..jpeg data..")
392 | }
393 |
394 | func Test_Render_Status204(t *testing.T) {
395 | res := httptest.NewRecorder()
396 | r := renderer{res, nil, nil, Options{}, ""}
397 | r.Status(204)
398 | expect(t, res.Code, 204)
399 | }
400 |
401 | func Test_Render_Error404(t *testing.T) {
402 | res := httptest.NewRecorder()
403 | r := renderer{res, nil, nil, Options{}, ""}
404 | r.Error(404)
405 | expect(t, res.Code, 404)
406 | }
407 |
408 | func Test_Render_Error500(t *testing.T) {
409 | res := httptest.NewRecorder()
410 | r := renderer{res, nil, nil, Options{}, ""}
411 | r.Error(500)
412 | expect(t, res.Code, 500)
413 | }
414 |
415 | func Test_Render_Redirect_Default(t *testing.T) {
416 | url, _ := url.Parse("http://localhost/path/one")
417 | req := http.Request{
418 | Method: "GET",
419 | URL: url,
420 | }
421 | res := httptest.NewRecorder()
422 |
423 | r := renderer{res, &req, nil, Options{}, ""}
424 | r.Redirect("two")
425 |
426 | expect(t, res.Code, 302)
427 | expect(t, res.HeaderMap["Location"][0], "/path/two")
428 | }
429 |
430 | func Test_Render_Redirect_Code(t *testing.T) {
431 | url, _ := url.Parse("http://localhost/path/one")
432 | req := http.Request{
433 | Method: "GET",
434 | URL: url,
435 | }
436 | res := httptest.NewRecorder()
437 |
438 | r := renderer{res, &req, nil, Options{}, ""}
439 | r.Redirect("two", 307)
440 |
441 | expect(t, res.Code, 307)
442 | expect(t, res.HeaderMap["Location"][0], "/path/two")
443 | }
444 |
445 | func Test_Render_Charset_JSON(t *testing.T) {
446 | m := martini.Classic()
447 | m.Use(Renderer(Options{
448 | Charset: "foobar",
449 | }))
450 |
451 | // routing
452 | m.Get("/foobar", func(r Render) {
453 | r.JSON(300, Greeting{"hello", "world"})
454 | })
455 |
456 | res := httptest.NewRecorder()
457 | req, _ := http.NewRequest("GET", "/foobar", nil)
458 |
459 | m.ServeHTTP(res, req)
460 |
461 | expect(t, res.Code, 300)
462 | expect(t, res.Header().Get(ContentType), ContentJSON+"; charset=foobar")
463 | expect(t, res.Body.String(), `{"one":"hello","two":"world"}`)
464 | }
465 |
466 | func Test_Render_Default_Charset_HTML(t *testing.T) {
467 | m := martini.Classic()
468 | m.Use(Renderer(Options{
469 | Directory: "fixtures/basic",
470 | }))
471 |
472 | // routing
473 | m.Get("/foobar", func(r Render) {
474 | r.HTML(200, "hello", "jeremy")
475 | })
476 |
477 | res := httptest.NewRecorder()
478 | req, _ := http.NewRequest("GET", "/foobar", nil)
479 |
480 | m.ServeHTTP(res, req)
481 |
482 | expect(t, res.Code, 200)
483 | expect(t, res.Header().Get(ContentType), ContentHTML+"; charset=UTF-8")
484 | // ContentLength should be deferred to the ResponseWriter and not Render
485 | expect(t, res.Header().Get(ContentLength), "")
486 | expect(t, res.Body.String(), "Hello jeremy
\n")
487 | }
488 |
489 | func Test_Render_Override_Layout(t *testing.T) {
490 | m := martini.Classic()
491 | m.Use(Renderer(Options{
492 | Directory: "fixtures/basic",
493 | Layout: "layout",
494 | }))
495 |
496 | // routing
497 | m.Get("/foobar", func(r Render) {
498 | r.HTML(200, "content", "jeremy", HTMLOptions{
499 | Layout: "another_layout",
500 | })
501 | })
502 |
503 | res := httptest.NewRecorder()
504 | req, _ := http.NewRequest("GET", "/foobar", nil)
505 |
506 | m.ServeHTTP(res, req)
507 |
508 | expect(t, res.Code, 200)
509 | expect(t, res.Header().Get(ContentType), ContentHTML+"; charset=UTF-8")
510 | expect(t, res.Body.String(), "another head\njeremy
\n\nanother foot\n")
511 | }
512 |
513 | func Test_Render_NoRace(t *testing.T) {
514 | // This test used to fail if run with -race
515 | m := martini.Classic()
516 | m.Use(Renderer(Options{
517 | Directory: "fixtures/basic",
518 | }))
519 |
520 | // routing
521 | m.Get("/foobar", func(r Render) {
522 | r.HTML(200, "hello", "world")
523 | })
524 |
525 | done := make(chan bool)
526 | doreq := func() {
527 | res := httptest.NewRecorder()
528 | req, _ := http.NewRequest("GET", "/foobar", nil)
529 |
530 | m.ServeHTTP(res, req)
531 |
532 | expect(t, res.Code, 200)
533 | expect(t, res.Header().Get(ContentType), ContentHTML+"; charset=UTF-8")
534 | // ContentLength should be deferred to the ResponseWriter and not Render
535 | expect(t, res.Header().Get(ContentLength), "")
536 | expect(t, res.Body.String(), "Hello world
\n")
537 | done <- true
538 | }
539 | // Run two requests to check there is no race condition
540 | go doreq()
541 | go doreq()
542 | <-done
543 | <-done
544 | }
545 |
546 | func Test_GetExt(t *testing.T) {
547 | expect(t, getExt("test"), "")
548 | expect(t, getExt("test.tmpl"), ".tmpl")
549 | expect(t, getExt("test.go.html"), ".go.html")
550 | }
551 |
552 | /* Test Helpers */
553 | func expect(t *testing.T, a interface{}, b interface{}) {
554 | if a != b {
555 | t.Errorf("Expected %v (type %v) - Got %v (type %v)", b, reflect.TypeOf(b), a, reflect.TypeOf(a))
556 | }
557 | }
558 |
559 | func refute(t *testing.T, a interface{}, b interface{}) {
560 | if a == b {
561 | t.Errorf("Did not expect %v (type %v) - Got %v (type %v)", b, reflect.TypeOf(b), a, reflect.TypeOf(a))
562 | }
563 | }
564 |
--------------------------------------------------------------------------------