1
0
mirror of https://github.com/kataras/iris.git synced 2025-12-18 02:17:05 +00:00

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
16 changed files with 316 additions and 44 deletions

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,