From dc3c38b189e4e7720908d6e1dd8520e532f79443 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Wed, 26 Sep 2018 11:37:11 +0300 Subject: [PATCH] add the ability to add custom parameter types to the interpreter and mapped macros with any number of macro functions - example added - although it's working it is not ready yet - I have to do some cleanup, doc comments and a TODO Former-commit-id: 8ac751b649a3b8e59948fd4c89ad53d25f49d0d5 --- README.md | 6 +- _examples/routing/macros/main.go | 64 +++ context/context.go | 134 +------ context/request_params.go | 159 ++++++++ core/memstore/memstore.go | 208 +++++----- core/router/api_builder.go | 12 +- core/router/macro.go | 290 +------------- core/router/macro/interpreter/ast/ast.go | 128 +++--- .../router/macro/interpreter/parser/parser.go | 67 +--- .../macro/interpreter/parser/parser_test.go | 58 ++- core/router/macro/macro.go | 222 ++++------- core/router/macro/macro_test.go | 50 +-- core/router/macro/macros.go | 375 ++++++++++++++++++ core/router/macro/template.go | 24 +- core/router/party.go | 6 +- core/router/route.go | 2 +- hero/di.go | 11 + hero/di/func.go | 7 +- hero/di/object.go | 5 + hero/di/reflect.go | 12 +- hero/handler.go | 18 +- hero/param.go | 58 +-- mvc/controller.go | 10 +- mvc/controller_handle_test.go | 8 +- mvc/controller_method_parser.go | 86 ++-- mvc/param.go | 86 ++-- 26 files changed, 1070 insertions(+), 1036 deletions(-) create mode 100644 _examples/routing/macros/main.go create mode 100644 context/request_params.go create mode 100644 core/router/macro/macros.go diff --git a/README.md b/README.md index 3ae3103a..9981c0b2 100644 --- a/README.md +++ b/README.md @@ -164,7 +164,7 @@ latLonRegex, _ := regexp.Compile(latLonExpr) // Register your custom argument-less macro function to the :string param type. // MatchString is a type of func(string) bool, so we use it as it is. -app.Macros().String.RegisterFunc("coordinate", latLonRegex.MatchString) +app.Macros().Get("string").RegisterFunc("coordinate", latLonRegex.MatchString) app.Get("/coordinates/{lat:string coordinate()}/{lon:string coordinate()}", func(ctx iris.Context) { ctx.Writef("Lat: %s | Lon: %s", ctx.Params().Get("lat"), ctx.Params().Get("lon")) @@ -175,7 +175,7 @@ Register your custom macro function which accepts two int arguments. ```go -app.Macros().String.RegisterFunc("range", func(minLength, maxLength int) func(string) bool { +app.Macros().Get("string").RegisterFunc("range", func(minLength, maxLength int) func(string) bool { return func(paramValue string) bool { return len(paramValue) >= minLength && len(paramValue) <= maxLength } @@ -191,7 +191,7 @@ app.Get("/limitchar/{name:string range(1,200) else 400}", func(ctx iris.Context) Register your custom macro function which accepts a slice of strings `[...,...]`. ```go -app.Macros().String.RegisterFunc("has", func(validNames []string) func(string) bool { +app.Macros().Get("string").RegisterFunc("has", func(validNames []string) func(string) bool { return func(paramValue string) bool { for _, validName := range validNames { if validName == paramValue { diff --git a/_examples/routing/macros/main.go b/_examples/routing/macros/main.go new file mode 100644 index 00000000..f45d4031 --- /dev/null +++ b/_examples/routing/macros/main.go @@ -0,0 +1,64 @@ +package main + +import ( + "fmt" + "reflect" + "strconv" + + "github.com/kataras/iris" + "github.com/kataras/iris/context" + // "github.com/kataras/iris/core/memstore" + "github.com/kataras/iris/hero" +) + +func main() { + app := iris.New() + app.Logger().SetLevel("debug") + + // Let's see how we can register a custom macro such as ":uint32" or ":small" for its alias (optionally) for Uint32 types. + app.Macros().Register("uint32", "small", false, false, func(paramValue string) bool { + _, err := strconv.ParseUint(paramValue, 10, 32) + return err == nil + }). + RegisterFunc("min", func(min uint32) func(string) bool { + return func(paramValue string) bool { + n, err := strconv.ParseUint(paramValue, 10, 32) + if err != nil { + return false + } + + return uint32(n) >= min + } + }) + + /* TODO: + somehow define one-time how the parameter should be parsed to a particular type (go std or custom) + tip: we can change the original value from string to X using the entry's.ValueRaw + */ + + context.ParamResolvers[reflect.Uint32] = func(paramIndex int) interface{} { + // return func(store memstore.Store) uint32 { + // param, _ := store.GetEntryAt(paramIndex) + // paramValueAsUint32, _ := strconv.ParseUint(param.String(), 10, 32) + // return uint32(paramValueAsUint32) + // } + return func(ctx context.Context) uint32 { + param := ctx.Params().GetEntryAt(paramIndex) + paramValueAsUint32, _ := strconv.ParseUint(param.String(), 10, 32) + return uint32(paramValueAsUint32) + } + } + // + + app.Get("/test_uint32/{myparam:uint32 min(10)}", hero.Handler(func(paramValue uint32) string { + return fmt.Sprintf("Value of the parameter is: %d\n", paramValue) + })) + + app.Get("test_uint64/{myparam:uint64}", handler) + + app.Run(iris.Addr(":8080")) +} + +func handler(ctx context.Context) { + ctx.Writef("Value of the parameter is: %s\n", ctx.Params().Get("myparam")) +} diff --git a/context/context.go b/context/context.go index 1ca344d0..a213b3b7 100644 --- a/context/context.go +++ b/context/context.go @@ -76,138 +76,6 @@ func (u UnmarshalerFunc) Unmarshal(data []byte, v interface{}) error { return u(data, v) } -// RequestParams is a key string - value string storage which -// context's request dynamic path params are being kept. -// Empty if the route is static. -type RequestParams struct { - store memstore.Store -} - -// Set adds a key-value pair to the path parameters values -// it's being called internally so it shouldn't be used as a local storage by the user, use `ctx.Values()` instead. -func (r *RequestParams) Set(key, value string) { - r.store.Set(key, value) -} - -// Visit accepts a visitor which will be filled -// by the key-value params. -func (r *RequestParams) Visit(visitor func(key string, value string)) { - r.store.Visit(func(k string, v interface{}) { - visitor(k, v.(string)) // always string here. - }) -} - -var emptyEntry memstore.Entry - -// GetEntryAt returns the internal Entry of the memstore based on its index, -// the stored index by the router. -// If not found then it returns a zero Entry and false. -func (r RequestParams) GetEntryAt(index int) (memstore.Entry, bool) { - if len(r.store) > index { - return r.store[index], true - } - return emptyEntry, false -} - -// GetEntry returns the internal Entry of the memstore based on its "key". -// If not found then it returns a zero Entry and false. -func (r RequestParams) GetEntry(key string) (memstore.Entry, bool) { - // we don't return the pointer here, we don't want to give the end-developer - // the strength to change the entry that way. - if e := r.store.GetEntry(key); e != nil { - return *e, true - } - return emptyEntry, false -} - -// Get returns a path parameter's value based on its route's dynamic path key. -func (r RequestParams) Get(key string) string { - return r.store.GetString(key) -} - -// GetTrim returns a path parameter's value without trailing spaces based on its route's dynamic path key. -func (r RequestParams) GetTrim(key string) string { - return strings.TrimSpace(r.Get(key)) -} - -// GetEscape returns a path parameter's double-url-query-escaped value based on its route's dynamic path key. -func (r RequestParams) GetEscape(key string) string { - return DecodeQuery(DecodeQuery(r.Get(key))) -} - -// GetDecoded returns a path parameter's double-url-query-escaped value based on its route's dynamic path key. -// same as `GetEscape`. -func (r RequestParams) GetDecoded(key string) string { - return r.GetEscape(key) -} - -// GetInt returns the path parameter's value as int, based on its key. -// It checks for all available types of int, including int64, strings etc. -// It will return -1 and a non-nil error if parameter wasn't found. -func (r RequestParams) GetInt(key string) (int, error) { - return r.store.GetInt(key) -} - -// GetInt64 returns the path paramete's value as int64, based on its key. -// It checks for all available types of int, including int, strings etc. -// It will return -1 and a non-nil error if parameter wasn't found. -func (r RequestParams) GetInt64(key string) (int64, error) { - return r.store.GetInt64(key) -} - -// GetFloat64 returns a path parameter's value based as float64 on its route's dynamic path key. -// It checks for all available types of int, including float64, int, strings etc. -// It will return -1 and a non-nil error if parameter wasn't found. -func (r RequestParams) GetFloat64(key string) (float64, error) { - return r.store.GetFloat64(key) -} - -// GetUint8 returns the path parameter's value as uint8, based on its key. -// It checks for all available types of int, including int, string. -// It will return 0 and a non-nil error if parameter wasn't found. -func (r RequestParams) GetUint8(key string) (uint8, error) { - return r.store.GetUint8(key) -} - -// GetUint64 returns the path parameter's value as uint64, based on its key. -// It checks for all available types of int, including int, uint64, int64, strings etc. -// It will return 0 and a non-nil error if parameter wasn't found. -func (r RequestParams) GetUint64(key string) (uint64, error) { - return r.store.GetUint64(key) -} - -// GetBool returns the path parameter's value as bool, based on its key. -// 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". -// Any other value returns an error. -func (r RequestParams) GetBool(key string) (bool, error) { - return r.store.GetBool(key) -} - -// GetIntUnslashed same as Get but it removes the first slash if found. -// Usage: Get an id from a wildcard path. -// -// Returns -1 with an error if the parameter couldn't be found. -func (r RequestParams) GetIntUnslashed(key string) (int, error) { - v := r.Get(key) - if v != "" { - if len(v) > 1 { - if v[0] == '/' { - v = v[1:] - } - } - return strconv.Atoi(v) - - } - - return -1, fmt.Errorf("unable to find int for '%s'", key) -} - -// Len returns the full length of the parameters. -func (r RequestParams) Len() int { - return r.store.Len() -} - // Context is the midle-man server's "object" for the clients. // // A New context is being acquired from a sync.Pool on each connection. @@ -1123,7 +991,7 @@ func NewContext(app Application) Context { func (ctx *context) BeginRequest(w http.ResponseWriter, r *http.Request) { ctx.handlers = nil // will be filled by router.Serve/HTTP ctx.values = ctx.values[0:0] // >> >> by context.Values().Set - ctx.params.store = ctx.params.store[0:0] + ctx.params.Store = ctx.params.Store[0:0] ctx.request = r ctx.currentHandlerIndex = 0 ctx.writer = AcquireResponseWriter() diff --git a/context/request_params.go b/context/request_params.go new file mode 100644 index 00000000..ebb20afb --- /dev/null +++ b/context/request_params.go @@ -0,0 +1,159 @@ +package context + +import ( + "reflect" + "strconv" + "strings" + + "github.com/kataras/iris/core/memstore" +) + +// RequestParams is a key string - value string storage which +// context's request dynamic path params are being kept. +// Empty if the route is static. +type RequestParams struct { + memstore.Store +} + +// GetEntryAt will return the parameter's internal store's `Entry` based on the index. +// If not found it will return an emptry `Entry`. +func (r *RequestParams) GetEntryAt(index int) memstore.Entry { + entry, _ := r.Store.GetEntryAt(index) + return entry +} + +// GetEntry will return the parameter's internal store's `Entry` based on its name/key. +// If not found it will return an emptry `Entry`. +func (r *RequestParams) GetEntry(key string) memstore.Entry { + entry, _ := r.Store.GetEntry(key) + return entry +} + +// Visit accepts a visitor which will be filled +// by the key-value params. +func (r *RequestParams) Visit(visitor func(key string, value string)) { + r.Store.Visit(func(k string, v interface{}) { + visitor(k, v.(string)) // always string here. + }) +} + +// Get returns a path parameter's value based on its route's dynamic path key. +func (r RequestParams) Get(key string) string { + return r.GetString(key) +} + +// GetTrim returns a path parameter's value without trailing spaces based on its route's dynamic path key. +func (r RequestParams) GetTrim(key string) string { + return strings.TrimSpace(r.Get(key)) +} + +// GetEscape returns a path parameter's double-url-query-escaped value based on its route's dynamic path key. +func (r RequestParams) GetEscape(key string) string { + return DecodeQuery(DecodeQuery(r.Get(key))) +} + +// GetDecoded returns a path parameter's double-url-query-escaped value based on its route's dynamic path key. +// same as `GetEscape`. +func (r RequestParams) GetDecoded(key string) string { + return r.GetEscape(key) +} + +// GetIntUnslashed same as Get but it removes the first slash if found. +// Usage: Get an id from a wildcard path. +// +// Returns -1 and false if not path parameter with that "key" found. +func (r RequestParams) GetIntUnslashed(key string) (int, bool) { + v := r.Get(key) + if v != "" { + if len(v) > 1 { + if v[0] == '/' { + v = v[1:] + } + } + + vInt, err := strconv.Atoi(v) + if err != nil { + return -1, false + } + return vInt, true + } + + return -1, false +} + +var ( + ParamResolvers = map[reflect.Kind]func(paramIndex int) interface{}{ + reflect.String: func(paramIndex int) interface{} { + return func(ctx Context) string { + return ctx.Params().GetEntryAt(paramIndex).ValueRaw.(string) + } + }, + reflect.Int: func(paramIndex int) interface{} { + return func(ctx Context) int { + v, _ := ctx.Params().GetEntryAt(paramIndex).IntDefault(0) + return v + } + }, + reflect.Int64: func(paramIndex int) interface{} { + return func(ctx Context) int64 { + v, _ := ctx.Params().GetEntryAt(paramIndex).Int64Default(0) + return v + } + }, + reflect.Uint8: func(paramIndex int) interface{} { + return func(ctx Context) uint8 { + v, _ := ctx.Params().GetEntryAt(paramIndex).Uint8Default(0) + return v + } + }, + reflect.Uint64: func(paramIndex int) interface{} { + return func(ctx Context) uint64 { + v, _ := ctx.Params().GetEntryAt(paramIndex).Uint64Default(0) + return v + } + }, + reflect.Bool: func(paramIndex int) interface{} { + return func(ctx Context) bool { + v, _ := ctx.Params().GetEntryAt(paramIndex).BoolDefault(false) + return v + } + }, + } +) + +// ParamResolverByKindAndIndex will return a function that can be used to bind path parameter's exact value by its Go std type +// and the parameter's index based on the registered path. +// Usage: nameResolver := ParamResolverByKindAndKey(reflect.String, 0) +// Inside a Handler: nameResolver.Call(ctx)[0] +// it will return the reflect.Value Of the exact type of the parameter(based on the path parameters and macros). +// It is only useful for dynamic binding of the parameter, it is used on "hero" package and it should be modified +// only when Macros are modified in such way that the default selections for the available go std types are not enough. +// +// Returns empty value and false if "k" does not match any valid parameter resolver. +func ParamResolverByKindAndIndex(k reflect.Kind, paramIndex int) (reflect.Value, bool) { + /* NO: + // This could work but its result is not exact type, so direct binding is not possible. + resolver := m.ParamResolver + fn := func(ctx context.Context) interface{} { + entry, _ := ctx.Params().GetEntry(paramName) + return resolver(entry) + } + // + + // This works but it is slower on serve-time. + paramNameValue := []reflect.Value{reflect.ValueOf(paramName)} + var fnSignature func(context.Context) string + return reflect.MakeFunc(reflect.ValueOf(&fnSignature).Elem().Type(), func(in []reflect.Value) []reflect.Value { + return in[0].MethodByName("Params").Call(emptyIn)[0].MethodByName("Get").Call(paramNameValue) + // return []reflect.Value{reflect.ValueOf(in[0].Interface().(context.Context).Params().Get(paramName))} + }) + // + */ + + r, ok := ParamResolvers[k] + if !ok || r == nil { + return reflect.Value{}, false + } + + return reflect.ValueOf(r(paramIndex)), true +} diff --git a/core/memstore/memstore.go b/core/memstore/memstore.go index 48986514..52c3b340 100644 --- a/core/memstore/memstore.go +++ b/core/memstore/memstore.go @@ -94,15 +94,17 @@ func (e Entry) IntDefault(def int) (int, error) { if v == nil { return def, errFindParse.Format("int", e.Key) } - if vint, ok := v.(int); ok { - return vint, nil - } else if vstring, sok := v.(string); sok && vstring != "" { - vint, err := strconv.Atoi(vstring) + + switch vv := v.(type) { + case string: + val, err := strconv.Atoi(vv) if err != nil { return def, err } - return vint, nil + return val, nil + case int: + return vv, nil } return def, errFindParse.Format("int", e.Key) @@ -116,16 +118,13 @@ func (e Entry) Int64Default(def int64) (int64, error) { return def, errFindParse.Format("int64", e.Key) } - if vint64, ok := v.(int64); ok { - return vint64, nil - } - - if vint, ok := v.(int); ok { - return int64(vint), nil - } - - if vstring, sok := v.(string); sok { - return strconv.ParseInt(vstring, 10, 64) + switch vv := v.(type) { + case string: + return strconv.ParseInt(vv, 10, 64) + case int64: + return vv, nil + case int: + return int64(vv), nil } return def, errFindParse.Format("int64", e.Key) @@ -135,30 +134,23 @@ func (e Entry) Int64Default(def int64) (int64, error) { // If not found returns "def" and a non-nil error. func (e Entry) Float64Default(def float64) (float64, error) { v := e.ValueRaw - if v == nil { return def, errFindParse.Format("float64", e.Key) } - if vfloat32, ok := v.(float32); ok { - return float64(vfloat32), nil - } - - if vfloat64, ok := v.(float64); ok { - return vfloat64, nil - } - - if vint, ok := v.(int); ok { - return float64(vint), nil - } - - if vstring, sok := v.(string); sok { - vfloat64, err := strconv.ParseFloat(vstring, 64) + switch vv := v.(type) { + case string: + val, err := strconv.ParseFloat(vv, 64) if err != nil { return def, err } - - return vfloat64, nil + return val, nil + case float32: + return float64(vv), nil + case float64: + return vv, nil + case int: + return float64(vv), nil } return def, errFindParse.Format("float64", e.Key) @@ -168,30 +160,24 @@ func (e Entry) Float64Default(def float64) (float64, error) { // If not found returns "def" and a non-nil error. func (e Entry) Float32Default(key string, def float32) (float32, error) { v := e.ValueRaw - if v == nil { return def, errFindParse.Format("float32", e.Key) } - if vfloat32, ok := v.(float32); ok { - return vfloat32, nil - } - - if vfloat64, ok := v.(float64); ok { - return float32(vfloat64), nil - } - - if vint, ok := v.(int); ok { - return float32(vint), nil - } - - if vstring, sok := v.(string); sok { - vfloat32, err := strconv.ParseFloat(vstring, 32) + switch vv := v.(type) { + case string: + val, err := strconv.ParseFloat(vv, 32) if err != nil { return def, err } - return float32(vfloat32), nil + return float32(val), nil + case float32: + return vv, nil + case float64: + return float32(vv), nil + case int: + return float32(vv), nil } return def, errFindParse.Format("float32", e.Key) @@ -205,26 +191,23 @@ func (e Entry) Uint8Default(def uint8) (uint8, error) { return def, errFindParse.Format("uint8", e.Key) } - if vuint8, ok := v.(uint8); ok { - return vuint8, nil - } - - if vint, ok := v.(int); ok { - if vint < 0 || vint > 255 { - return def, errFindParse.Format("uint8", e.Key) - } - return uint8(vint), nil - } - - if vstring, sok := v.(string); sok { - vuint64, err := strconv.ParseUint(vstring, 10, 8) + switch vv := v.(type) { + case string: + val, err := strconv.ParseUint(vv, 10, 8) if err != nil { return def, err } - if vuint64 > 255 { + if val > 255 { return def, errFindParse.Format("uint8", e.Key) } - return uint8(vuint64), nil + return uint8(val), nil + case uint8: + return vv, nil + case int: + if vv < 0 || vv > 255 { + return def, errFindParse.Format("uint8", e.Key) + } + return uint8(vv), nil } return def, errFindParse.Format("uint8", e.Key) @@ -238,20 +221,15 @@ func (e Entry) Uint64Default(def uint64) (uint64, error) { return def, errFindParse.Format("uint64", e.Key) } - if vuint64, ok := v.(uint64); ok { - return vuint64, nil - } - - if vint64, ok := v.(int64); ok { - return uint64(vint64), nil - } - - if vint, ok := v.(int); ok { - return uint64(vint), nil - } - - if vstring, sok := v.(string); sok { - return strconv.ParseUint(vstring, 10, 64) + switch vv := v.(type) { + case string: + return strconv.ParseUint(vv, 10, 64) + case uint64: + return vv, nil + case int64: + return uint64(vv), nil + case int: + return uint64(vv), nil } return def, errFindParse.Format("uint64", e.Key) @@ -269,20 +247,17 @@ func (e Entry) BoolDefault(def bool) (bool, error) { return def, errFindParse.Format("bool", e.Key) } - if vBoolean, ok := v.(bool); ok { - return vBoolean, nil - } - - if vString, ok := v.(string); ok { - b, err := strconv.ParseBool(vString) + switch vv := v.(type) { + case string: + val, err := strconv.ParseBool(vv) if err != nil { return def, err } - return b, nil - } - - if vInt, ok := v.(int); ok { - if vInt == 1 { + return val, nil + case bool: + return vv, nil + case int: + if vv == 1 { return true, nil } return false, nil @@ -394,28 +369,39 @@ func (r *Store) SetImmutable(key string, value interface{}) (Entry, bool) { return r.Save(key, value, true) } +var emptyEntry Entry + // GetEntry returns a pointer to the "Entry" found with the given "key" -// if nothing found then it returns nil, so be careful with that, -// it's not supposed to be used by end-developers. -func (r *Store) GetEntry(key string) *Entry { +// if nothing found then it returns an empty Entry and false. +func (r *Store) GetEntry(key string) (Entry, bool) { args := *r n := len(args) for i := 0; i < n; i++ { - kv := &args[i] - if kv.Key == key { - return kv + if kv := args[i]; kv.Key == key { + return kv, true } } - return nil + return emptyEntry, false +} + +// GetEntryAt returns the internal Entry of the memstore based on its index, +// the stored index by the router. +// If not found then it returns a zero Entry and false. +func (r *Store) GetEntryAt(index int) (Entry, bool) { + args := *r + if len(args) > index { + return args[index], true + } + return emptyEntry, false } // GetDefault returns the entry's value based on its key. // If not found returns "def". // This function checks for immutability as well, the rest don't. func (r *Store) GetDefault(key string, def interface{}) interface{} { - v := r.GetEntry(key) - if v == nil || v.ValueRaw == nil { + v, ok := r.GetEntry(key) + if !ok || v.ValueRaw == nil { return def } vv := v.Value() @@ -444,8 +430,8 @@ func (r *Store) Visit(visitor func(key string, value interface{})) { // GetStringDefault returns the entry's value as string, based on its key. // If not found returns "def". func (r *Store) GetStringDefault(key string, def string) string { - v := r.GetEntry(key) - if v == nil { + v, ok := r.GetEntry(key) + if !ok { return def } @@ -465,8 +451,8 @@ func (r *Store) GetStringTrim(name string) string { // GetInt returns the entry's value as int, based on its key. // If not found returns -1 and a non-nil error. func (r *Store) GetInt(key string) (int, error) { - v := r.GetEntry(key) - if v == nil { + v, ok := r.GetEntry(key) + if !ok { return 0, errFindParse.Format("int", key) } return v.IntDefault(-1) @@ -485,8 +471,8 @@ func (r *Store) GetIntDefault(key string, def int) int { // GetUint8 returns the entry's value as uint8, based on its key. // If not found returns 0 and a non-nil error. func (r *Store) GetUint8(key string) (uint8, error) { - v := r.GetEntry(key) - if v == nil { + v, ok := r.GetEntry(key) + if !ok { return 0, errFindParse.Format("uint8", key) } return v.Uint8Default(0) @@ -505,8 +491,8 @@ func (r *Store) GetUint8Default(key string, def uint8) uint8 { // GetUint64 returns the entry's value as uint64, based on its key. // If not found returns 0 and a non-nil error. func (r *Store) GetUint64(key string) (uint64, error) { - v := r.GetEntry(key) - if v == nil { + v, ok := r.GetEntry(key) + if !ok { return 0, errFindParse.Format("uint64", key) } return v.Uint64Default(0) @@ -525,8 +511,8 @@ func (r *Store) GetUint64Default(key string, def uint64) uint64 { // GetInt64 returns the entry's value as int64, based on its key. // If not found returns -1 and a non-nil error. func (r *Store) GetInt64(key string) (int64, error) { - v := r.GetEntry(key) - if v == nil { + v, ok := r.GetEntry(key) + if !ok { return -1, errFindParse.Format("int64", key) } return v.Int64Default(-1) @@ -545,8 +531,8 @@ func (r *Store) GetInt64Default(key string, def int64) int64 { // GetFloat64 returns the entry's value as float64, based on its key. // If not found returns -1 and a non nil error. func (r *Store) GetFloat64(key string) (float64, error) { - v := r.GetEntry(key) - if v == nil { + v, ok := r.GetEntry(key) + if !ok { return -1, errFindParse.Format("float64", key) } return v.Float64Default(-1) @@ -569,8 +555,8 @@ func (r *Store) GetFloat64Default(key string, def float64) float64 { // // If not found returns false and a non-nil error. func (r *Store) GetBool(key string) (bool, error) { - v := r.GetEntry(key) - if v == nil { + v, ok := r.GetEntry(key) + if !ok { return false, errFindParse.Format("bool", key) } diff --git a/core/router/api_builder.go b/core/router/api_builder.go index 0022428a..614bddac 100644 --- a/core/router/api_builder.go +++ b/core/router/api_builder.go @@ -68,7 +68,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 +116,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 +246,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. @@ -411,11 +411,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 } diff --git a/core/router/macro.go b/core/router/macro.go index e46a07c7..02c0ea95 100644 --- a/core/router/macro.go +++ b/core/router/macro.go @@ -1,295 +1,15 @@ package router import ( + "fmt" "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) - registerNumberMacroFuncs(out.Number) - registerInt64MacroFuncs(out.Int64) - registerUint8MacroFuncs(out.Uint8) - registerUint64MacroFuncs(out.Uint64) - 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) - } - }) -} - -// Number -// positive and negative numbers, number of digits depends on the arch. -func registerNumberMacroFuncs(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 - } - }) -} - -// Int64 -// -9223372036854775808 to 9223372036854775807. -func registerInt64MacroFuncs(out *macro.Macro) { - // checks if the param value's int64 representation is - // bigger or equal than 'min' - out.RegisterFunc("min", func(min int64) macro.EvaluatorFunc { - return func(paramValue string) bool { - n, err := strconv.ParseInt(paramValue, 10, 64) - if err != nil { - return false - } - return n >= min - } - }) - - // checks if the param value's int64 representation is - // smaller or equal than 'max' - out.RegisterFunc("max", func(max int64) macro.EvaluatorFunc { - return func(paramValue string) bool { - n, err := strconv.ParseInt(paramValue, 10, 64) - if err != nil { - return false - } - return n <= max - } - }) - - // checks if the param value's int64 representation is - // between min and max, including 'min' and 'max' - out.RegisterFunc("range", func(min, max int64) macro.EvaluatorFunc { - return func(paramValue string) bool { - n, err := strconv.ParseInt(paramValue, 10, 64) - if err != nil { - return false - } - - if n < min || n > max { - return false - } - return true - } - }) -} - -// Uint8 -// 0 to 255. -func registerUint8MacroFuncs(out *macro.Macro) { - // checks if the param value's uint8 representation is - // bigger or equal than 'min' - out.RegisterFunc("min", func(min uint8) macro.EvaluatorFunc { - return func(paramValue string) bool { - n, err := strconv.ParseUint(paramValue, 10, 8) - if err != nil { - return false - } - - return uint8(n) >= min - } - }) - - // checks if the param value's uint8 representation is - // smaller or equal than 'max' - out.RegisterFunc("max", func(max uint8) macro.EvaluatorFunc { - return func(paramValue string) bool { - n, err := strconv.ParseUint(paramValue, 10, 8) - if err != nil { - return false - } - return uint8(n) <= max - } - }) - - // checks if the param value's uint8 representation is - // between min and max, including 'min' and 'max' - out.RegisterFunc("range", func(min, max uint8) macro.EvaluatorFunc { - return func(paramValue string) bool { - n, err := strconv.ParseUint(paramValue, 10, 8) - if err != nil { - return false - } - - if v := uint8(n); v < min || v > max { - return false - } - return true - } - }) -} - -// Uint64 -// 0 to 18446744073709551615. -func registerUint64MacroFuncs(out *macro.Macro) { - // checks if the param value's uint64 representation is - // bigger or equal than 'min' - out.RegisterFunc("min", func(min uint64) macro.EvaluatorFunc { - return func(paramValue string) bool { - n, err := strconv.ParseUint(paramValue, 10, 64) - if err != nil { - return false - } - return n >= min - } - }) - - // checks if the param value's uint64 representation is - // smaller or equal than 'max' - out.RegisterFunc("max", func(max uint64) macro.EvaluatorFunc { - return func(paramValue string) bool { - n, err := strconv.ParseUint(paramValue, 10, 64) - if err != nil { - return false - } - return n <= max - } - }) - - // checks if the param value's uint64 representation is - // between min and max, including 'min' and 'max' - out.RegisterFunc("range", func(min, max uint64) macro.EvaluatorFunc { - return func(paramValue string) bool { - n, err := strconv.ParseUint(paramValue, 10, 64) - 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). // @@ -325,9 +45,9 @@ func convertTmplToNodePath(tmpl *macro.Template) (string, error) { // 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 ast.IsTrailing(p.Type) { if i != len(tmpl.Params)-1 { - return "", errors.New("parameter type \"ParamTypePath\" should be putted to the very last of a path") + return "", fmt.Errorf("parameter type \"%s\" should be putted to the very last of a path", p.Type.Indent()) } routePath = strings.Replace(routePath, p.Src, WildcardParam(p.Name), 1) } else { @@ -338,7 +58,7 @@ func convertTmplToNodePath(tmpl *macro.Template) (string, error) { return routePath, nil } -// note: returns nil if not needed, the caller(router) should be check for that before adding that on route's Middleware +// Note: returns nil if not needed, the caller(router) should check for that before adding that on route's Middleware. func convertTmplToHandler(tmpl *macro.Template) context.Handler { needMacroHandler := false @@ -347,7 +67,7 @@ func convertTmplToHandler(tmpl *macro.Template) context.Handler { // 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 { + if len(p.Funcs) == 0 && (ast.IsMaster(p.Type) || ast.IsTrailing(p.Type)) && p.ErrCode == http.StatusNotFound { } else { // println("we need handler for: " + tmpl.Src) needMacroHandler = true diff --git a/core/router/macro/interpreter/ast/ast.go b/core/router/macro/interpreter/ast/ast.go index fa695226..a3508373 100644 --- a/core/router/macro/interpreter/ast/ast.go +++ b/core/router/macro/interpreter/ast/ast.go @@ -1,43 +1,68 @@ package ast -import ( - "reflect" - "strings" +type ( + // ParamType holds the necessary information about a parameter type for the parser to lookup for. + ParamType interface { + // The name of the parameter type. + // Indent should contain the characters for the parser. + Indent() string + } + + // MasterParamType if implemented and its `Master()` returns true then empty type param will be translated to this param type. + // Also its functions will be available to the rest of the macro param type's funcs. + // + // Only one Master is allowed. + MasterParamType interface { + ParamType + Master() bool + } + + // TrailingParamType if implemented and its `Trailing()` returns true + // then it should be declared at the end of a route path and can accept any trailing path segment as one parameter. + TrailingParamType interface { + ParamType + Trailing() bool + } + + // AliasParamType if implemeneted nad its `Alias()` returns a non-empty string + // then the param type can be written with that string literal too. + AliasParamType interface { + ParamType + Alias() string + } ) -// ParamType holds the necessary information about a parameter type. -type ParamType struct { - Indent string // the name of the parameter type. - Aliases []string // any aliases, can be empty. - - GoType reflect.Kind // the go type useful for "mvc" and "hero" bindings. - - Default bool // if true then empty type param will target this and its functions will be available to the rest of the param type's funcs. - End bool // if true then it should be declared at the end of a route path and can accept any trailing path segment as one parameter. - - invalid bool // only true if returned by the parser via `LookupParamType`. +// IsMaster returns true if the "pt" param type is a master one. +func IsMaster(pt ParamType) bool { + p, ok := pt.(MasterParamType) + return ok && p.Master() } -// ParamTypeUnExpected is the unexpected parameter type. -var ParamTypeUnExpected = ParamType{invalid: true} - -func (pt ParamType) String() string { - return pt.Indent +// IsTrailing returns true if the "pt" param type is a marked as trailing, +// which should accept more than one path segment when in the end. +func IsTrailing(pt ParamType) bool { + p, ok := pt.(TrailingParamType) + return ok && p.Trailing() } -// Assignable returns true if the "k" standard type -// is assignabled to this ParamType. -func (pt ParamType) Assignable(k reflect.Kind) bool { - return pt.GoType == k +// HasAlias returns any alias of the "pt" param type. +// If alias is empty or not found then it returns false as its second output argument. +func HasAlias(pt ParamType) (string, bool) { + if p, ok := pt.(AliasParamType); ok { + alias := p.Alias() + return alias, len(alias) > 0 + } + + return "", false } -// GetDefaultParamType accepts a list of ParamType and returns its default. -// If no `Default` specified: +// GetMasterParamType accepts a list of ParamType and returns its master. +// If no `Master` specified: // and len(paramTypes) > 0 then it will return the first one, -// otherwise it returns a "string" parameter type. -func GetDefaultParamType(paramTypes ...ParamType) ParamType { +// otherwise it returns nil. +func GetMasterParamType(paramTypes ...ParamType) ParamType { for _, pt := range paramTypes { - if pt.Default == true { + if IsMaster(pt) { return pt } } @@ -46,24 +71,12 @@ func GetDefaultParamType(paramTypes ...ParamType) ParamType { return paramTypes[0] } - return ParamType{Indent: "string", GoType: reflect.String, Default: true} -} - -// ValidKind will return true if at least one param type is supported -// for this std kind. -func ValidKind(k reflect.Kind, paramTypes ...ParamType) bool { - for _, pt := range paramTypes { - if pt.GoType == k { - return true - } - } - - return false + return nil } // LookupParamType accepts the string // representation of a parameter type. -// Available: +// Example: // "string" // "number" or "int" // "long" or "int64" @@ -73,41 +86,20 @@ func ValidKind(k reflect.Kind, paramTypes ...ParamType) bool { // "alphabetical" // "file" // "path" -func LookupParamType(indent string, paramTypes ...ParamType) (ParamType, bool) { +func LookupParamType(indentOrAlias string, paramTypes ...ParamType) (ParamType, bool) { for _, pt := range paramTypes { - if pt.Indent == indent { + if pt.Indent() == indentOrAlias { return pt, true } - for _, alias := range pt.Aliases { - if alias == indent { + if alias, has := HasAlias(pt); has { + if alias == indentOrAlias { return pt, true } } } - return ParamTypeUnExpected, false -} - -// 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/number -// int64 matches to int64/long -// uint64 matches to uint64 -// bool matches to bool/boolean -func LookupParamTypeFromStd(goType string, paramTypes ...ParamType) (ParamType, bool) { - goType = strings.ToLower(goType) - for _, pt := range paramTypes { - if strings.ToLower(pt.GoType.String()) == goType { - return pt, true - } - } - - return ParamTypeUnExpected, false + return nil, false } // ParamStatement is a struct diff --git a/core/router/macro/interpreter/parser/parser.go b/core/router/macro/interpreter/parser/parser.go index 9ce125c9..b4bb0293 100644 --- a/core/router/macro/interpreter/parser/parser.go +++ b/core/router/macro/interpreter/parser/parser.go @@ -2,7 +2,6 @@ package parser import ( "fmt" - "reflect" "strconv" "strings" @@ -11,67 +10,12 @@ import ( "github.com/kataras/iris/core/router/macro/interpreter/token" ) -var ( - // paramTypeString is the string type. - // If parameter type is missing then it defaults to String type. - // Allows anything - // Declaration: /mypath/{myparam:string} or {myparam} - paramTypeString = ast.ParamType{Indent: "string", GoType: reflect.String, Default: true} - // ParamTypeNumber is the integer, a number type. - // Allows both positive and negative numbers, any number of digits. - // Declaration: /mypath/{myparam:number} or {myparam:int} for backwards-compatibility - paramTypeNumber = ast.ParamType{Indent: "number", Aliases: []string{"int"}, GoType: reflect.Int} - // ParamTypeInt64 is a number type. - // Allows only -9223372036854775808 to 9223372036854775807. - // Declaration: /mypath/{myparam:int64} or {myparam:long} - paramTypeInt64 = ast.ParamType{Indent: "int64", Aliases: []string{"long"}, GoType: reflect.Int64} - // ParamTypeUint8 a number type. - // Allows only 0 to 255. - // Declaration: /mypath/{myparam:uint8} - paramTypeUint8 = ast.ParamType{Indent: "uint8", GoType: reflect.Uint8} - // ParamTypeUint64 a number type. - // Allows only 0 to 18446744073709551615. - // Declaration: /mypath/{myparam:uint64} - paramTypeUint64 = ast.ParamType{Indent: "uint64", GoType: reflect.Uint64} - // ParamTypeBool 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:bool} or {myparam:boolean} - paramTypeBool = ast.ParamType{Indent: "bool", Aliases: []string{"boolean"}, GoType: reflect.Bool} - // ParamTypeAlphabetical is the alphabetical/letter type type. - // Allows letters only (upper or lowercase) - // Declaration: /mypath/{myparam:alphabetical} - paramTypeAlphabetical = ast.ParamType{Indent: "alphabetical", GoType: reflect.String} - // 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 = ast.ParamType{Indent: "file", GoType: reflect.String} - // ParamTypePath is the multi path (or wildcard) type. - // Allows anything, should be the last part - // Declaration: /mypath/{myparam:path} - paramTypePath = ast.ParamType{Indent: "path", GoType: reflect.String, End: true} -) - -// DefaultParamTypes are the built'n parameter types. -var DefaultParamTypes = []ast.ParamType{ - paramTypeString, - paramTypeNumber, paramTypeInt64, paramTypeUint8, paramTypeUint64, - paramTypeBool, - paramTypeAlphabetical, paramTypeFile, paramTypePath, -} - // Parse takes a route "fullpath" // and returns its param statements -// and an error on failure. -func Parse(fullpath string, paramTypes ...ast.ParamType) ([]*ast.ParamStatement, error) { +// or an error if failed. +func Parse(fullpath string, paramTypes []ast.ParamType) ([]*ast.ParamStatement, error) { if len(paramTypes) == 0 { - paramTypes = DefaultParamTypes + return nil, fmt.Errorf("empty parameter types") } pathParts := strings.SplitN(fullpath, "/", -1) @@ -94,7 +38,7 @@ func Parse(fullpath string, paramTypes ...ast.ParamType) ([]*ast.ParamStatement, return nil, err } // if we have param type path but it's not the last path part - if stmt.Type.End && i < len(pathParts)-1 { + if ast.IsTrailing(stmt.Type) && i < len(pathParts)-1 { return nil, fmt.Errorf("param type '%s' should be lived only inside the last path segment, but was inside: %s", stmt.Type, s) } @@ -166,7 +110,7 @@ func (p *ParamParser) Parse(paramTypes []ast.ParamType) (*ast.ParamStatement, er stmt := &ast.ParamStatement{ ErrorCode: DefaultParamErrorCode, - Type: ast.GetDefaultParamType(paramTypes...), + Type: ast.GetMasterParamType(paramTypes...), Src: p.src, } @@ -190,6 +134,7 @@ func (p *ParamParser) Parse(paramTypes []ast.ParamType) (*ast.ParamStatement, er // type can accept both letters and numbers but not symbols ofc. nextTok := l.NextToken() paramType, found := ast.LookupParamType(nextTok.Literal, paramTypes...) + if !found { p.appendErr("[%d:%d] unexpected parameter type: %s", t.Start, t.End, nextTok.Literal) } diff --git a/core/router/macro/interpreter/parser/parser_test.go b/core/router/macro/interpreter/parser/parser_test.go index ffdd5335..b492b361 100644 --- a/core/router/macro/interpreter/parser/parser_test.go +++ b/core/router/macro/interpreter/parser/parser_test.go @@ -9,6 +9,44 @@ import ( "github.com/kataras/iris/core/router/macro/interpreter/ast" ) +type simpleParamType string + +func (pt simpleParamType) Indent() string { return string(pt) } + +type masterParamType simpleParamType + +func (pt masterParamType) Indent() string { return string(pt) } +func (pt masterParamType) Master() bool { return true } + +type wildcardParamType string + +func (pt wildcardParamType) Indent() string { return string(pt) } +func (pt wildcardParamType) Trailing() bool { return true } + +type aliasedParamType []string + +func (pt aliasedParamType) Indent() string { return string(pt[0]) } +func (pt aliasedParamType) Alias() string { return pt[1] } + +var ( + paramTypeString = masterParamType("string") + paramTypeNumber = aliasedParamType{"number", "int"} + paramTypeInt64 = aliasedParamType{"int64", "long"} + paramTypeUint8 = simpleParamType("uint8") + paramTypeUint64 = simpleParamType("uint64") + paramTypeBool = aliasedParamType{"bool", "boolean"} + paramTypeAlphabetical = simpleParamType("alphabetical") + paramTypeFile = simpleParamType("file") + paramTypePath = wildcardParamType("path") +) + +var testParamTypes = []ast.ParamType{ + paramTypeString, + paramTypeNumber, paramTypeInt64, paramTypeUint8, paramTypeUint64, + paramTypeBool, + paramTypeAlphabetical, paramTypeFile, paramTypePath, +} + func TestParseParamError(t *testing.T) { // fail illegalChar := '$' @@ -16,7 +54,7 @@ func TestParseParamError(t *testing.T) { input := "{id" + string(illegalChar) + "int range(1,5) else 404}" p := NewParamParser(input) - _, err := p.Parse(DefaultParamTypes) + _, err := p.Parse(testParamTypes) if err == nil { t.Fatalf("expecting not empty error on input '%s'", input) @@ -32,7 +70,7 @@ func TestParseParamError(t *testing.T) { // success input2 := "{id:uint64 range(1,5) else 404}" p.Reset(input2) - _, err = p.Parse(DefaultParamTypes) + _, err = p.Parse(testParamTypes) if err != nil { t.Fatalf("expecting empty error on input '%s', but got: %s", input2, err.Error()) @@ -42,7 +80,7 @@ func TestParseParamError(t *testing.T) { // mustLookupParamType same as `ast.LookupParamType` but it panics if "indent" does not match with a valid Param Type. func mustLookupParamType(indent string) ast.ParamType { - pt, found := ast.LookupParamType(indent, DefaultParamTypes...) + pt, found := ast.LookupParamType(indent, testParamTypes...) if !found { panic("param type '" + indent + "' is not part of the provided param types") } @@ -113,14 +151,14 @@ func TestParseParam(t *testing.T) { ast.ParamStatement{ Src: "{myparam_:thisianunexpected}", Name: "myparam_", - Type: ast.ParamTypeUnExpected, + Type: nil, ErrorCode: 404, }}, // 5 {true, ast.ParamStatement{ Src: "{myparam2}", Name: "myparam2", // we now allow integers to the parameter names. - Type: ast.GetDefaultParamType(DefaultParamTypes...), + Type: ast.GetMasterParamType(testParamTypes...), ErrorCode: 404, }}, // 6 {true, @@ -152,7 +190,7 @@ func TestParseParam(t *testing.T) { ast.ParamStatement{ Src: "{id:long else 404}", Name: "id", - Type: mustLookupParamType("long"), // backwards-compatible test of LookupParamType. + Type: mustLookupParamType("int64"), // backwards-compatible test of LookupParamType. ErrorCode: 404, }}, // 10 {true, @@ -175,7 +213,7 @@ func TestParseParam(t *testing.T) { p := new(ParamParser) for i, tt := range tests { p.Reset(tt.expectedStatement.Src) - resultStmt, err := p.Parse(DefaultParamTypes) + resultStmt, err := p.Parse(testParamTypes) if tt.valid && err != nil { t.Fatalf("tests[%d] - error %s", i, err.Error()) @@ -216,7 +254,7 @@ func TestParse(t *testing.T) { }}, // 0 {"/admin/{id:uint64 range(1,5)}", true, []ast.ParamStatement{{ - Src: "{id:uint64 range(1,5)}", // test alternative (backwards-compatibility) "int" + Src: "{id:uint64 range(1,5)}", Name: "id", Type: paramTypeUint64, Funcs: []ast.ParamFunc{ @@ -260,7 +298,7 @@ func TestParse(t *testing.T) { []ast.ParamStatement{{ Src: "{myparam_:thisianunexpected}", Name: "myparam_", - Type: ast.ParamTypeUnExpected, + Type: nil, ErrorCode: 404, }, }}, // 5 @@ -282,7 +320,7 @@ func TestParse(t *testing.T) { }}, // 7 } for i, tt := range tests { - statements, err := Parse(tt.path) + statements, err := Parse(tt.path, testParamTypes) if tt.valid && err != nil { t.Fatalf("tests[%d] - error %s", i, err.Error()) diff --git a/core/router/macro/macro.go b/core/router/macro/macro.go index 7ea4630f..5cb304e0 100644 --- a/core/router/macro/macro.go +++ b/core/router/macro/macro.go @@ -7,8 +7,6 @@ import ( "strconv" "strings" "unicode" - - "github.com/kataras/iris/core/router/macro/interpreter/ast" ) // EvaluatorFunc is the signature for both param types and param funcs. @@ -108,53 +106,78 @@ func convertBuilderFunc(fn interface{}) ParamEvaluatorBuilder { // try to convert the string literal as we get it from the parser. var ( - v interface{} - err error + val interface{} + + panicIfErr = func(err error) { + if err != nil { + panic(fmt.Sprintf("on field index: %d: %v", i, err)) + } + } ) // try to get the value based on the expected type. switch field.Kind() { case reflect.Int: - v, err = strconv.Atoi(arg) + v, err := strconv.Atoi(arg) + panicIfErr(err) + val = v case reflect.Int8: - v, err = strconv.ParseInt(arg, 10, 8) + v, err := strconv.ParseInt(arg, 10, 8) + panicIfErr(err) + val = int8(v) case reflect.Int16: - v, err = strconv.ParseInt(arg, 10, 16) + v, err := strconv.ParseInt(arg, 10, 16) + panicIfErr(err) + val = int16(v) case reflect.Int32: - v, err = strconv.ParseInt(arg, 10, 32) + v, err := strconv.ParseInt(arg, 10, 32) + panicIfErr(err) + val = int32(v) case reflect.Int64: - v, err = strconv.ParseInt(arg, 10, 64) + v, err := strconv.ParseInt(arg, 10, 64) + panicIfErr(err) + val = v case reflect.Uint8: - v, err = strconv.ParseUint(arg, 10, 8) + v, err := strconv.ParseUint(arg, 10, 8) + panicIfErr(err) + val = uint8(v) case reflect.Uint16: - v, err = strconv.ParseUint(arg, 10, 16) + v, err := strconv.ParseUint(arg, 10, 16) + panicIfErr(err) + val = uint16(v) case reflect.Uint32: - v, err = strconv.ParseUint(arg, 10, 32) + v, err := strconv.ParseUint(arg, 10, 32) + panicIfErr(err) + val = uint32(v) case reflect.Uint64: - v, err = strconv.ParseUint(arg, 10, 64) + v, err := strconv.ParseUint(arg, 10, 64) + panicIfErr(err) + val = v case reflect.Float32: - v, err = strconv.ParseFloat(arg, 32) + v, err := strconv.ParseFloat(arg, 32) + panicIfErr(err) + val = float32(v) case reflect.Float64: - v, err = strconv.ParseFloat(arg, 64) + v, err := strconv.ParseFloat(arg, 64) + panicIfErr(err) + val = v case reflect.Bool: - v, err = strconv.ParseBool(arg) + v, err := strconv.ParseBool(arg) + panicIfErr(err) + val = v case reflect.Slice: if len(arg) > 1 { if arg[0] == '[' && arg[len(arg)-1] == ']' { // it is a single argument but as slice. - v = strings.Split(arg[1:len(arg)-1], ",") // only string slices. + val = strings.Split(arg[1:len(arg)-1], ",") // only string slices. } } default: - v = arg + val = arg } - if err != nil { - panic(fmt.Sprintf("on field index: %d: %v", i, err)) - } - - argValue := reflect.ValueOf(v) + argValue := reflect.ValueOf(val) if expected, got := field.Kind(), argValue.Kind(); expected != got { panic(fmt.Sprintf("fields should have the same type: [%d] expected %s but got %s", i, expected, got)) } @@ -190,6 +213,11 @@ type ( // and it can register param functions // to that macro which maps to a parameter type. Macro struct { + indent string + alias string + master bool + trailing bool + Evaluator EvaluatorFunc funcs []ParamFunc } @@ -212,19 +240,51 @@ type ( } ) -func newMacro(evaluator EvaluatorFunc) *Macro { - return &Macro{Evaluator: evaluator} +// NewMacro creates and returns a Macro that can be used as a registry for +// a new customized parameter type and its functions. +func NewMacro(indent, alias string, master, trailing bool, evaluator EvaluatorFunc) *Macro { + return &Macro{ + indent: indent, + alias: alias, + master: master, + trailing: trailing, + + Evaluator: evaluator, + } } +func (m *Macro) Indent() string { + return m.indent +} + +func (m *Macro) Alias() string { + return m.alias +} + +func (m *Macro) Master() bool { + return m.master +} + +func (m *Macro) Trailing() bool { + return m.trailing +} + +// func (m *Macro) SetParamResolver(fn func(memstore.Entry) interface{}) *Macro { +// m.ParamResolver = fn +// return m +// } + // 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{}) { +func (m *Macro) RegisterFunc(funcName string, fn interface{}) *Macro { fullFn := convertBuilderFunc(fn) m.registerFunc(funcName, fullFn) + + return m } func (m *Macro) registerFunc(funcName string, fullFn ParamEvaluatorBuilder) { @@ -256,113 +316,3 @@ func (m *Macro) getFunc(funcName string) ParamEvaluatorBuilder { } 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 - - // int type - // both positive and negative numbers, any number of digits. - Number *Macro - // int64 as int64 type - // -9223372036854775808 to 9223372036854775807. - Int64 *Macro - // uint8 as uint8 type - // 0 to 255. - Uint8 *Macro - // uint64 as uint64 type - // 0 to 18446744073709551615. - Uint64 *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 { - simpleNumberEvalutator := MustNewEvaluatorFromRegexp("^-?[0-9]+$") - return &Map{ - // it allows everything, so no need for a regexp here. - String: newMacro(func(string) bool { return true }), - Number: newMacro(simpleNumberEvalutator), //"^(-?0\\.[0-9]*[1-9]+[0-9]*$)|(^-?[1-9]+[0-9]*((\\.[0-9]*[1-9]+[0-9]*$)|(\\.[0-9]+)))|(^-?[1-9]+[0-9]*$)|(^0$){1}")), //("^-?[0-9]+$")), - Int64: newMacro(func(paramValue string) bool { - if !simpleNumberEvalutator(paramValue) { - return false - } - _, err := strconv.ParseInt(paramValue, 10, 64) - // if err == strconv.ErrRange... - return err == nil - }), //("^-[1-9]|-?[1-9][0-9]{1,14}|-?1000000000000000|-?10000000000000000|-?100000000000000000|-?[1-9]000000000000000000|-?9[0-2]00000000000000000|-?92[0-2]0000000000000000|-?922[0-3]000000000000000|-?9223[0-3]00000000000000|-?92233[0-7]0000000000000|-?922337[0-2]000000000000|-?92233720[0-3]0000000000|-?922337203[0-6]000000000|-?9223372036[0-8]00000000|-?92233720368[0-5]0000000|-?922337203685[0-4]000000|-?9223372036854[0-7]00000|-?92233720368547[0-7]0000|-?922337203685477[0-5]000|-?922337203685477[56]000|[0-9]$")), - Uint8: newMacro(MustNewEvaluatorFromRegexp("^([0-9]|[1-8][0-9]|9[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$")), - Uint64: newMacro(func(paramValue string) bool { - if !simpleNumberEvalutator(paramValue) { - return false - } - _, err := strconv.ParseUint(paramValue, 10, 64) - return err == nil - }), //("^[0-9]|[1-9][0-9]{1,14}|1000000000000000|10000000000000000|100000000000000000|1000000000000000000|1[0-8]000000000000000000|18[0-4]00000000000000000|184[0-4]0000000000000000|1844[0-6]000000000000000|18446[0-7]00000000000000|184467[0-4]0000000000000|1844674[0-4]000000000000|184467440[0-7]0000000000|1844674407[0-3]000000000|18446744073[0-7]00000000|1844674407370000000[0-9]|18446744073709[0-5]00000|184467440737095[0-5]0000|1844674407370955[0-2]000$")), - 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.ParamTypeNumber then it will return the m.Number. -// Returns the m.String if not matched. -func (m *Map) Lookup(typ ast.ParamType) *Macro { - switch typ { - case ast.ParamTypeNumber: - return m.Number - case ast.ParamTypeInt64: - return m.Int64 - case ast.ParamTypeUint8: - return m.Uint8 - case ast.ParamTypeUint64: - return m.Uint64 - 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 - } -} diff --git a/core/router/macro/macro_test.go b/core/router/macro/macro_test.go index 33f37fd0..20a29732 100644 --- a/core/router/macro/macro_test.go +++ b/core/router/macro/macro_test.go @@ -71,8 +71,6 @@ func testEvaluatorRaw(t *testing.T, macroEvaluator *Macro, input string, pass bo } func TestStringEvaluatorRaw(t *testing.T) { - f := NewMap() - tests := []struct { pass bool input string @@ -86,13 +84,11 @@ func TestStringEvaluatorRaw(t *testing.T) { } // 0 for i, tt := range tests { - testEvaluatorRaw(t, f.String, tt.input, tt.pass, i) + testEvaluatorRaw(t, String, tt.input, tt.pass, i) } } func TestNumberEvaluatorRaw(t *testing.T) { - f := NewMap() - tests := []struct { pass bool input string @@ -111,13 +107,11 @@ func TestNumberEvaluatorRaw(t *testing.T) { } for i, tt := range tests { - testEvaluatorRaw(t, f.Number, tt.input, tt.pass, i) + testEvaluatorRaw(t, Number, tt.input, tt.pass, i) } } func TestInt64EvaluatorRaw(t *testing.T) { - f := NewMap() - tests := []struct { pass bool input string @@ -138,13 +132,11 @@ func TestInt64EvaluatorRaw(t *testing.T) { } for i, tt := range tests { - testEvaluatorRaw(t, f.Int64, tt.input, tt.pass, i) + testEvaluatorRaw(t, Int64, tt.input, tt.pass, i) } } func TestUint8EvaluatorRaw(t *testing.T) { - f := NewMap() - tests := []struct { pass bool input string @@ -169,13 +161,11 @@ func TestUint8EvaluatorRaw(t *testing.T) { } for i, tt := range tests { - testEvaluatorRaw(t, f.Uint8, tt.input, tt.pass, i) + testEvaluatorRaw(t, Uint8, tt.input, tt.pass, i) } } func TestUint64EvaluatorRaw(t *testing.T) { - f := NewMap() - tests := []struct { pass bool input string @@ -196,13 +186,11 @@ func TestUint64EvaluatorRaw(t *testing.T) { } for i, tt := range tests { - testEvaluatorRaw(t, f.Uint64, tt.input, tt.pass, i) + testEvaluatorRaw(t, Uint64, tt.input, tt.pass, i) } } func TestAlphabeticalEvaluatorRaw(t *testing.T) { - f := NewMap() - tests := []struct { pass bool input string @@ -215,13 +203,11 @@ func TestAlphabeticalEvaluatorRaw(t *testing.T) { } for i, tt := range tests { - testEvaluatorRaw(t, f.Alphabetical, tt.input, tt.pass, i) + testEvaluatorRaw(t, Alphabetical, tt.input, tt.pass, i) } } func TestFileEvaluatorRaw(t *testing.T) { - f := NewMap() - tests := []struct { pass bool input string @@ -234,13 +220,11 @@ func TestFileEvaluatorRaw(t *testing.T) { } for i, tt := range tests { - testEvaluatorRaw(t, f.File, tt.input, tt.pass, i) + testEvaluatorRaw(t, File, tt.input, tt.pass, i) } } func TestPathEvaluatorRaw(t *testing.T) { - f := NewMap() - pathTests := []struct { pass bool input string @@ -254,28 +238,10 @@ func TestPathEvaluatorRaw(t *testing.T) { } for i, tt := range pathTests { - testEvaluatorRaw(t, f.Path, tt.input, tt.pass, i) + testEvaluatorRaw(t, Path, tt.input, tt.pass, i) } } -// 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(t, m.String, p.Src, false, 0) -// } - func TestConvertBuilderFunc(t *testing.T) { fn := func(min uint64, slice []string) func(string) bool { return func(paramValue string) bool { diff --git a/core/router/macro/macros.go b/core/router/macro/macros.go new file mode 100644 index 00000000..1a520af2 --- /dev/null +++ b/core/router/macro/macros.go @@ -0,0 +1,375 @@ +package macro + +import ( + "strconv" + "strings" + + "github.com/kataras/iris/core/router/macro/interpreter/ast" +) + +var ( + // String type + // Allows anything (single path segment, as everything except the `Path`). + String = NewMacro("string", "", true, false, func(string) bool { return true }). + RegisterFunc("regexp", func(expr string) EvaluatorFunc { + return MustNewEvaluatorFromRegexp(expr) + }). + // checks if param value starts with the 'prefix' arg + RegisterFunc("prefix", func(prefix string) EvaluatorFunc { + return func(paramValue string) bool { + return strings.HasPrefix(paramValue, prefix) + } + }). + // checks if param value ends with the 'suffix' arg + RegisterFunc("suffix", func(suffix string) EvaluatorFunc { + return func(paramValue string) bool { + return strings.HasSuffix(paramValue, suffix) + } + }). + // checks if param value contains the 's' arg + RegisterFunc("contains", func(s string) EvaluatorFunc { + return func(paramValue string) bool { + return strings.Contains(paramValue, s) + } + }). + // checks if param value's length is at least 'min' + RegisterFunc("min", func(min int) EvaluatorFunc { + return func(paramValue string) bool { + return len(paramValue) >= min + } + }). + // checks if param value's length is not bigger than 'max' + RegisterFunc("max", func(max int) EvaluatorFunc { + return func(paramValue string) bool { + return max >= len(paramValue) + } + }) + + simpleNumberEvalutator = MustNewEvaluatorFromRegexp("^-?[0-9]+$") + // Number or int type + // both positive and negative numbers, any number of digits. + Number = NewMacro("number", "int", false, false, simpleNumberEvalutator). + // checks if the param value's int representation is + // bigger or equal than 'min' + RegisterFunc("min", func(min int) 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'. + RegisterFunc("max", func(max int) 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'. + RegisterFunc("range", func(min, max int) 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 + } + }) + + // Int64 as int64 type + // -9223372036854775808 to 9223372036854775807. + Int64 = NewMacro("int64", "long", false, false, func(paramValue string) bool { + if !simpleNumberEvalutator(paramValue) { + return false + } + _, err := strconv.ParseInt(paramValue, 10, 64) + // if err == strconv.ErrRange... + return err == nil + }). + // checks if the param value's int64 representation is + // bigger or equal than 'min'. + RegisterFunc("min", func(min int64) EvaluatorFunc { + return func(paramValue string) bool { + n, err := strconv.ParseInt(paramValue, 10, 64) + if err != nil { + return false + } + return n >= min + } + }). + // checks if the param value's int64 representation is + // smaller or equal than 'max'. + RegisterFunc("max", func(max int64) EvaluatorFunc { + return func(paramValue string) bool { + n, err := strconv.ParseInt(paramValue, 10, 64) + if err != nil { + return false + } + return n <= max + } + }). + // checks if the param value's int64 representation is + // between min and max, including 'min' and 'max'. + RegisterFunc("range", func(min, max int64) EvaluatorFunc { + return func(paramValue string) bool { + n, err := strconv.ParseInt(paramValue, 10, 64) + if err != nil { + return false + } + + if n < min || n > max { + return false + } + return true + } + }) + + // Uint8 as uint8 type + // 0 to 255. + Uint8 = NewMacro("uint8", "", false, false, MustNewEvaluatorFromRegexp("^([0-9]|[1-8][0-9]|9[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$")). + // checks if the param value's uint8 representation is + // bigger or equal than 'min'. + RegisterFunc("min", func(min uint8) EvaluatorFunc { + return func(paramValue string) bool { + n, err := strconv.ParseUint(paramValue, 10, 8) + if err != nil { + return false + } + + return uint8(n) >= min + } + }). + // checks if the param value's uint8 representation is + // smaller or equal than 'max'. + RegisterFunc("max", func(max uint8) EvaluatorFunc { + return func(paramValue string) bool { + n, err := strconv.ParseUint(paramValue, 10, 8) + if err != nil { + return false + } + return uint8(n) <= max + } + }). + // checks if the param value's uint8 representation is + // between min and max, including 'min' and 'max'. + RegisterFunc("range", func(min, max uint8) EvaluatorFunc { + return func(paramValue string) bool { + n, err := strconv.ParseUint(paramValue, 10, 8) + if err != nil { + return false + } + + if v := uint8(n); v < min || v > max { + return false + } + return true + } + }) + + // Uint64 as uint64 type + // 0 to 18446744073709551615. + Uint64 = NewMacro("uint64", "", false, false, func(paramValue string) bool { + if !simpleNumberEvalutator(paramValue) { + return false + } + _, err := strconv.ParseUint(paramValue, 10, 64) + return err == nil + }). + // checks if the param value's uint64 representation is + // bigger or equal than 'min'. + RegisterFunc("min", func(min uint64) EvaluatorFunc { + return func(paramValue string) bool { + n, err := strconv.ParseUint(paramValue, 10, 64) + if err != nil { + return false + } + return n >= min + } + }). + // checks if the param value's uint64 representation is + // smaller or equal than 'max'. + RegisterFunc("max", func(max uint64) EvaluatorFunc { + return func(paramValue string) bool { + n, err := strconv.ParseUint(paramValue, 10, 64) + if err != nil { + return false + } + return n <= max + } + }). + // checks if the param value's uint64 representation is + // between min and max, including 'min' and 'max'. + RegisterFunc("range", func(min, max uint64) EvaluatorFunc { + return func(paramValue string) bool { + n, err := strconv.ParseUint(paramValue, 10, 64) + if err != nil { + return false + } + + if n < min || n > max { + return false + } + return true + } + }) + + // Bool or 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". + Bool = NewMacro("bool", "boolean", false, false, 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 letter type + // letters only (upper or lowercase) + Alphabetical = NewMacro("alphabetical", "", false, false, MustNewEvaluatorFromRegexp("^[a-zA-Z ]+$")) + // File type + // letters (upper or lowercase) + // numbers (0-9) + // underscore (_) + // dash (-) + // point (.) + // no spaces! or other character + File = NewMacro("file", "", false, false, MustNewEvaluatorFromRegexp("^[a-zA-Z0-9_.-]*$")) + // Path type + // anything, should be the last part + // + // 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 living in the latest path segment of a route path. + Path = NewMacro("path", "", false, true, func(string) bool { return true }) + + Defaults = &Macros{ + String, + Number, + Int64, + Uint8, + Uint64, + Bool, + Alphabetical, + Path, + } +) + +type Macros []*Macro + +func (ms *Macros) Register(indent, alias string, isMaster, isTrailing bool, evaluator EvaluatorFunc) *Macro { + macro := NewMacro(indent, alias, isMaster, isTrailing, evaluator) + if ms.register(macro) { + return macro + } + return nil +} + +func (ms *Macros) register(macro *Macro) bool { + if macro.Indent() == "" || macro.Evaluator == nil { + return false + } + + cp := *ms + + for _, m := range cp { + + // can't add more than one with the same ast characteristics. + if macro.Indent() == m.Indent() { + return false + } + + if macro.Alias() == m.Alias() || macro.Alias() == m.Indent() { + return false + } + + if macro.Master() && m.Master() { + return false + } + } + + cp = append(cp, macro) + + *ms = cp + return true +} + +func (ms *Macros) Unregister(indent string) bool { + cp := *ms + + for i, m := range cp { + if m.Indent() == indent { + copy(cp[i:], cp[i+1:]) + cp[len(cp)-1] = nil + cp = cp[:len(cp)-1] + + *ms = cp + return true + } + } + + return false +} + +func (ms *Macros) Lookup(pt ast.ParamType) *Macro { + if m := ms.Get(pt.Indent()); m != nil { + return m + } + + if alias, has := ast.HasAlias(pt); has { + if m := ms.Get(alias); m != nil { + return m + } + } + + return nil +} + +func (ms *Macros) Get(indentOrAlias string) *Macro { + if indentOrAlias == "" { + return nil + } + + for _, m := range *ms { + if m.Indent() == indentOrAlias { + return m + } + + if m.Alias() == indentOrAlias { + return m + } + } + + return nil +} + +func (ms *Macros) GetMaster() *Macro { + for _, m := range *ms { + if m.Master() { + return m + } + } + + return nil +} + +func (ms *Macros) GetTrailings() (macros []*Macro) { + for _, m := range *ms { + if m.Trailing() { + macros = append(macros, m) + } + } + + return +} diff --git a/core/router/macro/template.go b/core/router/macro/template.go index f26c1d06..3b30d2f7 100644 --- a/core/router/macro/template.go +++ b/core/router/macro/template.go @@ -25,6 +25,7 @@ type TemplateParam struct { // 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"` + Index int `json:"index"` ErrCode int `json:"errCode"` TypeEvaluator EvaluatorFunc `json:"-"` Funcs []EvaluatorFunc `json:"-"` @@ -34,15 +35,20 @@ type TemplateParam struct { // 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) +func Parse(src string, macros Macros) (*Template, error) { + types := make([]ast.ParamType, len(macros)) + for i, m := range macros { + types[i] = m + } + + params, err := parser.Parse(src, types) if err != nil { return nil, err } t := new(Template) t.Src = src - for _, p := range params { + for idx, p := range params { funcMap := macros.Lookup(p.Type) typEval := funcMap.Evaluator @@ -50,17 +56,23 @@ func Parse(src string, macros *Map) (*Template, error) { Src: p.Src, Type: p.Type, Name: p.Name, + Index: idx, 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 + if tmplFn == nil { // if not find on this type, check for Master's which is for global funcs too. + if m := macros.GetMaster(); m != nil { + tmplFn = m.getFunc(paramfn.Name) + } + + if tmplFn == nil { // if not found then just skip this param. continue } } + evalFn := tmplFn(paramfn.Args) if evalFn == nil { continue diff --git a/core/router/party.go b/core/router/party.go index 745d038e..99e482e1 100644 --- a/core/router/party.go +++ b/core/router/party.go @@ -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. diff --git a/core/router/route.go b/core/router/route.go index b581b659..11a69d6e 100644 --- a/core/router/route.go +++ b/core/router/route.go @@ -39,7 +39,7 @@ 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 { diff --git a/hero/di.go b/hero/di.go index 4244cfa0..d5066514 100644 --- a/hero/di.go +++ b/hero/di.go @@ -8,6 +8,17 @@ import ( func init() { di.DefaultHijacker = func(fieldOrFuncInput reflect.Type) (*di.BindObject, bool) { + // if IsExpectingStore(fieldOrFuncInput) { + // return &di.BindObject{ + // Type: memstoreTyp, + // BindType: di.Dynamic, + // ReturnValue: func(ctxValue []reflect.Value) reflect.Value { + // // return ctxValue[0].MethodByName("Params").Call(di.EmptyIn)[0] + // return ctxValue[0].MethodByName("Params").Call(di.EmptyIn)[0].Field(0) // the Params' memstore.Store. + // }, + // }, true + // } + if !IsContext(fieldOrFuncInput) { return nil, false } diff --git a/hero/di/func.go b/hero/di/func.go index da81d6f3..3c417d8f 100644 --- a/hero/di/func.go +++ b/hero/di/func.go @@ -132,9 +132,8 @@ func (s *FuncInjector) addValue(inputIndex int, value reflect.Value) bool { } if b.IsAssignable(inTyp) { - // println(inTyp.String() + " is assignable to " + val.Type().String()) // fmt.Printf("binded input index: %d for type: %s and value: %v with pointer: %v\n", - // i, b.Type.String(), value.String(), val.Pointer()) + // i, b.Type.String(), inTyp.String(), inTyp.Pointer()) s.inputs = append(s.inputs, &targetFuncInput{ InputIndex: inputIndex, Object: &b, @@ -194,8 +193,8 @@ func (s *FuncInjector) Inject(in *[]reflect.Value, ctx ...reflect.Value) { args := *in for _, input := range s.inputs { input.Object.Assign(ctx, func(v reflect.Value) { - // fmt.Printf("assign input index: %d for value: %v\n", - // input.InputIndex, v.String()) + // fmt.Printf("assign input index: %d for value: %v of type: %s\n", + // input.InputIndex, v.String(), v.Type().Name()) args[input.InputIndex] = v }) diff --git a/hero/di/object.go b/hero/di/object.go index 392abcc5..385162bd 100644 --- a/hero/di/object.go +++ b/hero/di/object.go @@ -101,6 +101,11 @@ func MakeReturnValue(fn reflect.Value, goodFunc TypeChecker) (func([]reflect.Val if !v.IsValid() { return zeroOutVal } + // if v.String() == "" { + // println("di/object.go: " + v.String()) + // // println("di/object.go: because it's interface{} it should be returned as: " + v.Elem().Type().String() + " and its value: " + v.Elem().Interface().(string)) + // return v.Elem() + // } return v } diff --git a/hero/di/reflect.go b/hero/di/reflect.go index 3b91ebf8..08fc7f2a 100644 --- a/hero/di/reflect.go +++ b/hero/di/reflect.go @@ -54,6 +54,7 @@ func IsZero(v reflect.Value) bool { // if can't interface, i.e return value from unexported field or method then return false return false } + zero := reflect.Zero(v.Type()) return v.Interface() == zero.Interface() } @@ -62,7 +63,10 @@ func IsZero(v reflect.Value) bool { // If "v" is a nil pointer, Indirect returns a zero Value. // If "v" is not a pointer, Indirect returns v. func IndirectValue(v reflect.Value) reflect.Value { - return reflect.Indirect(v) + if k := v.Kind(); k == reflect.Ptr { //|| k == reflect.Interface { + return v.Elem() + } + return v } // ValueOf returns the reflect.Value of "o". @@ -123,6 +127,11 @@ func equalTypes(got reflect.Type, expected reflect.Type) bool { // fmt.Printf("expected interface = %s and got to set on the arg is: %s\n", expected.String(), got.String()) return got.Implements(expected) } + + // if got.String() == "interface {}" { + // return true + // } + return false } @@ -161,7 +170,6 @@ func lookupFields(elemTyp reflect.Type, skipUnexported bool, parentIndex []int) for i, n := 0, elemTyp.NumField(); i < n; i++ { f := elemTyp.Field(i) - if IndirectType(f.Type).Kind() == reflect.Struct && !structFieldIgnored(f) { fields = append(fields, lookupFields(f.Type, skipUnexported, append(parentIndex, i))...) diff --git a/hero/handler.go b/hero/handler.go index bb427d77..6644dc83 100644 --- a/hero/handler.go +++ b/hero/handler.go @@ -5,19 +5,31 @@ import ( "reflect" "runtime" + "github.com/kataras/iris/context" + "github.com/kataras/iris/core/memstore" "github.com/kataras/iris/hero/di" "github.com/kataras/golog" - "github.com/kataras/iris/context" ) -var contextTyp = reflect.TypeOf((*context.Context)(nil)).Elem() +var ( + contextTyp = reflect.TypeOf((*context.Context)(nil)).Elem() + memstoreTyp = reflect.TypeOf(memstore.Store{}) +) // IsContext returns true if the "inTyp" is a type of Context. func IsContext(inTyp reflect.Type) bool { return inTyp.Implements(contextTyp) } +// IsExpectingStore returns true if the "inTyp" is a type of memstore.Store. +func IsExpectingStore(inTyp reflect.Type) bool { + print("di/handler.go: " + inTyp.String() + " vs " + memstoreTyp.String() + " : ") + println(inTyp == memstoreTyp) + + return inTyp == memstoreTyp +} + // checks if "handler" is context.Handler: func(context.Context). func isContextHandler(handler interface{}) (context.Handler, bool) { h, is := handler.(context.Handler) @@ -70,7 +82,7 @@ func makeHandler(handler interface{}, values ...reflect.Value) (context.Handler, // is invalid when input len and values are not match // or their types are not match, we will take look at the // second statement, here we will re-try it - // using binders for path parameters: string, int, int64, bool. + // using binders for path parameters: string, int, int64, uint8, uint64, bool and so on. // We don't have access to the path, so neither to the macros here, // but in mvc. So we have to do it here. if valid = funcInjector.Retry(new(params).resolve); !valid { diff --git a/hero/param.go b/hero/param.go index 4ea62fc2..dc34b10e 100644 --- a/hero/param.go +++ b/hero/param.go @@ -19,64 +19,8 @@ type params struct { func (p *params) resolve(index int, typ reflect.Type) (reflect.Value, bool) { currentParamIndex := p.next - v, ok := resolveParam(currentParamIndex, typ) + v, ok := context.ParamResolverByKindAndIndex(typ.Kind(), currentParamIndex) p.next = p.next + 1 return v, ok } - -func resolveParam(currentParamIndex int, typ reflect.Type) (reflect.Value, bool) { - var fn interface{} - - switch typ.Kind() { - case reflect.Int: - fn = func(ctx context.Context) int { - // the second "ok/found" check is not necessary, - // because even if the entry didn't found on that "index" - // it will return an empty entry which will return the - // default value passed from the xDefault(def) because its `ValueRaw` is nil. - entry, _ := ctx.Params().GetEntryAt(currentParamIndex) - v, _ := entry.IntDefault(0) - return v - } - case reflect.Int64: - fn = func(ctx context.Context) int64 { - entry, _ := ctx.Params().GetEntryAt(currentParamIndex) - v, _ := entry.Int64Default(0) - - return v - } - case reflect.Uint8: - fn = func(ctx context.Context) uint8 { - entry, _ := ctx.Params().GetEntryAt(currentParamIndex) - v, _ := entry.Uint8Default(0) - - return v - } - case reflect.Uint64: - fn = func(ctx context.Context) uint64 { - entry, _ := ctx.Params().GetEntryAt(currentParamIndex) - v, _ := entry.Uint64Default(0) - - return v - } - case reflect.Bool: - fn = func(ctx context.Context) bool { - entry, _ := ctx.Params().GetEntryAt(currentParamIndex) - v, _ := entry.BoolDefault(false) - return v - } - case reflect.String: - fn = func(ctx context.Context) string { - entry, _ := ctx.Params().GetEntryAt(currentParamIndex) - // print(entry.Key + " with index of: ") - // print(currentParamIndex) - // println(" and value: " + entry.String()) - return entry.String() - } - default: - return reflect.Value{}, false - } - - return reflect.ValueOf(fn), true -} diff --git a/mvc/controller.go b/mvc/controller.go index 41f37ac2..85675ec2 100644 --- a/mvc/controller.go +++ b/mvc/controller.go @@ -247,7 +247,7 @@ func (c *ControllerActivator) parseMethods() { } func (c *ControllerActivator) parseMethod(m reflect.Method) { - httpMethod, httpPath, err := parseMethod(m, c.isReservedMethod) + httpMethod, httpPath, err := parseMethod(*c.router.Macros(), m, c.isReservedMethod) if err != nil { if err != errSkip { c.addErr(fmt.Errorf("MVC: fail to parse the route path and HTTP method for '%s.%s': %v", c.fullName, m.Name, err)) @@ -283,7 +283,7 @@ func (c *ControllerActivator) Handle(method, path, funcName string, middleware . } // parse a route template which contains the parameters organised. - tmpl, err := macro.Parse(path, c.router.Macros()) + tmpl, err := macro.Parse(path, *c.router.Macros()) if err != nil { c.addErr(fmt.Errorf("MVC: fail to parse the path for '%s.%s': %v", c.fullName, funcName, err)) return nil @@ -338,6 +338,7 @@ func (c *ControllerActivator) handlerOf(m reflect.Method, funcDependencies []ref } // fmt.Printf("for %s | values: %s\n", funcName, funcDependencies) + funcInjector := di.Func(m.Func, funcDependencies...) // fmt.Printf("actual injector's inputs length: %d\n", funcInjector.Length) if funcInjector.Has { @@ -396,6 +397,11 @@ func (c *ControllerActivator) handlerOf(m reflect.Method, funcDependencies []ref in := make([]reflect.Value, n, n) in[0] = ctrl funcInjector.Inject(&in, ctxValue) + + // for idxx, inn := range in { + // println("controller.go: execution: in.Value = "+inn.String()+" and in.Type = "+inn.Type().Kind().String()+" of index: ", idxx) + // } + hero.DispatchFuncResult(ctx, call(in)) return } diff --git a/mvc/controller_handle_test.go b/mvc/controller_handle_test.go index 55e200d4..9864b3ad 100644 --- a/mvc/controller_handle_test.go +++ b/mvc/controller_handle_test.go @@ -37,6 +37,7 @@ type testControllerHandle struct { func (c *testControllerHandle) BeforeActivation(b BeforeActivation) { b.Handle("GET", "/histatic", "HiStatic") b.Handle("GET", "/hiservice", "HiService") + b.Handle("GET", "/hiservice/{ps:string}", "HiServiceBy") b.Handle("GET", "/hiparam/{ps:string}", "HiParamBy") b.Handle("GET", "/hiparamempyinput/{ps:string}", "HiParamEmptyInputBy") } @@ -84,6 +85,10 @@ func (c *testControllerHandle) HiService() string { return c.Service.Say("hi") } +func (c *testControllerHandle) HiServiceBy(v string) string { + return c.Service.Say("hi with param: " + v) +} + func (c *testControllerHandle) HiParamBy(v string) string { return v } @@ -116,7 +121,8 @@ func TestControllerHandle(t *testing.T) { // and can be used in a user-defined, dynamic "mvc handler". e.GET("/hiservice").Expect().Status(httptest.StatusOK). Body().Equal("service: hi") - + e.GET("/hiservice/value").Expect().Status(httptest.StatusOK). + Body().Equal("service: hi with param: value") // this worked with a temporary variadic on the resolvemethodfunc which is not // correct design, I should split the path and params with the rest of implementation // in order a simple template.Src can be given. diff --git a/mvc/controller_method_parser.go b/mvc/controller_method_parser.go index 97c1282e..e5477e9e 100644 --- a/mvc/controller_method_parser.go +++ b/mvc/controller_method_parser.go @@ -4,11 +4,12 @@ import ( "errors" "fmt" "reflect" + "strconv" "strings" "unicode" "github.com/kataras/iris/core/router" - "github.com/kataras/iris/core/router/macro/interpreter/ast" + "github.com/kataras/iris/core/router/macro" ) const ( @@ -95,47 +96,25 @@ func (l *methodLexer) peekPrev() (w string) { return w } -var posWords = map[int]string{ - 0: "", - 1: "first", - 2: "second", - 3: "third", - 4: "forth", - 5: "five", - 6: "sixth", - 7: "seventh", - 8: "eighth", - 9: "ninth", - 10: "tenth", - 11: "eleventh", - 12: "twelfth", - 13: "thirteenth", - 14: "fourteenth", - 15: "fifteenth", - 16: "sixteenth", - 17: "seventeenth", - 18: "eighteenth", - 19: "nineteenth", - 20: "twentieth", -} - func genParamKey(argIdx int) string { - return "arg" + posWords[argIdx] // argfirst, argsecond... + return "param" + strconv.Itoa(argIdx) // param0, param1, param2... } type methodParser struct { - lexer *methodLexer - fn reflect.Method + lexer *methodLexer + fn reflect.Method + macros macro.Macros } -func parseMethod(fn reflect.Method, skipper func(string) bool) (method, path string, err error) { +func parseMethod(macros macro.Macros, fn reflect.Method, skipper func(string) bool) (method, path string, err error) { if skipper(fn.Name) { return "", "", errSkip } p := &methodParser{ - fn: fn, - lexer: newMethodLexer(fn.Name), + fn: fn, + lexer: newMethodLexer(fn.Name), + macros: macros, } return p.parse() } @@ -211,34 +190,45 @@ func (p *methodParser) parsePathParam(path string, w string, funcArgPos int) (st var ( paramKey = genParamKey(funcArgPos) // argfirst, argsecond... - paramType = ast.ParamTypeString // default string + m = p.macros.GetMaster() // default (String by-default) + trailings = p.macros.GetTrailings() ) // string, int... - goType := typ.In(funcArgPos).Name() + goType := typ.In(funcArgPos).Kind() nextWord := p.lexer.peekNext() if nextWord == tokenWildcard { p.lexer.skip() // skip the Wildcard word. - paramType = ast.ParamTypePath - } else if pType := ast.LookupParamTypeFromStd(goType); pType != ast.ParamTypeUnExpected { - // it's not wildcard, so check base on our available macro types. - paramType = pType - } else { - if typ.NumIn() > funcArgPos { - // has more input arguments but we are not in the correct - // index now, maybe the first argument was an `iris/context.Context` - // so retry with the "funcArgPos" incremented. - // - // the "funcArgPos" will be updated to the caller as well - // because we return it among the path and the error. - return p.parsePathParam(path, w, funcArgPos+1) + if len(trailings) == 0 { + return "", 0, errors.New("no trailing path parameter found") + } + m = trailings[0] + } else { + // validMacros := p.macros.LookupForGoType(goType) + + // instead of mapping with a reflect.Kind which has its limitation, + // we map the param types with a go type as a string, + // so custom structs such as "user" can be mapped to a macro with indent || alias == "user". + m = p.macros.Get(strings.ToLower(goType.String())) + + if m == nil { + if typ.NumIn() > funcArgPos { + // has more input arguments but we are not in the correct + // index now, maybe the first argument was an `iris/context.Context` + // so retry with the "funcArgPos" incremented. + // + // the "funcArgPos" will be updated to the caller as well + // because we return it among the path and the error. + return p.parsePathParam(path, w, funcArgPos+1) + } + + return "", 0, fmt.Errorf("invalid syntax: the standard go type: %s found in controller's function: %s at position: %d does not match any valid macro", goType, p.fn.Name, funcArgPos) } - return "", 0, errors.New("invalid syntax for " + p.fn.Name) } // /{argfirst:path}, /{argfirst:long}... - path += fmt.Sprintf("/{%s:%s}", paramKey, paramType.String()) + path += fmt.Sprintf("/{%s:%s}", paramKey, m.Indent()) if nextWord == "" && typ.NumIn() > funcArgPos+1 { // By is the latest word but func is expected diff --git a/mvc/param.go b/mvc/param.go index c2f22990..a81d191c 100644 --- a/mvc/param.go +++ b/mvc/param.go @@ -5,7 +5,6 @@ import ( "github.com/kataras/iris/context" "github.com/kataras/iris/core/router/macro" - "github.com/kataras/iris/core/router/macro/interpreter/ast" ) func getPathParamsForInput(params []macro.TemplateParam, funcIn ...reflect.Type) (values []reflect.Value) { @@ -13,61 +12,40 @@ func getPathParamsForInput(params []macro.TemplateParam, funcIn ...reflect.Type) return } - consumedParams := make(map[int]bool, 0) - for _, in := range funcIn { - for j, p := range params { - if _, consumed := consumedParams[j]; consumed { - continue - } - paramType := p.Type - paramName := p.Name - // fmt.Printf("%s input arg type vs %s param type\n", in.Kind().String(), p.Type.Kind().String()) - if paramType.Assignable(in.Kind()) { - consumedParams[j] = true - // fmt.Printf("param.go: bind path param func for paramName = '%s' and paramType = '%s'\n", paramName, paramType.String()) - values = append(values, makeFuncParamGetter(paramType, paramName)) - } + // consumedParams := make(map[int]bool, 0) + // for _, in := range funcIn { + // for j, p := range params { + // if _, consumed := consumedParams[j]; consumed { + // continue + // } + + // // fmt.Printf("%s input arg type vs %s param type\n", in.Kind().String(), p.Type.Kind().String()) + // if m := macros.Lookup(p.Type); m != nil && m.GoType == in.Kind() { + // consumedParams[j] = true + // // fmt.Printf("param.go: bind path param func for paramName = '%s' and paramType = '%s'\n", paramName, paramType.String()) + // funcDep, ok := context.ParamResolverByKindAndIndex(m.GoType, p.Index) + // // funcDep, ok := context.ParamResolverByKindAndKey(in.Kind(), paramName) + // if !ok { + // // here we can add a logger about invalid parameter type although it should never happen here + // // unless the end-developer modified the macro/macros with a special type but not the context/ParamResolvers. + // continue + // } + // values = append(values, funcDep) + // } + // } + // } + + for i, param := range params { + if len(funcIn) <= i { + return } + funcDep, ok := context.ParamResolverByKindAndIndex(funcIn[i].Kind(), param.Index) + if !ok { + continue + } + + values = append(values, funcDep) } return } - -func makeFuncParamGetter(paramType ast.ParamType, paramName string) reflect.Value { - var fn interface{} - - switch paramType { - case ast.ParamTypeNumber: - fn = func(ctx context.Context) int { - v, _ := ctx.Params().GetInt(paramName) - return v - } - case ast.ParamTypeInt64: - fn = func(ctx context.Context) int64 { - v, _ := ctx.Params().GetInt64(paramName) - return v - } - case ast.ParamTypeUint8: - fn = func(ctx context.Context) uint8 { - v, _ := ctx.Params().GetUint8(paramName) - return v - } - case ast.ParamTypeUint64: - fn = func(ctx context.Context) uint64 { - v, _ := ctx.Params().GetUint64(paramName) - return v - } - case ast.ParamTypeBoolean: - fn = func(ctx context.Context) bool { - v, _ := ctx.Params().GetBool(paramName) - return v - } - default: - // string, path... - fn = func(ctx context.Context) string { - return ctx.Params().Get(paramName) - } - } - - return reflect.ValueOf(fn) -}