From 5d480dc80117a9084adc9bd4a03d04230e16410d Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Wed, 5 Aug 2020 19:34:55 +0300 Subject: [PATCH] Ability to register a view engine per group of routes or for the current a chain of handlers Example at: https://github.com/kataras/iris/tree/master/_examples/view/context-view-engine --- HISTORY.md | 7 +- _examples/view/context-view-engine/main.go | 65 +++++++++++++++++++ .../views/admin/index.html | 3 + .../views/admin/layouts/main.html | 14 ++++ .../views/on-fly/index.html | 2 + .../context-view-engine/views/public/500.html | 12 ++++ .../views/public/index.html | 1 + .../views/public/layouts/error.html | 13 ++++ .../views/public/layouts/main.html | 13 ++++ .../views/public/partials/footer.html | 1 + aliases.go | 3 + configuration.go | 44 +++++++++---- context/configuration.go | 3 + context/context.go | 46 ++++++++++--- context/view.go | 22 +++++++ core/router/api_builder.go | 18 +++++ core/router/party.go | 5 ++ iris.go | 2 +- view/ace.go | 5 +- view/engine.go | 31 --------- view/funcs.go | 10 --- view/view.go | 28 ++++++++ 22 files changed, 282 insertions(+), 66 deletions(-) create mode 100644 _examples/view/context-view-engine/main.go create mode 100644 _examples/view/context-view-engine/views/admin/index.html create mode 100644 _examples/view/context-view-engine/views/admin/layouts/main.html create mode 100644 _examples/view/context-view-engine/views/on-fly/index.html create mode 100644 _examples/view/context-view-engine/views/public/500.html create mode 100644 _examples/view/context-view-engine/views/public/index.html create mode 100644 _examples/view/context-view-engine/views/public/layouts/error.html create mode 100644 _examples/view/context-view-engine/views/public/layouts/main.html create mode 100644 _examples/view/context-view-engine/views/public/partials/footer.html create mode 100644 context/view.go delete mode 100644 view/engine.go delete mode 100644 view/funcs.go diff --git a/HISTORY.md b/HISTORY.md index eb9b1db1..fa7a16f6 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -359,6 +359,8 @@ Response: Other Improvements: +- Ability to register a view engine per group of routes or for the current chain of handlers through `Party.RegisterView` and `Context.ViewEngine` respectfully. + - Add [Blocks](_examples/view/template_blocks_0) template engine. - Add [Ace](_examples/view/template_ace_0) template parser to the view engine and other minor improvements. @@ -492,8 +494,9 @@ New Package-level Variables: New Context Methods: -- `Context.SetErr(error)` and `Context.GetErr() error` helpers -- `Context.CompressWriter(bool) error` and `Context.CompressReader(bool) error` +- `Context.ViewEngine(ViewEngine)` to set a view engine on-fly for the current chain of handlers, responsible to render templates through `ctx.View`. [Example](_examples/view/context-view-engine). +- `Context.SetErr(error)` and `Context.GetErr() error` helpers. +- `Context.CompressWriter(bool) error` and `Context.CompressReader(bool) error`. - `Context.Clone() Context` returns a copy of the Context. - `Context.IsCanceled() bool` reports whether the request has been canceled by the client. - `Context.IsSSL() bool` reports whether the request is under HTTPS SSL (New `Configuration.SSLProxyHeaders` and `HostProxyHeaders` fields too). diff --git a/_examples/view/context-view-engine/main.go b/_examples/view/context-view-engine/main.go new file mode 100644 index 00000000..ddc6d90f --- /dev/null +++ b/_examples/view/context-view-engine/main.go @@ -0,0 +1,65 @@ +package main + +import "github.com/kataras/iris/v12" + +func main() { + app := iris.New() + // Register a root view engine, as usual, + // will be used to render files through Context.View method + // when no Party or Handler-specific view engine is available. + app.RegisterView(iris.Blocks("./views/public", ".html")) + + // http://localhost:8080 + app.Get("/", index) + + // Register a view engine per group of routes. + adminGroup := app.Party("/admin") + adminGroup.RegisterView(iris.Blocks("./views/admin", ".html")) + + // http://localhost:8080/admin + adminGroup.Get("/", admin) + + // Register a view engine on-fly for the current chain of handlers. + views := iris.Blocks("./views/on-fly", ".html") + if err := views.Load(); err != nil { + app.Logger().Fatal(err) + } + + // http://localhost:8080/on-fly + app.Get("/on-fly", setViews(views), onFly) + + app.Listen(":8080") +} + +func index(ctx iris.Context) { + data := iris.Map{ + "Title": "Public Index Title", + } + + ctx.ViewLayout("main") + ctx.View("index", data) +} + +func admin(ctx iris.Context) { + data := iris.Map{ + "Title": "Admin Panel", + } + + ctx.ViewLayout("main") + ctx.View("index", data) +} + +func setViews(views iris.ViewEngine) iris.Handler { + return func(ctx iris.Context) { + ctx.ViewEngine(views) + ctx.Next() + } +} + +func onFly(ctx iris.Context) { + data := iris.Map{ + "Message": "View engine changed through 'setViews' custom middleware.", + } + + ctx.View("index", data) +} diff --git a/_examples/view/context-view-engine/views/admin/index.html b/_examples/view/context-view-engine/views/admin/index.html new file mode 100644 index 00000000..f537f535 --- /dev/null +++ b/_examples/view/context-view-engine/views/admin/index.html @@ -0,0 +1,3 @@ +{{ define "content" }} +

Hello, Admin!

+{{ end }} \ No newline at end of file diff --git a/_examples/view/context-view-engine/views/admin/layouts/main.html b/_examples/view/context-view-engine/views/admin/layouts/main.html new file mode 100644 index 00000000..d83d5062 --- /dev/null +++ b/_examples/view/context-view-engine/views/admin/layouts/main.html @@ -0,0 +1,14 @@ + + + + + + {{ .Title }} + + + {{ template "content" .}} + + +

Copyright © 2020 Admin

+ + \ No newline at end of file diff --git a/_examples/view/context-view-engine/views/on-fly/index.html b/_examples/view/context-view-engine/views/on-fly/index.html new file mode 100644 index 00000000..4b3fd536 --- /dev/null +++ b/_examples/view/context-view-engine/views/on-fly/index.html @@ -0,0 +1,2 @@ +

On-fly

+

{{.Message}}

\ No newline at end of file diff --git a/_examples/view/context-view-engine/views/public/500.html b/_examples/view/context-view-engine/views/public/500.html new file mode 100644 index 00000000..37c58b58 --- /dev/null +++ b/_examples/view/context-view-engine/views/public/500.html @@ -0,0 +1,12 @@ + +{{ define "content" }} +

Internal Server Error

+{{ end }} + +{{ define "message" }} +

{{.Message}}

+{{ end }} \ No newline at end of file diff --git a/_examples/view/context-view-engine/views/public/index.html b/_examples/view/context-view-engine/views/public/index.html new file mode 100644 index 00000000..b11758cf --- /dev/null +++ b/_examples/view/context-view-engine/views/public/index.html @@ -0,0 +1 @@ +

Index Body

\ No newline at end of file diff --git a/_examples/view/context-view-engine/views/public/layouts/error.html b/_examples/view/context-view-engine/views/public/layouts/error.html new file mode 100644 index 00000000..48598789 --- /dev/null +++ b/_examples/view/context-view-engine/views/public/layouts/error.html @@ -0,0 +1,13 @@ + + + + + + {{.Code}} + + + {{ template "content" .}} + + {{block "message" .}}{{end}} + + \ No newline at end of file diff --git a/_examples/view/context-view-engine/views/public/layouts/main.html b/_examples/view/context-view-engine/views/public/layouts/main.html new file mode 100644 index 00000000..5b5a509f --- /dev/null +++ b/_examples/view/context-view-engine/views/public/layouts/main.html @@ -0,0 +1,13 @@ + + + + + + {{ if .Title }}{{ .Title }}{{ else }}Default Main Title{{ end }} + + + {{ template "content" . }} + + + + \ No newline at end of file diff --git a/_examples/view/context-view-engine/views/public/partials/footer.html b/_examples/view/context-view-engine/views/public/partials/footer.html new file mode 100644 index 00000000..61ee8461 --- /dev/null +++ b/_examples/view/context-view-engine/views/public/partials/footer.html @@ -0,0 +1 @@ +

Footer Partial

\ No newline at end of file diff --git a/aliases.go b/aliases.go index 37bd621d..b2109572 100644 --- a/aliases.go +++ b/aliases.go @@ -23,6 +23,9 @@ type ( // Developers send responses to the client's request through a Context. // Developers get request information from the client's request by a Context. Context = *context.Context + // ViewEngine is an alias of `context.ViewEngine`. + // See HTML, Blocks, Django, Jet, Pug, Ace, Handlebars, Amber and e.t.c. + ViewEngine = context.ViewEngine // UnmarshalerFunc a shortcut, an alias for the `context#UnmarshalerFunc` type // which implements the `context#Unmarshaler` interface for reading request's body // via custom decoders, most of them already implement the `context#UnmarshalerFunc` diff --git a/configuration.go b/configuration.go index 5906a309..e9dcf3ab 100644 --- a/configuration.go +++ b/configuration.go @@ -756,18 +756,30 @@ type Configuration struct { // via a middleware through `SetVersion` method, e.g. `versioning.SetVersion(ctx, "1.0, 1.1")`. // Defaults to "iris.api.version". VersionContextKey string `json:"versionContextKey" yaml:"VersionContextKey" toml:"VersionContextKey"` - // GetViewLayoutContextKey is the key of the context's user values' key - // which is being used to set the template - // layout from a middleware or the main handler. - // Overrides the parent's or the configuration's. + // ViewEngineContextKey is the context's values key + // responsible to store and retrieve(view.Engine) the current view engine. + // A middleware or a Party can modify its associated value to change + // a view engine that `ctx.View` will render through. + // If not an engine is registered by the end-developer + // then its associated value is always nil, + // meaning that the default value is nil. + // See `Party.RegisterView` and `Context.ViewEngine` methods as well. // - // Defaults to "iris.ViewLayout" + // Defaults to "iris.view.engine". + ViewEngineContextKey string `json:"viewEngineContextKey,omitempty" yaml:"ViewEngineContextKey" toml:"ViewEngineContextKey"` + // ViewLayoutContextKey is the context's values key + // responsible to store and retrieve(string) the current view layout. + // A middleware can modify its associated value to change + // the layout that `ctx.View` will use to render a template. + // + // Defaults to "iris.view.layout". ViewLayoutContextKey string `json:"viewLayoutContextKey,omitempty" yaml:"ViewLayoutContextKey" toml:"ViewLayoutContextKey"` - // GetViewDataContextKey is the key of the context's user values' key - // which is being used to set the template - // binding data from a middleware or the main handler. + // ViewDataContextKey is the context's values key + // responsible to store and retrieve(interface{}) the current view binding data. + // A middleware can modify its associated value to change + // the template's data on-fly. // - // Defaults to "iris.viewData" + // Defaults to "iris.view.data". ViewDataContextKey string `json:"viewDataContextKey,omitempty" yaml:"ViewDataContextKey" toml:"ViewDataContextKey"` // RemoteAddrHeaders are the allowed request headers names // that can be valid to parse the client's IP based on. @@ -945,6 +957,11 @@ func (c Configuration) GetVersionContextKey() string { return c.VersionContextKey } +// GetViewEngineContextKey returns the ViewEngineContextKey field. +func (c Configuration) GetViewEngineContextKey() string { + return c.ViewEngineContextKey +} + // GetViewLayoutContextKey returns the ViewLayoutContextKey field. func (c Configuration) GetViewLayoutContextKey() string { return c.ViewLayoutContextKey @@ -1094,10 +1111,12 @@ func WithConfiguration(c Configuration) Configurator { main.VersionContextKey = v } + if v := c.ViewEngineContextKey; v != "" { + main.ViewEngineContextKey = v + } if v := c.ViewLayoutContextKey; v != "" { main.ViewLayoutContextKey = v } - if v := c.ViewDataContextKey; v != "" { main.ViewDataContextKey = v } @@ -1169,8 +1188,9 @@ func DefaultConfiguration() Configuration { LocaleContextKey: "iris.locale", LanguageContextKey: "iris.locale.language", VersionContextKey: "iris.api.version", - ViewLayoutContextKey: "iris.viewLayout", - ViewDataContextKey: "iris.viewData", + ViewEngineContextKey: "iris.view.engine", + ViewLayoutContextKey: "iris.view.layout", + ViewDataContextKey: "iris.view.data", RemoteAddrHeaders: nil, RemoteAddrHeadersForce: false, RemoteAddrPrivateSubnets: []netutil.IPRange{ diff --git a/context/configuration.go b/context/configuration.go index 26103e47..fa9091d7 100644 --- a/context/configuration.go +++ b/context/configuration.go @@ -51,6 +51,9 @@ type ConfigurationReadOnly interface { GetLanguageContextKey() string // GetVersionContextKey returns the VersionContextKey field. GetVersionContextKey() string + + // GetViewEngineContextKey returns the ViewEngineContextKey field. + GetViewEngineContextKey() string // GetViewLayoutContextKey returns the ViewLayoutContextKey field. GetViewLayoutContextKey() string // GetViewDataContextKey returns the ViewDataContextKey field. diff --git a/context/context.go b/context/context.go index 3986bb33..ab4f11bd 100644 --- a/context/context.go +++ b/context/context.go @@ -2392,16 +2392,22 @@ func (ctx *Context) CompressReader(enable bool) error { // | Rich Body Content Writers/Renderers | // +------------------------------------------------------------+ -const ( - // NoLayout to disable layout for a particular template file - NoLayout = "iris.nolayout" -) +// ViewEngine registers a view engine for the current chain of handlers. +// It overrides any previously registered engines, including the application's root ones. +// Note that, because performance is everything, +// the "engine" MUST be already ready-to-use, +// meaning that its `Load` method should be called once before this method call. +// +// To register a view engine per-group of groups too see `Party.RegisterView` instead. +func (ctx *Context) ViewEngine(engine ViewEngine) { + ctx.values.Set(ctx.app.ConfigurationReadOnly().GetViewEngineContextKey(), engine) +} // ViewLayout sets the "layout" option if and when .View // is being called afterwards, in the same request. // Useful when need to set or/and change a layout based on the previous handlers in the chain. // -// Note that the 'layoutTmplFile' argument can be set to iris.NoLayout || view.NoLayout || context.NoLayout +// Note that the 'layoutTmplFile' argument can be set to iris.NoLayout // to disable the layout for a specific view render action, // it disables the engine's configuration's layout property. // @@ -2418,7 +2424,7 @@ func (ctx *Context) ViewLayout(layoutTmplFile string) { // // If .View's "binding" argument is not nil and it's not a type of map // then these data are being ignored, binding has the priority, so the main route's handler can still decide. -// If binding is a map or context.Map then these data are being added to the view data +// If binding is a map or iris.Map then these data are being added to the view data // and passed to the template. // // After .View, the data are not destroyed, in order to be re-used if needed (again, in the same request as everything else), @@ -2457,7 +2463,7 @@ func (ctx *Context) ViewData(key string, value interface{}) { // A check for nil is always a good practise if different // kind of values or no data are registered via `ViewData`. // -// Similarly to `viewData := ctx.Values().Get("iris.viewData")` or +// Similarly to `viewData := ctx.Values().Get("iris.view.data")` or // `viewData := ctx.Values().Get(ctx.Application().ConfigurationReadOnly().GetViewDataContextKey())`. func (ctx *Context) GetViewData() map[string]interface{} { viewDataContextKey := ctx.app.ConfigurationReadOnly().GetViewDataContextKey() @@ -2514,7 +2520,21 @@ func (ctx *Context) View(filename string, optionalViewModel ...interface{}) erro bindingData = ctx.values.Get(cfg.GetViewDataContextKey()) } - err := ctx.app.View(ctx, filename, layout, bindingData) + if key := cfg.GetViewEngineContextKey(); key != "" { + if engineV := ctx.values.Get(key); engineV != nil { + if engine, ok := engineV.(ViewEngine); ok { + err := engine.ExecuteWriter(ctx, filename, layout, bindingData) + if err != nil { + ctx.app.Logger().Errorf("View [%v] [%T]: %v", ctx.getLogIdentifier(), engine, err) + return err + } + + return nil + } + } + } + + err := ctx.app.View(ctx, filename, layout, bindingData) // if failed it logs the error. if err != nil { ctx.StopWithStatus(http.StatusInternalServerError) } @@ -2522,6 +2542,16 @@ func (ctx *Context) View(filename string, optionalViewModel ...interface{}) erro return err } +// getLogIdentifier returns the ID, or the client remote IP address, +// useful for internal logging of context's method failure. +func (ctx *Context) getLogIdentifier() interface{} { + if id := ctx.GetID(); id != nil { + return id + } + + return ctx.RemoteAddr() +} + const ( // ContentBinaryHeaderValue header value for binary data. ContentBinaryHeaderValue = "application/octet-stream" diff --git a/context/view.go b/context/view.go new file mode 100644 index 00000000..ec8111e8 --- /dev/null +++ b/context/view.go @@ -0,0 +1,22 @@ +package context + +import "io" + +// ViewEngine is the interface which all view engines should be implemented in order to be registered inside iris. +type ViewEngine interface { + // Load should load the templates from a physical system directory or by an embedded one (assets/go-bindata). + Load() error + // ExecuteWriter should execute a template by its filename with an optional layout and bindingData. + ExecuteWriter(w io.Writer, filename string, layout string, bindingData interface{}) error + // Ext should return the final file extension which this view engine is responsible to render. + Ext() string +} + +// ViewEngineFuncer is an addition of a view engine, +// if a view engine implements that interface +// then iris can add some closed-relative iris functions +// like {{ url }}, {{ urlpath }} and {{ tr }}. +type ViewEngineFuncer interface { + // AddFunc should adds a function to the template's function map. + AddFunc(funcName string, funcBody interface{}) +} diff --git a/core/router/api_builder.go b/core/router/api_builder.go index fc5af9e0..e3d7c775 100644 --- a/core/router/api_builder.go +++ b/core/router/api_builder.go @@ -1022,6 +1022,24 @@ func (api *APIBuilder) OnAnyErrorCode(handlers ...context.Handler) (routes []*Ro return } +// RegisterView registers and loads a view engine middleware for that group of routes. +// It overrides any of the application's root registered view engines. +// To register a view engine per handler chain see the `Context.ViewEngine` instead. +// Read `Configuration.ViewEngineContextKey` documentation for more. +func (api *APIBuilder) RegisterView(viewEngine context.ViewEngine) { + if err := viewEngine.Load(); err != nil { + api.errors.Add(err) + return + } + + api.Use(func(ctx *context.Context) { + ctx.ViewEngine(viewEngine) + ctx.Next() + }) + // Note (@kataras): It does not return the Party in order + // to keep the iris.Application a compatible Party. +} + // Layout overrides the parent template layout with a more specific layout for this Party. // It returns the current Party. // diff --git a/core/router/party.go b/core/router/party.go index 8cb55a32..61b8e888 100644 --- a/core/router/party.go +++ b/core/router/party.go @@ -241,6 +241,11 @@ type Party interface { // Returns the GET *Route. Favicon(favPath string, requestPath ...string) *Route + // RegisterView registers and loads a view engine middleware for that group of routes. + // It overrides any of the application's root registered view engines. + // To register a view engine per handler chain see the `Context.ViewEngine` instead. + // Read `Configuration.ViewEngineContextKey` documentation for more. + RegisterView(viewEngine context.ViewEngine) // Layout overrides the parent template layout with a more specific layout for this Party. // It returns the current Party. // diff --git a/iris.go b/iris.go index f98640be..dd87d90b 100644 --- a/iris.go +++ b/iris.go @@ -280,7 +280,7 @@ func (app *Application) View(writer io.Writer, filename string, layout string, b } // ConfigureHost accepts one or more `host#Configuration`, these configurators functions -// can access the host created by `app.Run`, +// can access the host created by `app.Run` or `app.Listen`, // they're being executed when application is ready to being served to the public. // // It's an alternative way to interact with a host that is automatically created by diff --git a/view/ace.go b/view/ace.go index 329ffc1f..d658fd0c 100644 --- a/view/ace.go +++ b/view/ace.go @@ -21,10 +21,11 @@ func Ace(directory, extension string) *HTMLEngine { once := new(sync.Once) s.middleware = func(name string, text []byte) (contents string, err error) { once.Do(func() { // on first template parse, all funcs are given. - for k, v := range s.funcs { + for k, v := range emptyFuncs { funcs[k] = v } - for k, v := range emptyFuncs { + + for k, v := range s.funcs { funcs[k] = v } }) diff --git a/view/engine.go b/view/engine.go deleted file mode 100644 index 70fac87f..00000000 --- a/view/engine.go +++ /dev/null @@ -1,31 +0,0 @@ -package view - -import ( - "io" -) - -// NoLayout disables the configuration's layout for a specific execution. -const NoLayout = "iris.nolayout" - -// returns empty if it's no layout or empty layout and empty configuration's layout. -func getLayout(layout string, globalLayout string) string { - if layout == NoLayout { - return "" - } - - if layout == "" && globalLayout != "" { - return globalLayout - } - - return layout -} - -// Engine is the interface which all view engines should be implemented in order to be registered inside iris. -type Engine interface { - // Load should load the templates from a physical system directory or by an embedded one (assets/go-bindata). - Load() error - // ExecuteWriter should execute a template by its filename with an optional layout and bindingData. - ExecuteWriter(w io.Writer, filename string, layout string, bindingData interface{}) error - // Ext should return the final file extension which this view engine is responsible to render. - Ext() string -} diff --git a/view/funcs.go b/view/funcs.go deleted file mode 100644 index c3cdd1b5..00000000 --- a/view/funcs.go +++ /dev/null @@ -1,10 +0,0 @@ -package view - -// EngineFuncer is an addition of a view engine, -// if a view engine implements that interface -// then iris can add some closed-relative iris functions -// like {{ urlpath }} and {{ urlpath }}. -type EngineFuncer interface { - // AddFunc should adds a function to the template's function map. - AddFunc(funcName string, funcBody interface{}) -} diff --git a/view/view.go b/view/view.go index 9d2d1775..05abdf78 100644 --- a/view/view.go +++ b/view/view.go @@ -5,6 +5,18 @@ import ( "io" "path/filepath" "strings" + + "github.com/kataras/iris/v12/context" +) + +type ( + // Engine is the interface for a compatible Iris view engine. + // It's an alias of context.ViewEngine. + Engine = context.ViewEngine + // EngineFuncer is the interface for a compatible Iris view engine + // which accepts builtin framework functions such as url, urlpath and tr. + // It's an alias of context.ViewEngineFuncer. + EngineFuncer = context.ViewEngineFuncer ) // View is responsible to @@ -73,3 +85,19 @@ func (v *View) Load() error { } return nil } + +// NoLayout disables the configuration's layout for a specific execution. +const NoLayout = "iris.nolayout" + +// returns empty if it's no layout or empty layout and empty configuration's layout. +func getLayout(layout string, globalLayout string) string { + if layout == NoLayout { + return "" + } + + if layout == "" && globalLayout != "" { + return globalLayout + } + + return layout +}