diff --git a/HISTORY.md b/HISTORY.md index 0f416214..5ecc9f19 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -766,7 +766,6 @@ We have 8 policies, so far, and some of them have 'subpolicies' (the RouterRever - StaticPath - WildcardPath - URLPath - - RouteContextLinker - RouterBuilderPolicy - RouterWrapperPolicy - RenderPolicy diff --git a/adaptors/gorillamux/gorillamux.go b/adaptors/gorillamux/gorillamux.go index 12fd5401..9c7aa412 100644 --- a/adaptors/gorillamux/gorillamux.go +++ b/adaptors/gorillamux/gorillamux.go @@ -8,8 +8,8 @@ package gorillamux // package main // // import ( -// "gopkg.in/kataras/iris.v6/adaptors/gorillamux" // "gopkg.in/kataras/iris.v6" +// "gopkg.in/kataras/iris.v6/adaptors/gorillamux" // ) // // func main() { @@ -37,7 +37,8 @@ const dynamicSymbol = '{' // New returns a new gorilla mux router which can be plugged inside iris. // This is magic. func New() iris.Policies { - router := mux.NewRouter() + var router *mux.Router + var logger func(iris.LogMode, string) return iris.Policies{ EventPolicy: iris.EventPolicy{Boot: func(s *iris.Framework) { @@ -76,17 +77,11 @@ func New() iris.Policies { } return "" }, - RouteContextLinker: func(r iris.RouteInfo, ctx *iris.Context) { - if r == nil { - return - } - route := router.Get(r.Name()) - if route != nil { - mapToContext(ctx.Request, r.Middleware(), ctx) - } - }, }, RouterBuilderPolicy: func(repo iris.RouteRepository, context iris.ContextPool) http.Handler { + router = mux.NewRouter() // re-set the router here, + // the RouterBuilderPolicy re-runs on every method change (route "offline/online" states mostly) + repo.Visit(func(route iris.RouteInfo) { registerRoute(route, router, context) }) @@ -102,49 +97,47 @@ func New() iris.Policies { } } -func mapToContext(r *http.Request, middleware iris.Middleware, ctx *iris.Context) { - if params := mux.Vars(r); len(params) > 0 { - // set them with ctx.Set in order to be accesible by ctx.Param in the user's handler - for k, v := range params { - ctx.Set(k, v) - } - } - // including the iris.Default.Use/UseFunc and the route's middleware, - // main handler and any done handlers. - ctx.Middleware = middleware -} - // so easy: func registerRoute(route iris.RouteInfo, gorillaRouter *mux.Router, context iris.ContextPool) { - if route.IsOnline() { - handler := func(w http.ResponseWriter, r *http.Request) { - ctx := context.Acquire(w, r) - mapToContext(r, route.Middleware(), ctx) - ctx.Do() - - context.Release(ctx) - } - - // remember, we get a new iris.Route foreach of the HTTP Methods, so this should be work - methods := []string{route.Method()} - // if route has cors then we register the route with the "OPTIONS" method too - if route.HasCors() { - methods = append(methods, http.MethodOptions) - } - gorillaRoute := gorillaRouter.HandleFunc(route.Path(), handler).Methods(methods...).Name(route.Name()) - - subdomain := route.Subdomain() - if subdomain != "" { - if subdomain == "*." { - // it's an iris wildcard subdomain - // so register it as wildcard on gorilla mux too - subdomain = "{subdomain}." - } else { - // it's a static subdomain (which contains the dot) + handler := func(w http.ResponseWriter, r *http.Request) { + context.Run(w, r, func(ctx *iris.Context) { + if params := mux.Vars(ctx.Request); len(params) > 0 { + // set them with ctx.Set in order to be accesible by ctx.Param in the user's handler + for k, v := range params { + ctx.Set(k, v) + } } - // host = subdomain + listening host - gorillaRoute.Host(subdomain + context.Framework().Config.VHost) - } + // including the global middleware, done handlers too + ctx.Middleware = route.Middleware() + ctx.Do() + }) } + + // remember, we get a new iris.Route foreach of the HTTP Methods, so this should be work + methods := []string{route.Method()} + // if route has cors then we register the route with the "OPTIONS" method too + if route.HasCors() { + methods = append(methods, http.MethodOptions) + } + gorillaRoute := gorillaRouter.HandleFunc(route.Path(), handler). + Methods(methods...). + Name(route.Name()) + + subdomain := route.Subdomain() + if subdomain != "" { + if subdomain == "*." { + // it's an iris wildcard subdomain + // so register it as wildcard on gorilla mux too + subdomain = "{subdomain}." + } else { + // it's a static subdomain (which contains the dot) + } + // host = subdomain + listening host + gorillaRoute.Host(subdomain + context.Framework().Config.VHost) + } + + // Author's notes: even if the Method is iris.MethodNone + // the gorillamux saves the route, so we don't need to use the repo.OnMethodChanged + // and route.IsOnline() and we don't need the RouteContextLinker, we just serve like request on Offline routes* } diff --git a/adaptors/httprouter/httprouter.go b/adaptors/httprouter/httprouter.go index 78117a87..aed3a1e4 100644 --- a/adaptors/httprouter/httprouter.go +++ b/adaptors/httprouter/httprouter.go @@ -545,15 +545,12 @@ func New() iris.Policies { // // return fmt.Sprintf(r.formattedPath, arguments...) // }, - RouteContextLinker: func(r iris.RouteInfo, ctx *iris.Context) { - tree := mux.getTree(r.Method(), r.Subdomain()) - if tree != nil { - tree.entry.get(ctx.Request.URL.Path, ctx) - } - }, + }, RouterBuilderPolicy: func(repo iris.RouteRepository, context iris.ContextPool) http.Handler { fatalErr := false + mux.garden = mux.garden[0:0] // re-set the nodes + mux.hosts = false repo.Visit(func(r iris.RouteInfo) { if fatalErr { return diff --git a/context.go b/context.go index 2cbc09b0..60365a2c 100644 --- a/context.go +++ b/context.go @@ -277,7 +277,7 @@ func (ctx *Context) GetHandlerName() string { // BUT it isn't available by browsing, its handlers executed only when other handler's context call them // it can validate paths, has sessions, path parameters and all. // -// You can find the Route by iris.Lookup("theRouteName") +// You can find the Route by iris.Default.Routes().Lookup("theRouteName") // you can set a route name as: myRoute := iris.Default.Get("/mypath", handler)("theRouteName") // that will set a name to the route and returns its iris.Route instance for further usage. // @@ -288,8 +288,8 @@ func (ctx *Context) GetHandlerName() string { // For more details look: https://github.com/kataras/iris/issues/585 // // Example: https://github.com/iris-contrib/examples/tree/master/route_state -func (ctx *Context) ExecRoute(r RouteInfo) *Context { - return ctx.ExecRouteAgainst(r, ctx.Path()) +func (ctx *Context) ExecRoute(r RouteInfo) { + ctx.ExecRouteAgainst(r, ctx.Path()) } // ExecRouteAgainst calls any iris.Route against a 'virtually' request path @@ -298,7 +298,7 @@ func (ctx *Context) ExecRoute(r RouteInfo) *Context { // BUT it isn't available by browsing, its handlers executed only when other handler's context call them // it can validate paths, has sessions, path parameters and all. // -// You can find the Route by iris.Lookup("theRouteName") +// You can find the Route by iris.Default.Routes().Lookup("theRouteName") // you can set a route name as: myRoute := iris.Default.Get("/mypath", handler)("theRouteName") // that will set a name to the route and returns its iris.Route instance for further usage. // @@ -309,23 +309,34 @@ func (ctx *Context) ExecRoute(r RouteInfo) *Context { // For more details look: https://github.com/kataras/iris/issues/585 // // Example: https://github.com/iris-contrib/examples/tree/master/route_state -func (ctx *Context) ExecRouteAgainst(r RouteInfo, againstRequestPath string) *Context { - if r != nil { - context := &(*ctx) - context.Middleware = context.Middleware[0:0] - context.values.Reset() - context.Request.RequestURI = againstRequestPath - context.Request.URL.Path = againstRequestPath - context.Request.URL.RawPath = againstRequestPath - ctx.framework.policies.RouterReversionPolicy.RouteContextLinker(r, context) - // tree := ctx.framework.muxAPI.mux.getTree(r.Method(), r.Subdomain()) - // tree.entry.get(againstRequestPath, context) - if len(context.Middleware) > 0 { - context.Do() - return context - } +// +// User can get the response by simple using rec := ctx.Recorder(); rec.Body()/rec.StatusCode()/rec.Header() +// The route will be executed via the Router, as it would requested by client. +func (ctx *Context) ExecRouteAgainst(r RouteInfo, againstRequestPath string) { + if r != nil && againstRequestPath != "" { + // ok no need to clone the whole context, let's be dirty here for the sake of performance. + backupMidldeware := ctx.Middleware[0:] + backupPath := ctx.Path() + bakcupMethod := ctx.Method() + backupValues := ctx.values + backupPos := ctx.Pos + // sessions stays. + + ctx.values.Reset() + ctx.Middleware = ctx.Middleware[0:0] + ctx.Request.RequestURI = againstRequestPath + ctx.Request.URL.Path = againstRequestPath + ctx.Request.Method = r.Method() + + ctx.framework.Router.ServeHTTP(ctx.ResponseWriter, ctx.Request) + + ctx.Middleware = backupMidldeware + ctx.Request.RequestURI = backupPath + ctx.Request.URL.Path = backupPath + ctx.Request.Method = bakcupMethod + ctx.values = backupValues + ctx.Pos = backupPos } - return nil } // Prioritize is a middleware which executes a route against this path @@ -334,7 +345,7 @@ func (ctx *Context) ExecRouteAgainst(r RouteInfo, againstRequestPath string) *Co // if this function is not enough for you and you want to test more than one parameterized path // then use the: if c := ExecRoute(r); c == nil { /* move to the next, the route is not valid */ } // -// You can find the Route by iris.Lookup("theRouteName") +// You can find the Route by iris.Default.Routes().Lookup("theRouteName") // you can set a route name as: myRoute := iris.Default.Get("/mypath", handler)("theRouteName") // that will set a name to the route and returns its iris.Route instance for further usage. // @@ -346,10 +357,8 @@ func Prioritize(r RouteInfo) HandlerFunc { reqPath := ctx.Path() staticPath := ctx.framework.policies.RouterReversionPolicy.StaticPath(r.Path()) if strings.HasPrefix(reqPath, staticPath) { - newctx := ctx.ExecRouteAgainst(r, reqPath) - if newctx == nil { // route not found. - ctx.EmitError(StatusNotFound) - } + ctx.ExecRouteAgainst(r, reqPath) // returns 404 page from EmitErrors, these things depends on router adaptors + // we are done here. return } // execute the next handler if no prefix diff --git a/httptest/httptest.go b/httptest/httptest.go index 608728cc..fd633b6c 100644 --- a/httptest/httptest.go +++ b/httptest/httptest.go @@ -93,7 +93,7 @@ func New(app *iris.Framework, t *testing.T, setters ...OptionSetter) *httpexpect testConfiguration := httpexpect.Config{ BaseURL: baseURL, Client: &http.Client{ - Transport: httpexpect.NewBinder(app), + Transport: httpexpect.NewBinder(app.Router), Jar: httpexpect.NewJar(), }, Reporter: httpexpect.NewAssertReporter(t), diff --git a/iris.go b/iris.go index 126b6912..52b07337 100644 --- a/iris.go +++ b/iris.go @@ -340,6 +340,16 @@ func New(setters ...OptionSetter) *Framework { if routerBuilder != nil { // buid the router using user's selection build policy s.Router.build(routerBuilder) + + s.Router.repository.OnMethodChanged(func(route RouteInfo, oldMethod string) { + // set service not available temporarily until the router completes the building + // this won't take more than 100ms, but we want to inform the user. + s.Router.handler = ToNativeHandler(s, HandlerFunc(func(ctx *Context) { + ctx.EmitError(StatusServiceUnavailable) + })) + // Re-build the whole router if state changed (from offline to online state mostly) + s.Router.build(routerBuilder) + }) } } }}) diff --git a/policy.go b/policy.go index 63fbd477..ab899380 100644 --- a/policy.go +++ b/policy.go @@ -272,11 +272,6 @@ type ( // URLPath used for reverse routing on templates with {{ url }} and {{ path }} funcs. // Receives the route name and arguments and returns its http path URLPath func(r RouteInfo, args ...string) string - - // RouteContextLinker should put the route's handlers and named parameters(if any) to the ctx - // it's used to execute virtually an "offline" route - // against a context like it was requested by user, but it is not. - RouteContextLinker func(r RouteInfo, ctx *Context) } // RouterBuilderPolicy is the most useful Policy for custom routers. // A custom router should adapt this policy which is a func @@ -317,10 +312,6 @@ func (r RouterReversionPolicy) Adapt(frame *Policies) { if r.URLPath != nil { frame.RouterReversionPolicy.URLPath = r.URLPath } - - if r.RouteContextLinker != nil { - frame.RouterReversionPolicy.RouteContextLinker = r.RouteContextLinker - } } // Adapt adaps a RouterBuilderPolicy object to the main *Policies. diff --git a/policy_test.go b/policy_test.go index a0ba6111..33d77fcc 100644 --- a/policy_test.go +++ b/policy_test.go @@ -52,12 +52,6 @@ func newTestNativeRouter() Policies { } return path }, - RouteContextLinker: func(r RouteInfo, ctx *Context) { - if r == nil { - return - } - ctx.Middleware = r.Middleware() - }, }, RouterBuilderPolicy: func(repo RouteRepository, context ContextPool) http.Handler { servemux := http.NewServeMux() diff --git a/route.go b/route.go index 9992da29..86f5c463 100644 --- a/route.go +++ b/route.go @@ -237,6 +237,7 @@ func (r *routeRepository) ChangeMethod(routeInfo RouteInfo, } if valid { + route := r.getRouteByName(routeInfo.Name()) if route != nil && route.method != newMethod { oldMethod := route.method diff --git a/route_test.go b/route_test.go new file mode 100644 index 00000000..5cb08931 --- /dev/null +++ b/route_test.go @@ -0,0 +1,101 @@ +package iris_test + +import ( + "strconv" + "testing" + + "gopkg.in/kataras/iris.v6" + "gopkg.in/kataras/iris.v6/adaptors/gorillamux" + "gopkg.in/kataras/iris.v6/adaptors/httprouter" + "gopkg.in/kataras/iris.v6/httptest" +) + +func testRouteStateSimple(t *testing.T, router iris.Policy, offlineRoutePath string) { + app := iris.New() + app.Adapt(router) + + offlineRouteRequestedTestPath := "/api/user/42" + offlineBody := "user with id: 42" + + offlineRoute := app.None(offlineRoutePath, func(ctx *iris.Context) { + userid := ctx.Param("userid") + if userid != "42" { + // we are expecting userid 42 always in this test so + t.Fatalf("what happened? expected userid to be 42 but got %s", userid) + } + ctx.Writef(offlineBody) + }).ChangeName("api.users") // or an empty (), required, in order to get the Route instance. + + // change the "user.api" state from offline to online and online to offline + app.Get("/change", func(ctx *iris.Context) { + // here + if offlineRoute.IsOnline() { + // set to offline + app.Routes().Offline(offlineRoute) + } else { + // set to online if it was not online(so it was offline) + app.Routes().Online(offlineRoute, iris.MethodGet) + } + }) + + app.Get("/execute", func(ctx *iris.Context) { + // here + ctx.ExecRouteAgainst(offlineRoute, "/api/user/42") + }) + + // append the body and change the status code from an 'offline' route execution + app.Get("/execute_modified", func(ctx *iris.Context) { + ctx.Set("mykey", "myval") + // here + ctx.Record() // if we want to control the response + ctx.ExecRouteAgainst(offlineRoute, "/api/user/42") + ctx.Write([]byte("modified from status code: " + strconv.Itoa(ctx.StatusCode()))) + ctx.SetStatusCode(iris.StatusUseProxy) + + if ctx.Path() != "/execute_modified" { + t.Fatalf("Expected Request Path of this context NOT to change but got: '%s' ", ctx.Path()) + } + + if got := ctx.Get("mykey"); got != "myval" { + t.Fatalf("Expected Value 'mykey' of this context NOT to change('%s') but got: '%s' ", "myval", got) + } + ctx.Next() + }, func(ctx *iris.Context) { + ctx.Writef("-original_middleware_here") + }) + + hello := "Hello from index" + app.Get("/", func(ctx *iris.Context) { + ctx.Writef(hello) + }) + + e := httptest.New(app, t) + + e.GET("/").Expect().Status(iris.StatusOK).Body().Equal(hello) + // here + // the status should be not found, the route is invisible from outside world + e.GET(offlineRouteRequestedTestPath).Expect().Status(iris.StatusNotFound) + + // set the route online with the /change + e.GET("/change").Expect().Status(iris.StatusOK) + // try again, it should be online now + e.GET(offlineRouteRequestedTestPath).Expect().Status(iris.StatusOK).Body().Equal(offlineBody) + // change to offline again + e.GET("/change").Expect().Status(iris.StatusOK) + // and test again, it should be offline now + e.GET(offlineRouteRequestedTestPath).Expect().Status(iris.StatusNotFound) + + // finally test the execute on the offline route + // it should be remains offline but execute the route like it is from client request. + e.GET("/execute").Expect().Status(iris.StatusOK).Body().Equal(offlineBody) + e.GET(offlineRouteRequestedTestPath).Expect().Status(iris.StatusNotFound) + e.GET("/execute_modified").Expect().Status(iris.StatusUseProxy).Body(). + Equal(offlineBody + "modified from status code: 200-original_middleware_here") +} + +func TestRouteStateSimple(t *testing.T) { + // httprouter adaptor + testRouteStateSimple(t, httprouter.New(), "/api/user/:userid") + // gorillamux adaptor + testRouteStateSimple(t, gorillamux.New(), "/api/user/{userid}") +}