diff --git a/HISTORY.md b/HISTORY.md
index 6a705e657..eb6addcd2 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -21,6 +21,15 @@ Developers are not forced to upgrade if they don't really need it. Upgrade whene
**How to upgrade**: Open your command-line and execute this command: `go get github.com/kataras/iris/v12@latest`.
+# Mo, 10 February 2020 | v12.1.7
+
+Implement **new** `SetRegisterRule(iris.RouteOverride, RouteSkip, RouteError)` to resolve: https://github.com/kataras/iris/issues/1448
+
+New Examples:
+
+- [_examples/Docker](_examples/Docker)
+- [_examples/routing/route-register-rule](_examples/routing/route-register-rule)
+
# We, 05 February 2020 | v12.1.6
Fixes:
diff --git a/HISTORY_ES.md b/HISTORY_ES.md
index d9f238337..9cf22c7ed 100644
--- a/HISTORY_ES.md
+++ b/HISTORY_ES.md
@@ -21,9 +21,9 @@ Los desarrolladores no están obligados a actualizar si realmente no lo necesita
**Cómo actualizar**: Abra su línea de comandos y ejecute este comando: `go get github.com/kataras/iris/v12@latest`.
-# We, 05 February 2020 | v12.1.6
+# Mo, 10 February 2020 | v12.1.7
-Not translated yet, please navigate to the [english version](HISTORY.md#we-05-february-2020--v1216) instead.
+Not translated yet, please navigate to the [english version](HISTORY.md#mo-10-february-2020--v1217) instead.
# Sábado, 26 de octubre 2019 | v12.0.0
diff --git a/README.md b/README.md
index 3d4d59b63..ea501ab9e 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# News
-![](https://iris-go.com/images/release.png) Iris version **12.1.6** has been [released](HISTORY.md#we-05-february-2020--v1216)!
+![](https://iris-go.com/images/release.png) Iris version **12.1.7** has been [released](HISTORY.md#mo-10-february-2020--v1217)!
![](https://iris-go.com/images/cli.png) The official [Iris Command Line Interface](https://github.com/kataras/iris-cli) will soon be near you in 2020!
diff --git a/VERSION b/VERSION
index ec3291b45..9b41a4e0e 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-12.1.6:https://github.com/kataras/iris/releases/tag/v12.1.6
\ No newline at end of file
+12.1.7:https://github.com/kataras/iris/releases/tag/v12.1.7
\ No newline at end of file
diff --git a/_examples/README.md b/_examples/README.md
index fe2df2d8a..aa9380c83 100644
--- a/_examples/README.md
+++ b/_examples/README.md
@@ -143,6 +143,7 @@ Navigate through examples for a better understanding.
- [Writing a middleware](routing/writing-a-middleware)
* [per-route](routing/writing-a-middleware/per-route/main.go)
* [globally](routing/writing-a-middleware/globally/main.go)
+- [Route Register Rule](routing/route-register-rule/main.go) **NEW**
### Versioning
diff --git a/_examples/docker/go.mod b/_examples/docker/go.mod
index b9e400d06..bc58a1195 100644
--- a/_examples/docker/go.mod
+++ b/_examples/docker/go.mod
@@ -3,6 +3,6 @@ module app
go 1.13
require (
- github.com/kataras/iris/v12 v12.1.6
+ github.com/kataras/iris/v12 v12.1.7
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
)
diff --git a/_examples/routing/route-register-rule/main.go b/_examples/routing/route-register-rule/main.go
new file mode 100644
index 000000000..404039c00
--- /dev/null
+++ b/_examples/routing/route-register-rule/main.go
@@ -0,0 +1,41 @@
+package main
+
+import "github.com/kataras/iris/v12"
+
+func main() {
+ app := newApp()
+ // Navigate through https://github.com/kataras/iris/issues/1448 for details.
+ //
+ // GET: http://localhost:8080
+ // POST, PUT, DELETE, CONNECT, HEAD, PATCH, OPTIONS, TRACE : http://localhost:8080
+ app.Listen(":8080")
+}
+
+func newApp() *iris.Application {
+ app := iris.New()
+ // Skip and do NOT override existing regitered route, continue normally.
+ // Applies to a Party and its children, in this case the whole application's routes.
+ app.SetRegisterRule(iris.RouteSkip)
+
+ /* Read also:
+ // The default behavior, will override the getHandler to anyHandler on `app.Any` call.
+ app.SetRegistRule(iris.RouteOverride)
+
+ // Stops the execution and fires an error before server boot.
+ app.SetRegisterRule(iris.RouteError)
+ */
+
+ app.Get("/", getHandler)
+ // app.Any does NOT override the previous GET route because of `iris.RouteSkip` rule.
+ app.Any("/", anyHandler)
+
+ return app
+}
+
+func getHandler(ctx iris.Context) {
+ ctx.Writef("From %s", ctx.GetCurrentRoute().Trace())
+}
+
+func anyHandler(ctx iris.Context) {
+ ctx.Writef("From %s", ctx.GetCurrentRoute().Trace())
+}
diff --git a/_examples/routing/route-register-rule/main_test.go b/_examples/routing/route-register-rule/main_test.go
new file mode 100644
index 000000000..60c0d6e87
--- /dev/null
+++ b/_examples/routing/route-register-rule/main_test.go
@@ -0,0 +1,22 @@
+package main
+
+import (
+ "testing"
+
+ "github.com/kataras/iris/v12/core/router"
+ "github.com/kataras/iris/v12/httptest"
+)
+
+func TestRouteRegisterRuleExample(t *testing.T) {
+ app := newApp()
+ e := httptest.New(t, app)
+
+ for _, method := range router.AllMethods {
+ tt := e.Request(method, "/").Expect().Status(httptest.StatusOK).Body()
+ if method == "GET" {
+ tt.Equal("From [./main.go:28] GET: / -> github.com/kataras/iris/v12/_examples/routing/route-register-rule.getHandler()")
+ } else {
+ tt.Equal("From [./main.go:30] " + method + ": / -> github.com/kataras/iris/v12/_examples/routing/route-register-rule.anyHandler()")
+ }
+ }
+}
diff --git a/_examples/view/template_html_3/templates/page.html b/_examples/view/template_html_3/templates/page.html
index df4ef1251..8835b3d05 100644
--- a/_examples/view/template_html_3/templates/page.html
+++ b/_examples/view/template_html_3/templates/page.html
@@ -1,25 +1,55 @@
-/mypath
-
-
-
-/mypath2/{paramfirst}/{paramsecond}
-
-
-
-/mypath3/{paramfirst}/statichere/{paramsecond}
-
-
-
-
- /mypath4/{paramfirst}/statichere/{paramsecond}/{otherparam}/{something:path}
-
-
-
-
- /mypath5/{paramfirst}/statichere/{paramsecond}/{otherparam}/anything/{anything:path}
-
-
-
-
- /mypath6/{paramfirst}/{paramsecond}/statichere/{paramThirdAfterStatic}
-
+
+
+
+ template_html_3
+
+
+
+
+
+ /mypath
+
+
+
+ /mypath2/{paramfirst}/{paramsecond}
+
+
+
+ /mypath3/{paramfirst}/statichere/{paramsecond}
+
+
+
+
+ /mypath4/{paramfirst}/statichere/{paramsecond}/{otherparam}/{something:path}
+
+
+
+
+ /mypath5/{paramfirst}/statichere/{paramsecond}/{otherparam}/anything/{anything:path}
+
+
+
+
+ /mypath6/{paramfirst}/{paramsecond}/statichere/{paramThirdAfterStatic}
+
+
+
+
\ No newline at end of file
diff --git a/context/route.go b/context/route.go
index 1927b9b1c..5cdb4ccbf 100644
--- a/context/route.go
+++ b/context/route.go
@@ -43,6 +43,9 @@ type RouteReadOnly interface {
// ResolvePath returns the formatted path's %v replaced with the args.
ResolvePath(args ...string) string
+ // Trace returns some debug infos as a string sentence.
+ // Should be called after Build.
+ Trace() string
// Tmpl returns the path template,
// it contains the parsed template
diff --git a/core/router/api_builder.go b/core/router/api_builder.go
index ca56e8f8a..922c912c5 100644
--- a/core/router/api_builder.go
+++ b/core/router/api_builder.go
@@ -1,6 +1,7 @@
package router
import (
+ "fmt"
"net/http"
"os"
"path"
@@ -80,13 +81,20 @@ func (repo *repository) getAll() []*Route {
return repo.routes
}
-func (repo *repository) register(route *Route) {
+func (repo *repository) register(route *Route, rule RouteRegisterRule) (*Route, error) {
for i, r := range repo.routes {
// 14 August 2019 allow register same path pattern with different macro functions,
// see #1058
if route.DeepEqual(r) {
- // replace existing with the latest one.
- repo.routes = append(repo.routes[:i], repo.routes[i+1:]...)
+ if rule == RouteSkip {
+ return r, nil
+ } else if rule == RouteError {
+ return nil, fmt.Errorf("new route: %s conflicts with an already registered one: %s route", route.String(), r.String())
+ } else {
+ // replace existing with the latest one, the default behavior.
+ repo.routes = append(repo.routes[:i], repo.routes[i+1:]...)
+ }
+
continue
}
}
@@ -97,6 +105,7 @@ func (repo *repository) register(route *Route) {
}
repo.pos[route.tmpl.Src] = len(repo.routes) - 1
+ return route, nil
}
// APIBuilder the visible API for constructing the router
@@ -140,6 +149,8 @@ type APIBuilder struct {
// the per-party (and its children) execution rules for begin, main and done handlers.
handlerExecutionRules ExecutionRules
+ // the per-party (and its children) route registration rule, see `SetRegisterRule`.
+ routeRegisterRule RouteRegisterRule
}
var _ Party = (*APIBuilder)(nil)
@@ -210,15 +221,35 @@ func (api *APIBuilder) SetExecutionRules(executionRules ExecutionRules) Party {
return api
}
+// RouteRegisterRule is a type of uint8.
+// Defines the register rule for new routes that already exists.
+// Available values are: RouteOverride, RouteSkip and RouteError.
+//
+// See `Party#SetRegisterRule`.
+type RouteRegisterRule uint8
+
+const (
+ // RouteOverride an existing route with the new one, the default rule.
+ RouteOverride RouteRegisterRule = iota
+ // RouteSkip registering a new route twice.
+ RouteSkip
+ // RouteError log when a route already exists, shown after the `Build` state,
+ // server never starts.
+ RouteError
+)
+
+// SetRegisterRule sets a `RouteRegisterRule` for this Party and its children.
+// Available values are: RouteOverride (the default one), RouteSkip and RouteError.
+func (api *APIBuilder) SetRegisterRule(rule RouteRegisterRule) Party {
+ api.routeRegisterRule = rule
+ return api
+}
+
// CreateRoutes returns a list of Party-based Routes.
// It does NOT registers the route. Use `Handle, Get...` methods instead.
// This method can be used for third-parties Iris helpers packages and tools
// that want a more detailed view of Party-based Routes before take the decision to register them.
func (api *APIBuilder) CreateRoutes(methods []string, relativePath string, handlers ...context.Handler) []*Route {
- // if relativePath[0] != '/' {
- // return nil, errors.New("path should start with slash and should not be empty")
- // }
-
if len(methods) == 0 || methods[0] == "ALL" || methods[0] == "ANY" { // then use like it was .Any
return api.Any(relativePath, handlers...)
}
@@ -327,6 +358,7 @@ func (api *APIBuilder) Handle(method string, relativePath string, handlers ...co
routes := api.CreateRoutes([]string{method}, relativePath, handlers...)
var route *Route // the last one is returned.
+ var err error
for _, route = range routes {
if route == nil {
break
@@ -334,7 +366,10 @@ func (api *APIBuilder) Handle(method string, relativePath string, handlers ...co
// global
route.topLink = api.routes.getRelative(route)
- api.routes.register(route)
+ if route, err = api.routes.register(route, api.routeRegisterRule); err != nil {
+ api.errors.Add(err)
+ break
+ }
}
return route
@@ -441,7 +476,10 @@ func (api *APIBuilder) HandleDir(requestPath, directory string, opts ...DirOptio
for _, route := range routes {
route.MainHandlerName = `HandleDir(directory: "` + directory + `")`
- api.routes.register(route)
+ if _, err := api.routes.register(route, api.routeRegisterRule); err != nil {
+ api.errors.Add(err)
+ break
+ }
}
return getRoute
@@ -496,6 +534,7 @@ func (api *APIBuilder) Party(relativePath string, handlers ...context.Handler) P
relativePath: fullpath,
allowMethods: allowMethods,
handlerExecutionRules: api.handlerExecutionRules,
+ routeRegisterRule: api.routeRegisterRule,
}
}
diff --git a/core/router/party.go b/core/router/party.go
index 9f92b770e..e273272be 100644
--- a/core/router/party.go
+++ b/core/router/party.go
@@ -98,6 +98,9 @@ type Party interface {
//
// Example: https://github.com/kataras/iris/tree/master/_examples/mvc/middleware/without-ctx-next
SetExecutionRules(executionRules ExecutionRules) Party
+ // SetRegisterRule sets a `RouteRegisterRule` for this Party and its children.
+ // Available values are: RouteOverride (the default one), RouteSkip and RouteError.
+ SetRegisterRule(rule RouteRegisterRule) Party
// Handle registers a route to the server's router.
// if empty method is passed then handler(s) are being registered to all methods, same as .Any.
//
diff --git a/core/router/route_register_rule_test.go b/core/router/route_register_rule_test.go
new file mode 100644
index 000000000..a0fbb6559
--- /dev/null
+++ b/core/router/route_register_rule_test.go
@@ -0,0 +1,60 @@
+package router_test
+
+import (
+ "reflect"
+ "testing"
+
+ "github.com/kataras/iris/v12"
+ "github.com/kataras/iris/v12/core/router"
+ "github.com/kataras/iris/v12/httptest"
+)
+
+func TestRegisterRule(t *testing.T) {
+ app := iris.New()
+ v1 := app.Party("/v1")
+ v1.SetRegisterRule(iris.RouteSkip)
+
+ getHandler := func(ctx iris.Context) {
+ ctx.Writef("[get] %s", ctx.Method())
+ }
+
+ anyHandler := func(ctx iris.Context) {
+ ctx.Writef("[any] %s", ctx.Method())
+ }
+
+ getRoute := v1.Get("/", getHandler)
+ v1.Any("/", anyHandler)
+ if route := v1.Get("/", getHandler); !reflect.DeepEqual(route, getRoute) {
+ t.Fatalf("expected route to be equal with the original get route")
+ }
+
+ // test RouteSkip.
+ e := httptest.New(t, app, httptest.LogLevel("error"))
+ testRegisterRule(e, "[get] GET")
+
+ // test RouteOverride (default behavior).
+ v1.SetRegisterRule(iris.RouteOverride)
+ v1.Any("/", anyHandler)
+ app.RefreshRouter()
+ testRegisterRule(e, "[any] GET")
+
+ // test RouteError.
+ v1.SetRegisterRule(iris.RouteError)
+ if route := v1.Get("/", getHandler); route != nil {
+ t.Fatalf("expected duplicated route, with RouteError rule, to be nil but got: %#+v", route)
+ }
+ if expected, got := 1, len(v1.GetReporter().Errors); expected != got {
+ t.Fatalf("expected api builder's errors length to be: %d but got: %d", expected, got)
+ }
+}
+
+func testRegisterRule(e *httptest.Expect, expectedGetBody string) {
+ for _, method := range router.AllMethods {
+ tt := e.Request(method, "/v1").Expect().Status(httptest.StatusOK).Body()
+ if method == iris.MethodGet {
+ tt.Equal(expectedGetBody)
+ } else {
+ tt.Equal("[any] " + method)
+ }
+ }
+}
diff --git a/doc.go b/doc.go
index 1f2f6fd23..3e3ce9399 100644
--- a/doc.go
+++ b/doc.go
@@ -38,7 +38,7 @@ Source code and other details for the project are available at GitHub:
Current Version
-12.1.6
+12.1.7
Installation
diff --git a/iris.go b/iris.go
index 34cf205ea..268d4be6e 100644
--- a/iris.go
+++ b/iris.go
@@ -41,7 +41,7 @@ import (
)
// Version is the current version number of the Iris Web Framework.
-const Version = "12.1.6"
+const Version = "12.1.7"
// HTTP status codes as registered with IANA.
// See: http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml.
@@ -529,6 +529,18 @@ var (
XMLMap = context.XMLMap
)
+// Constants for input argument at `router.RouteRegisterRule`.
+// See `Party#SetRegisterRule`.
+const (
+ // RouteOverride an existing route with the new one, the default rule.
+ RouteOverride = router.RouteOverride
+ // RouteSkip registering a new route twice.
+ RouteSkip = router.RouteSkip
+ // RouteError log when a route already exists, shown after the `Build` state,
+ // server never starts.
+ RouteError = router.RouteError
+)
+
// Contains the enum values of the `Context.GetReferrer()` method,
// shortcuts of the context subpackage.
const (
@@ -668,6 +680,93 @@ func (app *Application) Shutdown(ctx stdContext.Context) error {
return nil
}
+// Build sets up, once, the framework.
+// It builds the default router with its default macros
+// and the template functions that are very-closed to iris.
+//
+// If error occurred while building the Application, the returns type of error will be an *errgroup.Group
+// which let the callers to inspect the errors and cause, usage:
+//
+// import "github.com/kataras/iris/v12/core/errgroup"
+//
+// errgroup.Walk(app.Build(), func(typ interface{}, err error) {
+// app.Logger().Errorf("%s: %s", typ, err)
+// })
+func (app *Application) Build() error {
+ rp := errgroup.New("Application Builder")
+
+ if !app.builded {
+ app.builded = true
+ rp.Err(app.APIBuilder.GetReporter())
+
+ if app.defaultMode { // the app.I18n and app.View will be not available until Build.
+ if !app.I18n.Loaded() {
+ for _, s := range []string{"./locales/*/*", "./locales/*", "./translations"} {
+ if _, err := os.Stat(s); os.IsNotExist(err) {
+ continue
+ }
+
+ if err := app.I18n.Load(s); err != nil {
+ continue
+ }
+
+ app.I18n.SetDefault("en-US")
+ break
+ }
+ }
+
+ if app.view.Len() == 0 {
+ for _, s := range []string{"./views", "./templates", "./web/views"} {
+ if _, err := os.Stat(s); os.IsNotExist(err) {
+ continue
+ }
+
+ app.RegisterView(HTML(s, ".html"))
+ break
+ }
+ }
+ }
+
+ if app.I18n.Loaded() {
+ // {{ tr "lang" "key" arg1 arg2 }}
+ app.view.AddFunc("tr", app.I18n.Tr)
+ app.WrapRouter(app.I18n.Wrapper())
+ }
+
+ if !app.Router.Downgraded() {
+ // router
+
+ if err := app.tryInjectLiveReload(); err != nil {
+ rp.Errf("LiveReload: init: failed: %v", err)
+ }
+
+ // create the request handler, the default routing handler
+ routerHandler := router.NewDefaultHandler()
+ err := app.Router.BuildRouter(app.ContextPool, routerHandler, app.APIBuilder, false)
+ if err != nil {
+ rp.Err(err)
+ }
+ // re-build of the router from outside can be done with
+ // app.RefreshRouter()
+ }
+
+ if app.view.Len() > 0 {
+ app.logger.Debugf("Application: %d registered view engine(s)", app.view.Len())
+ // view engine
+ // here is where we declare the closed-relative framework functions.
+ // Each engine has their defaults, i.e yield,render,render_r,partial, params...
+ rv := router.NewRoutePathReverser(app.APIBuilder)
+ app.view.AddFunc("urlpath", rv.Path)
+ // app.view.AddFunc("url", rv.URL)
+ if err := app.view.Load(); err != nil {
+ rp.Group("View Builder").Err(err)
+ }
+ }
+ }
+
+ return errgroup.Check(rp)
+}
+
// Runner is just an interface which accepts the framework instance
// and returns an error.
//
@@ -833,99 +932,26 @@ func Raw(f func() error) Runner {
}
}
-// Build sets up, once, the framework.
-// It builds the default router with its default macros
-// and the template functions that are very-closed to iris.
-//
-// If error occurred while building the Application, the returns type of error will be an *errgroup.Group
-// which let the callers to inspect the errors and cause, usage:
-//
-// import "github.com/kataras/iris/v12/core/errgroup"
-//
-// errgroup.Walk(app.Build(), func(typ interface{}, err error) {
-// app.Logger().Errorf("%s: %s", typ, err)
-// })
-func (app *Application) Build() error {
- rp := errgroup.New("Application Builder")
-
- if !app.builded {
- app.builded = true
- rp.Err(app.APIBuilder.GetReporter())
-
- if app.defaultMode { // the app.I18n and app.View will be not available until Build.
- if !app.I18n.Loaded() {
- for _, s := range []string{"./locales/*/*", "./locales/*", "./translations"} {
- if _, err := os.Stat(s); os.IsNotExist(err) {
- continue
- }
-
- if err := app.I18n.Load(s); err != nil {
- continue
- }
-
- app.I18n.SetDefault("en-US")
- break
- }
- }
-
- if app.view.Len() == 0 {
- for _, s := range []string{"./views", "./templates", "./web/views"} {
- if _, err := os.Stat(s); os.IsNotExist(err) {
- continue
- }
-
- app.RegisterView(HTML(s, ".html"))
- break
- }
- }
- }
-
- if app.I18n.Loaded() {
- // {{ tr "lang" "key" arg1 arg2 }}
- app.view.AddFunc("tr", app.I18n.Tr)
- app.WrapRouter(app.I18n.Wrapper())
- }
-
- if !app.Router.Downgraded() {
- // router
-
- if err := app.tryInjectLiveReload(); err != nil {
- rp.Errf("LiveReload: init: failed: %v", err)
- }
-
- // create the request handler, the default routing handler
- routerHandler := router.NewDefaultHandler()
- err := app.Router.BuildRouter(app.ContextPool, routerHandler, app.APIBuilder, false)
- if err != nil {
- rp.Err(err)
- }
- // re-build of the router from outside can be done with
- // app.RefreshRouter()
- }
-
- if app.view.Len() > 0 {
- app.logger.Debugf("Application: %d registered view engine(s)", app.view.Len())
- // view engine
- // here is where we declare the closed-relative framework functions.
- // Each engine has their defaults, i.e yield,render,render_r,partial, params...
- rv := router.NewRoutePathReverser(app.APIBuilder)
- app.view.AddFunc("urlpath", rv.Path)
- // app.view.AddFunc("url", rv.URL)
- if err := app.view.Load(); err != nil {
- rp.Group("View Builder").Err(err)
- }
- }
- }
-
- return errgroup.Check(rp)
-}
-
// ErrServerClosed is returned by the Server's Serve, ServeTLS, ListenAndServe,
// and ListenAndServeTLS methods after a call to Shutdown or Close.
//
// A shortcut for the `http#ErrServerClosed`.
var ErrServerClosed = http.ErrServerClosed
+// Listen builds the application and starts the server
+// on the TCP network address "host:port" which
+// handles requests on incoming connections.
+//
+// Listen always returns a non-nil error.
+// Ignore specific errors by using an `iris.WithoutServerError(iris.ErrServerClosed)`
+// as a second input argument.
+//
+// Listen is a shortcut of `app.Run(iris.Addr(hostPort, withOrWithout...))`.
+// See `Run` for details.
+func (app *Application) Listen(hostPort string, withOrWithout ...Configurator) error {
+ return app.Run(Addr(hostPort), withOrWithout...)
+}
+
// Run builds the framework and starts the desired `Runner` with or without configuration edits.
//
// Run should be called only once per Application instance, it blocks like http.Server.