From 435f2848153e6e3f971700a4e702766ca81a7585 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Sun, 24 Jan 2021 14:08:37 +0200 Subject: [PATCH] New feature: Fallback views. Read HISTORY.md --- HISTORY.md | 12 ++ _examples/view/fallback/main.go | 46 ++++++ _examples/view/fallback/view/fallback.html | 1 + aliases.go | 18 ++ configuration.go | 14 ++ context/configuration.go | 2 + context/context.go | 181 ++++++++++++++++++--- context/view.go | 22 ++- core/router/api_builder.go | 20 +++ core/router/party.go | 12 ++ iris.go | 6 +- view/amber.go | 4 +- view/django.go | 2 +- view/handlebars.go | 2 +- view/html.go | 4 +- view/view.go | 14 +- 16 files changed, 316 insertions(+), 44 deletions(-) create mode 100644 _examples/view/fallback/main.go create mode 100644 _examples/view/fallback/view/fallback.html diff --git a/HISTORY.md b/HISTORY.md index 307e4b65..fd3b914a 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -28,6 +28,18 @@ The codebase for Dependency Injection, Internationalization and localization and ## Fixes and Improvements +- New `FallbackView` feature, per-party or per handler chain. Example can be found at: [_examples/view/fallback](_examples/view/fallback). + +```go + app.FallbackView(iris.FallbackViewFunc(func(ctx iris.Context, err iris.ErrViewNotExist) error { + // err.Name is the previous template name. + // err.IsLayout reports whether the failure came from the layout template. + // err.Data is the template data provided to the previous View call. + // [...custom logic e.g. ctx.View("fallback.html", err.Data)] + return err + })) +``` + - New `versioning.Aliases` middleware and up to 80% faster version resolve. Example Code: ```go diff --git a/_examples/view/fallback/main.go b/_examples/view/fallback/main.go new file mode 100644 index 00000000..3c722673 --- /dev/null +++ b/_examples/view/fallback/main.go @@ -0,0 +1,46 @@ +package main + +import ( + "github.com/kataras/iris/v12" +) + +const defaultViewName = "fallback" + +func main() { + app := iris.New() + app.RegisterView(iris.HTML("./view", ".html")) + + // Use the FallbackView helper Register a fallback view + // filename per-party when the provided was not found. + app.FallbackView(iris.FallbackView("fallback.html")) + + // Use the FallbackViewLayout helper to register a fallback view layout. + app.FallbackView(iris.FallbackViewLayout("layout.html")) + + // Register a custom fallback function per-party to handle everything. + // You can register more than one. If fails (returns a not nil error of ErrViewNotExists) + // then it proceeds to the next registered fallback. + app.FallbackView(iris.FallbackViewFunc(func(ctx iris.Context, err iris.ErrViewNotExist) error { + // err.Name is the previous template name. + // err.IsLayout reports whether the failure came from the layout template. + // err.Data is the template data provided to the previous View call. + // [...custom logic e.g. ctx.View("fallback.html", err.Data)] + return err + })) + + app.Get("/", index) + + app.Listen(":8080") +} + +// Register fallback view(s) in a middleware. +// func fallbackInsideAMiddleware(ctx iris.Context) { +// ctx.FallbackView(...) +// To remove all previous registered fallbacks, pass nil. +// ctx.FallbackView(nil) +// ctx.Next() +// } + +func index(ctx iris.Context) { + ctx.View("blabla.html") +} diff --git a/_examples/view/fallback/view/fallback.html b/_examples/view/fallback/view/fallback.html new file mode 100644 index 00000000..c10ca04c --- /dev/null +++ b/_examples/view/fallback/view/fallback.html @@ -0,0 +1 @@ +

Fallback view

\ No newline at end of file diff --git a/aliases.go b/aliases.go index bda2c3b5..8d596cca 100644 --- a/aliases.go +++ b/aliases.go @@ -252,6 +252,24 @@ var ( Ace = view.Ace ) +type ( + // ErrViewNotExist reports whether a template was not found in the parsed templates tree. + ErrViewNotExist = context.ErrViewNotExist + // FallbackViewFunc is a function that can be registered + // to handle view fallbacks. It accepts the Context and + // a special error which contains information about the previous template error. + // It implements the FallbackViewProvider interface. + // + // See `Context.View` method. + FallbackViewFunc = context.FallbackViewFunc + // FallbackView is a helper to register a single template filename as a fallback + // when the provided tempate filename was not found. + FallbackView = context.FallbackView + // FallbackViewLayout is a helper to register a single template filename as a fallback + // layout when the provided layout filename was not found. + FallbackViewLayout = context.FallbackViewLayout +) + // PrefixDir returns a new FileSystem that opens files // by adding the given "prefix" to the directory tree of "fs". // diff --git a/configuration.go b/configuration.go index 7e96aac9..17b9a910 100644 --- a/configuration.go +++ b/configuration.go @@ -797,6 +797,11 @@ type Configuration struct { // // Defaults to "iris.view.data". ViewDataContextKey string `ini:"view_data_context_key" json:"viewDataContextKey,omitempty" yaml:"ViewDataContextKey" toml:"ViewDataContextKey"` + // FallbackViewContextKey is the context's values key + // responsible to store the view fallback information. + // + // Defaults to "iris.view.fallback". + FallbackViewContextKey string `ini:"fallback_view_context_key" json:"fallbackViewContextKey,omitempty" yaml:"FallbackViewContextKey" toml:"FallbackViewContextKey"` // RemoteAddrHeaders are the allowed request headers names // that can be valid to parse the client's IP based on. // By-default no "X-" header is consired safe to be used for retrieving the @@ -999,6 +1004,11 @@ func (c Configuration) GetViewDataContextKey() string { return c.ViewDataContextKey } +// GetFallbackViewContextKey returns the FallbackViewContextKey field. +func (c Configuration) GetFallbackViewContextKey() string { + return c.FallbackViewContextKey +} + // GetRemoteAddrHeaders returns the RemoteAddrHeaders field. func (c Configuration) GetRemoteAddrHeaders() []string { return c.RemoteAddrHeaders @@ -1155,6 +1165,9 @@ func WithConfiguration(c Configuration) Configurator { if v := c.ViewDataContextKey; v != "" { main.ViewDataContextKey = v } + if v := c.FallbackViewContextKey; v != "" { + main.FallbackViewContextKey = v + } if v := c.RemoteAddrHeaders; len(v) > 0 { main.RemoteAddrHeaders = v @@ -1228,6 +1241,7 @@ func DefaultConfiguration() Configuration { ViewEngineContextKey: "iris.view.engine", ViewLayoutContextKey: "iris.view.layout", ViewDataContextKey: "iris.view.data", + FallbackViewContextKey: "iris.view.fallback", RemoteAddrHeaders: nil, RemoteAddrHeadersForce: false, RemoteAddrPrivateSubnets: []netutil.IPRange{ diff --git a/context/configuration.go b/context/configuration.go index c2dcb6e6..4670666c 100644 --- a/context/configuration.go +++ b/context/configuration.go @@ -62,6 +62,8 @@ type ConfigurationReadOnly interface { GetViewLayoutContextKey() string // GetViewDataContextKey returns the ViewDataContextKey field. GetViewDataContextKey() string + // GetFallbackViewContextKey returns the FallbackViewContextKey field. + GetFallbackViewContextKey() string // GetRemoteAddrHeaders returns RemoteAddrHeaders field. GetRemoteAddrHeaders() []string diff --git a/context/context.go b/context/context.go index 22cf0ce1..1c4b4ffc 100644 --- a/context/context.go +++ b/context/context.go @@ -2971,6 +2971,147 @@ func (ctx *Context) GetViewData() map[string]interface{} { return nil } +// FallbackViewProvider is an interface which can be registered to the `Party.FallbackView` +// or `Context.FallbackView` methods to handle fallback views. +// See FallbackView, FallbackViewLayout and FallbackViewFunc. +type FallbackViewProvider interface { + FallbackView(ctx *Context, err ErrViewNotExist) error +} /* Notes(@kataras): If ever requested, this fallback logic (of ctx, error) can go to all necessary methods. + I've designed with a bit more complexity here instead of a simple filename fallback in order to give + the freedom to the developer to do whatever he/she wants with that template/layout not exists error, + e.g. have a list of fallbacks views to loop through until succeed or fire a different error than the default. + We also provide some helpers for common fallback actions (FallbackView, FallbackViewLayout). + This naming was chosen in order to be easy to follow up with the previous view-relative context features. + Also note that here we catch a specific error, we want the developer + to be aware of the rest template errors (e.g. when a template having parsing issues). +*/ + +// FallbackViewFunc is a function that can be registered +// to handle view fallbacks. It accepts the Context and +// a special error which contains information about the previous template error. +// It implements the FallbackViewProvider interface. +// +// See `Context.View` method. +type FallbackViewFunc func(ctx *Context, err ErrViewNotExist) error + +// FallbackView completes the FallbackViewProvider interface. +func (fn FallbackViewFunc) FallbackView(ctx *Context, err ErrViewNotExist) error { + return fn(ctx, err) +} + +var ( + _ FallbackViewProvider = FallbackView("") + _ FallbackViewProvider = FallbackViewLayout("") +) + +// FallbackView is a helper to register a single template filename as a fallback +// when the provided tempate filename was not found. +type FallbackView string + +// FallbackView completes the FallbackViewProvider interface. +func (f FallbackView) FallbackView(ctx *Context, err ErrViewNotExist) error { + if err.IsLayout { // Not responsible to render layouts. + return err + } + + // ctx.StatusCode(200) // Let's keep the previous status code here, developer can change it anyways. + return ctx.View(string(f), err.Data) +} + +// FallbackViewLayout is a helper to register a single template filename as a fallback +// layout when the provided layout filename was not found. +type FallbackViewLayout string + +// FallbackView completes the FallbackViewProvider interface. +func (f FallbackViewLayout) FallbackView(ctx *Context, err ErrViewNotExist) error { + if !err.IsLayout { + // Responsible to render layouts only. + return err + } + + ctx.ViewLayout(string(f)) + return ctx.View(err.Name, err.Data) +} + +const fallbackViewOnce = "iris.fallback.view.once" + +func (ctx *Context) fireFallbackViewOnce(err ErrViewNotExist) error { + // Note(@kataras): this is our way to keep the same View method for + // both fallback and normal views, remember, we export the whole + // Context functionality to the end-developer through the fallback view provider. + if ctx.values.Get(fallbackViewOnce) != nil { + return err + } + + v := ctx.values.Get(ctx.app.ConfigurationReadOnly().GetFallbackViewContextKey()) + if v == nil { + return err + } + + providers, ok := v.([]FallbackViewProvider) + if !ok { + return err + } + + ctx.values.Set(fallbackViewOnce, struct{}{}) + + var pErr error + for _, provider := range providers { + pErr = provider.FallbackView(ctx, err) + if pErr != nil { + if vErr, ok := pErr.(ErrViewNotExist); ok { + // This fallback view does not exist or it's not responsible to handle, + // try the next. + pErr = vErr + continue + } + } + + // If OK then we found the correct fallback. + // If the error was a parse error and not a template not found + // then exit and report the pErr error. + break + } + + return pErr +} + +// FallbackView registers one or more fallback views for a template or a template layout. +// When View cannot find the given filename to execute then this "provider" +// is responsible to handle the error or render a different view. +// +// Usage: +// FallbackView(iris.FallbackView("fallback.html")) +// FallbackView(iris.FallbackViewLayout("layouts/fallback.html")) +// OR +// FallbackView(iris.FallbackViewFunc(ctx iris.Context, err iris.ErrViewNotExist) error { +// err.Name is the previous template name. +// err.IsLayout reports whether the failure came from the layout template. +// err.Data is the template data provided to the previous View call. +// [...custom logic e.g. ctx.View("fallback", err.Data)] +// }) +func (ctx *Context) FallbackView(providers ...FallbackViewProvider) { + key := ctx.app.ConfigurationReadOnly().GetFallbackViewContextKey() + if key == "" { + return + } + + v := ctx.values.Get(key) + if v == nil { + ctx.values.Set(key, providers) + return + } + + // Can register more than one. + storedProviders, ok := v.([]FallbackViewProvider) + if !ok { + return + } + + storedProviders = append(storedProviders, providers...) + ctx.values.Set(key, storedProviders) +} + // View renders a template based on the registered view engine(s). // First argument accepts the filename, relative to the view engine's Directory and Extension, // i.e: if directory is "./templates" and want to render the "./templates/users/index.html" @@ -2985,8 +3126,26 @@ func (ctx *Context) GetViewData() map[string]interface{} { // Examples: https://github.com/kataras/iris/tree/master/_examples/view func (ctx *Context) View(filename string, optionalViewModel ...interface{}) error { ctx.ContentType(ContentHTMLHeaderValue) - cfg := ctx.app.ConfigurationReadOnly() + err := ctx.renderView(filename, optionalViewModel...) + if errNotExists, ok := err.(ErrViewNotExist); ok { + err = ctx.fireFallbackViewOnce(errNotExists) + } + + if err != nil { + if ctx.app.Logger().Level == golog.DebugLevel { + // send the error back to the client, when debug mode. + ctx.StopWithError(http.StatusInternalServerError, err) + } else { + ctx.StopWithStatus(http.StatusInternalServerError) + } + } + + return err +} + +func (ctx *Context) renderView(filename string, optionalViewModel ...interface{}) error { + cfg := ctx.app.ConfigurationReadOnly() layout := ctx.values.GetString(cfg.GetViewLayoutContextKey()) var bindingData interface{} @@ -3000,28 +3159,12 @@ func (ctx *Context) View(filename string, optionalViewModel ...interface{}) erro 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 + return engine.ExecuteWriter(ctx, filename, layout, bindingData) } } } - err := ctx.app.View(ctx, filename, layout, bindingData) // if failed it logs the error. - if err != nil { - if ctx.app.Logger().Level == golog.DebugLevel { - // send the error back to the client, when debug mode. - ctx.StopWithError(http.StatusInternalServerError, err) - } else { - ctx.StopWithStatus(http.StatusInternalServerError) - } - } - - return err + return ctx.app.View(ctx, filename, layout, bindingData) } // getLogIdentifier returns the ID, or the client remote IP address, diff --git a/context/view.go b/context/view.go index 35ac1788..dcf1bced 100644 --- a/context/view.go +++ b/context/view.go @@ -1,6 +1,26 @@ package context -import "io" +import ( + "fmt" + "io" +) + +// ErrViewNotExist it's an error. +// It reports whether a template was not found in the parsed templates tree. +type ErrViewNotExist struct { + Name string + IsLayout bool + Data interface{} +} + +// Error completes the `error` interface. +func (e ErrViewNotExist) Error() string { + title := "template" + if e.IsLayout { + title = "layout" + } + return fmt.Sprintf("%s '%s' does not exist", title, e.Name) +} // ViewEngine is the interface which all view engines should be implemented in order to be registered inside iris. type ViewEngine interface { diff --git a/core/router/api_builder.go b/core/router/api_builder.go index f29232dc..b960d91b 100644 --- a/core/router/api_builder.go +++ b/core/router/api_builder.go @@ -1491,6 +1491,26 @@ func (api *APIBuilder) RegisterView(viewEngine context.ViewEngine) { // to keep the iris.Application a compatible Party. } +// FallbackView registers one or more fallback views for a template or a template layout. +// Usage: +// FallbackView(iris.FallbackView("fallback.html")) +// FallbackView(iris.FallbackViewLayout("layouts/fallback.html")) +// OR +// FallbackView(iris.FallbackViewFunc(ctx iris.Context, err iris.ErrViewNotExist) error { +// err.Name is the previous template name. +// err.IsLayout reports whether the failure came from the layout template. +// err.Data is the template data provided to the previous View call. +// [...custom logic e.g. ctx.View("fallback", err.Data)] +// }) +func (api *APIBuilder) FallbackView(provider context.FallbackViewProvider) { + handler := func(ctx *context.Context) { + ctx.FallbackView(provider) + ctx.Next() + } + api.Use(handler) + api.UseError(handler) +} + // 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 06fe1f66..495e8f19 100644 --- a/core/router/party.go +++ b/core/router/party.go @@ -312,6 +312,18 @@ type Party interface { // To register a view engine per handler chain see the `Context.ViewEngine` instead. // Read `Configuration.ViewEngineContextKey` documentation for more. RegisterView(viewEngine context.ViewEngine) + // FallbackView registers one or more fallback views for a template or a template layout. + // Usage: + // FallbackView(iris.FallbackView("fallback.html")) + // FallbackView(iris.FallbackViewLayout("layouts/fallback.html")) + // OR + // FallbackView(iris.FallbackViewFunc(ctx iris.Context, err iris.ErrViewNotExist) error { + // err.Name is the previous template name. + // err.IsLayout reports whether the failure came from the layout template. + // err.Data is the template data provided to the previous View call. + // [...custom logic e.g. ctx.View("fallback", err.Data)] + // }) + FallbackView(provider context.FallbackViewProvider) // 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 aa4d6bbc..d5a5e714 100644 --- a/iris.go +++ b/iris.go @@ -410,11 +410,7 @@ func (app *Application) View(writer io.Writer, filename string, layout string, b return err } - err := app.view.ExecuteWriter(writer, filename, layout, bindingData) - if err != nil { - app.logger.Error(err) - } - return err + return app.view.ExecuteWriter(writer, filename, layout, bindingData) } // ConfigureHost accepts one or more `host#Configuration`, these configurators functions diff --git a/view/amber.go b/view/amber.go index 5e1f2b21..1bda5e1a 100644 --- a/view/amber.go +++ b/view/amber.go @@ -217,7 +217,7 @@ func (s *AmberEngine) executeTemplateBuf(name string, binding interface{}) (stri tmpl := s.fromCache(name) if tmpl == nil { s.bufPool.Put(buf) - return "", ErrNotExist{name, false} + return "", ErrNotExist{name, false, binding} } err := tmpl.ExecuteTemplate(buf, name, binding) @@ -253,5 +253,5 @@ func (s *AmberEngine) ExecuteWriter(w io.Writer, filename string, layout string, return tmpl.Execute(w, bindingData) } - return ErrNotExist{filename, false} + return ErrNotExist{filename, false, bindingData} } diff --git a/view/django.go b/view/django.go index c9427786..8d6b7d6a 100644 --- a/view/django.go +++ b/view/django.go @@ -307,5 +307,5 @@ func (s *DjangoEngine) ExecuteWriter(w io.Writer, filename string, _ string, bin return tmpl.ExecuteWriter(getPongoContext(bindingData), w) } - return ErrNotExist{filename, false} + return ErrNotExist{filename, false, bindingData} } diff --git a/view/handlebars.go b/view/handlebars.go index dd82cb49..a226f21c 100644 --- a/view/handlebars.go +++ b/view/handlebars.go @@ -235,5 +235,5 @@ func (s *HandlebarsEngine) ExecuteWriter(w io.Writer, filename string, layout st return err } - return ErrNotExist{fmt.Sprintf("%s (file: %s)", renderFilename, filename), false} + return ErrNotExist{fmt.Sprintf("%s (file: %s)", renderFilename, filename), false, bindingData} } diff --git a/view/html.go b/view/html.go index 94116ca3..151b9990 100644 --- a/view/html.go +++ b/view/html.go @@ -421,14 +421,14 @@ func (s *HTMLEngine) ExecuteWriter(w io.Writer, name string, layout string, bind t := s.Templates.Lookup(name) if t == nil { - return ErrNotExist{name, false} + return ErrNotExist{name, false, bindingData} } s.runtimeFuncsFor(t, name, bindingData) if layout = getLayout(layout, s.layout); layout != "" { lt := s.Templates.Lookup(layout) if lt == nil { - return ErrNotExist{layout, true} + return ErrNotExist{layout, true, bindingData} } s.layoutFuncsFor(lt, name, bindingData) diff --git a/view/view.go b/view/view.go index 24b33c15..86acf3b8 100644 --- a/view/view.go +++ b/view/view.go @@ -21,19 +21,7 @@ type ( ) // ErrNotExist reports whether a template was not found in the parsed templates tree. -type ErrNotExist struct { - Name string - IsLayout bool -} - -// Error implements the `error` interface. -func (e ErrNotExist) Error() string { - title := "template" - if e.IsLayout { - title = "layout" - } - return fmt.Sprintf("%s '%s' does not exist", title, e.Name) -} +type ErrNotExist = context.ErrViewNotExist // View is just a wrapper on top of the registered template engine. type View struct{ Engine }