mirror of
https://github.com/kataras/iris.git
synced 2025-01-23 02:31:04 +01:00
New feature: Fallback views. Read HISTORY.md
This commit is contained in:
parent
a2588e480d
commit
435f284815
12
HISTORY.md
12
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
|
||||
|
|
46
_examples/view/fallback/main.go
Normal file
46
_examples/view/fallback/main.go
Normal 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")
|
||||
}
|
1
_examples/view/fallback/view/fallback.html
Normal file
1
_examples/view/fallback/view/fallback.html
Normal file
|
@ -0,0 +1 @@
|
|||
<h1>Fallback view</h1>
|
18
aliases.go
18
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".
|
||||
//
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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.
|
||||
//
|
||||
|
|
|
@ -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.
|
||||
//
|
||||
|
|
6
iris.go
6
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
|
||||
|
|
|
@ -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}
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
14
view/view.go
14
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 }
|
||||
|
|
Loading…
Reference in New Issue
Block a user