diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 84bbef9a..1db030be 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,4 +1,4 @@ -- Version : **6.1.0** +- Version : **6.1.1** - Operating System: diff --git a/HISTORY.md b/HISTORY.md index a3cc0407..bd35a97a 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -2,6 +2,82 @@ **How to upgrade**: remove your `$GOPATH/src/github.com/kataras` folder, open your command-line and execute this command: `go get -u github.com/kataras/iris/iris`. +## 6.1.0 -> 6.1.1 + +- **NEW FEATURE**: `Offline routes`. + +- Discussion: https://github.com/kataras/iris/issues/585 +- Test: https://github.com/kataras/iris/blob/master/http_test.go#L735 +- Example: https://github.com/iris-contrib/examples/tree/master/route_state + +**What?** + +```go +package main + +import ( + "github.com/kataras/iris" +) + +func main() { + + iris.None("/api/user/:userid", func(ctx *iris.Context) { + userid := ctx.Param("userid") + ctx.Writef("user with id: %s", userid) + })("user.api") + + // change the "user.api" state from offline to online and online to offline + iris.Get("/change", func(ctx *iris.Context) { + routeName := "user.api" + if iris.Lookup(routeName).IsOnline() { + // set to offline + iris.SetRouteOffline(routeName) + } else { + // set to online if it was not online(so it was offline) + iris.SetRouteOnline(routeName, iris.MethodGet) + } + }) + + // iris.Get("/execute/:routename", func(ctx *iris.Context) { + // routeName := ctx.Param("routename") + // userAPICtx := ctx.ExecuteRoute(routeName) + // if userAPICtx == nil { + // ctx.Writef("Route with name: %s didnt' found or couldn't be validate with this request path!", routeName) + // } + // }) + + iris.Get("/execute", func(ctx *iris.Context) { + routeName := "user.api" + // change the path in order to be catcable from the ExecuteRoute + // ctx.Request.URL.Path = "/api/user/42" + // ctx.ExecRoute(routeName) + // or: + ctx.ExecRouteAgainst(routeName, "/api/user/42") + }) + + iris.Get("/", func(ctx *iris.Context) { + ctx.Writef("Hello from index /") + }) + + // + // START THE SERVER + // + // STEPS: + // 1. navigate to http://localhost:8080/user/api/42 + // you should get 404 error + // 2. now, navigate to http://localhost:8080/change + // you should see a blank page + // 3. now, navigate to http://localhost:8080/user/api/42 + // you should see the page working, NO 404 error + // go back to the http://localhost:8080/change + // you should get 404 error again + // You just dynamically changed the state of a route with 3 lines of code! + // you can do the same with group of routes and subdomains :) + iris.Listen(":8080") +} + +``` + ## 6.0.9 -> 6.1.0 - Fix a not found error when serving static files through custom subdomain, this should work again: `iris.Party("mysubdomain.").StaticWeb("/", "./static")` diff --git a/README.md b/README.md index 4d99c02a..21991094 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@
-CHANGELOG/HISTORY +CHANGELOG/HISTORY Examples @@ -947,7 +947,7 @@ I recommend testing your API using this new library, [httpexpect](https://github Versioning ------------ -Current: **v6.1.0** +Current: **v6.1.1** Older: **[v5/fasthttp](https://github.com/kataras/iris/tree/5.0.0)** diff --git a/context.go b/context.go index cb3a5aa7..4618f132 100644 --- a/context.go +++ b/context.go @@ -177,6 +177,53 @@ func (ctx *Context) GetHandlerName() string { return runtime.FuncForPC(reflect.ValueOf(ctx.Middleware[len(ctx.Middleware)-1]).Pointer()).Name() } +// ExecRoute calls any route by its name (mostly "offline" route) like it was requested by the user, but it is not. +// Offline means that the route is registered to the iris and have all features that a normal route has +// 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. +// +// It doesn't changes the global state, if a route was "offline" it remains offline. +// +// see ExecRouteAgainst(routeName, againstRequestPath string), +// iris.None(...) and iris.SetRouteOnline/SetRouteOffline +// 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(routeName string) *Context { + return ctx.ExecRouteAgainst(routeName, ctx.Path()) +} + +// ExecRouteAgainst calls any route by its name (mostly "offline" route) against a 'virtually' request path +// like it was requested by the user, but it is not. +// Offline means that the route is registered to the iris and have all features that a normal route has +// 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. +// +// It doesn't changes the global state, if a route was "offline" it remains offline. +// +// see ExecRoute(routeName), +// iris.None(...) and iris.SetRouteOnline/SetRouteOffline +// 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(routeName string, againstRequestPath string) *Context { + + r := ctx.framework.Lookup(routeName) + if r != nil { + context := &(*ctx) + context.Middleware = context.Middleware[0:0] + context.values.Reset() + tree := ctx.framework.muxAPI.mux.getTree(r.Method(), r.Subdomain()) + tree.entry.get(againstRequestPath, context) + if len(context.Middleware) > 0 { + context.Do() + return context + } + } + // if failed return nil in order to this fail to be catchable + return nil +} + // ------------------------------------------------------------------------------------- // ------------------------------------------------------------------------------------- // -----------------------------Request URL, Method, IP & Headers getters--------------- diff --git a/http.go b/http.go index 3b94799f..1ba16be4 100644 --- a/http.go +++ b/http.go @@ -37,10 +37,15 @@ const ( MethodOptions = "OPTIONS" // MethodTrace "TRACE" MethodTrace = "TRACE" + // MethodNone is a Virtual method + // to store the "offline" routes + // in the mux's tree + MethodNone = "NONE" ) var ( - // AllMethods "GET", "POST", "PUT", "DELETE", "CONNECT", "HEAD", "PATCH", "OPTIONS", "TRACE" + // AllMethods contains all the http valid methods: + // "GET", "POST", "PUT", "DELETE", "CONNECT", "HEAD", "PATCH", "OPTIONS", "TRACE" AllMethods = [...]string{MethodGet, MethodPost, MethodPut, MethodDelete, MethodConnect, MethodHead, MethodPatch, MethodOptions, MethodTrace} ) @@ -701,6 +706,8 @@ type ( Middleware() Middleware // SetMiddleware changes/sets the middleware(handler(s)) for this route SetMiddleware(Middleware) + // IsOnline returns true if the route is marked as "online" (state) + IsOnline() bool } route struct { @@ -798,6 +805,10 @@ func (r *route) SetMiddleware(m Middleware) { r.middleware = m } +func (r route) IsOnline() bool { + return r.method != MethodNone +} + // RouteConflicts checks for route's middleware conflicts func RouteConflicts(r *route, with string) bool { for _, h := range r.middleware { @@ -944,10 +955,17 @@ func (mux *serveMux) register(method string, subdomain string, path string, midd } // build collects all routes info and adds them to the registry in order to be served from the request handler -// this happens once when server is setting the mux's handler. +// this happens once(except when a route changes its state) when server is setting the mux's handler. func (mux *serveMux) build() (methodEqual func(string, string) bool) { sort.Sort(bySubdomain(mux.lookups)) + // clear them for any case + // build may called internally to re-build the routes. + // re-build happens from BuildHandler() when a route changes its state + // from offline to online or from online to offline + mux.garden = mux.garden[0:0] + // this is not used anywhere for now, but keep it here. + mux.maxParameters = 0 for i := range mux.lookups { r := mux.lookups[i] @@ -1014,7 +1032,12 @@ func HTMLEscape(s string) string { return htmlReplacer.Replace(s) } -// BuildHandler the default Iris router when iris.Handler is nil +// BuildHandler the default Iris router when iris.Router is nil +// +// NOTE: Is called and re-set to the iris.Router when +// a route changes its state from "online" to "offline" or "offline" to "online" +// look iris.None(...) for more +// and: https://github.com/kataras/iris/issues/585 func (mux *serveMux) BuildHandler() HandlerFunc { // initialize the router once diff --git a/http_test.go b/http_test.go index 5f172633..9ff66482 100644 --- a/http_test.go +++ b/http_test.go @@ -731,3 +731,64 @@ func TestRedirectHTTPS(t *testing.T) { e := httptest.New(api, t) e.GET("/redirect").Expect().Status(iris.StatusOK).Body().Equal(expectedBody) } + +func TestRouteStateSimple(t *testing.T) { + iris.ResetDefault() + // here + offlineRouteName := "user.api" + offlineRoutePath := "/api/user/:userid" + offlineRouteRequestedTestPath := "/api/user/42" + offlineBody := "user with id: 42" + + iris.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) + })(offlineRouteName) + + // change the "user.api" state from offline to online and online to offline + iris.Get("/change", func(ctx *iris.Context) { + // here + if iris.Lookup(offlineRouteName).IsOnline() { + // set to offline + iris.SetRouteOffline(offlineRouteName) + } else { + // set to online if it was not online(so it was offline) + iris.SetRouteOnline(offlineRouteName, iris.MethodGet) + } + }) + + iris.Get("/execute", func(ctx *iris.Context) { + // here + ctx.ExecRouteAgainst(offlineRouteName, "/api/user/42") + }) + + hello := "Hello from index" + iris.Get("/", func(ctx *iris.Context) { + ctx.Writef(hello) + }) + + e := httptest.New(iris.Default, 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) +} diff --git a/iris.go b/iris.go index 3abab865..33e803bd 100644 --- a/iris.go +++ b/iris.go @@ -81,7 +81,7 @@ const ( // IsLongTermSupport flag is true when the below version number is a long-term-support version IsLongTermSupport = false // Version is the current version number of the Iris web framework - Version = "6.1.0" + Version = "6.1.1" banner = ` _____ _ |_ _| (_) @@ -171,6 +171,10 @@ type ( Lookup(routeName string) Route Lookups() []Route + SetRouteOnline(routeName string, HTTPMethod string) bool + SetRouteOffline(routeName string) bool + ChangeRouteState(routeName string, HTTPMethod string) bool + Path(routeName string, optionalPathParameters ...interface{}) (routePath string) URL(routeName string, optionalPathParameters ...interface{}) (routeURL string) @@ -195,6 +199,8 @@ type ( HandleFunc(method string, reqPath string, middleware ...HandlerFunc) RouteNameFunc API(reqRelativeRootPath string, api HandlerAPI, middleware ...HandlerFunc) + // virtual method for "offline" routes new feature + None(reqRelativePath string, Middleware ...HandlerFunc) RouteNameFunc // http methods Get(reqRelativePath string, middleware ...HandlerFunc) RouteNameFunc Post(reqRelativePath string, middleware ...HandlerFunc) RouteNameFunc @@ -417,11 +423,7 @@ func (s *Framework) Build() { // build and get the default mux' handler(*Context) serve := s.mux.BuildHandler() // build the net/http.Handler to bind it to the servers - defaultHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ctx := s.AcquireCtx(w, r) - serve(ctx) - s.ReleaseCtx(ctx) - }) + defaultHandler := ToNativeHandler(s, serve) s.Router = defaultHandler } @@ -1048,6 +1050,88 @@ func (s *Framework) Lookups() (routes []Route) { return } +// SetRouteOnline sets the state of the route to "online" with a specific http method +// it re-builds the router +// +// returns true if state was actually changed +// +// see context.ExecRoute(routeName), +// iris.None(...) and iris.SetRouteOnline/SetRouteOffline +// For more details look: https://github.com/kataras/iris/issues/585 +// +// Example: https://github.com/iris-contrib/examples/tree/master/route_state +func SetRouteOnline(routeName string, HTTPMethod string) bool { + return Default.SetRouteOnline(routeName, HTTPMethod) +} + +// SetRouteOffline sets the state of the route to "offline" and re-builds the router +// +// returns true if state was actually changed +// +// see context.ExecRoute(routeName), +// iris.None(...) and iris.SetRouteOnline/SetRouteOffline +// For more details look: https://github.com/kataras/iris/issues/585 +// +// Example: https://github.com/iris-contrib/examples/tree/master/route_state +func SetRouteOffline(routeName string) bool { + return Default.SetRouteOffline(routeName) +} + +// ChangeRouteState changes the state of the route. +// iris.MethodNone for offline +// and iris.MethodGet/MethodPost/MethodPut/MethodDelete /MethodConnect/MethodOptions/MethodHead/MethodTrace/MethodPatch for online +// it re-builds the router +// +// returns true if state was actually changed +// +// see context.ExecRoute(routeName), +// iris.None(...) and iris.SetRouteOnline/SetRouteOffline +// For more details look: https://github.com/kataras/iris/issues/585 +// +// Example: https://github.com/iris-contrib/examples/tree/master/route_state +func ChangeRouteState(routeName string, HTTPMethod string) bool { + return Default.ChangeRouteState(routeName, HTTPMethod) +} + +// SetRouteOnline sets the state of the route to "online" with a specific http method +// it re-builds the router +// +// returns true if state was actually changed +func (s *Framework) SetRouteOnline(routeName string, HTTPMethod string) bool { + return s.ChangeRouteState(routeName, HTTPMethod) +} + +// SetRouteOffline sets the state of the route to "offline" and re-builds the router +// +// returns true if state was actually changed +func (s *Framework) SetRouteOffline(routeName string) bool { + return s.ChangeRouteState(routeName, MethodNone) +} + +// ChangeRouteState changes the state of the route. +// iris.MethodNone for offline +// and iris.MethodGet/MethodPost/MethodPut/MethodDelete /MethodConnect/MethodOptions/MethodHead/MethodTrace/MethodPatch for online +// it re-builds the router +// +// returns true if state was actually changed +func (s *Framework) ChangeRouteState(routeName string, HTTPMethod string) bool { + r := s.mux.lookup(routeName) + nonSpecificMethod := len(HTTPMethod) == 0 + if r != nil { + if r.method != HTTPMethod { + if nonSpecificMethod { + r.method = MethodGet // if no method given, then do it for "GET" only + } else { + r.method = HTTPMethod + } + // re-build the router/main handler + s.Router = ToNativeHandler(s, s.mux.BuildHandler()) + return true + } + } + return false +} + // Path used to check arguments with the route's named parameters and return the correct url // if parse failed returns empty string func Path(routeName string, args ...interface{}) string { @@ -1607,6 +1691,16 @@ func (api *muxAPI) API(path string, restAPI HandlerAPI, middleware ...HandlerFun } +// None registers an "offline" route +// see context.ExecRoute(routeName), +// iris.None(...) and iris.SetRouteOnline/SetRouteOffline +// For more details look: https://github.com/kataras/iris/issues/585 +// +// Example: https://github.com/iris-contrib/examples/tree/master/route_state +func None(path string, handlersFn ...HandlerFunc) RouteNameFunc { + return Default.None(path, handlersFn...) +} + // Get registers a route for the Get http method func Get(path string, handlersFn ...HandlerFunc) RouteNameFunc { return Default.Get(path, handlersFn...) @@ -1655,7 +1749,16 @@ func Trace(path string, handlersFn ...HandlerFunc) RouteNameFunc { // Any registers a route for ALL of the http methods (Get,Post,Put,Head,Patch,Options,Connect,Delete) func Any(registeredPath string, handlersFn ...HandlerFunc) { Default.Any(registeredPath, handlersFn...) +} +// None registers an "offline" route +// see context.ExecRoute(routeName), +// iris.None(...) and iris.SetRouteOnline/SetRouteOffline +// For more details look: https://github.com/kataras/iris/issues/585 +// +// Example: https://github.com/iris-contrib/examples/tree/master/route_state +func (api *muxAPI) None(path string, handlersFn ...HandlerFunc) RouteNameFunc { + return api.HandleFunc(MethodNone, path, handlersFn...) } // Get registers a route for the Get http method