|You will love this| New Feature: Offline routing, dynamic changes against a route's state | https://github.com/kataras/iris/issues/585

Read HISTORY.md for details
This commit is contained in:
Gerasimos (Makis) Maropoulos 2017-01-12 08:28:30 +02:00
parent 020e857b22
commit c91a1e6628
7 changed files with 322 additions and 12 deletions

View File

@ -1,4 +1,4 @@
- Version : **6.1.0** - Version : **6.1.1**
- Operating System: - Operating System:

View File

@ -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`. **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 ## 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")` - Fix a not found error when serving static files through custom subdomain, this should work again: `iris.Party("mysubdomain.").StaticWeb("/", "./static")`

View File

@ -18,7 +18,7 @@
<br/> <br/>
<a href="https://github.com/kataras/iris/blob/master/HISTORY.md"><img src="https://img.shields.io/badge/%20version%20-%206.1.0%20-blue.svg?style=flat-square" alt="CHANGELOG/HISTORY"></a> <a href="https://github.com/kataras/iris/blob/master/HISTORY.md"><img src="https://img.shields.io/badge/%20version%20-%206.1.1%20-blue.svg?style=flat-square" alt="CHANGELOG/HISTORY"></a>
<a href="https://github.com/iris-contrib/examples"><img src="https://img.shields.io/badge/%20examples-repository-3362c2.svg?style=flat-square" alt="Examples"></a> <a href="https://github.com/iris-contrib/examples"><img src="https://img.shields.io/badge/%20examples-repository-3362c2.svg?style=flat-square" alt="Examples"></a>
@ -947,7 +947,7 @@ I recommend testing your API using this new library, [httpexpect](https://github
Versioning Versioning
------------ ------------
Current: **v6.1.0** Current: **v6.1.1**
Older: **[v5/fasthttp](https://github.com/kataras/iris/tree/5.0.0)** Older: **[v5/fasthttp](https://github.com/kataras/iris/tree/5.0.0)**

View File

@ -177,6 +177,53 @@ func (ctx *Context) GetHandlerName() string {
return runtime.FuncForPC(reflect.ValueOf(ctx.Middleware[len(ctx.Middleware)-1]).Pointer()).Name() 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--------------- // -----------------------------Request URL, Method, IP & Headers getters---------------

29
http.go
View File

@ -37,10 +37,15 @@ const (
MethodOptions = "OPTIONS" MethodOptions = "OPTIONS"
// MethodTrace "TRACE" // MethodTrace "TRACE"
MethodTrace = "TRACE" MethodTrace = "TRACE"
// MethodNone is a Virtual method
// to store the "offline" routes
// in the mux's tree
MethodNone = "NONE"
) )
var ( 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} AllMethods = [...]string{MethodGet, MethodPost, MethodPut, MethodDelete, MethodConnect, MethodHead, MethodPatch, MethodOptions, MethodTrace}
) )
@ -701,6 +706,8 @@ type (
Middleware() Middleware Middleware() Middleware
// SetMiddleware changes/sets the middleware(handler(s)) for this route // SetMiddleware changes/sets the middleware(handler(s)) for this route
SetMiddleware(Middleware) SetMiddleware(Middleware)
// IsOnline returns true if the route is marked as "online" (state)
IsOnline() bool
} }
route struct { route struct {
@ -798,6 +805,10 @@ func (r *route) SetMiddleware(m Middleware) {
r.middleware = m r.middleware = m
} }
func (r route) IsOnline() bool {
return r.method != MethodNone
}
// RouteConflicts checks for route's middleware conflicts // RouteConflicts checks for route's middleware conflicts
func RouteConflicts(r *route, with string) bool { func RouteConflicts(r *route, with string) bool {
for _, h := range r.middleware { 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 // 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) { func (mux *serveMux) build() (methodEqual func(string, string) bool) {
sort.Sort(bySubdomain(mux.lookups)) 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 { for i := range mux.lookups {
r := mux.lookups[i] r := mux.lookups[i]
@ -1014,7 +1032,12 @@ func HTMLEscape(s string) string {
return htmlReplacer.Replace(s) 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 { func (mux *serveMux) BuildHandler() HandlerFunc {
// initialize the router once // initialize the router once

View File

@ -731,3 +731,64 @@ func TestRedirectHTTPS(t *testing.T) {
e := httptest.New(api, t) e := httptest.New(api, t)
e.GET("/redirect").Expect().Status(iris.StatusOK).Body().Equal(expectedBody) 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)
}

115
iris.go
View File

@ -81,7 +81,7 @@ const (
// IsLongTermSupport flag is true when the below version number is a long-term-support version // IsLongTermSupport flag is true when the below version number is a long-term-support version
IsLongTermSupport = false IsLongTermSupport = false
// Version is the current version number of the Iris web framework // Version is the current version number of the Iris web framework
Version = "6.1.0" Version = "6.1.1"
banner = ` _____ _ banner = ` _____ _
|_ _| (_) |_ _| (_)
@ -171,6 +171,10 @@ type (
Lookup(routeName string) Route Lookup(routeName string) Route
Lookups() []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) Path(routeName string, optionalPathParameters ...interface{}) (routePath string)
URL(routeName string, optionalPathParameters ...interface{}) (routeURL string) URL(routeName string, optionalPathParameters ...interface{}) (routeURL string)
@ -195,6 +199,8 @@ type (
HandleFunc(method string, reqPath string, middleware ...HandlerFunc) RouteNameFunc HandleFunc(method string, reqPath string, middleware ...HandlerFunc) RouteNameFunc
API(reqRelativeRootPath string, api HandlerAPI, middleware ...HandlerFunc) API(reqRelativeRootPath string, api HandlerAPI, middleware ...HandlerFunc)
// virtual method for "offline" routes new feature
None(reqRelativePath string, Middleware ...HandlerFunc) RouteNameFunc
// http methods // http methods
Get(reqRelativePath string, middleware ...HandlerFunc) RouteNameFunc Get(reqRelativePath string, middleware ...HandlerFunc) RouteNameFunc
Post(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) // build and get the default mux' handler(*Context)
serve := s.mux.BuildHandler() serve := s.mux.BuildHandler()
// build the net/http.Handler to bind it to the servers // build the net/http.Handler to bind it to the servers
defaultHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defaultHandler := ToNativeHandler(s, serve)
ctx := s.AcquireCtx(w, r)
serve(ctx)
s.ReleaseCtx(ctx)
})
s.Router = defaultHandler s.Router = defaultHandler
} }
@ -1048,6 +1050,88 @@ func (s *Framework) Lookups() (routes []Route) {
return 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 // Path used to check arguments with the route's named parameters and return the correct url
// if parse failed returns empty string // if parse failed returns empty string
func Path(routeName string, args ...interface{}) 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 // Get registers a route for the Get http method
func Get(path string, handlersFn ...HandlerFunc) RouteNameFunc { func Get(path string, handlersFn ...HandlerFunc) RouteNameFunc {
return Default.Get(path, handlersFn...) 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) // Any registers a route for ALL of the http methods (Get,Post,Put,Head,Patch,Options,Connect,Delete)
func Any(registeredPath string, handlersFn ...HandlerFunc) { func Any(registeredPath string, handlersFn ...HandlerFunc) {
Default.Any(registeredPath, handlersFn...) 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 // Get registers a route for the Get http method