New feature: Fallback views. Read HISTORY.md

This commit is contained in:
Gerasimos (Makis) Maropoulos 2021-01-24 14:08:37 +02:00
parent a2588e480d
commit 435f284815
No known key found for this signature in database
GPG Key ID: 5DBE766BD26A54E7
16 changed files with 316 additions and 44 deletions

View File

@ -28,6 +28,18 @@ The codebase for Dependency Injection, Internationalization and localization and
## Fixes and Improvements ## 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: - New `versioning.Aliases` middleware and up to 80% faster version resolve. Example Code:
```go ```go

View File

@ -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")
}

View File

@ -0,0 +1 @@
<h1>Fallback view</h1>

View File

@ -252,6 +252,24 @@ var (
Ace = view.Ace 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 // PrefixDir returns a new FileSystem that opens files
// by adding the given "prefix" to the directory tree of "fs". // by adding the given "prefix" to the directory tree of "fs".
// //

View File

@ -797,6 +797,11 @@ type Configuration struct {
// //
// Defaults to "iris.view.data". // Defaults to "iris.view.data".
ViewDataContextKey string `ini:"view_data_context_key" json:"viewDataContextKey,omitempty" yaml:"ViewDataContextKey" toml:"ViewDataContextKey"` 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 // RemoteAddrHeaders are the allowed request headers names
// that can be valid to parse the client's IP based on. // 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 // 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 return c.ViewDataContextKey
} }
// GetFallbackViewContextKey returns the FallbackViewContextKey field.
func (c Configuration) GetFallbackViewContextKey() string {
return c.FallbackViewContextKey
}
// GetRemoteAddrHeaders returns the RemoteAddrHeaders field. // GetRemoteAddrHeaders returns the RemoteAddrHeaders field.
func (c Configuration) GetRemoteAddrHeaders() []string { func (c Configuration) GetRemoteAddrHeaders() []string {
return c.RemoteAddrHeaders return c.RemoteAddrHeaders
@ -1155,6 +1165,9 @@ func WithConfiguration(c Configuration) Configurator {
if v := c.ViewDataContextKey; v != "" { if v := c.ViewDataContextKey; v != "" {
main.ViewDataContextKey = v main.ViewDataContextKey = v
} }
if v := c.FallbackViewContextKey; v != "" {
main.FallbackViewContextKey = v
}
if v := c.RemoteAddrHeaders; len(v) > 0 { if v := c.RemoteAddrHeaders; len(v) > 0 {
main.RemoteAddrHeaders = v main.RemoteAddrHeaders = v
@ -1228,6 +1241,7 @@ func DefaultConfiguration() Configuration {
ViewEngineContextKey: "iris.view.engine", ViewEngineContextKey: "iris.view.engine",
ViewLayoutContextKey: "iris.view.layout", ViewLayoutContextKey: "iris.view.layout",
ViewDataContextKey: "iris.view.data", ViewDataContextKey: "iris.view.data",
FallbackViewContextKey: "iris.view.fallback",
RemoteAddrHeaders: nil, RemoteAddrHeaders: nil,
RemoteAddrHeadersForce: false, RemoteAddrHeadersForce: false,
RemoteAddrPrivateSubnets: []netutil.IPRange{ RemoteAddrPrivateSubnets: []netutil.IPRange{

View File

@ -62,6 +62,8 @@ type ConfigurationReadOnly interface {
GetViewLayoutContextKey() string GetViewLayoutContextKey() string
// GetViewDataContextKey returns the ViewDataContextKey field. // GetViewDataContextKey returns the ViewDataContextKey field.
GetViewDataContextKey() string GetViewDataContextKey() string
// GetFallbackViewContextKey returns the FallbackViewContextKey field.
GetFallbackViewContextKey() string
// GetRemoteAddrHeaders returns RemoteAddrHeaders field. // GetRemoteAddrHeaders returns RemoteAddrHeaders field.
GetRemoteAddrHeaders() []string GetRemoteAddrHeaders() []string

View File

@ -2971,6 +2971,147 @@ func (ctx *Context) GetViewData() map[string]interface{} {
return nil 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). // 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, // 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" // 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 // Examples: https://github.com/kataras/iris/tree/master/_examples/view
func (ctx *Context) View(filename string, optionalViewModel ...interface{}) error { func (ctx *Context) View(filename string, optionalViewModel ...interface{}) error {
ctx.ContentType(ContentHTMLHeaderValue) 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()) layout := ctx.values.GetString(cfg.GetViewLayoutContextKey())
var bindingData interface{} var bindingData interface{}
@ -3000,28 +3159,12 @@ func (ctx *Context) View(filename string, optionalViewModel ...interface{}) erro
if key := cfg.GetViewEngineContextKey(); key != "" { if key := cfg.GetViewEngineContextKey(); key != "" {
if engineV := ctx.values.Get(key); engineV != nil { if engineV := ctx.values.Get(key); engineV != nil {
if engine, ok := engineV.(ViewEngine); ok { if engine, ok := engineV.(ViewEngine); ok {
err := engine.ExecuteWriter(ctx, filename, layout, bindingData) return 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. return ctx.app.View(ctx, filename, layout, bindingData)
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
} }
// getLogIdentifier returns the ID, or the client remote IP address, // getLogIdentifier returns the ID, or the client remote IP address,

View File

@ -1,6 +1,26 @@
package context 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. // ViewEngine is the interface which all view engines should be implemented in order to be registered inside iris.
type ViewEngine interface { type ViewEngine interface {

View File

@ -1491,6 +1491,26 @@ func (api *APIBuilder) RegisterView(viewEngine context.ViewEngine) {
// to keep the iris.Application a compatible Party. // 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. // Layout overrides the parent template layout with a more specific layout for this Party.
// It returns the current Party. // It returns the current Party.
// //

View File

@ -312,6 +312,18 @@ type Party interface {
// To register a view engine per handler chain see the `Context.ViewEngine` instead. // To register a view engine per handler chain see the `Context.ViewEngine` instead.
// Read `Configuration.ViewEngineContextKey` documentation for more. // Read `Configuration.ViewEngineContextKey` documentation for more.
RegisterView(viewEngine context.ViewEngine) 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. // Layout overrides the parent template layout with a more specific layout for this Party.
// It returns the current Party. // It returns the current Party.
// //

View File

@ -410,11 +410,7 @@ func (app *Application) View(writer io.Writer, filename string, layout string, b
return err return err
} }
err := app.view.ExecuteWriter(writer, filename, layout, bindingData) return app.view.ExecuteWriter(writer, filename, layout, bindingData)
if err != nil {
app.logger.Error(err)
}
return err
} }
// ConfigureHost accepts one or more `host#Configuration`, these configurators functions // ConfigureHost accepts one or more `host#Configuration`, these configurators functions

View File

@ -217,7 +217,7 @@ func (s *AmberEngine) executeTemplateBuf(name string, binding interface{}) (stri
tmpl := s.fromCache(name) tmpl := s.fromCache(name)
if tmpl == nil { if tmpl == nil {
s.bufPool.Put(buf) s.bufPool.Put(buf)
return "", ErrNotExist{name, false} return "", ErrNotExist{name, false, binding}
} }
err := tmpl.ExecuteTemplate(buf, name, 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 tmpl.Execute(w, bindingData)
} }
return ErrNotExist{filename, false} return ErrNotExist{filename, false, bindingData}
} }

View File

@ -307,5 +307,5 @@ func (s *DjangoEngine) ExecuteWriter(w io.Writer, filename string, _ string, bin
return tmpl.ExecuteWriter(getPongoContext(bindingData), w) return tmpl.ExecuteWriter(getPongoContext(bindingData), w)
} }
return ErrNotExist{filename, false} return ErrNotExist{filename, false, bindingData}
} }

View File

@ -235,5 +235,5 @@ func (s *HandlebarsEngine) ExecuteWriter(w io.Writer, filename string, layout st
return err return err
} }
return ErrNotExist{fmt.Sprintf("%s (file: %s)", renderFilename, filename), false} return ErrNotExist{fmt.Sprintf("%s (file: %s)", renderFilename, filename), false, bindingData}
} }

View File

@ -421,14 +421,14 @@ func (s *HTMLEngine) ExecuteWriter(w io.Writer, name string, layout string, bind
t := s.Templates.Lookup(name) t := s.Templates.Lookup(name)
if t == nil { if t == nil {
return ErrNotExist{name, false} return ErrNotExist{name, false, bindingData}
} }
s.runtimeFuncsFor(t, name, bindingData) s.runtimeFuncsFor(t, name, bindingData)
if layout = getLayout(layout, s.layout); layout != "" { if layout = getLayout(layout, s.layout); layout != "" {
lt := s.Templates.Lookup(layout) lt := s.Templates.Lookup(layout)
if lt == nil { if lt == nil {
return ErrNotExist{layout, true} return ErrNotExist{layout, true, bindingData}
} }
s.layoutFuncsFor(lt, name, bindingData) s.layoutFuncsFor(lt, name, bindingData)

View File

@ -21,19 +21,7 @@ type (
) )
// ErrNotExist reports whether a template was not found in the parsed templates tree. // ErrNotExist reports whether a template was not found in the parsed templates tree.
type ErrNotExist struct { type ErrNotExist = context.ErrViewNotExist
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)
}
// View is just a wrapper on top of the registered template engine. // View is just a wrapper on top of the registered template engine.
type View struct{ Engine } type View struct{ Engine }