Implementation of https://github.com/kataras/iris/issues/412 , as requested. Read HISTORY for a code snippet

This commit is contained in:
Gerasimos Maropoulos 2016-09-29 17:05:22 +03:00
parent 4bf1d65877
commit f4b4dd0275
6 changed files with 161 additions and 40 deletions

View File

@ -2,6 +2,33 @@
**How to upgrade**: remove your `$GOPATH/src/github.com/kataras` folder, open your command-line and execute this command: `go get -u github.com/kataras/iris/iris`.
## 4.4.0 -> 4.4.1
- **NEW**: Template PreRenders, as requested [here](https://github.com/kataras/iris/issues/412).
```go
// ...
iris.UsePreRender(func(ctx *iris.Context, filename string, binding interface{}, options ...map[string]interface{}) bool {
// put the 'Error' binding here, for the shake of the test
if b, isMap := binding.(map[string]interface{}); isMap {
b["Error"] = "error!"
}
// true= continue to the next PreRender
// false= do not continue to the next PreRender
// * but the actual Render will be called at any case *
return true
})
iris.Get("/", func(ctx *Context) {
ctx.Render("hi.html", map[string]interface{}{"Username": "anybody"})
// hi.html: <h1>HI {{.Username}}. Error: {{.Error}}</h1>
})
// ...
```
## 4.3.0 -> 4.4.0
**NOTE**: For normal users this update offers nothing, read that only if you run Iris behind a proxy or balancer like `nginx` or you need to serve using a custom `net.Listener`.

View File

@ -19,7 +19,7 @@
<br/>
<a href="https://github.com/kataras/iris/releases"><img src="https://img.shields.io/badge/%20version%20-%204.4.0%20-blue.svg?style=flat-square" alt="Releases"></a>
<a href="https://github.com/kataras/iris/releases"><img src="https://img.shields.io/badge/%20version%20-%204.4.1%20-blue.svg?style=flat-square" alt="Releases"></a>
<a href="https://github.com/iris-contrib/examples"><img src="https://img.shields.io/badge/%20examples-repository-3362c2.svg?style=flat-square" alt="Examples"></a>
@ -197,7 +197,7 @@ I recommend writing your API tests using this new library, [httpexpect](https://
Versioning
------------
Current: **v4.4.0**
Current: **v4.4.1**
> Iris is an active project
@ -213,6 +213,7 @@ Todo
- [x] Implement, even, a better way to manage configuration/options, devs will be able to set their own custom options inside there. ` I'm thinking of something the last days, but it will have breaking changes. `
- [x] Implement an internal updater, as requested [here](https://github.com/kataras/iris/issues/401).
- [x] Support of using native servers and net.Listener instead of Iris' defined.[*](https://github.com/kataras/iris/issues/438)
- [x] Add context processors(PreRenders).[*](https://github.com/kataras/iris/issues/412)
Iris is a **Community-Driven** Project, waiting for your suggestions and [feature requests](https://github.com/kataras/iris/issues?utf8=%E2%9C%93&q=label%3A%22feature%20request%22)!

View File

@ -596,7 +596,7 @@ func (ctx *Context) RenderTemplateSource(status int, src string, binding interfa
// Note: the options: "gzip" and "charset" are built'n support by Iris, so you can pass these on any template engine or serialize engines
func (ctx *Context) RenderWithStatus(status int, name string, binding interface{}, options ...map[string]interface{}) (err error) {
if strings.IndexByte(name, '.') > -1 { //we have template
err = ctx.framework.templates.render(ctx, name, binding, options...)
err = ctx.framework.templates.renderFile(ctx, name, binding, options...)
} else {
err = ctx.renderSerialized(name, binding, options...)
}

View File

@ -657,5 +657,48 @@ func TestContextRenderRest(t *testing.T) {
markdownT := e.GET("/markdown").Expect().Status(StatusOK)
markdownT.Header("Content-Type").Equal("text/html; charset=UTF-8")
markdownT.Body().Equal("<h1>" + markdownContents[2:] + "</h1>\n")
}
func TestContextPreRender(t *testing.T) {
initDefault()
errMsg1 := "thereIsAnError"
UsePreRender(func(ctx *Context, src string, binding interface{}, options ...map[string]interface{}) bool {
// put the 'Error' binding here, for the shake of the test
if b, isMap := binding.(map[string]interface{}); isMap {
b["Error"] = errMsg1
}
// continue to the next prerender
return true
})
errMsg2 := "thereIsASecondError"
UsePreRender(func(ctx *Context, src string, binding interface{}, options ...map[string]interface{}) bool {
// put the 'Error' binding here, for the shake of the test
if b, isMap := binding.(map[string]interface{}); isMap {
prev := b["Error"].(string)
msg := prev + errMsg2
b["Error"] = msg
}
// DO NOT CONTINUE to the next prerender
return false
})
errMsg3 := "thereisAThirdError"
UsePreRender(func(ctx *Context, src string, binding interface{}, options ...map[string]interface{}) bool {
// put the 'Error' binding here, for the shake of the test
if b, isMap := binding.(map[string]interface{}); isMap {
prev := b["Error"].(string)
msg := prev + errMsg3
b["Error"] = msg
}
// doesn't matters the return statement, we don't have other prerender
return true
})
Get("/", func(ctx *Context) {
ctx.RenderTemplateSource(StatusOK, "<h1>HI {{.Username}}. Error: {{.Error}}</h1>", map[string]interface{}{"Username": "kataras"})
})
e := Tester(t)
expected := "<h1>HI kataras. Error: " + errMsg1 + errMsg2 + "</h1>"
e.GET("/").Expect().Status(StatusOK).Body().Contains(expected)
}

40
iris.go
View File

@ -79,7 +79,7 @@ import (
const (
// Version is the current version of the Iris web framework
Version = "4.4.0"
Version = "4.4.1"
banner = ` _____ _
|_ _| (_)
@ -149,6 +149,7 @@ type (
UseSessionDB(sessions.Database)
UseSerializer(string, serializer.Serializer)
UseTemplate(template.Engine) *template.Loader
UsePreRender(PreRender)
UseGlobal(...Handler)
UseGlobalFunc(...HandlerFunc)
Lookup(string) Route
@ -773,6 +774,28 @@ func (s *Framework) UseSerializer(forContentType string, e serializer.Serializer
s.serializers.For(forContentType, e)
}
// UsePreRender adds a Template's PreRender
// PreRender is typeof func(*iris.Context, filenameOrSource string, binding interface{}, options ...map[string]interface{}) bool
// PreRenders helps developers to pass middleware between the route Handler and a context.Render call
// all parameter receivers can be changed before passing it to the actual context's Render
// so, you can change the filenameOrSource, the page binding, the options, and even add cookies, session value or a flash message through ctx
// the return value of a PreRender is a boolean, if returns false then the next PreRender will not be executed, keep note
// that the actual context's Render will be called at any case.
func UsePreRender(pre PreRender) {
Default.UsePreRender(pre)
}
// UsePreRender adds a Template's PreRender
// PreRender is typeof func(*iris.Context, filenameOrSource string, binding interface{}, options ...map[string]interface{}) bool
// PreRenders helps developers to pass middleware between the route Handler and a context.Render call
// all parameter receivers can be changed before passing it to the actual context's Render
// so, you can change the filenameOrSource, the page binding, the options, and even add cookies, session value or a flash message through ctx
// the return value of a PreRender is a boolean, if returns false then the next PreRender will not be executed, keep note
// that the actual context's Render will be called at any case.
func (s *Framework) UsePreRender(pre PreRender) {
s.templates.usePreRender(pre)
}
// UseTemplate adds a template engine to the iris view system
// it does not build/load them yet
func UseTemplate(e template.Engine) *template.Loader {
@ -1942,6 +1965,20 @@ func (api *muxAPI) Favicon(favPath string, requestPath ...string) RouteNameFunc
return api.Get(reqPath, h)
}
// Layout oerrides the parent template layout with a more specific layout for this Party
// returns this Party, to continue as normal
// example:
// my := iris.Party("/my").Layout("layouts/mylayout.html")
// {
// my.Get("/", func(ctx *iris.Context) {
// ctx.MustRender("page1.html", nil)
// })
// }
//
func Layout(tmplLayoutFile string) MuxAPI {
return Default.Layout(tmplLayoutFile)
}
// Layout oerrides the parent template layout with a more specific layout for this Party
// returns this Party, to continue as normal
// example:
@ -1957,6 +1994,7 @@ func (api *muxAPI) Layout(tmplLayoutFile string) MuxAPI {
ctx.Set(TemplateLayoutContextKey, tmplLayoutFile)
ctx.Next()
})
return api
}

View File

@ -22,11 +22,21 @@ type (
// an example of this is the "layout" or "gzip" option
// same as Map but more specific name
RenderOptions map[string]interface{}
// PreRender is typeof func(*iris.Context, string, interface{},...map[string]interface{}) bool
// PreRenders helps developers to pass middleware between
// the route Handler and a context.Render (THAT can render 'rest' types also but the PreRender applies ONLY to template rendering(file or source)) call
// all parameter receivers can be changed before passing it to the actual context's Render
// so, you can change the filenameOrSource, the page binding, the options, and even add cookies, session value or a flash message through ctx
// the return value of a PreRender is a boolean, if returns false then the next PreRender will not be executed, keep note
// that the actual context's Render will be called at any case.
PreRender func(ctx *Context, filenameOrSource string, binding interface{}, options ...map[string]interface{}) bool
)
// templateEngines just a wrapper of template.Mux in order to use it's execute without break the whole of the API
type templateEngines struct {
*template.Mux
prerenders []PreRender
}
func newTemplateEngines(sharedFuncs map[string]interface{}) *templateEngines {
@ -51,12 +61,30 @@ func getCharsetOption(defaultValue string, options map[string]interface{}) strin
return defaultValue
}
func (t *templateEngines) usePreRender(pre PreRender) {
if t.prerenders == nil {
t.prerenders = make([]PreRender, 0)
}
t.prerenders = append(t.prerenders, pre)
}
// render executes a template and write its result to the context's body
// options are the optional runtime options can be passed by user and catched by the template engine when render
// an example of this is the "layout"
// note that gzip option is an iris dynamic option which exists for all template engines
// the gzip and charset options are built'n with iris
func (t *templateEngines) render(ctx *Context, filename string, binding interface{}, options ...map[string]interface{}) (err error) {
// template is passed as file or souce
func (t *templateEngines) render(isFile bool, ctx *Context, filenameOrSource string, binding interface{}, options []map[string]interface{}) error {
if len(t.prerenders) > 0 {
for i := range t.prerenders {
// I'm not making any checks here for performance reasons, means that
// if binding is pointer it can be changed, otherwise not.
if shouldContinue := t.prerenders[i](ctx, filenameOrSource, binding, options...); !shouldContinue {
break
}
}
}
// we do all these because we don't want to initialize a new map for each execution...
gzipEnabled := ctx.framework.Config.Gzip
charset := ctx.framework.Config.Charset
@ -65,12 +93,14 @@ func (t *templateEngines) render(ctx *Context, filename string, binding interfac
charset = getCharsetOption(charset, options[0])
}
ctxLayout := ctx.GetString(TemplateLayoutContextKey)
if ctxLayout != "" {
if len(options) > 0 {
options[0]["layout"] = ctxLayout
} else {
options = []map[string]interface{}{map[string]interface{}{"layout": ctxLayout}}
if isFile {
ctxLayout := ctx.GetString(TemplateLayoutContextKey)
if ctxLayout != "" {
if len(options) > 0 {
options[0]["layout"] = ctxLayout
} else {
options = []map[string]interface{}{map[string]interface{}{"layout": ctxLayout}}
}
}
}
@ -87,35 +117,17 @@ func (t *templateEngines) render(ctx *Context, filename string, binding interfac
} else {
out = ctx.Response.BodyWriter()
}
if isFile {
return t.ExecuteWriter(out, filenameOrSource, binding, options...)
}
return t.ExecuteRaw(filenameOrSource, out, binding)
err = t.ExecuteWriter(out, filename, binding, options...)
return err
}
// renderSource executes a template source raw contents (string) and write its result to the context's body
// note that gzip option is an iris dynamic option which exists for all template engines
// the gzip and charset options are built'n with iris
func (t *templateEngines) renderSource(ctx *Context, src string, binding interface{}, options ...map[string]interface{}) (err error) {
// we do all these because we don't want to initialize a new map for each execution...
gzipEnabled := ctx.framework.Config.Gzip
charset := ctx.framework.Config.Charset
if len(options) > 0 {
gzipEnabled = template.GetGzipOption(gzipEnabled, options[0])
charset = template.GetCharsetOption(charset, options[0])
}
ctx.SetContentType(contentHTML + "; charset=" + charset)
var out io.Writer
if gzipEnabled && ctx.clientAllowsGzip() {
ctx.RequestCtx.Response.Header.Add(varyHeader, acceptEncodingHeader)
ctx.SetHeader(contentEncodingHeader, "gzip")
gzipWriter := fs.AcquireGzipWriter(ctx.Response.BodyWriter())
defer fs.ReleaseGzipWriter(gzipWriter)
out = gzipWriter
} else {
out = ctx.Response.BodyWriter()
}
return t.ExecuteRaw(src, out, binding)
func (t *templateEngines) renderFile(ctx *Context, filename string, binding interface{}, options ...map[string]interface{}) error {
return t.render(true, ctx, filename, binding, options)
}
func (t *templateEngines) renderSource(ctx *Context, src string, binding interface{}, options ...map[string]interface{}) error {
return t.render(false, ctx, src, binding, options)
}