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
This commit is contained in:
Gerasimos (Makis) Maropoulos 2017-02-28 15:01:18 +02:00
parent b214df742b
commit 064282036c
8 changed files with 163 additions and 71 deletions

View File

@ -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

View File

@ -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"}}.
//

View File

@ -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"
//

100
iris.go
View File

@ -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.

View File

@ -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
}

View File

@ -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")
}

View File

@ -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")
}

View File

@ -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