Skip to content

Commit

Permalink
New feature: Fallback views. Read HISTORY.md
Browse files Browse the repository at this point in the history
  • Loading branch information
kataras committed Jan 24, 2021
1 parent a2588e4 commit 435f284
Show file tree
Hide file tree
Showing 16 changed files with 316 additions and 44 deletions.
12 changes: 12 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
46 changes: 46 additions & 0 deletions _examples/view/fallback/main.go
Original file line number Diff line number Diff line change
@@ -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 change: 1 addition & 0 deletions _examples/view/fallback/view/fallback.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<h1>Fallback view</h1>
18 changes: 18 additions & 0 deletions aliases.go
Original file line number Diff line number Diff line change
Expand Up @@ -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".
//
Expand Down
14 changes: 14 additions & 0 deletions configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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{
Expand Down
2 changes: 2 additions & 0 deletions context/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
181 changes: 162 additions & 19 deletions context/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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{}
Expand All @@ -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,
Expand Down
22 changes: 21 additions & 1 deletion context/view.go
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
Loading

0 comments on commit 435f284

Please sign in to comment.