Skip to content

Commit

Permalink
new feature: handle different param types in the exact same path pattern
Browse files Browse the repository at this point in the history
implements #1315
  • Loading branch information
kataras committed Jul 29, 2019
1 parent 1616d87 commit 92c4c20
Show file tree
Hide file tree
Showing 10 changed files with 168 additions and 28 deletions.
16 changes: 11 additions & 5 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,16 @@

Developers are not forced to upgrade if they don't really need it. Upgrade whenever you feel ready.

**How to upgrade**: Open your command-line and execute this command: `go get github.com/kataras/iris@v11.2.0`.
**How to upgrade**: Open your command-line and execute this command: `go get github.com/kataras/iris@master`.

# Tu, 30 July 2019 | v11.2.3

# We, 24 July 2019 | v11.2.1
TODO:

- https://github.com/kataras/iris/issues/1298
- https://github.com/kataras/iris/issues/1207
- Different parameter types in the same path (done).
- [Content negotiation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation) (in-progress)

## v11.2.2
# We, 24 July 2019 | v11.2.2

Sessions as middleware:

Expand All @@ -47,6 +48,11 @@ app.Get("/path", func(ctx iris.Context){
- Add `Session.Len() int` to return the total number of stored values/entries.
- Make `Context.HTML` and `Context.Text` to accept an optional, variadic, `args ...interface{}` input arg(s) too.

## v11.1.1

- https://github.com/kataras/iris/issues/1298
- https://github.com/kataras/iris/issues/1207

# Tu, 23 July 2019 | v11.2.0

Read about the new release at: https://dev.to/kataras/iris-version-11-2-released-22bc
4 changes: 3 additions & 1 deletion _examples/miscellaneous/pprof/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ func main() {
ctx.HTML("<h1> Please click <a href='/debug/pprof'>here</a>")
})

app.Any("/debug/pprof/{action:path}", pprof.New())
p := pprof.New()
app.Any("/debug/pprof", p)
app.Any("/debug/pprof/{action:path}", p)
// ___________
app.Run(iris.Addr(":8080"))
}
42 changes: 42 additions & 0 deletions _examples/routing/basic/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

func main() {
app := iris.New()
app.Logger().SetLevel("debug")

// registers a custom handler for 404 not found http (error) status code,
// fires when route not found or manually by ctx.StatusCode(iris.StatusNotFound).
Expand All @@ -26,6 +27,42 @@ func main() {
ctx.Writef(`Same as app.Handle("GET", "/", [...])`)
})

// Different path parameters types in the same path.
app.Get("/u/{username:string}", func(ctx iris.Context) {
ctx.Writef("before username (string), current route name: %s\n", ctx.RouteName())
ctx.Next()
}, func(ctx iris.Context) {
ctx.Writef("username (string): %s", ctx.Params().Get("username"))
})

app.Get("/u/{id:int}", func(ctx iris.Context) {
ctx.Writef("before id (int), current route name: %s\n", ctx.RouteName())
ctx.Next()
}, func(ctx iris.Context) {
ctx.Writef("id (int): %d", ctx.Params().GetIntDefault("id", 0))
})

app.Get("/u/{uid:uint}", func(ctx iris.Context) {
ctx.Writef("before uid (uint), current route name: %s\n", ctx.RouteName())
ctx.Next()
}, func(ctx iris.Context) {
ctx.Writef("uid (uint): %d", ctx.Params().GetUintDefault("uid", 0))
})

app.Get("/u/{firstname:alphabetical}", func(ctx iris.Context) {
ctx.Writef("before firstname (alphabetical), current route name: %s\n", ctx.RouteName())
ctx.Next()
}, func(ctx iris.Context) {
ctx.Writef("firstname (alphabetical): %s", ctx.Params().Get("firstname"))
})

/*
/u/abcd maps to :alphabetical (if :alphabetical registered otherwise :string)
/u/42 maps to :uint (if :uint registered otherwise :int)
/u/-1 maps to :int (if :int registered otherwise :string)
/u/abcd123 maps to :string
*/

app.Get("/donate", donateHandler, donateFinishHandler)

// Pssst, don't forget dynamic-path example for more "magic"!
Expand Down Expand Up @@ -128,6 +165,11 @@ func main() {
// http://localhost:8080/api/users/blabla
// http://localhost:8080/wontfound
//
// http://localhost:8080/u/abcd
// http://localhost:8080/u/42
// http://localhost:8080/u/-1
// http://localhost:8080/u/abcd123
//
// if hosts edited:
// http://v1.localhost:8080
// http://v1.localhost:8080/api/users
Expand Down
17 changes: 17 additions & 0 deletions core/router/api_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/kataras/iris/context"
"github.com/kataras/iris/core/errors"
"github.com/kataras/iris/macro"
macroHandler "github.com/kataras/iris/macro/handler"
)

// MethodNone is a Virtual method
Expand Down Expand Up @@ -109,6 +110,20 @@ func (repo *repository) get(routeName string) *Route {
return nil
}

func (repo *repository) getRelative(r *Route) *Route {
if r.tmpl.IsTrailing() || !macroHandler.CanMakeHandler(r.tmpl) {
return nil
}

for _, route := range repo.routes {
if r.Subdomain == route.Subdomain && r.Method == route.Method && r.FormattedPath == route.FormattedPath && !route.tmpl.IsTrailing() {
return route
}
}

return nil
}

func (repo *repository) getByPath(tmplPath string) *Route {
if repo.pos != nil {
if idx, ok := repo.pos[tmplPath]; ok {
Expand Down Expand Up @@ -345,6 +360,8 @@ func (api *APIBuilder) Handle(method string, relativePath string, handlers ...co
var route *Route // the last one is returned.
for _, route = range routes {
// global

route.topLink = api.routes.getRelative(route)
api.routes.register(route)
}

Expand Down
61 changes: 48 additions & 13 deletions core/router/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ import (
"sort"
"strings"

"github.com/kataras/golog"

"github.com/kataras/iris/context"
"github.com/kataras/iris/core/errors"
"github.com/kataras/iris/core/netutil"
macroHandler "github.com/kataras/iris/macro/handler"

"github.com/kataras/golog"
)

// RequestHandler the middle man between acquiring a context and releasing it.
Expand Down Expand Up @@ -116,29 +117,63 @@ func (h *routerHandler) Build(provider RoutesProvider) error {
})

for _, r := range registeredRoutes {
// build the r.Handlers based on begin and done handlers, if any.
r.BuildHandlers()
if r.topLink != nil {
bindMultiParamTypesHandler(r.topLink, r)
}
}

for _, r := range registeredRoutes {
if r.Subdomain != "" {
h.hosts = true
}

// the only "bad" with this is if the user made an error
// on route, it will be stacked shown in this build state
// and no in the lines of the user's action, they should read
// the docs better. Or TODO: add a link here in order to help new users.
if err := h.addRoute(r); err != nil {
// node errors:
rp.Add("%v -> %s", err, r.String())
continue
if r.topLink == nil {
// build the r.Handlers based on begin and done handlers, if any.
r.BuildHandlers()

// the only "bad" with this is if the user made an error
// on route, it will be stacked shown in this build state
// and no in the lines of the user's action, they should read
// the docs better. Or TODO: add a link here in order to help new users.
if err := h.addRoute(r); err != nil {
// node errors:
rp.Add("%v -> %s", err, r.String())
continue
}
}

golog.Debugf(r.Trace())
golog.Debugf(r.Trace()) // keep log different parameter types in the same path as different routes.
}

return rp.Return()
}

func bindMultiParamTypesHandler(top *Route, r *Route) {
r.BuildHandlers()

h := r.Handlers[1:] // remove the macro evaluator handler as we manually check below.
f := macroHandler.MakeFilter(r.tmpl)
if f == nil {
return // should never happen, previous checks made to set the top link.
}

decisionHandler := func(ctx context.Context) {
currentRouteName := ctx.RouteName()
if f(ctx) {
ctx.SetCurrentRouteName(r.Name)
ctx.HandlerIndex(0)
ctx.Do(h)
return
}

ctx.SetCurrentRouteName(currentRouteName)
ctx.StatusCode(http.StatusOK)
ctx.Next()
}

r.topLink.beginHandlers = append(context.Handlers{decisionHandler}, r.topLink.beginHandlers...)
}

func (h *routerHandler) HandleRequest(ctx context.Context) {
method := ctx.Method()
path := ctx.Path()
Expand Down
2 changes: 2 additions & 0 deletions core/router/route.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ type Route struct {
// route, manually or automatic by the framework,
// get the route by `Application#GetRouteByPath(staticSite.RequestPath)`.
StaticSites []context.StaticSite `json:"staticSites"`

topLink *Route
}

// NewRoute returns a new route based on its method,
Expand Down
4 changes: 2 additions & 2 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,13 @@ Source code and other details for the project are available at GitHub:
Current Version
11.2.2
11.2.3
Installation
The only requirement is the Go Programming Language, at least version 1.12.
$ go get github.com/kataras/iris@v11.2.2
$ go get github.com/kataras/iris@master
Wiki:
Expand Down
2 changes: 1 addition & 1 deletion iris.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import (

var (
// Version is the current version number of the Iris Web Framework.
Version = "11.2.2"
Version = "11.2.3"
)

// HTTP status codes as registered with IANA.
Expand Down
43 changes: 37 additions & 6 deletions macro/handler/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,23 +34,54 @@ func CanMakeHandler(tmpl macro.Template) (needsMacroHandler bool) {
// If the template does not contain any dynamic attributes and a special handler is NOT required
// then it returns a nil handler.
func MakeHandler(tmpl macro.Template) context.Handler {
filter := MakeFilter(tmpl)

return func(ctx context.Context) {
if !filter(ctx) {
ctx.StopExecution()
return
}

// if all passed, just continue.
ctx.Next()
}
}

// MakeFilter returns a Filter which reports whether a specific macro template
// and its parameters pass the serve-time validation.
func MakeFilter(tmpl macro.Template) context.Filter {
if !CanMakeHandler(tmpl) {
return nil
}

return func(ctx context.Context) {
return func(ctx context.Context) bool {
for _, p := range tmpl.Params {
if !p.CanEval() {
continue // allow.
}

if !p.Eval(ctx.Params().Get(p.Name), &ctx.Params().Store) {
// 07-29-2019
// changed to retrieve by param index in order to support
// different parameter names for routes with
// different param types (and probably different param names i.e {name:string}, {id:uint64})
// in the exact same path pattern.
//
// Same parameter names are not allowed, different param types in the same path
// should have different name e.g. {name} {id:uint64};
// something like {name} and {name:uint64}
// is bad API design and we do NOT allow it by-design.
entry, found := ctx.Params().Store.GetEntryAt(p.Index)
if !found {
// should never happen.
return false
}

if !p.Eval(entry.String(), &ctx.Params().Store) {
ctx.StatusCode(p.ErrCode)
ctx.StopExecution()
return
return false
}
}
// if all passed, just continue.
ctx.Next()

return true
}
}
5 changes: 5 additions & 0 deletions macro/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ type Template struct {
Params []TemplateParam `json:"params"`
}

// IsTrailing reports whether this Template is a traling one.
func (t *Template) IsTrailing() bool {
return len(t.Params) > 0 && ast.IsTrailing(t.Params[len(t.Params)-1].Type)
}

// TemplateParam is the parsed macro parameter's template
// they are being used to describe the param's syntax result.
type TemplateParam struct {
Expand Down

0 comments on commit 92c4c20

Please sign in to comment.