From 064282036c8fc73145821d0576b954f4d5dc740c Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Tue, 28 Feb 2017 15:01:18 +0200 Subject: [PATCH] Helpers for third-party adaptors and middleware authors to generate route paths without even know the router that has being selected by user Former-commit-id: b21147f2bc306d5c41539a1be0c83456c3d62651 --- HISTORY.md | 3 +- adaptors/gorillamux/gorillamux.go | 3 + adaptors/httprouter/httprouter.go | 3 + iris.go | 100 +++++++++++++++++++++++++++++- policy.go | 23 ++++++- policy_gorillamux_test.go | 17 +++++ policy_httprouter_test.go | 16 +++++ router.go | 69 +-------------------- 8 files changed, 163 insertions(+), 71 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 98962275..cb5929d1 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -5,7 +5,7 @@ ## 6.1.4 -> 6.2.0 (√Νεxτ) -_Last update: 22 Feb 2017_ +_Last update: 28 Feb 2017_ > Note: I want you to know that I spent more than 200 hours (16 days of ~10-15 hours per-day, do the math) for this release, two days to write these changes, please read the sections before think that you have an issue and post a new question, thanks! @@ -770,6 +770,7 @@ We have 8 policies, so far, and some of them have 'subpolicies' (the RouterRever - RouterReversionPolicy - StaticPath - WildcardPath + - Param - URLPath - RouterBuilderPolicy - RouterWrapperPolicy diff --git a/adaptors/gorillamux/gorillamux.go b/adaptors/gorillamux/gorillamux.go index 33684964..97124fd6 100644 --- a/adaptors/gorillamux/gorillamux.go +++ b/adaptors/gorillamux/gorillamux.go @@ -74,6 +74,9 @@ func New() iris.Policies { // finally return the path given + the wildcard path part return path + wildcardPart }, + Param: func(paramName string) string { + return "{" + paramName + "}" + }, // Note: on gorilla mux the {{ url }} and {{ path}} should give the key and the value, not only the values by order. // {{ url "nameOfTheRoute" "parameterName" "parameterValue"}}. // diff --git a/adaptors/httprouter/httprouter.go b/adaptors/httprouter/httprouter.go index 0c619538..db8e8b59 100644 --- a/adaptors/httprouter/httprouter.go +++ b/adaptors/httprouter/httprouter.go @@ -545,6 +545,9 @@ func New() iris.Policies { // finally return the path given + the wildcard path part return path + wildcardPart }, + Param: func(paramName string) string { + return string(parameterStartByte) + paramName + }, // path = "/api/users/:id", args = ["42"] // return "/api/users/42" // diff --git a/iris.go b/iris.go index 9df71cc6..2e8358c7 100644 --- a/iris.go +++ b/iris.go @@ -19,6 +19,7 @@ import ( "net/url" "os" "os/signal" + "regexp" "strconv" "strings" "sync" @@ -89,8 +90,8 @@ type Framework struct { // - RouterReversionPolicy // - StaticPath // - WildcardPath + // - Param // - URLPath - // - RouteContextLinker // - RouterBuilderPolicy // - RouterWrapperPolicy // - RenderPolicy @@ -884,6 +885,103 @@ func (s *Framework) URL(routeName string, args ...interface{}) (url string) { return } +// Regex takes pairs with the named path (without symbols) following by its expression +// and returns a middleware which will do a pure but effective validation using the regexp package. +// +// Note: '/adaptors/gorillamux' already supports regex path validation. +// It's useful while the developer uses the '/adaptors/httprouter' instead. +func (s *Framework) Regex(pairParamExpr ...string) HandlerFunc { + srvErr := func(ctx *Context) { + ctx.EmitError(StatusInternalServerError) + } + + wp := s.policies.RouterReversionPolicy.WildcardPath + if wp == nil { + s.Log(ProdMode, "regex cannot be used when a router policy is missing\n"+errRouterIsMissing.Format(s.Config.VHost).Error()) + return srvErr + } + + if len(pairParamExpr)%2 != 0 { + s.Log(ProdMode, + "regex pre-compile error: the correct format is paramName, expression"+ + "paramName2, expression2. The len should be %2==0") + return srvErr + } + pairs := make(map[string]*regexp.Regexp, len(pairParamExpr)/2) + + for i := 0; i < len(pairParamExpr)-1; i++ { + expr := pairParamExpr[i+1] + r, err := regexp.Compile(expr) + if err != nil { + s.Log(ProdMode, "regex '"+expr+"' failed. Trace: "+err.Error()) + return srvErr + } + + pairs[pairParamExpr[i]] = r + i++ + } + + // return the middleware + return func(ctx *Context) { + for k, v := range pairs { + pathPart := ctx.Param(k) + if pathPart == "" { + // take care, the router already + // does the param validations + // so if it's empty here it means that + // the router has label it as optional. + // so we skip it, and continue to the next. + continue + } + // the improtant thing: + // if the path part didn't match with the relative exp, then fire status not found. + if !v.MatchString(pathPart) { + ctx.EmitError(StatusNotFound) + return + } + } + // otherwise continue to the next handler... + ctx.Next() + } +} + +// RouteParam returns a named parameter as each router defines named path parameters. +// For example, with the httprouter(: as named param symbol): +// userid should return :userid. +// with gorillamux, userid should return {userid} +// or userid[1-9]+ should return {userid[1-9]+}. +// so basically we just wrap the raw parameter name +// with the start (and end) dynamic symbols of each router implementing the RouterReversionPolicy. +// It's an optional functionality but it can be used to create adaptors without even know the router +// that the user uses (which can be taken by app.Config.Other[iris.RouterNameConfigKey]. +// +// Note: we don't need a function like ToWildcardParam because the developer +// can use the RouterParam with a combination with RouteWildcardPath. +// +// Example: https://github.com/iris-contrib/adaptors/blob/master/oauth/oauth.go +func (s *Framework) RouteParam(paramName string) string { + if s.policies.RouterReversionPolicy.Param == nil { + // all Iris' routers are implementing all features but third-parties may not, so make sure that the user + // will get a useful message back. + s.Log(DevMode, "cannot wrap a route named path parameter because the functionality was not implemented by the current router.") + return "" + } + + return s.policies.RouterReversionPolicy.Param(paramName) +} + +// RouteWildcardPath returns a path converted to a 'dynamic' path +// for example, with the httprouter(wildcard symbol: '*'): +// ("/static", "path") should return /static/*path +// ("/myfiles/assets", "anything") should return /myfiles/assets/*anything +func (s *Framework) RouteWildcardPath(path string, paramName string) string { + if s.policies.RouterReversionPolicy.WildcardPath == nil { + s.Log(DevMode, "please use WildcardPath after .Adapt(router).\n"+errRouterIsMissing.Format(s.Config.VHost).Error()) + return "" + } + return s.policies.RouterReversionPolicy.WildcardPath(path, paramName) +} + // DecodeQuery returns the uri parameter as url (string) // useful when you want to pass something to a database and be valid to retrieve it via context.Param // use it only for special cases, when the default behavior doesn't suits you. diff --git a/policy.go b/policy.go index e3067735..07dfca06 100644 --- a/policy.go +++ b/policy.go @@ -92,7 +92,7 @@ const ( // ProdMode the production level logger write mode, // responsible to fatal errors, errors that happen which // your app can't continue running. - ProdMode LogMode = 1 << iota + ProdMode LogMode = iota // DevMode is the development level logger write mode, // responsible to the rest of the errors, for example // if you set a app.Favicon("myfav.ico"..) and that fav doesn't exists @@ -258,17 +258,30 @@ type ( // which custom routers should create and adapt to the Policies. RouterReversionPolicy struct { // StaticPath should return the static part of the route path - // for example, with the default router (: and *): + // for example, with the httprouter(: and *): // /api/user/:userid should return /api/user // /api/user/:userid/messages/:messageid should return /api/user // /dynamicpath/*path should return /dynamicpath // /my/path should return /my/path StaticPath func(path string) string // WildcardPath should return a path converted to a 'dynamic' path - // for example, with the default router(wildcard symbol: '*'): + // for example, with the httprouter(wildcard symbol: '*'): // ("/static", "path") should return /static/*path // ("/myfiles/assets", "anything") should return /myfiles/assets/*anything WildcardPath func(path string, paramName string) string + // Param should return a named parameter as each router defines named path parameters. + // For example, with the httprouter(: as named param symbol): + // userid should return :userid. + // with gorillamux, userid should return {userid} + // or userid[1-9]+ should return {userid[1-9]+}. + // so basically we just wrap the raw parameter name + // with the start (and end) dynamic symbols of each router implementing the RouterReversionPolicy. + // It's an optional functionality but it can be used to create adaptors without even know the router + // that the user uses (which can be taken by app.Config.Other[iris.RouterNameConfigKey]. + // + // Note: we don't need a function like WildcardParam because the developer + // can use the Param with a combination with WildcardPath. + Param func(paramName string) string // 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 @@ -317,6 +330,10 @@ func (r RouterReversionPolicy) Adapt(frame *Policies) { } } + if r.Param != nil { + frame.RouterReversionPolicy.Param = r.Param + } + if r.URLPath != nil { frame.RouterReversionPolicy.URLPath = r.URLPath } diff --git a/policy_gorillamux_test.go b/policy_gorillamux_test.go index 0c03a686..3dbb01ac 100644 --- a/policy_gorillamux_test.go +++ b/policy_gorillamux_test.go @@ -194,3 +194,20 @@ func TestGorillaMuxRouteURLPath(t *testing.T) { t.Fatalf("gorillamux' reverse routing 'URLPath' error: expected %s but got %s", expected, got) } } + +func TestGorillaMuxRouteParamAndWildcardPath(t *testing.T) { + app := iris.New() + app.Adapt(gorillamux.New()) + routePath := app.RouteWildcardPath("/profile/"+app.RouteParam("user_id")+"/"+app.RouteParam("ref")+"/", "anything") + expectedRoutePath := "/profile/{user_id}/{ref}/{anything:.*}" + if routePath != expectedRoutePath { + t.Fatalf("Gorilla Mux Error on RouteParam and RouteWildcardPath, expecting '%s' but got '%s'", expectedRoutePath, routePath) + } + + app.Get(routePath, func(ctx *iris.Context) { + ctx.Writef(ctx.Path()) + }) + e := httptest.New(app, t) + + e.GET("/profile/42/areference/anythinghere").Expect().Status(iris.StatusOK).Body().Equal("/profile/42/areference/anythinghere") +} diff --git a/policy_httprouter_test.go b/policy_httprouter_test.go index 8a7f5675..d0bb3292 100644 --- a/policy_httprouter_test.go +++ b/policy_httprouter_test.go @@ -246,3 +246,19 @@ func TestHTTPRouterRegexMiddleware(t *testing.T) { e.GET("/profile/gerasimosmaropoulos").Expect().Status(iris.StatusOK) e.GET("/profile/anumberof42").Expect().Status(iris.StatusNotFound) } + +func TestHTTPRouterRouteParamAndWildcardPath(t *testing.T) { + app := newHTTPRouterApp() + routePath := app.RouteWildcardPath("/profile/"+app.RouteParam("user_id")+"/"+app.RouteParam("ref")+"/", "anything") + expectedRoutePath := "/profile/:user_id/:ref/*anything" + if routePath != expectedRoutePath { + t.Fatalf("HTTPRouter Error on RouteParam and RouteWildcardPath, expecting '%s' but got '%s'", expectedRoutePath, routePath) + } + + app.Get(routePath, func(ctx *iris.Context) { + ctx.Writef(ctx.Path()) + }) + e := httptest.New(app, t) + + e.GET("/profile/42/areference/anythinghere").Expect().Status(iris.StatusOK).Body().Equal("/profile/42/areference/anythinghere") +} diff --git a/router.go b/router.go index a69e9d99..6165ff84 100644 --- a/router.go +++ b/router.go @@ -4,7 +4,6 @@ import ( "net/http" "os" "path" - "regexp" "strings" "time" @@ -131,71 +130,6 @@ type Router struct { relativePath string } -// Regex takes pairs with the named path (without symbols) following by its expression -// and returns a middleware which will do a pure but effective validation using the regexp package. -// -// Note: '/adaptors/gorillamux' already supports regex path validation. -// It's useful while the developer uses the '/adaptors/httprouter' instead. -func (s *Framework) Regex(pairParamExpr ...string) HandlerFunc { - srvErr := func(ctx *Context) { - ctx.EmitError(StatusInternalServerError) - } - - wp := s.policies.RouterReversionPolicy.WildcardPath - if wp == nil { - s.Log(ProdMode, "expr cannot be used when a router policy is missing\n"+errRouterIsMissing.Format(s.Config.VHost).Error()) - return srvErr - } - - if len(pairParamExpr)%2 != 0 { - s.Log(ProdMode, - "regexp expr pre-compile error: the correct format is paramName, expression"+ - "paramName2, expression2. The len should be %2==0") - return srvErr - } - pairs := make(map[string]*regexp.Regexp, len(pairParamExpr)/2) - - for i := 0; i < len(pairParamExpr)-1; i++ { - expr := pairParamExpr[i+1] - r, err := regexp.Compile(expr) - if err != nil { - s.Log(ProdMode, "expr: regexp failed on: "+expr+". Trace:"+err.Error()) - return srvErr - } - - pairs[pairParamExpr[i]] = r - i++ - } - - // return the middleware - return func(ctx *Context) { - for k, v := range pairs { - pathPart := ctx.Param(k) - if pathPart == "" { - // take care, the router already - // does the param validations - // so if it's empty here it means that - // the router has label it as optional. - // so we skip it, and continue to the next. - continue - } - // the improtant thing: - // if the path part didn't match with the relative exp, then fire status not found. - if !v.MatchString(pathPart) { - ctx.EmitError(StatusNotFound) - return - } - } - // otherwise continue to the next handler... - ctx.Next() - } -} - -var ( - // errDirectoryFileNotFound returns an error with message: 'Directory or file %s couldn't found. Trace: +error trace' - errDirectoryFileNotFound = errors.New("Directory or file %s couldn't found. Trace: %s") -) - func (router *Router) build(builder RouterBuilderPolicy) { router.repository.sort() // sort - priority to subdomains router.handler = builder(router.repository, router.Context) @@ -561,6 +495,9 @@ func (router *Router) StaticEmbedded(requestPath string, vdir string, assetFn fu return router.registerResourceRoute(requestPath, h) } +// errDirectoryFileNotFound returns an error with message: 'Directory or file %s couldn't found. Trace: +error trace' +var errDirectoryFileNotFound = errors.New("Directory or file %s couldn't found. Trace: %s") + // Favicon serves static favicon // accepts 2 parameters, second is optional // favPath (string), declare the system directory path of the __.ico