diff --git a/_examples/experimental-handlers/cors/simple/main.go b/_examples/experimental-handlers/cors/simple/main.go index c149044a..ea8f4e70 100644 --- a/_examples/experimental-handlers/cors/simple/main.go +++ b/_examples/experimental-handlers/cors/simple/main.go @@ -1,7 +1,6 @@ package main -// $ go get github.com/rs/cors -// $ go run main.go +// go get -u github.com/iris-contrib/middleware/... import ( "github.com/kataras/iris" @@ -12,14 +11,12 @@ import ( func main() { app := iris.New() - // `crs := cors.NewAllowAllPartyMiddleware()`, or: - crs := cors.NewPartyMiddleware(cors.Options{ + crs := cors.New(cors.Options{ AllowedOrigins: []string{"*"}, // allows everything, use that to change the hosts. AllowCredentials: true, }) - v1 := app.Party("/api/v1") - v1.ConfigureParty(crs) + v1 := app.Party("/api/v1", crs).AllowMethods(iris.MethodOptions) // <- important for the preflight. { v1.Get("/home", func(ctx iris.Context) { ctx.WriteString("Hello from /home") @@ -38,13 +35,5 @@ func main() { }) } - // or use that to wrap the entire router - // even before the path and method matching - // this should work better and with all cors' features. - // Use that instead, if suits you. - // app.WrapRouter(cors.WrapNext(cors.Options{ - // AllowedOrigins: []string{"*"}, - // AllowCredentials: true, - // })) app.Run(iris.Addr("localhost:8080")) } diff --git a/_examples/routing/fallback-handlers/main.go b/_examples/routing/fallback-handlers/main.go deleted file mode 100644 index 6d2b2d2d..00000000 --- a/_examples/routing/fallback-handlers/main.go +++ /dev/null @@ -1,29 +0,0 @@ -package main - -import "github.com/kataras/iris" - -func main() { - app := iris.New() - - // add a fallback handler to process requests that would not be declared in the router. - app.Fallback(fallbackHandler) - - // this works as expected now, - // will handle *all* expect DELETE requests, even if there is no routes. - app.Get("/action/{p}", h) - - app.Run(iris.Addr(":8080"), iris.WithoutServerError(iris.ErrServerClosed)) -} - -func h(ctx iris.Context) { - ctx.Writef("[%s] %s : Parameter = `%s`", ctx.Method(), ctx.Path(), ctx.Params().Get("p")) -} - -func fallbackHandler(ctx iris.Context) { - if ctx.Method() == iris.MethodDelete { - ctx.NextOrNotFound() - return - } - - ctx.Writef("[%s] %s : From fallback handler", ctx.Method(), ctx.Path()) -} diff --git a/context/application.go b/context/application.go index 6ac8bd1f..9a94fbcb 100644 --- a/context/application.go +++ b/context/application.go @@ -49,6 +49,7 @@ type Application interface { // then it creates & registers a new trivial handler on the-fly. FireErrorCode(ctx Context) - // RouteExists checks if a route exists - RouteExists(method string, path string, ctx Context) bool + // RouteExists reports whether a particular route exists + // It will search from the current subdomain of context's host, if not inside the root domain. + RouteExists(ctx Context, method, path string) bool } diff --git a/context/context.go b/context/context.go index e083306f..040258ee 100644 --- a/context/context.go +++ b/context/context.go @@ -910,7 +910,7 @@ type Context interface { // TransactionsSkipped returns true if the transactions skipped or canceled at all. TransactionsSkipped() bool - // Exec calls the framewrok's ServeCtx + // Exec calls the `context/Application#ServeCtx` // based on this context but with a changed method and path // like it was requested by the user, but it is not. // @@ -933,10 +933,11 @@ type Context interface { // Context's Values and the Session are kept in order to be able to communicate via the result route. // // It's for extreme use cases, 99% of the times will never be useful for you. - Exec(method string, path string) + Exec(method, path string) - // RouteExists checks if a route exists - RouteExists(method string, path string) bool + // RouteExists reports whether a particular route exists + // It will search from the current subdomain of context's host, if not inside the root domain. + RouteExists(method, path string) bool // Application returns the iris app instance which belongs to this context. // Worth to notice that this function returns an interface @@ -3164,53 +3165,57 @@ func (ctx *context) TransactionsSkipped() bool { // // It's for extreme use cases, 99% of the times will never be useful for you. func (ctx *context) Exec(method string, path string) { - if path != "" { - if method == "" { - method = "GET" - } - - // backup the handlers - backupHandlers := ctx.Handlers()[0:] - backupPos := ctx.HandlerIndex(-1) - - // backup the request path information - backupPath := ctx.Path() - backupMethod := ctx.Method() - // don't backupValues := ctx.Values().ReadOnly() - - // [sessions stays] - // [values stays] - // reset handlers - ctx.SetHandlers(nil) - - req := ctx.Request() - // set the request to be align with the 'againstRequestPath' - req.RequestURI = path - req.URL.Path = path - req.Method = method - // execute the route from the (internal) context router - // this way we keep the sessions and the values - ctx.Application().ServeHTTPC(ctx) - - // set back the old handlers and the last known index - ctx.SetHandlers(backupHandlers) - ctx.HandlerIndex(backupPos) - // set the request back to its previous state - req.RequestURI = backupPath - req.URL.Path = backupPath - req.Method = backupMethod - - // don't fill the values in order to be able to communicate from and to. - // // fill the values as they were before - // backupValues.Visit(func(key string, value interface{}) { - // ctx.Values().Set(key, value) - // }) + if path == "" { + return } + + if method == "" { + method = "GET" + } + + // backup the handlers + backupHandlers := ctx.Handlers()[0:] + backupPos := ctx.HandlerIndex(-1) + + // backup the request path information + backupPath := ctx.Path() + backupMethod := ctx.Method() + // don't backupValues := ctx.Values().ReadOnly() + + // [values stays] + // reset handlers + ctx.SetHandlers(nil) + + req := ctx.Request() + // set the request to be align with the 'againstRequestPath' + req.RequestURI = path + req.URL.Path = path + req.Method = method + req.Host = req.Host + + // execute the route from the (internal) context router + // this way we keep the sessions and the values + ctx.Application().ServeHTTPC(ctx) + + // set back the old handlers and the last known index + ctx.SetHandlers(backupHandlers) + ctx.HandlerIndex(backupPos) + // set the request back to its previous state + req.RequestURI = backupPath + req.URL.Path = backupPath + req.Method = backupMethod + + // don't fill the values in order to be able to communicate from and to. + // // fill the values as they were before + // backupValues.Visit(func(key string, value interface{}) { + // ctx.Values().Set(key, value) + // }) } -// RouteExists checks if a route exists -func (ctx *context) RouteExists(method string, path string) bool { - return ctx.Application().RouteExists(method, path, ctx) +// RouteExists reports whether a particular route exists +// It will search from the current subdomain of context's host, if not inside the root domain. +func (ctx *context) RouteExists(method, path string) bool { + return ctx.Application().RouteExists(ctx, method, path) } // Application returns the iris app instance which belongs to this context. diff --git a/core/router/api_builder.go b/core/router/api_builder.go index c5e0cc65..739641a1 100644 --- a/core/router/api_builder.go +++ b/core/router/api_builder.go @@ -42,6 +42,12 @@ type repository struct { } func (r *repository) register(route *Route) { + for _, r := range r.routes { + if r.String() == route.String() { + return // do not register any duplicates, the sooner the better. + } + } + r.routes = append(r.routes, route) } @@ -90,14 +96,17 @@ type APIBuilder struct { doneHandlers context.Handlers // global done handlers, order doesn't matter doneGlobalHandlers context.Handlers - // fallback stack, LIFO order, initialized on first `Fallback`. - fallbackStack *FallbackStack // the per-party relativePath string + // allowMethods are filled with the `AllowMethods` func. + // They are used to create new routes + // per any party's (and its children) routes registered + // if the method "x" wasn't registered already via the `Handle` (and its extensions like `Get`, `Post`...). + allowMethods []string } -var _ Party = &APIBuilder{} -var _ RoutesProvider = &APIBuilder{} // passed to the default request handler (routerHandler) +var _ Party = (*APIBuilder)(nil) +var _ RoutesProvider = (*APIBuilder)(nil) // passed to the default request handler (routerHandler) // NewAPIBuilder creates & returns a new builder // which is responsible to build the API and the router handler. @@ -108,20 +117,11 @@ func NewAPIBuilder() *APIBuilder { reporter: errors.NewReporter(), relativePath: "/", routes: new(repository), - fallbackStack: NewFallbackStack(), } return api } -// ConfigureParty configures this party like `iris.Application#Configure` -// That allows middlewares focused on the Party like CORS middleware -func (api *APIBuilder) ConfigureParty(conf ...PartyConfigurator) { - for _, h := range conf { - h(api) - } -} - // GetRelPath returns the current party's relative path. // i.e: // if r := app.Party("/users"), then the `r.GetRelPath()` is the "/users". @@ -140,6 +140,16 @@ func (api *APIBuilder) GetReporter() *errors.Reporter { return api.reporter } +// AllowMethods will re-register the future routes that will be registered +// via `Handle`, `Get`, `Post`, ... to the given "methods" on that Party and its children "Parties", +// duplicates are not registered. +// +// Call of `AllowMethod` will override any previous allow methods. +func (api *APIBuilder) AllowMethods(methods ...string) Party { + api.allowMethods = methods + return api +} + // Handle registers a route to the server's api. // if empty method is passed then handler(s) are being registered to all methods, same as .Any. // @@ -181,23 +191,30 @@ func (api *APIBuilder) Handle(method string, relativePath string, handlers ...co // here we separate the subdomain and relative path subdomain, path := splitSubdomainAndPath(fullpath) - r, err := NewRoute(method, subdomain, path, possibleMainHandlerName, routeHandlers, api.macros) - if err != nil { // template path parser errors: - api.reporter.Add("%v -> %s:%s:%s", err, method, subdomain, path) - return nil + // if allowMethods are empty, then simply register with the passed, main, method. + methods := append(api.allowMethods, method) + + var ( + route *Route // the latest one is this route registered, see methods append. + err error // not used outside of loop scope. + ) + + for _, m := range methods { + route, err = NewRoute(m, subdomain, path, possibleMainHandlerName, routeHandlers, api.macros) + if err != nil { // template path parser errors: + api.reporter.Add("%v -> %s:%s:%s", err, method, subdomain, path) + return nil // fail on first error. + } + + // Add UseGlobal & DoneGlobal Handlers + route.use(api.beginGlobalHandlers) + route.done(api.doneGlobalHandlers) + + // global + api.routes.register(route) } - // Add UseGlobal & DoneGlobal Handlers - r.use(api.beginGlobalHandlers) - r.done(api.doneGlobalHandlers) - - // global - api.routes.register(r) - - // per -party, used for done handlers - // api.apiRoutes = append(api.apiRoutes, r) - - return r + return route } // HandleMany works like `Handle` but can receive more than one @@ -270,6 +287,10 @@ func (api *APIBuilder) Party(relativePath string, handlers ...context.Handler) P // append the parent's + child's handlers middleware := joinHandlers(api.middleware, handlers) + // the allow methods per party and its children. + allowMethods := make([]string, len(api.allowMethods)) + copy(allowMethods, api.allowMethods) + return &APIBuilder{ // global/api builder macros: api.macros, @@ -279,10 +300,10 @@ func (api *APIBuilder) Party(relativePath string, handlers ...context.Handler) P doneGlobalHandlers: api.doneGlobalHandlers, reporter: api.reporter, // per-party/children - middleware: middleware, - doneHandlers: api.doneHandlers, - fallbackStack: api.fallbackStack.Fork(), - relativePath: fullpath, + middleware: middleware, + doneHandlers: api.doneHandlers[0:], + relativePath: fullpath, + allowMethods: allowMethods, } } @@ -434,21 +455,6 @@ func (api *APIBuilder) DoneGlobal(handlers ...context.Handler) { api.doneGlobalHandlers = append(api.doneGlobalHandlers, handlers...) } -// Fallback appends Handler(s) to the current fallback stack. -// Handler(s) is(are) called from Fallback stack when no route found and before sending NotFound status. -// Therefore Handler(s) in Fallback stack could send another thing than NotFound status, -// if `context.NextOrNotFound()` method is not called. -// Done & DoneGlobal Handlers are not called. -func (api *APIBuilder) Fallback(middleware ...context.Handler) { - api.fallbackStack.Add(middleware) -} - -// GetFallBackStack returns Fallback stack, this is implementation of interface RoutesProvider -// that is used in Router building by the RequestHandler. -func (api *APIBuilder) GetFallBackStack() *FallbackStack { - return api.fallbackStack -} - // Reset removes all the begin and done handlers that may derived from the parent party via `Use` & `Done`, // note that the `Reset` will not reset the handlers that are registered via `UseGlobal` & `DoneGlobal`. // diff --git a/core/router/fallback_stack.go b/core/router/fallback_stack.go deleted file mode 100644 index 9ff89dd6..00000000 --- a/core/router/fallback_stack.go +++ /dev/null @@ -1,65 +0,0 @@ -package router - -import "github.com/kataras/iris/context" - -// FallbackStack is a stack (with LIFO calling order) for fallback handlers -// A fallback handler(s) is(are) called from Fallback stack -// when no route found and before sending NotFound status. -// Therefore Handler(s) in Fallback stack could send another thing than NotFound status, -// if `context#NextOrNotFound()` method is not called. -// Done & DoneGlobal Handlers are not called. -type FallbackStack struct { - parent *FallbackStack - handlers context.Handlers -} - -// _size is a terminal recursive method for computing size the stack -func (stk *FallbackStack) _size(i int) int { - res := i + len(stk.handlers) - - if stk.parent == nil { - return res - } - - return stk.parent._size(res) -} - -// populate is a recursive method for concatenating handlers to `list` parameter -func (stk *FallbackStack) populate(list context.Handlers) { - n := copy(list, stk.handlers) - - if stk.parent != nil { - stk.parent.populate(list[n:]) - } -} - -// Size gives the size of the full stack hierarchy -func (stk *FallbackStack) Size() int { - return stk._size(0) -} - -// Add appends handlers to the beginning of the stack to have a LIFO calling order -func (stk *FallbackStack) Add(h context.Handlers) { - stk.handlers = append(stk.handlers, h...) - - copy(stk.handlers[len(h):], stk.handlers) - copy(stk.handlers, h) -} - -// Fork make a new stack from this stack, and so create a stack child (leaf from a tree of stacks) -func (stk *FallbackStack) Fork() *FallbackStack { - return &FallbackStack{ - parent: stk, - } -} - -// List concatenate all handlers in stack hierarchy -func (stk *FallbackStack) List() context.Handlers { - res := make(context.Handlers, stk.Size()) - stk.populate(res) - - return res -} - -// NewFallbackStack create a new empty Fallback stack. -func NewFallbackStack() *FallbackStack { return &FallbackStack{} } diff --git a/core/router/fallback_stack_test.go b/core/router/fallback_stack_test.go deleted file mode 100644 index da229c89..00000000 --- a/core/router/fallback_stack_test.go +++ /dev/null @@ -1,111 +0,0 @@ -package router_test - -import ( - "testing" - - "github.com/kataras/iris" - "github.com/kataras/iris/context" - "github.com/kataras/iris/core/router" - "github.com/kataras/iris/httptest" -) - -func TestFallbackStackAdd(t *testing.T) { - l := make([]string, 0) - - stk := &router.FallbackStack{} - stk.Add(context.Handlers{ - func(context.Context) { - l = append(l, "POS1") - }, - }) - - stk.Add(context.Handlers{ - func(context.Context) { - l = append(l, "POS2") - }, - }) - - if stk.Size() != 2 { - t.Fatalf("Bad size (%d != 2)", stk.Size()) - } - - for _, h := range stk.List() { - h(nil) - } - - if (l[0] != "POS2") || (l[1] != "POS1") { - t.Fatal("Bad positions: ", l) - } -} - -func TestFallbackStackFork(t *testing.T) { - l := make([]string, 0) - - stk := &router.FallbackStack{} - - stk.Add(context.Handlers{ - func(context.Context) { - l = append(l, "POS1") - }, - }) - - stk.Add(context.Handlers{ - func(context.Context) { - l = append(l, "POS2") - }, - }) - - stk = stk.Fork() - - stk.Add(context.Handlers{ - func(context.Context) { - l = append(l, "POS3") - }, - }) - - stk.Add(context.Handlers{ - func(context.Context) { - l = append(l, "POS4") - }, - }) - - if stk.Size() != 4 { - t.Fatalf("Bad size (%d != 4)", stk.Size()) - } - - for _, h := range stk.List() { - h(nil) - } - - if (l[0] != "POS4") || (l[1] != "POS3") || (l[2] != "POS2") || (l[3] != "POS1") { - t.Fatal("Bad positions: ", l) - } -} - -func TestFallbackStackCall(t *testing.T) { - // build the api - app := iris.New() - - // setup an existing routes - app.Handle("GET", "/route", func(ctx context.Context) { - ctx.WriteString("ROUTED") - }) - - // setup fallback handler - app.Fallback(func(ctx context.Context) { - if ctx.Method() != "GET" { - ctx.NextOrNotFound() // it checks if we have next, otherwise fire 404 not found. - return - } - - ctx.WriteString("FALLBACK") - }) - - // run the tests - e := httptest.New(t, app, httptest.Debug(false)) - - e.Request("GET", "/route").Expect().Status(iris.StatusOK).Body().Equal("ROUTED") - e.Request("POST", "/route").Expect().Status(iris.StatusNotFound) - e.Request("POST", "/noroute").Expect().Status(iris.StatusNotFound) - e.Request("GET", "/noroute").Expect().Status(iris.StatusOK).Body().Equal("FALLBACK") -} diff --git a/core/router/handler.go b/core/router/handler.go index b9b79202..323e693e 100644 --- a/core/router/handler.go +++ b/core/router/handler.go @@ -22,8 +22,8 @@ type RequestHandler interface { HandleRequest(context.Context) // Build should builds the handler, it's being called on router's BuildRouter. Build(provider RoutesProvider) error - // RouteExists checks if a route exists - RouteExists(method, path string, ctx context.Context) bool + // RouteExists reports whether a particular route exists. + RouteExists(ctx context.Context, method, path string) bool } type tree struct { @@ -35,14 +35,8 @@ type tree struct { } type routerHandler struct { - trees []*tree - hosts bool // true if at least one route contains a Subdomain. - fallbackStack *FallbackStack - // on build: true if fallbackStack.Size() > 0, - // reduces the checks because fallbackStack is NEVER nil (api_builder.go always initializes it). - // If re-checked needed (serve-time fallback handler added) - // then a re-build/refresh of the application's router is necessary, as with every handler. - hasFallbackHandlers bool + trees []*tree + hosts bool // true if at least one route contains a Subdomain. } var _ RequestHandler = &routerHandler{} @@ -90,15 +84,11 @@ func NewDefaultHandler() RequestHandler { type RoutesProvider interface { // api builder GetRoutes() []*Route GetRoute(routeName string) *Route - - GetFallBackStack() *FallbackStack } func (h *routerHandler) Build(provider RoutesProvider) error { registeredRoutes := provider.GetRoutes() h.trees = h.trees[0:0] // reset, inneed when rebuilding. - h.fallbackStack = provider.GetFallBackStack() - h.hasFallbackHandlers = h.fallbackStack.Size() > 0 // sort, subdomains goes first. sort.Slice(registeredRoutes, func(i, j int) bool { @@ -262,16 +252,12 @@ func (h *routerHandler) HandleRequest(ctx context.Context) { } } - if h.hasFallbackHandlers { - ctx.Do(h.fallbackStack.List()) - return - } - ctx.StatusCode(http.StatusNotFound) } -// RouteExists checks if a route exists -func (h *routerHandler) RouteExists(method, path string, ctx context.Context) bool { +// RouteExists reports whether a particular route exists +// It will search from the current subdomain of context's host, if not inside the root domain. +func (h *routerHandler) RouteExists(ctx context.Context, method, path string) bool { for i := range h.trees { t := h.trees[i] if method != t.Method { diff --git a/core/router/party.go b/core/router/party.go index 8a91e727..c83f508a 100644 --- a/core/router/party.go +++ b/core/router/party.go @@ -6,21 +6,11 @@ import ( "github.com/kataras/iris/core/router/macro" ) -// Party is here to separate the concept of -// api builder and the sub api builder. - -// PartyConfigurator is handler for configuring a party (it works with iris.Application) -type PartyConfigurator func(party Party) - // Party is just a group joiner of routes which have the same prefix and share same middleware(s) also. // Party could also be named as 'Join' or 'Node' or 'Group' , Party chosen because it is fun. // // Look the "APIBuilder" for its implementation. type Party interface { - // ConfigureParty configures this party like `iris.Application#Configure` - // That allows middlewares focused on the Party like CORS middleware - ConfigureParty(...PartyConfigurator) - // GetRelPath returns the current party's relative path. // i.e: // if r := app.Party("/users"), then the `r.GetRelPath()` is the "/users". @@ -67,13 +57,6 @@ type Party interface { // If the current Party is the root, then it registers the middleware to all child Parties' routes too. Use(middleware ...context.Handler) - // Fallback appends Handler(s) to the current Party's fallback stack. - // Handler(s) is(are) called from Fallback stack when no route found and before sending NotFound status. - // Therefore Handler(s) in Fallback stack could send another thing than NotFound status, - // if `Context.Next()` method is not called. - // Done Handler(s) is(are) not called. - Fallback(middleware ...context.Handler) - // Done appends to the very end, Handler(s) to the current Party's routes and child routes. // The difference from .Use is that this/or these Handler(s) are being always running last. Done(handlers ...context.Handler) @@ -82,6 +65,14 @@ type Party interface { // // Returns this Party. Reset() Party + + // AllowMethods will re-register the future routes that will be registered + // via `Handle`, `Get`, `Post`, ... to the given "methods" on that Party and its children "Parties", + // duplicates are not registered. + // + // Call of `AllowMethod` will override any previous allow methods. + AllowMethods(methods ...string) 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/router.go b/core/router/router.go index 891813e1..50526395 100644 --- a/core/router/router.go +++ b/core/router/router.go @@ -147,9 +147,10 @@ func (router *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) { router.mainHandler(w, r) } -// RouteExists checks if a route exists -func (router *Router) RouteExists(method, path string, ctx context.Context) bool { - return router.requestHandler.RouteExists(method, path, ctx) +// RouteExists reports whether a particular route exists +// It will search from the current subdomain of context's host, if not inside the root domain. +func (router *Router) RouteExists(ctx context.Context, method, path string) bool { + return router.requestHandler.RouteExists(ctx, method, path) } type wrapper struct { diff --git a/mvc/mvc.go b/mvc/mvc.go index 35b21cba..f11e02d4 100644 --- a/mvc/mvc.go +++ b/mvc/mvc.go @@ -176,13 +176,6 @@ func (app *Application) Handle(controller interface{}) *Application { return app } -// Fallback is an alias to `app.Router.Fallback(handlers...)` -// -// See `core/router#Party.Fallback` -func (app *Application) Fallback(handlers ...context.Handler) { - app.Router.Fallback(handlers...) -} - // Clone returns a new mvc Application which has the dependencies // of the current mvc Mpplication's dependencies. //