├── 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 [![wercker status](https://app.wercker.com/status/fcf6b26a1b41f53540200b1949b48dec "wercker status")](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\n

jeremy

\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\n

jeremy

\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\n

jeremy

\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 | --------------------------------------------------------------------------------