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.