diff --git a/HISTORY.md b/HISTORY.md index bd35a97a..90dbef93 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -8,10 +8,14 @@ - 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 +- Example 1: https://github.com/iris-contrib/examples/tree/master/route_state +- Example 2, SPA: https://github.com/iris-contrib/examples/tree/master/spa_2_using_offline_routing **What?** + +- Give priority to an API path inside a Static route + ```go package main @@ -21,38 +25,57 @@ import ( func main() { - iris.None("/api/user/:userid", func(ctx *iris.Context) { + usersAPI := iris.None("/api/users/:userid", func(ctx *iris.Context) { + ctx.Writef("user with id: %s", ctx.Param("userid")) + })("api.users.id") + + iris.StaticWeb("/", "./www", usersAPI) + + // + // START THE SERVER + // + iris.Listen("localhost:8080") +} + + +``` + +- Play with(very advanced usage, used by big companies): enable(online) or disable(offline) routes at runtime with one line of code. + +```go +package main + +import ( + "github.com/kataras/iris" +) + +func main() { + + // You can find the Route by iris.Lookup("theRouteName") + // you can set a route name as: myRoute := iris.Get("/mypath", handler)("theRouteName") + // that will set a name to the route and returns its iris.Route instance for further usage. + api := iris.None("/api/users/:userid", func(ctx *iris.Context) { userid := ctx.Param("userid") ctx.Writef("user with id: %s", userid) - })("user.api") + })("users.api") - // change the "user.api" state from offline to online and online to offline + // change the "users.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() { + if api.IsOnline() { // set to offline - iris.SetRouteOffline(routeName) + iris.SetRouteOffline(api) } else { // set to online if it was not online(so it was offline) - iris.SetRouteOnline(routeName, iris.MethodGet) + iris.SetRouteOnline(api, 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) + // ctx.Request.URL.Path = "/api/users/42" + // ctx.ExecRoute(iris.Route) // or: - ctx.ExecRouteAgainst(routeName, "/api/user/42") + ctx.ExecRouteAgainst(api, "/api/users/42") }) iris.Get("/", func(ctx *iris.Context) { @@ -63,11 +86,11 @@ func main() { // START THE SERVER // // STEPS: - // 1. navigate to http://localhost:8080/user/api/42 + // 1. navigate to http://localhost:8080/api/users/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 + // 3. now, navigate to http://localhost:8080/api/users/42 // you should see the page working, NO 404 error // go back to the http://localhost:8080/change // you should get 404 error again @@ -78,6 +101,25 @@ func main() { ``` +- New built'n Middleware: `iris.Prioritize(route)` in order to give priority to a route inside other handler (used internally on StaticWeb's builder) + +```go +usersAPI := iris.None("/api/users/:userid", func(ctx *iris.Context) { + ctx.Writef("user with id: %s", ctx.Param("userid")) +})("api.users.id") // we need to call empty ("") in order to get its iris.Route instance +// or ("the name of the route") +// which later on can be found with iris.Lookup("the name of the route") + +static := iris.StaticHandler("/", "./www", false, false) +// manually give a priority to the usersAPI, if not found then continue to the static handler +iris.Get("/*file", iris.Prioritize(usersAPI), static) + +iris.Get("/*file", static) + +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/context.go b/context.go index 4618f132..54903919 100644 --- a/context.go +++ b/context.go @@ -177,11 +177,15 @@ 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. +// ExecRoute calls any route (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. // +// You can find the Route by iris.Lookup("theRouteName") +// you can set a route name as: myRoute := iris.Get("/mypath", handler)("theRouteName") +// that will set a name to the route and returns its iris.Route instance for further usage. +// // It doesn't changes the global state, if a route was "offline" it remains offline. // // see ExecRouteAgainst(routeName, againstRequestPath string), @@ -189,16 +193,20 @@ 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(routeName string) *Context { - return ctx.ExecRouteAgainst(routeName, ctx.Path()) +func (ctx *Context) ExecRoute(r Route) *Context { + return ctx.ExecRouteAgainst(r, ctx.Path()) } -// ExecRouteAgainst calls any route by its name (mostly "offline" route) against a 'virtually' request path +// ExecRouteAgainst calls any iris.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. // +// You can find the Route by iris.Lookup("theRouteName") +// you can set a route name as: myRoute := iris.Get("/mypath", handler)("theRouteName") +// that will set a name to the route and returns its iris.Route instance for further usage. +// // It doesn't changes the global state, if a route was "offline" it remains offline. // // see ExecRoute(routeName), @@ -206,9 +214,7 @@ func (ctx *Context) ExecRoute(routeName string) *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(routeName string, againstRequestPath string) *Context { - - r := ctx.framework.Lookup(routeName) +func (ctx *Context) ExecRouteAgainst(r Route, againstRequestPath string) *Context { if r != nil { context := &(*ctx) context.Middleware = context.Middleware[0:0] @@ -220,10 +226,42 @@ func (ctx *Context) ExecRouteAgainst(routeName string, againstRequestPath string return context } } - // if failed return nil in order to this fail to be catchable return nil } +// Prioritize is a middleware which executes a route against this path +// when the request's Path has a prefix of the route's STATIC PART +// is not executing ExecRoute to determinate if it's valid, for performance reasons +// 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 set a route name as: myRoute := iris.Get("/mypath", handler)("theRouteName") +// that will set a name to the route and returns its iris.Route instance for further usage. +// +// if the route found then it executes that and don't continue to the next handler +// if not found then continue to the next handler +func Prioritize(r Route) HandlerFunc { + if r != nil { + return func(ctx *Context) { + reqPath := ctx.Path() + if strings.HasPrefix(reqPath, r.StaticPath()) { + newctx := ctx.ExecRouteAgainst(r, reqPath) + if newctx == nil { // route not found. + ctx.EmitError(StatusNotFound) + } + return + } + // execute the next handler if no prefix + // here look, the only error we catch is the 404, + // we can't go ctx.Next() and believe that the next handler will manage the error + // because it will not, we are not on the router. + ctx.Next() + } + } + return func(ctx *Context) { ctx.Next() } +} + // ------------------------------------------------------------------------------------- // ------------------------------------------------------------------------------------- // -----------------------------Request URL, Method, IP & Headers getters--------------- diff --git a/http.go b/http.go index 1ba16be4..ef2eccaa 100644 --- a/http.go +++ b/http.go @@ -698,8 +698,16 @@ type ( Subdomain() string // Method returns the http method Method() string + // SetMethod sets the route's method + // requires re-build of the iris.Router + SetMethod(string) + // Path returns the path Path() string + + // staticPath returns the static part of the path + StaticPath() string + // SetPath changes/sets the path for this route SetPath(string) // Middleware returns the slice of Handler([]Handler) registered to this route @@ -716,6 +724,7 @@ type ( subdomain string method string path string + staticPath string middleware Middleware formattedPath string formattedParts int @@ -740,6 +749,7 @@ var _ Route = &route{} func newRoute(method string, subdomain string, path string, middleware Middleware) *route { r := &route{name: path + subdomain, method: method, subdomain: subdomain, path: path, middleware: middleware} r.formatPath() + r.calculateStaticPath() return r } @@ -773,8 +783,20 @@ func (r *route) formatPath() { r.formattedPath = tempPath } -func (r *route) setName(newName string) { +func (r *route) calculateStaticPath() { + for i := 0; i < len(r.path); i++ { + if r.path[i] == matchEverythingByte || r.path[i] == parameterStartByte { + r.staticPath = r.path[0 : i-1] // stop at the first dynamic path symbol and set the static path to its [0:previous] + return + } + } + // not a dynamic symbol found, set its static path to its path. + r.staticPath = r.path +} + +func (r *route) setName(newName string) Route { r.name = newName + return r } func (r route) Name() string { @@ -789,10 +811,18 @@ func (r route) Method() string { return r.method } +func (r *route) SetMethod(method string) { + r.method = method +} + func (r route) Path() string { return r.path } +func (r route) StaticPath() string { + return r.staticPath +} + func (r *route) SetPath(s string) { r.path = s } diff --git a/http_test.go b/http_test.go index 9ff66482..eca75b1e 100644 --- a/http_test.go +++ b/http_test.go @@ -734,36 +734,34 @@ func TestRedirectHTTPS(t *testing.T) { 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) { + offlineRoute := 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) + })("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 iris.Get("/change", func(ctx *iris.Context) { // here - if iris.Lookup(offlineRouteName).IsOnline() { + if offlineRoute.IsOnline() { // set to offline - iris.SetRouteOffline(offlineRouteName) + iris.SetRouteOffline(offlineRoute) } else { // set to online if it was not online(so it was offline) - iris.SetRouteOnline(offlineRouteName, iris.MethodGet) + iris.SetRouteOnline(offlineRoute, iris.MethodGet) } }) iris.Get("/execute", func(ctx *iris.Context) { // here - ctx.ExecRouteAgainst(offlineRouteName, "/api/user/42") + ctx.ExecRouteAgainst(offlineRoute, "/api/user/42") }) hello := "Hello from index" diff --git a/iris.go b/iris.go index 33e803bd..f00bf0fe 100644 --- a/iris.go +++ b/iris.go @@ -171,9 +171,9 @@ type ( Lookup(routeName string) Route Lookups() []Route - SetRouteOnline(routeName string, HTTPMethod string) bool - SetRouteOffline(routeName string) bool - ChangeRouteState(routeName string, HTTPMethod string) bool + SetRouteOnline(r Route, HTTPMethod string) bool + SetRouteOffline(r Route) bool + ChangeRouteState(r Route, HTTPMethod string) bool Path(routeName string, optionalPathParameters ...interface{}) (routePath string) URL(routeName string, optionalPathParameters ...interface{}) (routeURL string) @@ -219,8 +219,8 @@ type ( StaticEmbedded(reqRelativePath string, contentType string, assets func(string) ([]byte, error), assetsNames func() []string) RouteNameFunc Favicon(systemFilePath string, optionalReqRelativePath ...string) RouteNameFunc // static file system - StaticHandler(reqRelativePath string, systemPath string, showList bool, enableGzip bool) HandlerFunc - StaticWeb(reqRelativePath string, systemPath string) RouteNameFunc + StaticHandler(reqRelativePath string, systemPath string, showList bool, enableGzip bool, exceptRoutes ...Route) HandlerFunc + StaticWeb(reqRelativePath string, systemPath string, exceptRoutes ...Route) RouteNameFunc // party layout for template engines Layout(layoutTemplateFileName string) MuxAPI @@ -231,7 +231,12 @@ type ( } // RouteNameFunc the func returns from the MuxAPi's methods, optionally sets the name of the Route (*route) - RouteNameFunc func(customRouteName string) + // + // You can find the Route by iris.Lookup("theRouteName") + // you can set a route name as: myRoute := iris.Get("/mypath", handler)("theRouteName") + // that will set a name to the route and returns its iris.Route instance for further usage. + // + RouteNameFunc func(customRouteName string) Route ) // Framework is our God |\| Google.Search('Greek mythology Iris') @@ -1060,8 +1065,8 @@ func (s *Framework) Lookups() (routes []Route) { // 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) +func SetRouteOnline(r Route, HTTPMethod string) bool { + return Default.SetRouteOnline(r, HTTPMethod) } // SetRouteOffline sets the state of the route to "offline" and re-builds the router @@ -1073,8 +1078,8 @@ func SetRouteOnline(routeName string, HTTPMethod string) bool { // 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) +func SetRouteOffline(r Route) bool { + return Default.SetRouteOffline(r) } // ChangeRouteState changes the state of the route. @@ -1089,23 +1094,23 @@ func SetRouteOffline(routeName string) bool { // 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) +func ChangeRouteState(r Route, HTTPMethod string) bool { + return Default.ChangeRouteState(r, 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) +func (s *Framework) SetRouteOnline(r Route, HTTPMethod string) bool { + return s.ChangeRouteState(r, 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) +func (s *Framework) SetRouteOffline(r Route) bool { + return s.ChangeRouteState(r, MethodNone) } // ChangeRouteState changes the state of the route. @@ -1114,15 +1119,14 @@ func (s *Framework) SetRouteOffline(routeName string) bool { // 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 +func (s *Framework) ChangeRouteState(r Route, HTTPMethod string) bool { if r != nil { - if r.method != HTTPMethod { + nonSpecificMethod := len(HTTPMethod) == 0 + if r.Method() != HTTPMethod { if nonSpecificMethod { - r.method = MethodGet // if no method given, then do it for "GET" only + r.SetMethod(MethodGet) // if no method given, then do it for "GET" only } else { - r.method = HTTPMethod + r.SetMethod(HTTPMethod) } // re-build the router/main handler s.Router = ToNativeHandler(s, s.mux.BuildHandler()) @@ -2064,32 +2068,13 @@ func (api *muxAPI) Favicon(favPath string, requestPath ...string) RouteNameFunc return api.registerResourceRoute(reqPath, h) } -// StripPrefix returns a handler that serves HTTP requests -// by removing the given prefix from the request URL's Path -// and invoking the handler h. StripPrefix handles a -// request for a path that doesn't begin with prefix by -// replying with an HTTP 404 not found error. -func StripPrefix(prefix string, h HandlerFunc) HandlerFunc { - if prefix == "" { - return h - } - return func(ctx *Context) { - if p := strings.TrimPrefix(ctx.Request.URL.Path, prefix); len(p) < len(ctx.Request.URL.Path) { - ctx.Request.URL.Path = p - h(ctx) - } else { - ctx.NotFound() - } - } -} - // StaticHandler returns a new Handler which serves static files func StaticHandler(reqPath string, systemPath string, showList bool, enableGzip bool) HandlerFunc { return Default.StaticHandler(reqPath, systemPath, showList, enableGzip) } // StaticHandler returns a new Handler which serves static files -func (api *muxAPI) StaticHandler(reqPath string, systemPath string, showList bool, enableGzip bool) HandlerFunc { +func (api *muxAPI) StaticHandler(reqPath string, systemPath string, showList bool, enableGzip bool, exceptRoutes ...Route) HandlerFunc { // here we separate the path from the subdomain (if any), we care only for the path // fixes a bug when serving static files via a subdomain fullpath := api.relativePath + reqPath @@ -2102,6 +2087,7 @@ func (api *muxAPI) StaticHandler(reqPath string, systemPath string, showList boo Path(path). Listing(showList). Gzip(enableGzip). + Except(exceptRoutes...). Build() managedStaticHandler := func(ctx *Context) { @@ -2124,6 +2110,8 @@ func (api *muxAPI) StaticHandler(reqPath string, systemPath string, showList boo // // first parameter: the route path // second parameter: the system directory +// third OPTIONAL parameter: the exception routes +// (= give priority to these routes instead of the static handler) // for more options look iris.StaticHandler. // // iris.StaticWeb("/static", "./static") @@ -2133,8 +2121,8 @@ func (api *muxAPI) StaticHandler(reqPath string, systemPath string, showList boo // "index.html". // // StaticWeb calls the StaticHandler(reqPath, systemPath, listingDirectories: false, gzip: false ). -func StaticWeb(reqPath string, systemPath string) RouteNameFunc { - return Default.StaticWeb(reqPath, systemPath) +func StaticWeb(reqPath string, systemPath string, exceptRoutes ...Route) RouteNameFunc { + return Default.StaticWeb(reqPath, systemPath, exceptRoutes...) } // StaticWeb returns a handler that serves HTTP requests @@ -2142,6 +2130,8 @@ func StaticWeb(reqPath string, systemPath string) RouteNameFunc { // // first parameter: the route path // second parameter: the system directory +// third OPTIONAL parameter: the exception routes +// (= give priority to these routes instead of the static handler) // for more options look iris.StaticHandler. // // iris.StaticWeb("/static", "./static") @@ -2151,8 +2141,8 @@ func StaticWeb(reqPath string, systemPath string) RouteNameFunc { // "index.html". // // StaticWeb calls the StaticHandler(reqPath, systemPath, listingDirectories: false, gzip: false ). -func (api *muxAPI) StaticWeb(reqPath string, systemPath string) RouteNameFunc { - h := api.StaticHandler(reqPath, systemPath, false, false) +func (api *muxAPI) StaticWeb(reqPath string, systemPath string, exceptRoutes ...Route) RouteNameFunc { + h := api.StaticHandler(reqPath, systemPath, false, false, exceptRoutes...) routePath := validateWildcard(reqPath, "file") return api.registerResourceRoute(routePath, h) } diff --git a/webfs.go b/webfs.go index 87427e19..a9f9b54f 100644 --- a/webfs.go +++ b/webfs.go @@ -14,6 +14,7 @@ type StaticHandlerBuilder interface { Gzip(enable bool) StaticHandlerBuilder Listing(listDirectoriesOnOff bool) StaticHandlerBuilder StripPath(yesNo bool) StaticHandlerBuilder + Except(r ...Route) StaticHandlerBuilder Build() HandlerFunc } @@ -27,6 +28,7 @@ type webfs struct { // these are init on the Build() call filesystem http.FileSystem once sync.Once + exceptions []Route handler HandlerFunc } @@ -88,6 +90,13 @@ func (w *webfs) StripPath(yesNo bool) StaticHandlerBuilder { return w } +// Except add a route exception, +// gives priority to that Route over the static handler. +func (w *webfs) Except(r ...Route) StaticHandlerBuilder { + w.exceptions = append(w.exceptions, r...) + return w +} + type ( noListFile struct { http.File @@ -130,7 +139,7 @@ func (w *webfs) Build() HandlerFunc { fsHandler = http.StripPrefix(prefix, fileserver) } - w.handler = func(ctx *Context) { + h := func(ctx *Context) { writer := ctx.ResponseWriter if w.gzip && ctx.clientAllowsGzip() { @@ -143,7 +152,41 @@ func (w *webfs) Build() HandlerFunc { fsHandler.ServeHTTP(writer, ctx.Request) } + + if len(w.exceptions) > 0 { + middleware := make(Middleware, len(w.exceptions)+1) + for i := range w.exceptions { + middleware[i] = Prioritize(w.exceptions[i]) + } + middleware[len(w.exceptions)] = HandlerFunc(h) + + w.handler = func(ctx *Context) { + ctx.Middleware = append(middleware, ctx.Middleware...) + ctx.Do() + } + } else { + w.handler = h + } }) return w.handler } + +// StripPrefix returns a handler that serves HTTP requests +// by removing the given prefix from the request URL's Path +// and invoking the handler h. StripPrefix handles a +// request for a path that doesn't begin with prefix by +// replying with an HTTP 404 not found error. +func StripPrefix(prefix string, h HandlerFunc) HandlerFunc { + if prefix == "" { + return h + } + return func(ctx *Context) { + if p := strings.TrimPrefix(ctx.Request.URL.Path, prefix); len(p) < len(ctx.Request.URL.Path) { + ctx.Request.URL.Path = p + h(ctx) + } else { + ctx.NotFound() + } + } +}