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
This commit is contained in:
Gerasimos (Makis) Maropoulos 2018-09-26 11:37:11 +03:00
parent 52a07df0f4
commit dc3c38b189
26 changed files with 1070 additions and 1036 deletions

View File

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

View File

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

View File

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

159
context/request_params.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

375
core/router/macro/macros.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -101,6 +101,11 @@ func MakeReturnValue(fn reflect.Value, goodFunc TypeChecker) (func([]reflect.Val
if !v.IsValid() {
return zeroOutVal
}
// if v.String() == "<interface {} Value>" {
// 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
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,40 +96,17 @@ 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
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
}
@ -136,6 +114,7 @@ func parseMethod(fn reflect.Method, skipper func(string) bool) (method, path str
p := &methodParser{
fn: fn,
lexer: newMethodLexer(fn.Name),
macros: macros,
}
return p.parse()
}
@ -211,20 +190,29 @@ 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
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`
@ -234,11 +222,13 @@ func (p *methodParser) parsePathParam(path string, w string, funcArgPos int) (st
// because we return it among the path and the error.
return p.parsePathParam(path, w, funcArgPos+1)
}
return "", 0, errors.New("invalid syntax for " + p.fn.Name)
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)
}
}
// /{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

View File

@ -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 {
// 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
}
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))
}
}
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)
}