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

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
)
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".
//

View File

@ -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{

View File

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

View File

@ -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,

View File

@ -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 {

View File

@ -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.
//

View File

@ -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.
//

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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