mirror of
https://github.com/kataras/iris.git
synced 2026-01-23 20:05:59 +00:00
Version 11 released. Read https://github.com/kataras/iris/blob/master/HISTORY.md#su-21-october-2018--v1100
Former-commit-id: fe6305deed00e170bf4d39a12c0644fe686e0a24
This commit is contained in:
@@ -9,20 +9,18 @@ import (
|
||||
|
||||
"github.com/kataras/iris/context"
|
||||
"github.com/kataras/iris/core/errors"
|
||||
"github.com/kataras/iris/core/router/macro"
|
||||
"github.com/kataras/iris/macro"
|
||||
)
|
||||
|
||||
const (
|
||||
// MethodNone is a Virtual method
|
||||
// to store the "offline" routes.
|
||||
MethodNone = "NONE"
|
||||
)
|
||||
// MethodNone is a Virtual method
|
||||
// to store the "offline" routes.
|
||||
const MethodNone = "NONE"
|
||||
|
||||
var (
|
||||
// AllMethods contains the valid http methods:
|
||||
// "GET", "POST", "PUT", "DELETE", "CONNECT", "HEAD",
|
||||
// "PATCH", "OPTIONS", "TRACE".
|
||||
AllMethods = [...]string{
|
||||
AllMethods = []string{
|
||||
"GET",
|
||||
"POST",
|
||||
"PUT",
|
||||
@@ -68,7 +66,7 @@ func (r *repository) getAll() []*Route {
|
||||
// and child routers.
|
||||
type APIBuilder struct {
|
||||
// the api builder global macros registry
|
||||
macros *macro.Map
|
||||
macros *macro.Macros
|
||||
// the api builder global handlers per status code registry (used for custom http errors)
|
||||
errorCodeHandlers *ErrorCodeHandlers
|
||||
// the api builder global routes repository
|
||||
@@ -116,7 +114,7 @@ var _ RoutesProvider = (*APIBuilder)(nil) // passed to the default request handl
|
||||
// which is responsible to build the API and the router handler.
|
||||
func NewAPIBuilder() *APIBuilder {
|
||||
api := &APIBuilder{
|
||||
macros: defaultMacros(),
|
||||
macros: macro.Defaults,
|
||||
errorCodeHandlers: defaultErrorCodeHandlers(),
|
||||
reporter: errors.NewReporter(),
|
||||
relativePath: "/",
|
||||
@@ -246,7 +244,7 @@ func (api *APIBuilder) Handle(method string, relativePath string, handlers ...co
|
||||
)
|
||||
|
||||
for _, m := range methods {
|
||||
route, err = NewRoute(m, subdomain, path, possibleMainHandlerName, routeHandlers, api.macros)
|
||||
route, err = NewRoute(m, subdomain, path, possibleMainHandlerName, routeHandlers, *api.macros)
|
||||
if err != nil { // template path parser errors:
|
||||
api.reporter.Add("%v -> %s:%s:%s", err, method, subdomain, path)
|
||||
return nil // fail on first error.
|
||||
@@ -270,10 +268,10 @@ func (api *APIBuilder) Handle(method string, relativePath string, handlers ...co
|
||||
// otherwise use `Party` which can handle many paths with different handlers and middlewares.
|
||||
//
|
||||
// Usage:
|
||||
// app.HandleMany("GET", "/user /user/{id:int} /user/me", genericUserHandler)
|
||||
// app.HandleMany("GET", "/user /user/{id:uint64} /user/me", genericUserHandler)
|
||||
// At the other side, with `Handle` we've had to write:
|
||||
// app.Handle("GET", "/user", userHandler)
|
||||
// app.Handle("GET", "/user/{id:int}", userByIDHandler)
|
||||
// app.Handle("GET", "/user/{id:uint64}", userByIDHandler)
|
||||
// app.Handle("GET", "/user/me", userMeHandler)
|
||||
//
|
||||
// This method is used behind the scenes at the `Controller` function
|
||||
@@ -411,11 +409,11 @@ func (api *APIBuilder) WildcardSubdomain(middleware ...context.Handler) Party {
|
||||
return api.Subdomain(SubdomainWildcardIndicator, middleware...)
|
||||
}
|
||||
|
||||
// Macros returns the macro map which is responsible
|
||||
// to register custom macro functions for all routes.
|
||||
// Macros returns the macro collection that is responsible
|
||||
// to register custom macros with their own parameter types and their macro functions for all routes.
|
||||
//
|
||||
// Learn more at: https://github.com/kataras/iris/tree/master/_examples/routing/dynamic-path
|
||||
func (api *APIBuilder) Macros() *macro.Map {
|
||||
func (api *APIBuilder) Macros() *macro.Macros {
|
||||
return api.macros
|
||||
}
|
||||
|
||||
|
||||
@@ -11,40 +11,30 @@ import (
|
||||
"github.com/kataras/iris/context"
|
||||
"github.com/kataras/iris/core/errors"
|
||||
"github.com/kataras/iris/core/netutil"
|
||||
"github.com/kataras/iris/core/router/node"
|
||||
)
|
||||
|
||||
// RequestHandler the middle man between acquiring a context and releasing it.
|
||||
// By-default is the router algorithm.
|
||||
type RequestHandler interface {
|
||||
// HandleRequest is same as context.Handler but its usage is only about routing,
|
||||
// separate the concept here.
|
||||
// HandleRequest should handle the request based on the Context.
|
||||
HandleRequest(context.Context)
|
||||
// Build should builds the handler, it's being called on router's BuildRouter.
|
||||
// Build should builds the handler, it's being called on router's BuildRouter.
|
||||
Build(provider RoutesProvider) error
|
||||
// RouteExists reports whether a particular route exists.
|
||||
RouteExists(ctx context.Context, method, path string) bool
|
||||
}
|
||||
|
||||
type tree struct {
|
||||
Method string
|
||||
// subdomain is empty for default-hostname routes,
|
||||
// ex: mysubdomain.
|
||||
Subdomain string
|
||||
Nodes *node.Nodes
|
||||
}
|
||||
|
||||
type routerHandler struct {
|
||||
trees []*tree
|
||||
trees []*trie
|
||||
hosts bool // true if at least one route contains a Subdomain.
|
||||
}
|
||||
|
||||
var _ RequestHandler = &routerHandler{}
|
||||
|
||||
func (h *routerHandler) getTree(method, subdomain string) *tree {
|
||||
func (h *routerHandler) getTree(method, subdomain string) *trie {
|
||||
for i := range h.trees {
|
||||
t := h.trees[i]
|
||||
if t.Method == method && t.Subdomain == subdomain {
|
||||
if t.method == method && t.subdomain == subdomain {
|
||||
return t
|
||||
}
|
||||
}
|
||||
@@ -64,12 +54,14 @@ func (h *routerHandler) addRoute(r *Route) error {
|
||||
t := h.getTree(method, subdomain)
|
||||
|
||||
if t == nil {
|
||||
n := node.Nodes{}
|
||||
n := newTrieNode()
|
||||
// first time we register a route to this method with this subdomain
|
||||
t = &tree{Method: method, Subdomain: subdomain, Nodes: &n}
|
||||
t = &trie{method: method, subdomain: subdomain, root: n}
|
||||
h.trees = append(h.trees, t)
|
||||
}
|
||||
return t.Nodes.Add(routeName, path, handlers)
|
||||
|
||||
t.insert(path, routeName, handlers)
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewDefaultHandler returns the handler which is responsible
|
||||
@@ -189,11 +181,11 @@ func (h *routerHandler) HandleRequest(ctx context.Context) {
|
||||
|
||||
for i := range h.trees {
|
||||
t := h.trees[i]
|
||||
if method != t.Method {
|
||||
if method != t.method {
|
||||
continue
|
||||
}
|
||||
|
||||
if h.hosts && t.Subdomain != "" {
|
||||
if h.hosts && t.subdomain != "" {
|
||||
requestHost := ctx.Host()
|
||||
if netutil.IsLoopbackSubdomain(requestHost) {
|
||||
// this fixes a bug when listening on
|
||||
@@ -202,7 +194,7 @@ func (h *routerHandler) HandleRequest(ctx context.Context) {
|
||||
continue // it's not a subdomain, it's something like 127.0.0.1 probably
|
||||
}
|
||||
// it's a dynamic wildcard subdomain, we have just to check if ctx.subdomain is not empty
|
||||
if t.Subdomain == SubdomainWildcardIndicator {
|
||||
if t.subdomain == SubdomainWildcardIndicator {
|
||||
// mydomain.com -> invalid
|
||||
// localhost -> invalid
|
||||
// sub.mydomain.com -> valid
|
||||
@@ -220,14 +212,14 @@ func (h *routerHandler) HandleRequest(ctx context.Context) {
|
||||
continue
|
||||
}
|
||||
// continue to that, any subdomain is valid.
|
||||
} else if !strings.HasPrefix(requestHost, t.Subdomain) { // t.Subdomain contains the dot.
|
||||
} else if !strings.HasPrefix(requestHost, t.subdomain) { // t.subdomain contains the dot.
|
||||
continue
|
||||
}
|
||||
}
|
||||
routeName, handlers := t.Nodes.Find(path, ctx.Params())
|
||||
if len(handlers) > 0 {
|
||||
ctx.SetCurrentRouteName(routeName)
|
||||
ctx.Do(handlers)
|
||||
n := t.search(path, ctx.Params())
|
||||
if n != nil {
|
||||
ctx.SetCurrentRouteName(n.RouteName)
|
||||
ctx.Do(n.Handlers)
|
||||
// found
|
||||
return
|
||||
}
|
||||
@@ -238,15 +230,12 @@ func (h *routerHandler) HandleRequest(ctx context.Context) {
|
||||
if ctx.Application().ConfigurationReadOnly().GetFireMethodNotAllowed() {
|
||||
for i := range h.trees {
|
||||
t := h.trees[i]
|
||||
// a bit slower than previous implementation but @kataras let me to apply this change
|
||||
// because it's more reliable.
|
||||
//
|
||||
// if `Configuration#FireMethodNotAllowed` is kept as defaulted(false) then this function will not
|
||||
// run, therefore performance kept as before.
|
||||
if t.Nodes.Exists(path) {
|
||||
if h.subdomainAndPathAndMethodExists(ctx, t, "", path) {
|
||||
// RCF rfc2616 https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
|
||||
// The response MUST include an Allow header containing a list of valid methods for the requested resource.
|
||||
ctx.Header("Allow", t.Method)
|
||||
ctx.Header("Allow", t.method)
|
||||
ctx.StatusCode(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
@@ -256,55 +245,55 @@ func (h *routerHandler) HandleRequest(ctx context.Context) {
|
||||
ctx.StatusCode(http.StatusNotFound)
|
||||
}
|
||||
|
||||
func (h *routerHandler) subdomainAndPathAndMethodExists(ctx context.Context, t *trie, method, path string) bool {
|
||||
if method != "" && method != t.method {
|
||||
return false
|
||||
}
|
||||
|
||||
if h.hosts && t.subdomain != "" {
|
||||
requestHost := ctx.Host()
|
||||
if netutil.IsLoopbackSubdomain(requestHost) {
|
||||
// this fixes a bug when listening on
|
||||
// 127.0.0.1:8080 for example
|
||||
// and have a wildcard subdomain and a route registered to root domain.
|
||||
return false // it's not a subdomain, it's something like 127.0.0.1 probably
|
||||
}
|
||||
// it's a dynamic wildcard subdomain, we have just to check if ctx.subdomain is not empty
|
||||
if t.subdomain == SubdomainWildcardIndicator {
|
||||
// mydomain.com -> invalid
|
||||
// localhost -> invalid
|
||||
// sub.mydomain.com -> valid
|
||||
// sub.localhost -> valid
|
||||
serverHost := ctx.Application().ConfigurationReadOnly().GetVHost()
|
||||
if serverHost == requestHost {
|
||||
return false // it's not a subdomain, it's a full domain (with .com...)
|
||||
}
|
||||
|
||||
dotIdx := strings.IndexByte(requestHost, '.')
|
||||
slashIdx := strings.IndexByte(requestHost, '/')
|
||||
if dotIdx > 0 && (slashIdx == -1 || slashIdx > dotIdx) {
|
||||
// if "." was found anywhere but not at the first path segment (host).
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
// continue to that, any subdomain is valid.
|
||||
} else if !strings.HasPrefix(requestHost, t.subdomain) { // t.subdomain contains the dot.
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
n := t.search(path, ctx.Params())
|
||||
return n != nil
|
||||
}
|
||||
|
||||
// RouteExists reports whether a particular route exists
|
||||
// It will search from the current subdomain of context's host, if not inside the root domain.
|
||||
func (h *routerHandler) RouteExists(ctx context.Context, method, path string) bool {
|
||||
for i := range h.trees {
|
||||
t := h.trees[i]
|
||||
if method != t.Method {
|
||||
continue
|
||||
}
|
||||
|
||||
if h.hosts && t.Subdomain != "" {
|
||||
requestHost := ctx.Host()
|
||||
if netutil.IsLoopbackSubdomain(requestHost) {
|
||||
// this fixes a bug when listening on
|
||||
// 127.0.0.1:8080 for example
|
||||
// and have a wildcard subdomain and a route registered to root domain.
|
||||
continue // it's not a subdomain, it's something like 127.0.0.1 probably
|
||||
}
|
||||
// it's a dynamic wildcard subdomain, we have just to check if ctx.subdomain is not empty
|
||||
if t.Subdomain == SubdomainWildcardIndicator {
|
||||
// mydomain.com -> invalid
|
||||
// localhost -> invalid
|
||||
// sub.mydomain.com -> valid
|
||||
// sub.localhost -> valid
|
||||
serverHost := ctx.Application().ConfigurationReadOnly().GetVHost()
|
||||
if serverHost == requestHost {
|
||||
continue // it's not a subdomain, it's a full domain (with .com...)
|
||||
}
|
||||
|
||||
dotIdx := strings.IndexByte(requestHost, '.')
|
||||
slashIdx := strings.IndexByte(requestHost, '/')
|
||||
if dotIdx > 0 && (slashIdx == -1 || slashIdx > dotIdx) {
|
||||
// if "." was found anywhere but not at the first path segment (host).
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
// continue to that, any subdomain is valid.
|
||||
} else if !strings.HasPrefix(requestHost, t.Subdomain) { // t.Subdomain contains the dot.
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
_, handlers := t.Nodes.Find(path, ctx.Params())
|
||||
if len(handlers) > 0 {
|
||||
// found
|
||||
if h.subdomainAndPathAndMethodExists(ctx, t, method, path) {
|
||||
return true
|
||||
}
|
||||
|
||||
// not found or method not allowed.
|
||||
break
|
||||
}
|
||||
|
||||
return false
|
||||
|
||||
@@ -1,253 +0,0 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/kataras/iris/context"
|
||||
"github.com/kataras/iris/core/errors"
|
||||
"github.com/kataras/iris/core/router/macro"
|
||||
"github.com/kataras/iris/core/router/macro/interpreter/ast"
|
||||
)
|
||||
|
||||
// defaultMacros returns a new macro map which
|
||||
// contains the default router's named param types functions.
|
||||
func defaultMacros() *macro.Map {
|
||||
macros := macro.NewMap()
|
||||
// registers the String and Int default macro funcs
|
||||
// user can add or override of his own funcs later on
|
||||
// i.e:
|
||||
// app.Macro.String.RegisterFunc("equal", func(eqWith string) func(string) bool {
|
||||
// return func(paramValue string) bool {
|
||||
// return eqWith == paramValue
|
||||
// }})
|
||||
registerBuiltinsMacroFuncs(macros)
|
||||
|
||||
return macros
|
||||
}
|
||||
|
||||
func registerBuiltinsMacroFuncs(out *macro.Map) {
|
||||
// register the String which is the default type if not
|
||||
// parameter type is specified or
|
||||
// if a given parameter into path given but the func doesn't exist on the
|
||||
// parameter type's function list.
|
||||
//
|
||||
// these can be overridden by the user, later on.
|
||||
registerStringMacroFuncs(out.String)
|
||||
registerIntMacroFuncs(out.Int)
|
||||
registerIntMacroFuncs(out.Long)
|
||||
registerAlphabeticalMacroFuncs(out.Alphabetical)
|
||||
registerFileMacroFuncs(out.File)
|
||||
registerPathMacroFuncs(out.Path)
|
||||
}
|
||||
|
||||
// String
|
||||
// anything one part
|
||||
func registerStringMacroFuncs(out *macro.Macro) {
|
||||
// this can be used everywhere, it's to help users to define custom regexp expressions
|
||||
// on all macros
|
||||
out.RegisterFunc("regexp", func(expr string) macro.EvaluatorFunc {
|
||||
regexpEvaluator := macro.MustNewEvaluatorFromRegexp(expr)
|
||||
return regexpEvaluator
|
||||
})
|
||||
|
||||
// checks if param value starts with the 'prefix' arg
|
||||
out.RegisterFunc("prefix", func(prefix string) macro.EvaluatorFunc {
|
||||
return func(paramValue string) bool {
|
||||
return strings.HasPrefix(paramValue, prefix)
|
||||
}
|
||||
})
|
||||
|
||||
// checks if param value ends with the 'suffix' arg
|
||||
out.RegisterFunc("suffix", func(suffix string) macro.EvaluatorFunc {
|
||||
return func(paramValue string) bool {
|
||||
return strings.HasSuffix(paramValue, suffix)
|
||||
}
|
||||
})
|
||||
|
||||
// checks if param value contains the 's' arg
|
||||
out.RegisterFunc("contains", func(s string) macro.EvaluatorFunc {
|
||||
return func(paramValue string) bool {
|
||||
return strings.Contains(paramValue, s)
|
||||
}
|
||||
})
|
||||
|
||||
// checks if param value's length is at least 'min'
|
||||
out.RegisterFunc("min", func(min int) macro.EvaluatorFunc {
|
||||
return func(paramValue string) bool {
|
||||
return len(paramValue) >= min
|
||||
}
|
||||
})
|
||||
// checks if param value's length is not bigger than 'max'
|
||||
out.RegisterFunc("max", func(max int) macro.EvaluatorFunc {
|
||||
return func(paramValue string) bool {
|
||||
return max >= len(paramValue)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Int
|
||||
// only numbers (0-9)
|
||||
func registerIntMacroFuncs(out *macro.Macro) {
|
||||
// checks if the param value's int representation is
|
||||
// bigger or equal than 'min'
|
||||
out.RegisterFunc("min", func(min int) macro.EvaluatorFunc {
|
||||
return func(paramValue string) bool {
|
||||
n, err := strconv.Atoi(paramValue)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return n >= min
|
||||
}
|
||||
})
|
||||
|
||||
// checks if the param value's int representation is
|
||||
// smaller or equal than 'max'
|
||||
out.RegisterFunc("max", func(max int) macro.EvaluatorFunc {
|
||||
return func(paramValue string) bool {
|
||||
n, err := strconv.Atoi(paramValue)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return n <= max
|
||||
}
|
||||
})
|
||||
|
||||
// checks if the param value's int representation is
|
||||
// between min and max, including 'min' and 'max'
|
||||
out.RegisterFunc("range", func(min, max int) macro.EvaluatorFunc {
|
||||
return func(paramValue string) bool {
|
||||
n, err := strconv.Atoi(paramValue)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if n < min || n > max {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Alphabetical
|
||||
// letters only (upper or lowercase)
|
||||
func registerAlphabeticalMacroFuncs(out *macro.Macro) {
|
||||
|
||||
}
|
||||
|
||||
// File
|
||||
// letters (upper or lowercase)
|
||||
// numbers (0-9)
|
||||
// underscore (_)
|
||||
// dash (-)
|
||||
// point (.)
|
||||
// no spaces! or other character
|
||||
func registerFileMacroFuncs(out *macro.Macro) {
|
||||
|
||||
}
|
||||
|
||||
// Path
|
||||
// File+slashes(anywhere)
|
||||
// should be the latest param, it's the wildcard
|
||||
func registerPathMacroFuncs(out *macro.Macro) {
|
||||
|
||||
}
|
||||
|
||||
// compileRoutePathAndHandlers receives a route info and returns its parsed/"compiled" path
|
||||
// and the new handlers (prepend all the macro's handler, if any).
|
||||
//
|
||||
// It's not exported for direct use.
|
||||
func compileRoutePathAndHandlers(handlers context.Handlers, tmpl *macro.Template) (string, context.Handlers, error) {
|
||||
// parse the path to node's path, now.
|
||||
path, err := convertTmplToNodePath(tmpl)
|
||||
if err != nil {
|
||||
return tmpl.Src, handlers, err
|
||||
}
|
||||
// prepend the macro handler to the route, now,
|
||||
// right before the register to the tree, so routerbuilder.UseGlobal will work as expected.
|
||||
if len(tmpl.Params) > 0 {
|
||||
macroEvaluatorHandler := convertTmplToHandler(tmpl)
|
||||
// may return nil if no really need a macro handler evaluator
|
||||
if macroEvaluatorHandler != nil {
|
||||
handlers = append(context.Handlers{macroEvaluatorHandler}, handlers...)
|
||||
}
|
||||
}
|
||||
|
||||
return path, handlers, nil
|
||||
}
|
||||
|
||||
func convertTmplToNodePath(tmpl *macro.Template) (string, error) {
|
||||
routePath := tmpl.Src
|
||||
if len(tmpl.Params) > 0 {
|
||||
if routePath[len(routePath)-1] == '/' {
|
||||
routePath = routePath[0 : len(routePath)-2] // remove the last "/" if macro syntax instead of underline's
|
||||
}
|
||||
}
|
||||
|
||||
// if it has started with {} and it's valid
|
||||
// then the tmpl.Params will be filled,
|
||||
// so no any further check needed
|
||||
for i, p := range tmpl.Params {
|
||||
if p.Type == ast.ParamTypePath {
|
||||
if i != len(tmpl.Params)-1 {
|
||||
return "", errors.New("parameter type \"ParamTypePath\" should be putted to the very last of a path")
|
||||
}
|
||||
routePath = strings.Replace(routePath, p.Src, WildcardParam(p.Name), 1)
|
||||
} else {
|
||||
routePath = strings.Replace(routePath, p.Src, Param(p.Name), 1)
|
||||
}
|
||||
}
|
||||
|
||||
return routePath, nil
|
||||
}
|
||||
|
||||
// note: returns nil if not needed, the caller(router) should be check for that before adding that on route's Middleware
|
||||
func convertTmplToHandler(tmpl *macro.Template) context.Handler {
|
||||
|
||||
needMacroHandler := false
|
||||
|
||||
// check if we have params like: {name:string} or {name} or {anything:path} without else keyword or any functions used inside these params.
|
||||
// 1. if we don't have, then we don't need to add a handler before the main route's handler (as I said, no performance if macro is not really used)
|
||||
// 2. if we don't have any named params then we don't need a handler too.
|
||||
for _, p := range tmpl.Params {
|
||||
if len(p.Funcs) == 0 && (p.Type == ast.ParamTypeUnExpected || p.Type == ast.ParamTypeString || p.Type == ast.ParamTypePath) && p.ErrCode == http.StatusNotFound {
|
||||
} else {
|
||||
// println("we need handler for: " + tmpl.Src)
|
||||
needMacroHandler = true
|
||||
}
|
||||
}
|
||||
|
||||
if !needMacroHandler {
|
||||
// println("we don't need handler for: " + tmpl.Src)
|
||||
return nil
|
||||
}
|
||||
|
||||
return func(tmpl macro.Template) context.Handler {
|
||||
return func(ctx context.Context) {
|
||||
for _, p := range tmpl.Params {
|
||||
paramValue := ctx.Params().Get(p.Name)
|
||||
// first, check for type evaluator
|
||||
if !p.TypeEvaluator(paramValue) {
|
||||
ctx.StatusCode(p.ErrCode)
|
||||
ctx.StopExecution()
|
||||
return
|
||||
}
|
||||
|
||||
// then check for all of its functions
|
||||
for _, evalFunc := range p.Funcs {
|
||||
if !evalFunc(paramValue) {
|
||||
ctx.StatusCode(p.ErrCode)
|
||||
ctx.StopExecution()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
// if all passed, just continue
|
||||
ctx.Next()
|
||||
}
|
||||
}(*tmpl)
|
||||
|
||||
}
|
||||
@@ -1,215 +0,0 @@
|
||||
package ast
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// ParamType is a specific uint8 type
|
||||
// which holds the parameter types' type.
|
||||
type ParamType uint8
|
||||
|
||||
const (
|
||||
// ParamTypeUnExpected is an unexpected parameter type.
|
||||
ParamTypeUnExpected ParamType = iota
|
||||
// ParamTypeString is the string type.
|
||||
// If parameter type is missing then it defaults to String type.
|
||||
// Allows anything
|
||||
// Declaration: /mypath/{myparam:string} or /mypath{myparam}
|
||||
ParamTypeString
|
||||
// ParamTypeInt is the integer, a number type.
|
||||
// Allows only positive numbers (0-9)
|
||||
// Declaration: /mypath/{myparam:int}
|
||||
ParamTypeInt
|
||||
// ParamTypeLong is the integer, a number type.
|
||||
// Allows only positive numbers (0-9)
|
||||
// Declaration: /mypath/{myparam:long}
|
||||
ParamTypeLong
|
||||
// ParamTypeBoolean is the bool type.
|
||||
// Allows only "1" or "t" or "T" or "TRUE" or "true" or "True"
|
||||
// or "0" or "f" or "F" or "FALSE" or "false" or "False".
|
||||
// Declaration: /mypath/{myparam:boolean}
|
||||
ParamTypeBoolean
|
||||
// ParamTypeAlphabetical is the alphabetical/letter type type.
|
||||
// Allows letters only (upper or lowercase)
|
||||
// Declaration: /mypath/{myparam:alphabetical}
|
||||
ParamTypeAlphabetical
|
||||
// ParamTypeFile is the file single path type.
|
||||
// Allows:
|
||||
// letters (upper or lowercase)
|
||||
// numbers (0-9)
|
||||
// underscore (_)
|
||||
// dash (-)
|
||||
// point (.)
|
||||
// no spaces! or other character
|
||||
// Declaration: /mypath/{myparam:file}
|
||||
ParamTypeFile
|
||||
// ParamTypePath is the multi path (or wildcard) type.
|
||||
// Allows anything, should be the last part
|
||||
// Declaration: /mypath/{myparam:path}
|
||||
ParamTypePath
|
||||
)
|
||||
|
||||
func (pt ParamType) String() string {
|
||||
for k, v := range paramTypes {
|
||||
if v == pt {
|
||||
return k
|
||||
}
|
||||
}
|
||||
|
||||
return "unexpected"
|
||||
}
|
||||
|
||||
// Not because for a single reason
|
||||
// a string may be a
|
||||
// ParamTypeString or a ParamTypeFile
|
||||
// or a ParamTypePath or ParamTypeAlphabetical.
|
||||
//
|
||||
// func ParamTypeFromStd(k reflect.Kind) ParamType {
|
||||
|
||||
// Kind returns the std kind of this param type.
|
||||
func (pt ParamType) Kind() reflect.Kind {
|
||||
switch pt {
|
||||
case ParamTypeAlphabetical:
|
||||
fallthrough
|
||||
case ParamTypeFile:
|
||||
fallthrough
|
||||
case ParamTypePath:
|
||||
fallthrough
|
||||
case ParamTypeString:
|
||||
return reflect.String
|
||||
case ParamTypeInt:
|
||||
return reflect.Int
|
||||
case ParamTypeLong:
|
||||
return reflect.Int64
|
||||
case ParamTypeBoolean:
|
||||
return reflect.Bool
|
||||
}
|
||||
return reflect.Invalid // 0
|
||||
}
|
||||
|
||||
// ValidKind will return true if at least one param type is supported
|
||||
// for this std kind.
|
||||
func ValidKind(k reflect.Kind) bool {
|
||||
switch k {
|
||||
case reflect.String:
|
||||
fallthrough
|
||||
case reflect.Int:
|
||||
fallthrough
|
||||
case reflect.Int64:
|
||||
fallthrough
|
||||
case reflect.Bool:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Assignable returns true if the "k" standard type
|
||||
// is assignabled to this ParamType.
|
||||
func (pt ParamType) Assignable(k reflect.Kind) bool {
|
||||
return pt.Kind() == k
|
||||
}
|
||||
|
||||
var paramTypes = map[string]ParamType{
|
||||
"string": ParamTypeString,
|
||||
"int": ParamTypeInt,
|
||||
"long": ParamTypeLong,
|
||||
"boolean": ParamTypeBoolean,
|
||||
"alphabetical": ParamTypeAlphabetical,
|
||||
"file": ParamTypeFile,
|
||||
"path": ParamTypePath,
|
||||
// could be named also:
|
||||
// "tail":
|
||||
// "wild"
|
||||
// "wildcard"
|
||||
|
||||
}
|
||||
|
||||
// LookupParamType accepts the string
|
||||
// representation of a parameter type.
|
||||
// Available:
|
||||
// "string"
|
||||
// "int"
|
||||
// "long"
|
||||
// "alphabetical"
|
||||
// "file"
|
||||
// "path"
|
||||
func LookupParamType(ident string) ParamType {
|
||||
if typ, ok := paramTypes[ident]; ok {
|
||||
return typ
|
||||
}
|
||||
return ParamTypeUnExpected
|
||||
}
|
||||
|
||||
// LookupParamTypeFromStd accepts the string representation of a standard go type.
|
||||
// It returns a ParamType, but it may differs for example
|
||||
// the alphabetical, file, path and string are all string go types, so
|
||||
// make sure that caller resolves these types before this call.
|
||||
//
|
||||
// string matches to string
|
||||
// int matches to int
|
||||
// int64 matches to long
|
||||
// bool matches to boolean
|
||||
func LookupParamTypeFromStd(goType string) ParamType {
|
||||
switch goType {
|
||||
case "string":
|
||||
return ParamTypeString
|
||||
case "int":
|
||||
return ParamTypeInt
|
||||
case "int64":
|
||||
return ParamTypeLong
|
||||
case "bool":
|
||||
return ParamTypeBoolean
|
||||
default:
|
||||
return ParamTypeUnExpected
|
||||
}
|
||||
}
|
||||
|
||||
// ParamStatement is a struct
|
||||
// which holds all the necessary information about a macro parameter.
|
||||
// It holds its type (string, int, alphabetical, file, path),
|
||||
// its source ({param:type}),
|
||||
// its name ("param"),
|
||||
// its attached functions by the user (min, max...)
|
||||
// and the http error code if that parameter
|
||||
// failed to be evaluated.
|
||||
type ParamStatement struct {
|
||||
Src string // the original unparsed source, i.e: {id:int range(1,5) else 404}
|
||||
Name string // id
|
||||
Type ParamType // int
|
||||
Funcs []ParamFunc // range
|
||||
ErrorCode int // 404
|
||||
}
|
||||
|
||||
// ParamFuncArg represents a single parameter function's argument
|
||||
type ParamFuncArg interface{}
|
||||
|
||||
// ParamFuncArgToInt converts and returns
|
||||
// any type of "a", to an integer.
|
||||
func ParamFuncArgToInt(a ParamFuncArg) (int, error) {
|
||||
switch a.(type) {
|
||||
case int:
|
||||
return a.(int), nil
|
||||
case string:
|
||||
return strconv.Atoi(a.(string))
|
||||
case int64:
|
||||
return int(a.(int64)), nil
|
||||
default:
|
||||
return -1, fmt.Errorf("unexpected function argument type: %q", a)
|
||||
}
|
||||
}
|
||||
|
||||
// ParamFunc holds the name of a parameter's function
|
||||
// and its arguments (values)
|
||||
// A param func is declared with:
|
||||
// {param:int range(1,5)},
|
||||
// the range is the
|
||||
// param function name
|
||||
// the 1 and 5 are the two param function arguments
|
||||
// range(1,5)
|
||||
type ParamFunc struct {
|
||||
Name string // range
|
||||
Args []ParamFuncArg // [1,5]
|
||||
}
|
||||
@@ -1,202 +0,0 @@
|
||||
package lexer
|
||||
|
||||
import (
|
||||
"github.com/kataras/iris/core/router/macro/interpreter/token"
|
||||
)
|
||||
|
||||
// Lexer helps us to read/scan characters of a source and resolve their token types.
|
||||
type Lexer struct {
|
||||
input string
|
||||
pos int // current pos in input, current char
|
||||
readPos int // current reading pos in input, after current char
|
||||
ch byte // current char under examination
|
||||
}
|
||||
|
||||
// New takes a source, series of chars, and returns
|
||||
// a new, ready to read from the first letter, lexer.
|
||||
func New(src string) *Lexer {
|
||||
l := &Lexer{
|
||||
input: src,
|
||||
}
|
||||
// step to the first character in order to be ready
|
||||
l.readChar()
|
||||
return l
|
||||
}
|
||||
|
||||
func (l *Lexer) readChar() {
|
||||
if l.readPos >= len(l.input) {
|
||||
l.ch = 0
|
||||
} else {
|
||||
l.ch = l.input[l.readPos]
|
||||
}
|
||||
l.pos = l.readPos
|
||||
l.readPos++
|
||||
}
|
||||
|
||||
const (
|
||||
// Begin is the symbol which lexer should scan forward to.
|
||||
Begin = '{' // token.LBRACE
|
||||
// End is the symbol which lexer should stop scanning.
|
||||
End = '}' // token.RBRACE
|
||||
)
|
||||
|
||||
func resolveTokenType(ch byte) token.Type {
|
||||
switch ch {
|
||||
case Begin:
|
||||
return token.LBRACE
|
||||
case End:
|
||||
return token.RBRACE
|
||||
// Let's keep it simple, no evaluation for logical operators, we are not making a new programming language, keep it simple makis.
|
||||
// ||
|
||||
// case '|':
|
||||
// if l.peekChar() == '|' {
|
||||
// ch := l.ch
|
||||
// l.readChar()
|
||||
// t = token.Token{Type: token.OR, Literal: string(ch) + string(l.ch)}
|
||||
// }
|
||||
// ==
|
||||
case ':':
|
||||
return token.COLON
|
||||
case '(':
|
||||
return token.LPAREN
|
||||
case ')':
|
||||
return token.RPAREN
|
||||
case ',':
|
||||
return token.COMMA
|
||||
// literals
|
||||
case 0:
|
||||
return token.EOF
|
||||
default:
|
||||
return token.IDENT //
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// NextToken returns the next token in the series of characters.
|
||||
// It can be a single symbol, a token type or a literal.
|
||||
// It's able to return an EOF token too.
|
||||
//
|
||||
// It moves the cursor forward.
|
||||
func (l *Lexer) NextToken() (t token.Token) {
|
||||
l.skipWhitespace()
|
||||
typ := resolveTokenType(l.ch)
|
||||
t.Type = typ
|
||||
switch typ {
|
||||
case token.EOF:
|
||||
t.Literal = ""
|
||||
case token.IDENT:
|
||||
if isLetter(l.ch) {
|
||||
// letters
|
||||
lit := l.readIdentifier()
|
||||
typ := token.LookupIdent(lit)
|
||||
t = l.newToken(typ, lit)
|
||||
return
|
||||
}
|
||||
if isDigit(l.ch) {
|
||||
// numbers
|
||||
lit := l.readNumber()
|
||||
t = l.newToken(token.INT, lit)
|
||||
return
|
||||
}
|
||||
|
||||
t = l.newTokenRune(token.ILLEGAL, l.ch)
|
||||
default:
|
||||
t = l.newTokenRune(typ, l.ch)
|
||||
}
|
||||
l.readChar() // set the pos to the next
|
||||
return
|
||||
}
|
||||
|
||||
// NextDynamicToken doesn't cares about the grammar.
|
||||
// It reads numbers or any unknown symbol,
|
||||
// it's being used by parser to skip all characters
|
||||
// between parameter function's arguments inside parenthesis,
|
||||
// in order to allow custom regexp on the end-language too.
|
||||
//
|
||||
// It moves the cursor forward.
|
||||
func (l *Lexer) NextDynamicToken() (t token.Token) {
|
||||
// calculate anything, even spaces.
|
||||
|
||||
// numbers
|
||||
lit := l.readNumber()
|
||||
if lit != "" {
|
||||
return l.newToken(token.INT, lit)
|
||||
}
|
||||
|
||||
lit = l.readIdentifierFuncArgument()
|
||||
return l.newToken(token.IDENT, lit)
|
||||
}
|
||||
|
||||
// used to skip any illegal token if inside parenthesis, used to be able to set custom regexp inside a func.
|
||||
func (l *Lexer) readIdentifierFuncArgument() string {
|
||||
pos := l.pos
|
||||
for resolveTokenType(l.ch) != token.RPAREN {
|
||||
l.readChar()
|
||||
}
|
||||
|
||||
return l.input[pos:l.pos]
|
||||
}
|
||||
|
||||
// PeekNextTokenType returns only the token type
|
||||
// of the next character and it does not move forward the cursor.
|
||||
// It's being used by parser to recognise empty functions, i.e `even()`
|
||||
// as valid functions with zero input arguments.
|
||||
func (l *Lexer) PeekNextTokenType() token.Type {
|
||||
if len(l.input)-1 > l.pos {
|
||||
ch := l.input[l.pos]
|
||||
return resolveTokenType(ch)
|
||||
}
|
||||
return resolveTokenType(0) // EOF
|
||||
}
|
||||
|
||||
func (l *Lexer) newToken(tokenType token.Type, lit string) token.Token {
|
||||
t := token.Token{
|
||||
Type: tokenType,
|
||||
Literal: lit,
|
||||
Start: l.pos,
|
||||
End: l.pos,
|
||||
}
|
||||
// remember, l.pos is the last char
|
||||
// and we want to include both start and end
|
||||
// in order to be easy to the user to see by just marking the expression
|
||||
if l.pos > 1 && len(lit) > 1 {
|
||||
t.End = l.pos - 1
|
||||
t.Start = t.End - len(lit) + 1
|
||||
}
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
func (l *Lexer) newTokenRune(tokenType token.Type, ch byte) token.Token {
|
||||
return l.newToken(tokenType, string(ch))
|
||||
}
|
||||
|
||||
func (l *Lexer) skipWhitespace() {
|
||||
for l.ch == ' ' || l.ch == '\t' || l.ch == '\n' || l.ch == '\r' {
|
||||
l.readChar()
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Lexer) readIdentifier() string {
|
||||
pos := l.pos
|
||||
for isLetter(l.ch) {
|
||||
l.readChar()
|
||||
}
|
||||
return l.input[pos:l.pos]
|
||||
}
|
||||
|
||||
func isLetter(ch byte) bool {
|
||||
return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || ch == '_'
|
||||
}
|
||||
|
||||
func (l *Lexer) readNumber() string {
|
||||
pos := l.pos
|
||||
for isDigit(l.ch) {
|
||||
l.readChar()
|
||||
}
|
||||
return l.input[pos:l.pos]
|
||||
}
|
||||
|
||||
func isDigit(ch byte) bool {
|
||||
return '0' <= ch && ch <= '9'
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
package lexer
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/kataras/iris/core/router/macro/interpreter/token"
|
||||
)
|
||||
|
||||
func TestNextToken(t *testing.T) {
|
||||
input := `{id:int min(1) max(5) else 404}`
|
||||
|
||||
tests := []struct {
|
||||
expectedType token.Type
|
||||
expectedLiteral string
|
||||
}{
|
||||
{token.LBRACE, "{"}, // 0
|
||||
{token.IDENT, "id"}, // 1
|
||||
{token.COLON, ":"}, // 2
|
||||
{token.IDENT, "int"}, // 3
|
||||
{token.IDENT, "min"}, // 4
|
||||
{token.LPAREN, "("}, // 5
|
||||
{token.INT, "1"}, // 6
|
||||
{token.RPAREN, ")"}, // 7
|
||||
{token.IDENT, "max"}, // 8
|
||||
{token.LPAREN, "("}, // 9
|
||||
{token.INT, "5"}, // 10
|
||||
{token.RPAREN, ")"}, // 11
|
||||
{token.ELSE, "else"}, // 12
|
||||
{token.INT, "404"}, // 13
|
||||
{token.RBRACE, "}"}, // 14
|
||||
}
|
||||
|
||||
l := New(input)
|
||||
|
||||
for i, tt := range tests {
|
||||
tok := l.NextToken()
|
||||
|
||||
if tok.Type != tt.expectedType {
|
||||
t.Fatalf("tests[%d] - tokentype wrong. expected=%q, got=%q",
|
||||
i, tt.expectedType, tok.Type)
|
||||
}
|
||||
|
||||
if tok.Literal != tt.expectedLiteral {
|
||||
t.Fatalf("tests[%d] - literal wrong. expected=%q, got=%q",
|
||||
i, tt.expectedLiteral, tok.Literal)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// EMEINA STO:
|
||||
// 30/232 selida apto making a interpeter in Go.
|
||||
// den ekana to skipWhitespaces giati skeftomai
|
||||
// an borei na to xreiastw 9a dw aurio.
|
||||
@@ -1,195 +0,0 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/kataras/iris/core/router/macro/interpreter/ast"
|
||||
"github.com/kataras/iris/core/router/macro/interpreter/lexer"
|
||||
"github.com/kataras/iris/core/router/macro/interpreter/token"
|
||||
)
|
||||
|
||||
// Parse takes a route "fullpath"
|
||||
// and returns its param statements
|
||||
// and an error on failure.
|
||||
func Parse(fullpath string) ([]*ast.ParamStatement, error) {
|
||||
pathParts := strings.SplitN(fullpath, "/", -1)
|
||||
p := new(ParamParser)
|
||||
statements := make([]*ast.ParamStatement, 0)
|
||||
for i, s := range pathParts {
|
||||
if s == "" { // if starts with /
|
||||
continue
|
||||
}
|
||||
|
||||
// if it's not a named path parameter of the new syntax then continue to the next
|
||||
if s[0] != lexer.Begin || s[len(s)-1] != lexer.End {
|
||||
continue
|
||||
}
|
||||
|
||||
p.Reset(s)
|
||||
stmt, err := p.Parse()
|
||||
if err != nil {
|
||||
// exit on first error
|
||||
return nil, err
|
||||
}
|
||||
// if we have param type path but it's not the last path part
|
||||
if stmt.Type == ast.ParamTypePath && i < len(pathParts)-1 {
|
||||
return nil, fmt.Errorf("param type 'path' should be lived only inside the last path segment, but was inside: %s", s)
|
||||
}
|
||||
|
||||
statements = append(statements, stmt)
|
||||
}
|
||||
|
||||
return statements, nil
|
||||
}
|
||||
|
||||
// ParamParser is the parser
|
||||
// which is being used by the Parse function
|
||||
// to parse path segments one by one
|
||||
// and return their parsed parameter statements (param name, param type its functions and the inline route's functions).
|
||||
type ParamParser struct {
|
||||
src string
|
||||
errors []string
|
||||
}
|
||||
|
||||
// NewParamParser receives a "src" of a single parameter
|
||||
// and returns a new ParamParser, ready to Parse.
|
||||
func NewParamParser(src string) *ParamParser {
|
||||
p := new(ParamParser)
|
||||
p.Reset(src)
|
||||
return p
|
||||
}
|
||||
|
||||
// Reset resets this ParamParser,
|
||||
// reset the errors and set the source to the input "src".
|
||||
func (p *ParamParser) Reset(src string) {
|
||||
p.src = src
|
||||
p.errors = []string{}
|
||||
}
|
||||
|
||||
func (p *ParamParser) appendErr(format string, a ...interface{}) {
|
||||
p.errors = append(p.errors, fmt.Sprintf(format, a...))
|
||||
}
|
||||
|
||||
const (
|
||||
// DefaultParamErrorCode is the default http error code, 404 not found,
|
||||
// per-parameter. An error code can be setted via
|
||||
// the "else" keyword inside a route's path.
|
||||
DefaultParamErrorCode = 404
|
||||
// DefaultParamType when parameter type is missing use this param type, defaults to string
|
||||
// and it should be remains unless earth split in two.
|
||||
DefaultParamType = ast.ParamTypeString
|
||||
)
|
||||
|
||||
func parseParamFuncArg(t token.Token) (a ast.ParamFuncArg, err error) {
|
||||
if t.Type == token.INT {
|
||||
return ast.ParamFuncArgToInt(t.Literal)
|
||||
}
|
||||
return t.Literal, nil
|
||||
}
|
||||
|
||||
func (p ParamParser) Error() error {
|
||||
if len(p.errors) > 0 {
|
||||
return fmt.Errorf(strings.Join(p.errors, "\n"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parse parses the p.src and returns its param statement
|
||||
// and an error on failure.
|
||||
func (p *ParamParser) Parse() (*ast.ParamStatement, error) {
|
||||
l := lexer.New(p.src)
|
||||
|
||||
stmt := &ast.ParamStatement{
|
||||
ErrorCode: DefaultParamErrorCode,
|
||||
Type: DefaultParamType,
|
||||
Src: p.src,
|
||||
}
|
||||
|
||||
lastParamFunc := ast.ParamFunc{}
|
||||
|
||||
for {
|
||||
t := l.NextToken()
|
||||
if t.Type == token.EOF {
|
||||
if stmt.Name == "" {
|
||||
p.appendErr("[1:] parameter name is missing")
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
switch t.Type {
|
||||
case token.LBRACE:
|
||||
// name, alphabetical and _, param names are not allowed to contain any number.
|
||||
nextTok := l.NextToken()
|
||||
stmt.Name = nextTok.Literal
|
||||
case token.COLON:
|
||||
// type
|
||||
nextTok := l.NextToken()
|
||||
paramType := ast.LookupParamType(nextTok.Literal)
|
||||
if paramType == ast.ParamTypeUnExpected {
|
||||
p.appendErr("[%d:%d] unexpected parameter type: %s", t.Start, t.End, nextTok.Literal)
|
||||
}
|
||||
stmt.Type = paramType
|
||||
// param func
|
||||
case token.IDENT:
|
||||
lastParamFunc.Name = t.Literal
|
||||
case token.LPAREN:
|
||||
// param function without arguments ()
|
||||
if l.PeekNextTokenType() == token.RPAREN {
|
||||
// do nothing, just continue to the RPAREN
|
||||
continue
|
||||
}
|
||||
|
||||
argValTok := l.NextDynamicToken() // catch anything from "(" and forward, until ")", because we need to
|
||||
// be able to use regex expression as a macro type's func argument too.
|
||||
argVal, err := parseParamFuncArg(argValTok)
|
||||
if err != nil {
|
||||
p.appendErr("[%d:%d] expected param func argument to be a string or number but got %s", t.Start, t.End, argValTok.Literal)
|
||||
continue
|
||||
}
|
||||
|
||||
// fmt.Printf("argValTok: %#v\n", argValTok)
|
||||
// fmt.Printf("argVal: %#v\n", argVal)
|
||||
lastParamFunc.Args = append(lastParamFunc.Args, argVal)
|
||||
|
||||
case token.COMMA:
|
||||
argValTok := l.NextToken()
|
||||
argVal, err := parseParamFuncArg(argValTok)
|
||||
if err != nil {
|
||||
p.appendErr("[%d:%d] expected param func argument to be a string or number type but got %s", t.Start, t.End, argValTok.Literal)
|
||||
continue
|
||||
}
|
||||
|
||||
lastParamFunc.Args = append(lastParamFunc.Args, argVal)
|
||||
case token.RPAREN:
|
||||
stmt.Funcs = append(stmt.Funcs, lastParamFunc)
|
||||
lastParamFunc = ast.ParamFunc{} // reset
|
||||
case token.ELSE:
|
||||
errCodeTok := l.NextToken()
|
||||
if errCodeTok.Type != token.INT {
|
||||
p.appendErr("[%d:%d] expected error code to be an integer but got %s", t.Start, t.End, errCodeTok.Literal)
|
||||
continue
|
||||
}
|
||||
errCode, err := strconv.Atoi(errCodeTok.Literal)
|
||||
if err != nil {
|
||||
// this is a bug on lexer if throws because we already check for token.INT
|
||||
p.appendErr("[%d:%d] unexpected lexer error while trying to convert error code to an integer, %s", t.Start, t.End, err.Error())
|
||||
continue
|
||||
}
|
||||
stmt.ErrorCode = errCode
|
||||
case token.RBRACE:
|
||||
// check if } but not {
|
||||
if stmt.Name == "" {
|
||||
p.appendErr("[%d:%d] illegal token: }, forgot '{' ?", t.Start, t.End)
|
||||
}
|
||||
break
|
||||
case token.ILLEGAL:
|
||||
p.appendErr("[%d:%d] illegal token: %s", t.Start, t.End, t.Literal)
|
||||
default:
|
||||
p.appendErr("[%d:%d] unexpected token type: %q with value %s", t.Start, t.End, t.Type, t.Literal)
|
||||
}
|
||||
}
|
||||
|
||||
return stmt, p.Error()
|
||||
}
|
||||
@@ -1,270 +0,0 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/kataras/iris/core/router/macro/interpreter/ast"
|
||||
)
|
||||
|
||||
func TestParseParamError(t *testing.T) {
|
||||
// fail
|
||||
illegalChar := '$'
|
||||
|
||||
input := "{id" + string(illegalChar) + "int range(1,5) else 404}"
|
||||
p := NewParamParser(input)
|
||||
|
||||
_, err := p.Parse()
|
||||
|
||||
if err == nil {
|
||||
t.Fatalf("expecting not empty error on input '%s'", input)
|
||||
}
|
||||
|
||||
illIdx := strings.IndexRune(input, illegalChar)
|
||||
expectedErr := fmt.Sprintf("[%d:%d] illegal token: %s", illIdx, illIdx, "$")
|
||||
if got := err.Error(); got != expectedErr {
|
||||
t.Fatalf("expecting error to be '%s' but got: %s", expectedErr, got)
|
||||
}
|
||||
//
|
||||
|
||||
// success
|
||||
input2 := "{id:int range(1,5) else 404}"
|
||||
p.Reset(input2)
|
||||
_, err = p.Parse()
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("expecting empty error on input '%s', but got: %s", input2, err.Error())
|
||||
}
|
||||
//
|
||||
}
|
||||
|
||||
func TestParseParam(t *testing.T) {
|
||||
tests := []struct {
|
||||
valid bool
|
||||
expectedStatement ast.ParamStatement
|
||||
}{
|
||||
{true,
|
||||
ast.ParamStatement{
|
||||
Src: "{id:int min(1) max(5) else 404}",
|
||||
Name: "id",
|
||||
Type: ast.ParamTypeInt,
|
||||
Funcs: []ast.ParamFunc{
|
||||
{
|
||||
Name: "min",
|
||||
Args: []ast.ParamFuncArg{1}},
|
||||
{
|
||||
Name: "max",
|
||||
Args: []ast.ParamFuncArg{5}},
|
||||
},
|
||||
ErrorCode: 404,
|
||||
}}, // 0
|
||||
|
||||
{true,
|
||||
ast.ParamStatement{
|
||||
Src: "{id:int range(1,5)}",
|
||||
Name: "id",
|
||||
Type: ast.ParamTypeInt,
|
||||
Funcs: []ast.ParamFunc{
|
||||
{
|
||||
Name: "range",
|
||||
Args: []ast.ParamFuncArg{1, 5}},
|
||||
},
|
||||
ErrorCode: 404,
|
||||
}}, // 1
|
||||
{true,
|
||||
ast.ParamStatement{
|
||||
Src: "{file:path contains(.)}",
|
||||
Name: "file",
|
||||
Type: ast.ParamTypePath,
|
||||
Funcs: []ast.ParamFunc{
|
||||
{
|
||||
Name: "contains",
|
||||
Args: []ast.ParamFuncArg{"."}},
|
||||
},
|
||||
ErrorCode: 404,
|
||||
}}, // 2
|
||||
{true,
|
||||
ast.ParamStatement{
|
||||
Src: "{username:alphabetical}",
|
||||
Name: "username",
|
||||
Type: ast.ParamTypeAlphabetical,
|
||||
ErrorCode: 404,
|
||||
}}, // 3
|
||||
{true,
|
||||
ast.ParamStatement{
|
||||
Src: "{myparam}",
|
||||
Name: "myparam",
|
||||
Type: ast.ParamTypeString,
|
||||
ErrorCode: 404,
|
||||
}}, // 4
|
||||
{false,
|
||||
ast.ParamStatement{
|
||||
Src: "{myparam_:thisianunexpected}",
|
||||
Name: "myparam_",
|
||||
Type: ast.ParamTypeUnExpected,
|
||||
ErrorCode: 404,
|
||||
}}, // 5
|
||||
{false, // false because it will give an error of unexpeced token type with value 2
|
||||
ast.ParamStatement{
|
||||
Src: "{myparam2}",
|
||||
Name: "myparam", // expected "myparam" because we don't allow integers to the parameter names.
|
||||
Type: ast.ParamTypeString,
|
||||
ErrorCode: 404,
|
||||
}}, // 6
|
||||
{true,
|
||||
ast.ParamStatement{
|
||||
Src: "{id:int even()}", // test param funcs without any arguments (LPAREN peek for RPAREN)
|
||||
Name: "id",
|
||||
Type: ast.ParamTypeInt,
|
||||
Funcs: []ast.ParamFunc{
|
||||
{
|
||||
Name: "even"},
|
||||
},
|
||||
ErrorCode: 404,
|
||||
}}, // 7
|
||||
{true,
|
||||
ast.ParamStatement{
|
||||
Src: "{id:long else 404}",
|
||||
Name: "id",
|
||||
Type: ast.ParamTypeLong,
|
||||
ErrorCode: 404,
|
||||
}}, // 8
|
||||
{true,
|
||||
ast.ParamStatement{
|
||||
Src: "{has:boolean else 404}",
|
||||
Name: "has",
|
||||
Type: ast.ParamTypeBoolean,
|
||||
ErrorCode: 404,
|
||||
}}, // 9
|
||||
|
||||
}
|
||||
|
||||
p := new(ParamParser)
|
||||
for i, tt := range tests {
|
||||
p.Reset(tt.expectedStatement.Src)
|
||||
resultStmt, err := p.Parse()
|
||||
|
||||
if tt.valid && err != nil {
|
||||
t.Fatalf("tests[%d] - error %s", i, err.Error())
|
||||
} else if !tt.valid && err == nil {
|
||||
t.Fatalf("tests[%d] - expected to be a failure", i)
|
||||
}
|
||||
|
||||
if resultStmt != nil { // is valid here
|
||||
if !reflect.DeepEqual(tt.expectedStatement, *resultStmt) {
|
||||
t.Fatalf("tests[%d] - wrong statement, expected and result differs. Details:\n%#v\n%#v", i, tt.expectedStatement, *resultStmt)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
tests := []struct {
|
||||
path string
|
||||
valid bool
|
||||
expectedStatements []ast.ParamStatement
|
||||
}{
|
||||
{"/api/users/{id:int min(1) max(5) else 404}", true,
|
||||
[]ast.ParamStatement{{
|
||||
Src: "{id:int min(1) max(5) else 404}",
|
||||
Name: "id",
|
||||
Type: ast.ParamTypeInt,
|
||||
Funcs: []ast.ParamFunc{
|
||||
{
|
||||
Name: "min",
|
||||
Args: []ast.ParamFuncArg{1}},
|
||||
{
|
||||
Name: "max",
|
||||
Args: []ast.ParamFuncArg{5}},
|
||||
},
|
||||
ErrorCode: 404,
|
||||
},
|
||||
}}, // 0
|
||||
{"/admin/{id:int range(1,5)}", true,
|
||||
[]ast.ParamStatement{{
|
||||
Src: "{id:int range(1,5)}",
|
||||
Name: "id",
|
||||
Type: ast.ParamTypeInt,
|
||||
Funcs: []ast.ParamFunc{
|
||||
{
|
||||
Name: "range",
|
||||
Args: []ast.ParamFuncArg{1, 5}},
|
||||
},
|
||||
ErrorCode: 404,
|
||||
},
|
||||
}}, // 1
|
||||
{"/files/{file:path contains(.)}", true,
|
||||
[]ast.ParamStatement{{
|
||||
Src: "{file:path contains(.)}",
|
||||
Name: "file",
|
||||
Type: ast.ParamTypePath,
|
||||
Funcs: []ast.ParamFunc{
|
||||
{
|
||||
Name: "contains",
|
||||
Args: []ast.ParamFuncArg{"."}},
|
||||
},
|
||||
ErrorCode: 404,
|
||||
},
|
||||
}}, // 2
|
||||
{"/profile/{username:alphabetical}", true,
|
||||
[]ast.ParamStatement{{
|
||||
Src: "{username:alphabetical}",
|
||||
Name: "username",
|
||||
Type: ast.ParamTypeAlphabetical,
|
||||
ErrorCode: 404,
|
||||
},
|
||||
}}, // 3
|
||||
{"/something/here/{myparam}", true,
|
||||
[]ast.ParamStatement{{
|
||||
Src: "{myparam}",
|
||||
Name: "myparam",
|
||||
Type: ast.ParamTypeString,
|
||||
ErrorCode: 404,
|
||||
},
|
||||
}}, // 4
|
||||
{"/unexpected/{myparam_:thisianunexpected}", false,
|
||||
[]ast.ParamStatement{{
|
||||
Src: "{myparam_:thisianunexpected}",
|
||||
Name: "myparam_",
|
||||
Type: ast.ParamTypeUnExpected,
|
||||
ErrorCode: 404,
|
||||
},
|
||||
}}, // 5
|
||||
{"/p2/{myparam2}", false, // false because it will give an error of unexpeced token type with value 2
|
||||
[]ast.ParamStatement{{
|
||||
Src: "{myparam2}",
|
||||
Name: "myparam", // expected "myparam" because we don't allow integers to the parameter names.
|
||||
Type: ast.ParamTypeString,
|
||||
ErrorCode: 404,
|
||||
},
|
||||
}}, // 6
|
||||
{"/assets/{file:path}/invalid", false, // path should be in the end segment
|
||||
[]ast.ParamStatement{{
|
||||
Src: "{file:path}",
|
||||
Name: "file",
|
||||
Type: ast.ParamTypePath,
|
||||
ErrorCode: 404,
|
||||
},
|
||||
}}, // 7
|
||||
}
|
||||
for i, tt := range tests {
|
||||
statements, err := Parse(tt.path)
|
||||
|
||||
if tt.valid && err != nil {
|
||||
t.Fatalf("tests[%d] - error %s", i, err.Error())
|
||||
} else if !tt.valid && err == nil {
|
||||
t.Fatalf("tests[%d] - expected to be a failure", i)
|
||||
}
|
||||
for j := range statements {
|
||||
for l := range tt.expectedStatements {
|
||||
if !reflect.DeepEqual(tt.expectedStatements[l], *statements[j]) {
|
||||
t.Fatalf("tests[%d] - wrong statements, expected and result differs. Details:\n%#v\n%#v", i, tt.expectedStatements[l], *statements[j])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package token
|
||||
|
||||
// Type is a specific type of int which describes the symbols.
|
||||
type Type int
|
||||
|
||||
// Token describes the letter(s) or symbol, is a result of the lexer.
|
||||
type Token struct {
|
||||
Type Type
|
||||
Literal string
|
||||
Start int // including the first char
|
||||
End int // including the last char
|
||||
}
|
||||
|
||||
// /about/{fullname:alphabetical}
|
||||
// /profile/{anySpecialName:string}
|
||||
// {id:int range(1,5) else 404}
|
||||
// /admin/{id:int eq(1) else 402}
|
||||
// /file/{filepath:file else 405}
|
||||
const (
|
||||
EOF = iota // 0
|
||||
ILLEGAL
|
||||
|
||||
// Identifiers + literals
|
||||
LBRACE // {
|
||||
RBRACE // }
|
||||
// PARAM_IDENTIFIER // id
|
||||
COLON // :
|
||||
LPAREN // (
|
||||
RPAREN // )
|
||||
// PARAM_FUNC_ARG // 1
|
||||
COMMA
|
||||
IDENT // string or keyword
|
||||
// Keywords
|
||||
// keywords_start
|
||||
ELSE // else
|
||||
// keywords_end
|
||||
INT // 42
|
||||
)
|
||||
|
||||
const eof rune = 0
|
||||
|
||||
var keywords = map[string]Type{
|
||||
"else": ELSE,
|
||||
}
|
||||
|
||||
// LookupIdent receives a series of chars
|
||||
// and tries to resolves the token type.
|
||||
func LookupIdent(ident string) Type {
|
||||
if tok, ok := keywords[ident]; ok {
|
||||
return tok
|
||||
}
|
||||
return IDENT
|
||||
}
|
||||
@@ -1,292 +0,0 @@
|
||||
package macro
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"unicode"
|
||||
|
||||
"github.com/kataras/iris/core/router/macro/interpreter/ast"
|
||||
)
|
||||
|
||||
// EvaluatorFunc is the signature for both param types and param funcs.
|
||||
// It should accepts the param's value as string
|
||||
// and return true if validated otherwise false.
|
||||
type EvaluatorFunc func(paramValue string) bool
|
||||
|
||||
// NewEvaluatorFromRegexp accepts a regexp "expr" expression
|
||||
// and returns an EvaluatorFunc based on that regexp.
|
||||
// the regexp is compiled before return.
|
||||
//
|
||||
// Returns a not-nil error on regexp compile failure.
|
||||
func NewEvaluatorFromRegexp(expr string) (EvaluatorFunc, error) {
|
||||
if expr == "" {
|
||||
return nil, fmt.Errorf("empty regex expression")
|
||||
}
|
||||
|
||||
// add the last $ if missing (and not wildcard(?))
|
||||
if i := expr[len(expr)-1]; i != '$' && i != '*' {
|
||||
expr += "$"
|
||||
}
|
||||
|
||||
r, err := regexp.Compile(expr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return r.MatchString, nil
|
||||
}
|
||||
|
||||
// MustNewEvaluatorFromRegexp same as NewEvaluatorFromRegexp
|
||||
// but it panics on the "expr" parse failure.
|
||||
func MustNewEvaluatorFromRegexp(expr string) EvaluatorFunc {
|
||||
r, err := NewEvaluatorFromRegexp(expr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
var (
|
||||
goodParamFuncReturnType = reflect.TypeOf(func(string) bool { return false })
|
||||
goodParamFuncReturnType2 = reflect.TypeOf(EvaluatorFunc(func(string) bool { return false }))
|
||||
)
|
||||
|
||||
func goodParamFunc(typ reflect.Type) bool {
|
||||
// should be a func
|
||||
// which returns a func(string) bool
|
||||
if typ.Kind() == reflect.Func {
|
||||
if typ.NumOut() == 1 {
|
||||
typOut := typ.Out(0)
|
||||
if typOut == goodParamFuncReturnType || typOut == goodParamFuncReturnType2 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// goodParamFuncName reports whether the function name is a valid identifier.
|
||||
func goodParamFuncName(name string) bool {
|
||||
if name == "" {
|
||||
return false
|
||||
}
|
||||
// valid names are only letters and _
|
||||
for _, r := range name {
|
||||
switch {
|
||||
case r == '_':
|
||||
case !unicode.IsLetter(r):
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// the convertBuilderFunc return value is generating at boot time.
|
||||
// convertFunc converts an interface to a valid full param function.
|
||||
func convertBuilderFunc(fn interface{}) ParamEvaluatorBuilder {
|
||||
|
||||
typFn := reflect.TypeOf(fn)
|
||||
if !goodParamFunc(typFn) {
|
||||
return nil
|
||||
}
|
||||
|
||||
numFields := typFn.NumIn()
|
||||
|
||||
return func(args []ast.ParamFuncArg) EvaluatorFunc {
|
||||
if len(args) != numFields {
|
||||
// no variadics support, for now.
|
||||
panic("args should be the same len as numFields")
|
||||
}
|
||||
var argValues []reflect.Value
|
||||
for i := 0; i < numFields; i++ {
|
||||
field := typFn.In(i)
|
||||
arg := args[i]
|
||||
|
||||
if field.Kind() != reflect.TypeOf(arg).Kind() {
|
||||
panic("fields should have the same type")
|
||||
}
|
||||
|
||||
argValues = append(argValues, reflect.ValueOf(arg))
|
||||
}
|
||||
|
||||
evalFn := reflect.ValueOf(fn).Call(argValues)[0].Interface()
|
||||
|
||||
var evaluator EvaluatorFunc
|
||||
// check for typed and not typed
|
||||
if _v, ok := evalFn.(EvaluatorFunc); ok {
|
||||
evaluator = _v
|
||||
} else if _v, ok = evalFn.(func(string) bool); ok {
|
||||
evaluator = _v
|
||||
}
|
||||
return func(paramValue string) bool {
|
||||
return evaluator(paramValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type (
|
||||
// Macro represents the parsed macro,
|
||||
// which holds
|
||||
// the evaluator (param type's evaluator + param functions evaluators)
|
||||
// and its param functions.
|
||||
//
|
||||
// Any type contains its own macro
|
||||
// instance, so an String type
|
||||
// contains its type evaluator
|
||||
// which is the "Evaluator" field
|
||||
// and it can register param functions
|
||||
// to that macro which maps to a parameter type.
|
||||
Macro struct {
|
||||
Evaluator EvaluatorFunc
|
||||
funcs []ParamFunc
|
||||
}
|
||||
|
||||
// ParamEvaluatorBuilder is a func
|
||||
// which accepts a param function's arguments (values)
|
||||
// and returns an EvaluatorFunc, its job
|
||||
// is to make the macros to be registered
|
||||
// by user at the most generic possible way.
|
||||
ParamEvaluatorBuilder func([]ast.ParamFuncArg) EvaluatorFunc
|
||||
|
||||
// ParamFunc represents the parsed
|
||||
// parameter function, it holds
|
||||
// the parameter's name
|
||||
// and the function which will build
|
||||
// the evaluator func.
|
||||
ParamFunc struct {
|
||||
Name string
|
||||
Func ParamEvaluatorBuilder
|
||||
}
|
||||
)
|
||||
|
||||
func newMacro(evaluator EvaluatorFunc) *Macro {
|
||||
return &Macro{Evaluator: evaluator}
|
||||
}
|
||||
|
||||
// RegisterFunc registers a parameter function
|
||||
// to that macro.
|
||||
// Accepts the func name ("range")
|
||||
// and the function body, which should return an EvaluatorFunc
|
||||
// a bool (it will be converted to EvaluatorFunc later on),
|
||||
// i.e RegisterFunc("min", func(minValue int) func(paramValue string) bool){})
|
||||
func (m *Macro) RegisterFunc(funcName string, fn interface{}) {
|
||||
fullFn := convertBuilderFunc(fn)
|
||||
m.registerFunc(funcName, fullFn)
|
||||
}
|
||||
|
||||
func (m *Macro) registerFunc(funcName string, fullFn ParamEvaluatorBuilder) {
|
||||
if !goodParamFuncName(funcName) {
|
||||
return
|
||||
}
|
||||
|
||||
for _, fn := range m.funcs {
|
||||
if fn.Name == funcName {
|
||||
fn.Func = fullFn
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
m.funcs = append(m.funcs, ParamFunc{
|
||||
Name: funcName,
|
||||
Func: fullFn,
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Macro) getFunc(funcName string) ParamEvaluatorBuilder {
|
||||
for _, fn := range m.funcs {
|
||||
if fn.Name == funcName {
|
||||
if fn.Func == nil {
|
||||
continue
|
||||
}
|
||||
return fn.Func
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Map contains the default macros mapped to their types.
|
||||
// This is the manager which is used by the caller to register custom
|
||||
// parameter functions per param-type (String, Int, Long, Boolean, Alphabetical, File, Path).
|
||||
type Map struct {
|
||||
// string type
|
||||
// anything
|
||||
String *Macro
|
||||
// uint type
|
||||
// only positive numbers (+0-9)
|
||||
// it could be uint/uint32 but we keep int for simplicity
|
||||
Int *Macro
|
||||
// long an int64 type
|
||||
// only positive numbers (+0-9)
|
||||
// it could be uint64 but we keep int64 for simplicity
|
||||
Long *Macro
|
||||
// boolean as bool type
|
||||
// a string which is "1" or "t" or "T" or "TRUE" or "true" or "True"
|
||||
// or "0" or "f" or "F" or "FALSE" or "false" or "False".
|
||||
Boolean *Macro
|
||||
// alphabetical/letter type
|
||||
// letters only (upper or lowercase)
|
||||
Alphabetical *Macro
|
||||
// file type
|
||||
// letters (upper or lowercase)
|
||||
// numbers (0-9)
|
||||
// underscore (_)
|
||||
// dash (-)
|
||||
// point (.)
|
||||
// no spaces! or other character
|
||||
File *Macro
|
||||
// path type
|
||||
// anything, should be the last part
|
||||
Path *Macro
|
||||
}
|
||||
|
||||
// NewMap returns a new macro Map with default
|
||||
// type evaluators.
|
||||
//
|
||||
// Learn more at: https://github.com/kataras/iris/tree/master/_examples/routing/dynamic-path
|
||||
func NewMap() *Map {
|
||||
return &Map{
|
||||
// it allows everything, so no need for a regexp here.
|
||||
String: newMacro(func(string) bool { return true }),
|
||||
Int: newMacro(MustNewEvaluatorFromRegexp("^[0-9]+$")),
|
||||
Long: newMacro(MustNewEvaluatorFromRegexp("^[0-9]+$")),
|
||||
Boolean: newMacro(func(paramValue string) bool {
|
||||
// a simple if statement is faster than regex ^(true|false|True|False|t|0|f|FALSE|TRUE)$
|
||||
// in this case.
|
||||
_, err := strconv.ParseBool(paramValue)
|
||||
return err == nil
|
||||
}),
|
||||
Alphabetical: newMacro(MustNewEvaluatorFromRegexp("^[a-zA-Z ]+$")),
|
||||
File: newMacro(MustNewEvaluatorFromRegexp("^[a-zA-Z0-9_.-]*$")),
|
||||
// it allows everything, we have String and Path as different
|
||||
// types because I want to give the opportunity to the user
|
||||
// to organise the macro functions based on wildcard or single dynamic named path parameter.
|
||||
// Should be the last.
|
||||
Path: newMacro(func(string) bool { return true }),
|
||||
}
|
||||
}
|
||||
|
||||
// Lookup returns the specific Macro from the map
|
||||
// based on the parameter type.
|
||||
// i.e if ast.ParamTypeInt then it will return the m.Int.
|
||||
// Returns the m.String if not matched.
|
||||
func (m *Map) Lookup(typ ast.ParamType) *Macro {
|
||||
switch typ {
|
||||
case ast.ParamTypeInt:
|
||||
return m.Int
|
||||
case ast.ParamTypeLong:
|
||||
return m.Long
|
||||
case ast.ParamTypeBoolean:
|
||||
return m.Boolean
|
||||
case ast.ParamTypeAlphabetical:
|
||||
return m.Alphabetical
|
||||
case ast.ParamTypeFile:
|
||||
return m.File
|
||||
case ast.ParamTypePath:
|
||||
return m.Path
|
||||
default:
|
||||
return m.String
|
||||
}
|
||||
}
|
||||
@@ -1,186 +0,0 @@
|
||||
package macro
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Most important tests to look:
|
||||
// ../parser/parser_test.go
|
||||
// ../lexer/lexer_test.go
|
||||
|
||||
func TestGoodParamFunc(t *testing.T) {
|
||||
good1 := func(min int, max int) func(string) bool {
|
||||
return func(paramValue string) bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
good2 := func(min int, max int) func(string) bool {
|
||||
return func(paramValue string) bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
notgood1 := func(min int, max int) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
if !goodParamFunc(reflect.TypeOf(good1)) {
|
||||
t.Fatalf("expected good1 func to be good but it's not")
|
||||
}
|
||||
|
||||
if !goodParamFunc(reflect.TypeOf(good2)) {
|
||||
t.Fatalf("expected good2 func to be good but it's not")
|
||||
}
|
||||
|
||||
if goodParamFunc(reflect.TypeOf(notgood1)) {
|
||||
t.Fatalf("expected notgood1 func to be the worst")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoodParamFuncName(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
good bool
|
||||
}{
|
||||
{"range", true},
|
||||
{"_range", true},
|
||||
{"range_", true},
|
||||
{"r_ange", true},
|
||||
// numbers or other symbols are invalid.
|
||||
{"range1", false},
|
||||
{"2range", false},
|
||||
{"r@nge", false},
|
||||
{"rang3", false},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
isGood := goodParamFuncName(tt.name)
|
||||
if tt.good && !isGood {
|
||||
t.Fatalf("tests[%d] - expecting valid name but got invalid for name %s", i, tt.name)
|
||||
} else if !tt.good && isGood {
|
||||
t.Fatalf("tests[%d] - expecting invalid name but got valid for name %s", i, tt.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testEvaluatorRaw(macroEvaluator *Macro, input string, pass bool, i int, t *testing.T) {
|
||||
if got := macroEvaluator.Evaluator(input); pass != got {
|
||||
t.Fatalf("tests[%d] - expecting %v but got %v", i, pass, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStringEvaluatorRaw(t *testing.T) {
|
||||
f := NewMap()
|
||||
|
||||
tests := []struct {
|
||||
pass bool
|
||||
input string
|
||||
}{
|
||||
{true, "astring"}, // 0
|
||||
{true, "astringwith_numb3rS_and_symbol$"}, // 1
|
||||
{true, "32321"}, // 2
|
||||
{true, "main.css"}, // 3
|
||||
{true, "/assets/main.css"}, // 4
|
||||
// false never
|
||||
} // 0
|
||||
|
||||
for i, tt := range tests {
|
||||
testEvaluatorRaw(f.String, tt.input, tt.pass, i, t)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntEvaluatorRaw(t *testing.T) {
|
||||
f := NewMap()
|
||||
|
||||
tests := []struct {
|
||||
pass bool
|
||||
input string
|
||||
}{
|
||||
{false, "astring"}, // 0
|
||||
{false, "astringwith_numb3rS_and_symbol$"}, // 1
|
||||
{true, "32321"}, // 2
|
||||
{false, "main.css"}, // 3
|
||||
{false, "/assets/main.css"}, // 4
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
testEvaluatorRaw(f.Int, tt.input, tt.pass, i, t)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlphabeticalEvaluatorRaw(t *testing.T) {
|
||||
f := NewMap()
|
||||
|
||||
tests := []struct {
|
||||
pass bool
|
||||
input string
|
||||
}{
|
||||
{true, "astring"}, // 0
|
||||
{false, "astringwith_numb3rS_and_symbol$"}, // 1
|
||||
{false, "32321"}, // 2
|
||||
{false, "main.css"}, // 3
|
||||
{false, "/assets/main.css"}, // 4
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
testEvaluatorRaw(f.Alphabetical, tt.input, tt.pass, i, t)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileEvaluatorRaw(t *testing.T) {
|
||||
f := NewMap()
|
||||
|
||||
tests := []struct {
|
||||
pass bool
|
||||
input string
|
||||
}{
|
||||
{true, "astring"}, // 0
|
||||
{false, "astringwith_numb3rS_and_symbol$"}, // 1
|
||||
{true, "32321"}, // 2
|
||||
{true, "main.css"}, // 3
|
||||
{false, "/assets/main.css"}, // 4
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
testEvaluatorRaw(f.File, tt.input, tt.pass, i, t)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathEvaluatorRaw(t *testing.T) {
|
||||
f := NewMap()
|
||||
|
||||
pathTests := []struct {
|
||||
pass bool
|
||||
input string
|
||||
}{
|
||||
{true, "astring"}, // 0
|
||||
{true, "astringwith_numb3rS_and_symbol$"}, // 1
|
||||
{true, "32321"}, // 2
|
||||
{true, "main.css"}, // 3
|
||||
{true, "/assets/main.css"}, // 4
|
||||
{true, "disk/assets/main.css"}, // 5
|
||||
}
|
||||
|
||||
for i, tt := range pathTests {
|
||||
testEvaluatorRaw(f.Path, tt.input, tt.pass, i, t)
|
||||
}
|
||||
}
|
||||
|
||||
// func TestMapRegisterFunc(t *testing.T) {
|
||||
// m := NewMap()
|
||||
// m.String.RegisterFunc("prefix", func(prefix string) EvaluatorFunc {
|
||||
// return func(paramValue string) bool {
|
||||
// return strings.HasPrefix(paramValue, prefix)
|
||||
// }
|
||||
// })
|
||||
|
||||
// p, err := Parse("/user/@iris")
|
||||
// if err != nil {
|
||||
// t.Fatalf(err)
|
||||
// }
|
||||
|
||||
// // p.Params = append(p.)
|
||||
|
||||
// testEvaluatorRaw(m.String, p.Src, false, 0, t)
|
||||
// }
|
||||
@@ -1,75 +0,0 @@
|
||||
package macro
|
||||
|
||||
import (
|
||||
"github.com/kataras/iris/core/router/macro/interpreter/ast"
|
||||
"github.com/kataras/iris/core/router/macro/interpreter/parser"
|
||||
)
|
||||
|
||||
// Template contains a route's path full parsed template.
|
||||
//
|
||||
// Fields:
|
||||
// Src is the raw source of the path, i.e /users/{id:int min(1)}
|
||||
// Params is the list of the Params that are being used to the
|
||||
// path, i.e the min as param name and 1 as the param argument.
|
||||
type Template struct {
|
||||
// Src is the original template given by the client
|
||||
Src string `json:"src"`
|
||||
Params []TemplateParam `json:"params"`
|
||||
}
|
||||
|
||||
// TemplateParam is the parsed macro parameter's template
|
||||
// they are being used to describe the param's syntax result.
|
||||
type TemplateParam struct {
|
||||
Src string `json:"src"` // the unparsed param'false source
|
||||
// Type is not useful anywhere here but maybe
|
||||
// it's useful on host to decide how to convert the path template to specific router's syntax
|
||||
Type ast.ParamType `json:"type"`
|
||||
Name string `json:"name"`
|
||||
ErrCode int `json:"errCode"`
|
||||
TypeEvaluator EvaluatorFunc `json:"-"`
|
||||
Funcs []EvaluatorFunc `json:"-"`
|
||||
}
|
||||
|
||||
// Parse takes a full route path and a macro map (macro map contains the macro types with their registered param functions)
|
||||
// and returns a new Template.
|
||||
// It builds all the parameter functions for that template
|
||||
// and their evaluators, it's the api call that makes use the interpeter's parser -> lexer.
|
||||
func Parse(src string, macros *Map) (*Template, error) {
|
||||
params, err := parser.Parse(src)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t := new(Template)
|
||||
t.Src = src
|
||||
|
||||
for _, p := range params {
|
||||
funcMap := macros.Lookup(p.Type)
|
||||
typEval := funcMap.Evaluator
|
||||
|
||||
tmplParam := TemplateParam{
|
||||
Src: p.Src,
|
||||
Type: p.Type,
|
||||
Name: p.Name,
|
||||
ErrCode: p.ErrorCode,
|
||||
TypeEvaluator: typEval,
|
||||
}
|
||||
for _, paramfn := range p.Funcs {
|
||||
tmplFn := funcMap.getFunc(paramfn.Name)
|
||||
if tmplFn == nil { // if not find on this type, check for String's which is for global funcs too
|
||||
tmplFn = macros.String.getFunc(paramfn.Name)
|
||||
if tmplFn == nil { // if not found then just skip this param
|
||||
continue
|
||||
}
|
||||
}
|
||||
evalFn := tmplFn(paramfn.Args)
|
||||
if evalFn == nil {
|
||||
continue
|
||||
}
|
||||
tmplParam.Funcs = append(tmplParam.Funcs, evalFn)
|
||||
}
|
||||
|
||||
t.Params = append(t.Params, tmplParam)
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
@@ -1,448 +0,0 @@
|
||||
package node
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/kataras/iris/context"
|
||||
"github.com/kataras/iris/core/errors"
|
||||
)
|
||||
|
||||
// Nodes a conversion type for []*node.
|
||||
type Nodes []*node
|
||||
|
||||
type node struct {
|
||||
s string
|
||||
routeName string
|
||||
wildcardParamName string // name of the wildcard parameter, only one per whole Node is allowed
|
||||
paramNames []string // only-names
|
||||
childrenNodes Nodes
|
||||
handlers context.Handlers
|
||||
root bool
|
||||
rootWildcard bool // if it's a wildcard {path} type on root, it should allow everything but it is not conflicts with
|
||||
// any other static or dynamic or wildcard paths if exists on other nodes.
|
||||
}
|
||||
|
||||
// ErrDublicate returnned from `Add` when two or more routes have the same registered path.
|
||||
var ErrDublicate = errors.New("two or more routes have the same registered path")
|
||||
|
||||
/// TODO: clean up needed until v8.5
|
||||
|
||||
// Add adds a node to the tree, returns an ErrDublicate error on failure.
|
||||
func (nodes *Nodes) Add(routeName string, path string, handlers context.Handlers) error {
|
||||
// println("[Add] adding path: " + path)
|
||||
// resolve params and if that node should be added as root
|
||||
var params []string
|
||||
var paramStart, paramEnd int
|
||||
for {
|
||||
paramStart = strings.IndexByte(path[paramEnd:], ':')
|
||||
if paramStart == -1 {
|
||||
break
|
||||
}
|
||||
paramStart += paramEnd
|
||||
paramStart++
|
||||
paramEnd = strings.IndexByte(path[paramStart:], '/')
|
||||
|
||||
if paramEnd == -1 {
|
||||
params = append(params, path[paramStart:])
|
||||
path = path[:paramStart]
|
||||
break
|
||||
}
|
||||
paramEnd += paramStart
|
||||
params = append(params, path[paramStart:paramEnd])
|
||||
path = path[:paramStart] + path[paramEnd:]
|
||||
paramEnd -= paramEnd - paramStart
|
||||
}
|
||||
|
||||
var p []int
|
||||
for i := 0; i < len(path); i++ {
|
||||
idx := strings.IndexByte(path[i:], ':')
|
||||
if idx == -1 {
|
||||
break
|
||||
}
|
||||
p = append(p, idx+i)
|
||||
i = idx + i
|
||||
}
|
||||
|
||||
for _, idx := range p {
|
||||
// print("-2 nodes.Add: path: " + path + " params len: ")
|
||||
// println(len(params))
|
||||
if err := nodes.add(routeName, path[:idx], nil, nil, true); err != nil {
|
||||
return err
|
||||
}
|
||||
// print("-1 nodes.Add: path: " + path + " params len: ")
|
||||
// println(len(params))
|
||||
if nidx := idx + 1; len(path) > nidx {
|
||||
if err := nodes.add(routeName, path[:nidx], nil, nil, true); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// print("nodes.Add: path: " + path + " params len: ")
|
||||
// println(len(params))
|
||||
if err := nodes.add(routeName, path, params, handlers, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// prioritize by static path remember, they were already sorted by subdomains too.
|
||||
nodes.prioritize()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (nodes *Nodes) add(routeName, path string, paramNames []string, handlers context.Handlers, root bool) (err error) {
|
||||
// println("[add] route name: " + routeName)
|
||||
// println("[add] adding path: " + path)
|
||||
|
||||
// wraia etsi doulevei ara
|
||||
// na to kanw na exei to node to diko tou wildcard parameter name
|
||||
// kai sto telos na pernei auto, me vasi to *paramname
|
||||
// alla edw mesa 9a ginete register vasi tou last /
|
||||
|
||||
// set the wildcard param name to the root and its children.
|
||||
wildcardIdx := strings.IndexByte(path, '*')
|
||||
wildcardParamName := ""
|
||||
if wildcardIdx > 0 && len(paramNames) == 0 { // 27 Oct comment: && len(paramNames) == 0 {
|
||||
wildcardParamName = path[wildcardIdx+1:]
|
||||
path = path[0:wildcardIdx-1] + "/" // replace *paramName with single slash
|
||||
|
||||
// if path[len(path)-1] == '/' {
|
||||
// if root wildcard, then add it as it's and return
|
||||
rootWildcard := path == "/"
|
||||
if rootWildcard {
|
||||
path += "/" // if root wildcard, then do it like "//" instead of simple "/"
|
||||
}
|
||||
|
||||
n := &node{
|
||||
rootWildcard: rootWildcard,
|
||||
s: path,
|
||||
routeName: routeName,
|
||||
wildcardParamName: wildcardParamName,
|
||||
paramNames: paramNames,
|
||||
handlers: handlers,
|
||||
root: root,
|
||||
}
|
||||
*nodes = append(*nodes, n)
|
||||
// println("1. nodes.Add path: " + path)
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
loop:
|
||||
for _, n := range *nodes {
|
||||
if n.rootWildcard {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(n.paramNames) == 0 && n.wildcardParamName != "" {
|
||||
continue
|
||||
}
|
||||
|
||||
minlen := len(n.s)
|
||||
if len(path) < minlen {
|
||||
minlen = len(path)
|
||||
}
|
||||
|
||||
for i := 0; i < minlen; i++ {
|
||||
if n.s[i] == path[i] {
|
||||
continue
|
||||
}
|
||||
if i == 0 {
|
||||
continue loop
|
||||
}
|
||||
|
||||
*n = node{
|
||||
s: n.s[:i],
|
||||
childrenNodes: Nodes{
|
||||
{
|
||||
s: n.s[i:],
|
||||
routeName: n.routeName,
|
||||
wildcardParamName: n.wildcardParamName, // wildcardParamName
|
||||
paramNames: n.paramNames,
|
||||
childrenNodes: n.childrenNodes,
|
||||
handlers: n.handlers,
|
||||
},
|
||||
{
|
||||
s: path[i:],
|
||||
routeName: routeName,
|
||||
wildcardParamName: wildcardParamName,
|
||||
paramNames: paramNames,
|
||||
handlers: handlers,
|
||||
},
|
||||
},
|
||||
root: n.root,
|
||||
}
|
||||
|
||||
// println("2. change n and return " + n.s[:i] + " and " + path[i:])
|
||||
return
|
||||
}
|
||||
|
||||
if len(path) < len(n.s) {
|
||||
// println("3. change n and return | n.s[:len(path)] = " + n.s[:len(path)-1] + " and child: " + n.s[len(path)-1:])
|
||||
|
||||
*n = node{
|
||||
s: n.s[:len(path)],
|
||||
routeName: routeName,
|
||||
wildcardParamName: wildcardParamName,
|
||||
paramNames: paramNames,
|
||||
childrenNodes: Nodes{
|
||||
{
|
||||
s: n.s[len(path):],
|
||||
routeName: n.routeName,
|
||||
wildcardParamName: n.wildcardParamName, // wildcardParamName
|
||||
paramNames: n.paramNames,
|
||||
childrenNodes: n.childrenNodes,
|
||||
handlers: n.handlers,
|
||||
},
|
||||
},
|
||||
handlers: handlers,
|
||||
root: n.root,
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if len(path) > len(n.s) {
|
||||
if n.wildcardParamName != "" {
|
||||
n := &node{
|
||||
s: path,
|
||||
routeName: routeName,
|
||||
wildcardParamName: wildcardParamName,
|
||||
paramNames: paramNames,
|
||||
handlers: handlers,
|
||||
root: root,
|
||||
}
|
||||
// println("3.5. nodes.Add path: " + n.s)
|
||||
*nodes = append(*nodes, n)
|
||||
return
|
||||
}
|
||||
|
||||
pathToAdd := path[len(n.s):]
|
||||
// println("4. nodes.Add route name: " + routeName)
|
||||
// println("4. nodes.Add path: " + pathToAdd)
|
||||
err = n.childrenNodes.add(routeName, pathToAdd, paramNames, handlers, false)
|
||||
return err
|
||||
}
|
||||
|
||||
if len(handlers) == 0 { // missing handlers
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(n.handlers) > 0 { // n.handlers already setted
|
||||
return ErrDublicate
|
||||
}
|
||||
n.paramNames = paramNames
|
||||
n.handlers = handlers
|
||||
n.routeName = routeName
|
||||
return
|
||||
}
|
||||
|
||||
// START
|
||||
// Author's note:
|
||||
// 27 Oct 2017; fixes s|i|l+static+p
|
||||
// without breaking the current tests.
|
||||
if wildcardIdx > 0 {
|
||||
wildcardParamName = path[wildcardIdx+1:]
|
||||
path = path[0:wildcardIdx-1] + "/"
|
||||
}
|
||||
// END
|
||||
|
||||
n := &node{
|
||||
s: path,
|
||||
routeName: routeName,
|
||||
wildcardParamName: wildcardParamName,
|
||||
paramNames: paramNames,
|
||||
handlers: handlers,
|
||||
root: root,
|
||||
}
|
||||
*nodes = append(*nodes, n)
|
||||
|
||||
// println("5. node add on path: " + path + " n.s: " + n.s + " wildcard param: " + n.wildcardParamName)
|
||||
return
|
||||
}
|
||||
|
||||
// Find resolves the path, fills its params
|
||||
// and returns the registered to the resolved node's handlers.
|
||||
func (nodes Nodes) Find(path string, params *context.RequestParams) (string, context.Handlers) {
|
||||
n, paramValues := nodes.findChild(path, nil)
|
||||
if n != nil {
|
||||
// map the params,
|
||||
// n.params are the param names
|
||||
if len(paramValues) > 0 {
|
||||
// println("-----------")
|
||||
// print("param values returned len: ")
|
||||
// println(len(paramValues))
|
||||
// println("first value is: " + paramValues[0])
|
||||
// print("n.paramNames len: ")
|
||||
// println(len(n.paramNames))
|
||||
for i, name := range n.paramNames {
|
||||
// println("setting param name: " + name + " = " + paramValues[i])
|
||||
params.Set(name, paramValues[i])
|
||||
}
|
||||
// last is the wildcard,
|
||||
// if paramValues are exceed from the registered param names.
|
||||
// Note that n.wildcardParamName can be not empty but that doesn't meaning
|
||||
// that it contains a wildcard path, so the check is required.
|
||||
if len(paramValues) > len(n.paramNames) {
|
||||
// println("len(paramValues) > len(n.paramNames)")
|
||||
lastWildcardVal := paramValues[len(paramValues)-1]
|
||||
// println("setting wildcard param name: " + n.wildcardParamName + " = " + lastWildcardVal)
|
||||
params.Set(n.wildcardParamName, lastWildcardVal)
|
||||
}
|
||||
}
|
||||
|
||||
return n.routeName, n.handlers
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// Exists returns true if a node with that "path" exists,
|
||||
// otherise false.
|
||||
//
|
||||
// We don't care about parameters here.
|
||||
func (nodes Nodes) Exists(path string) bool {
|
||||
n, _ := nodes.findChild(path, nil)
|
||||
return n != nil && len(n.handlers) > 0
|
||||
}
|
||||
|
||||
func (nodes Nodes) findChild(path string, params []string) (*node, []string) {
|
||||
|
||||
for _, n := range nodes {
|
||||
if n.s == ":" {
|
||||
paramEnd := strings.IndexByte(path, '/')
|
||||
if paramEnd == -1 {
|
||||
if len(n.handlers) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return n, append(params, path)
|
||||
}
|
||||
return n.childrenNodes.findChild(path[paramEnd:], append(params, path[:paramEnd]))
|
||||
}
|
||||
|
||||
// println("n.s: " + n.s)
|
||||
// print("n.childrenNodes len: ")
|
||||
// println(len(n.childrenNodes))
|
||||
// print("n.root: ")
|
||||
// println(n.root)
|
||||
|
||||
// by runtime check of:,
|
||||
// if n.s == "//" && n.root && n.wildcardParamName != "" {
|
||||
// but this will slow down, so we have a static field on the node itself:
|
||||
if n.rootWildcard {
|
||||
// println("return from n.rootWildcard")
|
||||
// single root wildcard
|
||||
if len(path) < 2 {
|
||||
// do not remove that, it seems useless but it's not,
|
||||
// we had an error while production, this fixes that.
|
||||
path = "/" + path
|
||||
}
|
||||
return n, append(params, path[1:])
|
||||
}
|
||||
|
||||
// second conditional may be unnecessary
|
||||
// because of the n.rootWildcard before, but do it.
|
||||
if n.wildcardParamName != "" && len(path) > 2 {
|
||||
// println("n has wildcard n.s: " + n.s + " on path: " + path)
|
||||
// n.s = static/, path = static
|
||||
|
||||
// println(n.s + " vs path: " + path)
|
||||
|
||||
// we could have /other/ as n.s so
|
||||
// we must do this check, remember:
|
||||
// now wildcards live on their own nodes
|
||||
if len(path) == len(n.s)-1 {
|
||||
// then it's like:
|
||||
// path = /other2
|
||||
// ns = /other2/
|
||||
if path == n.s[0:len(n.s)-1] {
|
||||
return n, params
|
||||
}
|
||||
}
|
||||
|
||||
// othwerwise path = /other2/dsadas
|
||||
// ns= /other2/
|
||||
if strings.HasPrefix(path, n.s) {
|
||||
if len(path) > len(n.s)+1 {
|
||||
return n, append(params, path[len(n.s):]) // without slash
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(path, n.s) {
|
||||
// fmt.Printf("---here root: %v, n.s: "+n.s+" and path: "+path+" is dynamic: %v , wildcardParamName: %s, children len: %v \n", n.root, n.isDynamic(), n.wildcardParamName, len(n.childrenNodes))
|
||||
// println(path + " n.s: " + n.s + " continue...")
|
||||
continue
|
||||
}
|
||||
|
||||
if len(path) == len(n.s) {
|
||||
if len(n.handlers) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return n, params
|
||||
}
|
||||
|
||||
child, childParamNames := n.childrenNodes.findChild(path[len(n.s):], params)
|
||||
|
||||
// print("childParamNames len: ")
|
||||
// println(len(childParamNames))
|
||||
|
||||
// if len(childParamNames) > 0 {
|
||||
// println("childParamsNames[0] = " + childParamNames[0])
|
||||
// }
|
||||
|
||||
if child == nil || len(child.handlers) == 0 {
|
||||
if n.s[len(n.s)-1] == '/' && !(n.root && (n.s == "/" || len(n.childrenNodes) > 0)) {
|
||||
if len(n.handlers) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// println("if child == nil.... | n.s = " + n.s)
|
||||
// print("n.paramNames len: ")
|
||||
// println(n.paramNames)
|
||||
// print("n.wildcardParamName is: ")
|
||||
// println(n.wildcardParamName)
|
||||
// print("return n, append(params, path[len(n.s) | params: ")
|
||||
// println(path[len(n.s):])
|
||||
return n, append(params, path[len(n.s):])
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
return child, childParamNames
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// childLen returns all the children's and their children's length.
|
||||
func (n *node) childLen() (i int) {
|
||||
for _, n := range n.childrenNodes {
|
||||
i++
|
||||
i += n.childLen()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (n *node) isDynamic() bool {
|
||||
return n.s == ":" || n.wildcardParamName != "" || n.rootWildcard
|
||||
}
|
||||
|
||||
// prioritize sets the static paths first.
|
||||
func (nodes Nodes) prioritize() {
|
||||
sort.Slice(nodes, func(i, j int) bool {
|
||||
if nodes[i].isDynamic() {
|
||||
return false
|
||||
}
|
||||
if nodes[j].isDynamic() {
|
||||
return true
|
||||
}
|
||||
|
||||
return nodes[i].childLen() > nodes[j].childLen()
|
||||
})
|
||||
|
||||
for _, n := range nodes {
|
||||
n.childrenNodes.prioritize()
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ package router
|
||||
import (
|
||||
"github.com/kataras/iris/context"
|
||||
"github.com/kataras/iris/core/errors"
|
||||
"github.com/kataras/iris/core/router/macro"
|
||||
"github.com/kataras/iris/macro"
|
||||
)
|
||||
|
||||
// Party is just a group joiner of routes which have the same prefix and share same middleware(s) also.
|
||||
@@ -18,11 +18,11 @@ type Party interface {
|
||||
GetRelPath() string
|
||||
// GetReporter returns the reporter for adding errors
|
||||
GetReporter() *errors.Reporter
|
||||
// Macros returns the macro map which is responsible
|
||||
// to register custom macro functions for all routes.
|
||||
// Macros returns the macro collection that is responsible
|
||||
// to register custom macros with their own parameter types and their macro functions for all routes.
|
||||
//
|
||||
// Learn more at: https://github.com/kataras/iris/tree/master/_examples/routing/dynamic-path
|
||||
Macros() *macro.Map
|
||||
Macros() *macro.Macros
|
||||
|
||||
// Party groups routes which may have the same prefix and share same handlers,
|
||||
// returns that new rich subrouter.
|
||||
@@ -110,10 +110,10 @@ type Party interface {
|
||||
// otherwise use `Party` which can handle many paths with different handlers and middlewares.
|
||||
//
|
||||
// Usage:
|
||||
// app.HandleMany(iris.MethodGet, "/user /user/{id:int} /user/me", userHandler)
|
||||
// app.HandleMany(iris.MethodGet, "/user /user/{id:uint64} /user/me", userHandler)
|
||||
// At the other side, with `Handle` we've had to write:
|
||||
// app.Handle(iris.MethodGet, "/user", userHandler)
|
||||
// app.Handle(iris.MethodGet, "/user/{id:int}", userHandler)
|
||||
// app.Handle(iris.MethodGet, "/user/{id:uint64}", userHandler)
|
||||
// app.Handle(iris.MethodGet, "/user/me", userHandler)
|
||||
//
|
||||
// This method is used behind the scenes at the `Controller` function
|
||||
|
||||
@@ -7,15 +7,9 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/kataras/iris/core/netutil"
|
||||
"github.com/kataras/iris/core/router/macro/interpreter/lexer"
|
||||
)
|
||||
|
||||
const (
|
||||
// ParamStart the character in string representation where the underline router starts its dynamic named parameter.
|
||||
ParamStart = ":"
|
||||
// WildcardParamStart the character in string representation where the underline router starts its dynamic wildcard
|
||||
// path parameter.
|
||||
WildcardParamStart = "*"
|
||||
"github.com/kataras/iris/macro"
|
||||
"github.com/kataras/iris/macro/interpreter/ast"
|
||||
"github.com/kataras/iris/macro/interpreter/lexer"
|
||||
)
|
||||
|
||||
// Param receives a parameter name prefixed with the ParamStart symbol.
|
||||
@@ -31,6 +25,26 @@ func WildcardParam(name string) string {
|
||||
return prefix(name, WildcardParamStart)
|
||||
}
|
||||
|
||||
func convertMacroTmplToNodePath(tmpl macro.Template) string {
|
||||
routePath := tmpl.Src
|
||||
if len(routePath) > 1 && routePath[len(routePath)-1] == '/' {
|
||||
routePath = routePath[0 : len(routePath)-1] // remove any last "/"
|
||||
}
|
||||
|
||||
// if it has started with {} and it's valid
|
||||
// then the tmpl.Params will be filled,
|
||||
// so no any further check needed.
|
||||
for _, p := range tmpl.Params {
|
||||
if ast.IsTrailing(p.Type) {
|
||||
routePath = strings.Replace(routePath, p.Src, WildcardParam(p.Name), 1)
|
||||
} else {
|
||||
routePath = strings.Replace(routePath, p.Src, Param(p.Name), 1)
|
||||
}
|
||||
}
|
||||
|
||||
return routePath
|
||||
}
|
||||
|
||||
func prefix(s string, prefix string) string {
|
||||
if !strings.HasPrefix(s, prefix) {
|
||||
return prefix + s
|
||||
|
||||
@@ -27,8 +27,8 @@ func TestCleanPath(t *testing.T) {
|
||||
"/total/{year:string regexp(\\d{4})}/more/{s:string regexp(\\d{7})}"},
|
||||
{"/single_no_params",
|
||||
"/single_no_params"},
|
||||
{"/single/{id:int}",
|
||||
"/single/{id:int}"},
|
||||
{"/single/{id:uint64}",
|
||||
"/single/{id:uint64}"},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
@@ -45,8 +45,10 @@ func TestSplitPath(t *testing.T) {
|
||||
}{
|
||||
{"/v2/stores/{id:string format(uuid)} /v3",
|
||||
[]string{"/v2/stores/{id:string format(uuid)}", "/v3"}},
|
||||
{"/user/{id:int} /admin/{id:int}",
|
||||
[]string{"/user/{id:int}", "/admin/{id:int}"}},
|
||||
{"/user/{id:uint64} /admin/{id:uint64}",
|
||||
[]string{"/user/{id:uint64}", "/admin/{id:uint64}"}},
|
||||
{"/users/{id:int} /admins/{id:int64}",
|
||||
[]string{"/users/{id:int}", "/admins/{id:int64}"}},
|
||||
{"/user /admin",
|
||||
[]string{"/user", "/admin"}},
|
||||
{"/single_no_params",
|
||||
|
||||
@@ -5,18 +5,19 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/kataras/iris/context"
|
||||
"github.com/kataras/iris/core/router/macro"
|
||||
"github.com/kataras/iris/macro"
|
||||
"github.com/kataras/iris/macro/handler"
|
||||
)
|
||||
|
||||
// Route contains the information about a registered Route.
|
||||
// If any of the following fields are changed then the
|
||||
// caller should Refresh the router.
|
||||
type Route struct {
|
||||
Name string `json:"name"` // "userRoute"
|
||||
Method string `json:"method"` // "GET"
|
||||
methodBckp string // if Method changed to something else (which is possible at runtime as well, via RefreshRouter) then this field will be filled with the old one.
|
||||
Subdomain string `json:"subdomain"` // "admin."
|
||||
tmpl *macro.Template // Tmpl().Src: "/api/user/{id:int}"
|
||||
Name string `json:"name"` // "userRoute"
|
||||
Method string `json:"method"` // "GET"
|
||||
methodBckp string // if Method changed to something else (which is possible at runtime as well, via RefreshRouter) then this field will be filled with the old one.
|
||||
Subdomain string `json:"subdomain"` // "admin."
|
||||
tmpl macro.Template // Tmpl().Src: "/api/user/{id:uint64}"
|
||||
// temp storage, they're appended to the Handlers on build.
|
||||
// Execution happens before Handlers, can be empty.
|
||||
beginHandlers context.Handlers
|
||||
@@ -39,16 +40,19 @@ type Route struct {
|
||||
// It parses the path based on the "macros",
|
||||
// handlers are being changed to validate the macros at serve time, if needed.
|
||||
func NewRoute(method, subdomain, unparsedPath, mainHandlerName string,
|
||||
handlers context.Handlers, macros *macro.Map) (*Route, error) {
|
||||
handlers context.Handlers, macros macro.Macros) (*Route, error) {
|
||||
|
||||
tmpl, err := macro.Parse(unparsedPath, macros)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
path, handlers, err := compileRoutePathAndHandlers(handlers, tmpl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
path := convertMacroTmplToNodePath(tmpl)
|
||||
// prepend the macro handler to the route, now,
|
||||
// right before the register to the tree, so APIBuilder#UseGlobal will work as expected.
|
||||
if handler.CanMakeHandler(tmpl) {
|
||||
macroEvaluatorHandler := handler.MakeHandler(tmpl)
|
||||
handlers = append(context.Handlers{macroEvaluatorHandler}, handlers...)
|
||||
}
|
||||
|
||||
path = cleanPath(path) // maybe unnecessary here but who cares in this moment
|
||||
@@ -152,7 +156,18 @@ func (r Route) String() string {
|
||||
// via Tmpl().Src, Route.Path is the path
|
||||
// converted to match the underline router's specs.
|
||||
func (r Route) Tmpl() macro.Template {
|
||||
return *r.tmpl
|
||||
return r.tmpl
|
||||
}
|
||||
|
||||
// RegisteredHandlersLen returns the end-developer's registered handlers, all except the macro evaluator handler
|
||||
// if was required by the build process.
|
||||
func (r Route) RegisteredHandlersLen() int {
|
||||
n := len(r.Handlers)
|
||||
if handler.CanMakeHandler(r.tmpl) {
|
||||
n--
|
||||
}
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
// IsOnline returns true if the route is marked as "online" (state).
|
||||
@@ -198,7 +213,7 @@ func formatPath(path string) string {
|
||||
|
||||
// StaticPath returns the static part of the original, registered route path.
|
||||
// if /user/{id} it will return /user
|
||||
// if /user/{id}/friend/{friendid:int} it will return /user too
|
||||
// if /user/{id}/friend/{friendid:uint64} it will return /user too
|
||||
// if /assets/{filepath:path} it will return /assets.
|
||||
func (r Route) StaticPath() string {
|
||||
src := r.tmpl.Src
|
||||
@@ -242,7 +257,8 @@ func (r Route) Trace() string {
|
||||
printfmt += fmt.Sprintf(" %s", r.Subdomain)
|
||||
}
|
||||
printfmt += fmt.Sprintf(" %s ", r.Tmpl().Src)
|
||||
if l := len(r.Handlers); l > 1 {
|
||||
|
||||
if l := r.RegisteredHandlersLen(); l > 1 {
|
||||
printfmt += fmt.Sprintf("-> %s() and %d more", r.MainHandlerName, l-1)
|
||||
} else {
|
||||
printfmt += fmt.Sprintf("-> %s()", r.MainHandlerName)
|
||||
|
||||
@@ -31,7 +31,7 @@ func NewRouter() *Router { return &Router{} }
|
||||
// RefreshRouter re-builds the router. Should be called when a route's state
|
||||
// changed (i.e Method changed at serve-time).
|
||||
func (router *Router) RefreshRouter() error {
|
||||
return router.BuildRouter(router.cPool, router.requestHandler, router.routesProvider)
|
||||
return router.BuildRouter(router.cPool, router.requestHandler, router.routesProvider, true)
|
||||
}
|
||||
|
||||
// BuildRouter builds the router based on
|
||||
@@ -41,7 +41,7 @@ func (router *Router) RefreshRouter() error {
|
||||
// its wrapper.
|
||||
//
|
||||
// Use of RefreshRouter to re-build the router if needed.
|
||||
func (router *Router) BuildRouter(cPool *context.Pool, requestHandler RequestHandler, routesProvider RoutesProvider) error {
|
||||
func (router *Router) BuildRouter(cPool *context.Pool, requestHandler RequestHandler, routesProvider RoutesProvider, force bool) error {
|
||||
|
||||
if requestHandler == nil {
|
||||
return errors.New("router: request handler is nil")
|
||||
@@ -60,9 +60,23 @@ func (router *Router) BuildRouter(cPool *context.Pool, requestHandler RequestHan
|
||||
defer router.mu.Unlock()
|
||||
|
||||
// store these for RefreshRouter's needs.
|
||||
router.cPool = cPool
|
||||
router.requestHandler = requestHandler
|
||||
router.routesProvider = routesProvider
|
||||
if force {
|
||||
router.cPool = cPool
|
||||
router.requestHandler = requestHandler
|
||||
router.routesProvider = routesProvider
|
||||
} else {
|
||||
if router.cPool == nil {
|
||||
router.cPool = cPool
|
||||
}
|
||||
|
||||
if router.requestHandler == nil {
|
||||
router.requestHandler = requestHandler
|
||||
}
|
||||
|
||||
if router.routesProvider == nil && routesProvider != nil {
|
||||
router.routesProvider = routesProvider
|
||||
}
|
||||
}
|
||||
|
||||
// the important
|
||||
router.mainHandler = func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -122,20 +122,25 @@ func TestRouterWildcardRootMany(t *testing.T) {
|
||||
|
||||
func TestRouterWildcardRootManyAndRootStatic(t *testing.T) {
|
||||
var tt = []testRoute{
|
||||
// all routes will be handlded by "h" because we added wildcard to root,
|
||||
// routes that may return 404 will be handled by the below route ("h" handler) because we added wildcard to root,
|
||||
// this feature is very important and can remove noumerous of previous hacks on our apps.
|
||||
//
|
||||
// Static paths and parameters have priority over wildcard, all three types can be registered in the same path prefix.
|
||||
//
|
||||
// Remember, all of those routes are registered don't be tricked by the visual appearance of the below test blocks.
|
||||
{"GET", "/{p:path}", h, []testRouteRequest{
|
||||
{"GET", "", "/other2almost/some", iris.StatusOK, same_as_request_path},
|
||||
}},
|
||||
{"GET", "/static/{p:path}", h, []testRouteRequest{
|
||||
{"GET", "", "/static", iris.StatusOK, same_as_request_path},
|
||||
{"GET", "", "/static", iris.StatusOK, same_as_request_path}, // HERE<- IF NOT FOUND THEN BACKWARDS TO WILDCARD IF THERE IS ONE, HMM.
|
||||
{"GET", "", "/static/something/here", iris.StatusOK, same_as_request_path},
|
||||
}},
|
||||
{"GET", "/", h, []testRouteRequest{
|
||||
{"GET", "", "/", iris.StatusOK, same_as_request_path},
|
||||
}},
|
||||
{"GET", "/other/{paramother:path}", h2, []testRouteRequest{
|
||||
{"GET", "", "/other", iris.StatusForbidden, same_as_request_path},
|
||||
// OK and not h2 because of the root wildcard.
|
||||
{"GET", "", "/other", iris.StatusOK, same_as_request_path},
|
||||
{"GET", "", "/other/wildcard", iris.StatusForbidden, same_as_request_path},
|
||||
{"GET", "", "/other/wildcard/here", iris.StatusForbidden, same_as_request_path},
|
||||
}},
|
||||
@@ -145,6 +150,7 @@ func TestRouterWildcardRootManyAndRootStatic(t *testing.T) {
|
||||
}},
|
||||
{"GET", "/other2/static", h3, []testRouteRequest{
|
||||
{"GET", "", "/other2/static", iris.StatusOK, prefix_static_path_following_by_request_path},
|
||||
// h2(Forbiddenn) instead of h3 OK because it will be handled by the /other2/{paramothersecond:path}'s handler which gives 403.
|
||||
{"GET", "", "/other2/staticed", iris.StatusForbidden, same_as_request_path},
|
||||
}},
|
||||
}
|
||||
@@ -165,6 +171,7 @@ func testTheRoutes(t *testing.T, tests []testRoute, debug bool) {
|
||||
// run the tests
|
||||
for _, tt := range tests {
|
||||
for _, req := range tt.requests {
|
||||
// t.Logf("req: %s:%s\n", tt.method, tt.path)
|
||||
method := req.method
|
||||
if method == "" {
|
||||
method = tt.method
|
||||
|
||||
268
core/router/trie.go
Normal file
268
core/router/trie.go
Normal file
@@ -0,0 +1,268 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/kataras/iris/context"
|
||||
)
|
||||
|
||||
const (
|
||||
// ParamStart the character in string representation where the underline router starts its dynamic named parameter.
|
||||
ParamStart = ":"
|
||||
// WildcardParamStart the character in string representation where the underline router starts its dynamic wildcard
|
||||
// path parameter.
|
||||
WildcardParamStart = "*"
|
||||
)
|
||||
|
||||
// An iris-specific identical version of the https://github.com/kataras/muxie version 1.0.0 released at 15 Oct 2018
|
||||
type trieNode struct {
|
||||
parent *trieNode
|
||||
|
||||
children map[string]*trieNode
|
||||
hasDynamicChild bool // does one of the children contains a parameter or wildcard?
|
||||
childNamedParameter bool // is the child a named parameter (single segmnet)
|
||||
childWildcardParameter bool // or it is a wildcard (can be more than one path segments) ?
|
||||
paramKeys []string // the param keys without : or *.
|
||||
end bool // it is a complete node, here we stop and we can say that the node is valid.
|
||||
key string // if end == true then key is filled with the original value of the insertion's key.
|
||||
// if key != "" && its parent has childWildcardParameter == true,
|
||||
// we need it to track the static part for the closest-wildcard's parameter storage.
|
||||
staticKey string
|
||||
|
||||
// insert data.
|
||||
Handlers context.Handlers
|
||||
RouteName string
|
||||
}
|
||||
|
||||
func newTrieNode() *trieNode {
|
||||
n := new(trieNode)
|
||||
return n
|
||||
}
|
||||
|
||||
func (tn *trieNode) hasChild(s string) bool {
|
||||
return tn.getChild(s) != nil
|
||||
}
|
||||
|
||||
func (tn *trieNode) getChild(s string) *trieNode {
|
||||
if tn.children == nil {
|
||||
tn.children = make(map[string]*trieNode)
|
||||
}
|
||||
|
||||
return tn.children[s]
|
||||
}
|
||||
|
||||
func (tn *trieNode) addChild(s string, n *trieNode) {
|
||||
if tn.children == nil {
|
||||
tn.children = make(map[string]*trieNode)
|
||||
}
|
||||
|
||||
if _, exists := tn.children[s]; exists {
|
||||
return
|
||||
}
|
||||
|
||||
n.parent = tn
|
||||
tn.children[s] = n
|
||||
}
|
||||
|
||||
func (tn *trieNode) findClosestParentWildcardNode() *trieNode {
|
||||
tn = tn.parent
|
||||
for tn != nil {
|
||||
if tn.childWildcardParameter {
|
||||
return tn.getChild(WildcardParamStart)
|
||||
}
|
||||
|
||||
tn = tn.parent
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tn *trieNode) String() string {
|
||||
return tn.key
|
||||
}
|
||||
|
||||
type trie struct {
|
||||
root *trieNode
|
||||
|
||||
// if true then it will handle any path if not other parent wildcard exists,
|
||||
// so even 404 (on http services) is up to it, see trie#insert.
|
||||
hasRootWildcard bool
|
||||
|
||||
method string
|
||||
// subdomain is empty for default-hostname routes,
|
||||
// ex: mysubdomain.
|
||||
subdomain string
|
||||
}
|
||||
|
||||
func newTrie() *trie {
|
||||
return &trie{
|
||||
root: newTrieNode(),
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
pathSep = "/"
|
||||
pathSepB = '/'
|
||||
)
|
||||
|
||||
func slowPathSplit(path string) []string {
|
||||
if path == "/" {
|
||||
return []string{"/"}
|
||||
}
|
||||
|
||||
return strings.Split(path, pathSep)[1:]
|
||||
}
|
||||
|
||||
func (tr *trie) insert(path, routeName string, handlers context.Handlers) {
|
||||
input := slowPathSplit(path)
|
||||
|
||||
n := tr.root
|
||||
var paramKeys []string
|
||||
|
||||
for _, s := range input {
|
||||
c := s[0]
|
||||
|
||||
if isParam, isWildcard := c == ParamStart[0], c == WildcardParamStart[0]; isParam || isWildcard {
|
||||
n.hasDynamicChild = true
|
||||
paramKeys = append(paramKeys, s[1:]) // without : or *.
|
||||
|
||||
// if node has already a wildcard, don't force a value, check for true only.
|
||||
if isParam {
|
||||
n.childNamedParameter = true
|
||||
s = ParamStart
|
||||
}
|
||||
|
||||
if isWildcard {
|
||||
n.childWildcardParameter = true
|
||||
s = WildcardParamStart
|
||||
if tr.root == n {
|
||||
tr.hasRootWildcard = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !n.hasChild(s) {
|
||||
child := newTrieNode()
|
||||
n.addChild(s, child)
|
||||
}
|
||||
|
||||
n = n.getChild(s)
|
||||
}
|
||||
|
||||
n.RouteName = routeName
|
||||
n.Handlers = handlers
|
||||
n.paramKeys = paramKeys
|
||||
n.key = path
|
||||
n.end = true
|
||||
|
||||
i := strings.Index(path, ParamStart)
|
||||
if i == -1 {
|
||||
i = strings.Index(path, WildcardParamStart)
|
||||
}
|
||||
if i == -1 {
|
||||
i = len(n.key)
|
||||
}
|
||||
|
||||
n.staticKey = path[:i]
|
||||
}
|
||||
|
||||
func (tr *trie) search(q string, params *context.RequestParams) *trieNode {
|
||||
end := len(q)
|
||||
|
||||
if end == 0 || (end == 1 && q[0] == pathSepB) {
|
||||
return tr.root.getChild(pathSep)
|
||||
}
|
||||
|
||||
n := tr.root
|
||||
start := 1
|
||||
i := 1
|
||||
var paramValues []string
|
||||
|
||||
for {
|
||||
if i == end || q[i] == pathSepB {
|
||||
if child := n.getChild(q[start:i]); child != nil {
|
||||
n = child
|
||||
} else if n.childNamedParameter {
|
||||
n = n.getChild(ParamStart)
|
||||
if ln := len(paramValues); cap(paramValues) > ln {
|
||||
paramValues = paramValues[:ln+1]
|
||||
paramValues[ln] = q[start:i]
|
||||
} else {
|
||||
paramValues = append(paramValues, q[start:i])
|
||||
}
|
||||
} else if n.childWildcardParameter {
|
||||
n = n.getChild(WildcardParamStart)
|
||||
if ln := len(paramValues); cap(paramValues) > ln {
|
||||
paramValues = paramValues[:ln+1]
|
||||
paramValues[ln] = q[start:]
|
||||
} else {
|
||||
paramValues = append(paramValues, q[start:])
|
||||
}
|
||||
break
|
||||
} else {
|
||||
n = n.findClosestParentWildcardNode()
|
||||
if n != nil {
|
||||
// means that it has :param/static and *wildcard, we go trhough the :param
|
||||
// but the next path segment is not the /static, so go back to *wildcard
|
||||
// instead of not found.
|
||||
//
|
||||
// Fixes:
|
||||
// /hello/*p
|
||||
// /hello/:p1/static/:p2
|
||||
// req: http://localhost:8080/hello/dsadsa/static/dsadsa => found
|
||||
// req: http://localhost:8080/hello/dsadsa => but not found!
|
||||
// and
|
||||
// /second/wild/*p
|
||||
// /second/wild/static/otherstatic/
|
||||
// req: /second/wild/static/otherstatic/random => but not found!
|
||||
params.Set(n.paramKeys[0], q[len(n.staticKey):])
|
||||
return n
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if i == end {
|
||||
break
|
||||
}
|
||||
|
||||
i++
|
||||
start = i
|
||||
continue
|
||||
}
|
||||
|
||||
i++
|
||||
}
|
||||
|
||||
if n == nil || !n.end {
|
||||
if n != nil { // we need it on both places, on last segment (below) or on the first unnknown (above).
|
||||
if n = n.findClosestParentWildcardNode(); n != nil {
|
||||
params.Set(n.paramKeys[0], q[len(n.staticKey):])
|
||||
return n
|
||||
}
|
||||
}
|
||||
|
||||
if tr.hasRootWildcard {
|
||||
// that's the case for root wildcard, tests are passing
|
||||
// even without it but stick with it for reference.
|
||||
// Note ote that something like:
|
||||
// Routes: /other2/*myparam and /other2/static
|
||||
// Reqs: /other2/staticed will be handled
|
||||
// the /other2/*myparam and not the root wildcard, which is what we want.
|
||||
//
|
||||
n = tr.root.getChild(WildcardParamStart)
|
||||
params.Set(n.paramKeys[0], q[1:])
|
||||
return n
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
for i, paramValue := range paramValues {
|
||||
if len(n.paramKeys) > i {
|
||||
params.Set(n.paramKeys[i], paramValue)
|
||||
}
|
||||
}
|
||||
|
||||
return n
|
||||
}
|
||||
Reference in New Issue
Block a user