package macro import ( "fmt" "reflect" "regexp" "unicode" "github.com/kataras/iris/core/router/macro/interpreter/ast" ) // EvaluatorFunc is the signature for both param types and param funcs. // It should accepts the param's value as string // and return true if validated otherwise false. type EvaluatorFunc func(paramValue string) bool // NewEvaluatorFromRegexp accepts a regexp "expr" expression // and returns an EvaluatorFunc based on that regexp. // the regexp is compiled before return. // // Returns a not-nil error on regexp compile failure. func NewEvaluatorFromRegexp(expr string) (EvaluatorFunc, error) { if expr == "" { return nil, fmt.Errorf("empty regex expression") } // add the last $ if missing (and not wildcard(?)) if i := expr[len(expr)-1]; i != '$' && i != '*' { expr += "$" } r, err := regexp.Compile(expr) if err != nil { return nil, err } return r.MatchString, nil } // MustNewEvaluatorFromRegexp same as NewEvaluatorFromRegexp // but it panics on the "expr" parse failure. func MustNewEvaluatorFromRegexp(expr string) EvaluatorFunc { r, err := NewEvaluatorFromRegexp(expr) if err != nil { panic(err) } return r } var ( goodParamFuncReturnType = reflect.TypeOf(func(string) bool { return false }) goodParamFuncReturnType2 = reflect.TypeOf(EvaluatorFunc(func(string) bool { return false })) ) func goodParamFunc(typ reflect.Type) bool { // should be a func // which returns a func(string) bool if typ.Kind() == reflect.Func { if typ.NumOut() == 1 { typOut := typ.Out(0) if typOut == goodParamFuncReturnType || typOut == goodParamFuncReturnType2 { return true } } } return false } // goodParamFuncName reports whether the function name is a valid identifier. func goodParamFuncName(name string) bool { if name == "" { return false } // valid names are only letters and _ for _, r := range name { switch { case r == '_': case !unicode.IsLetter(r): return false } } return true } // the convertBuilderFunc return value is generating at boot time. // convertFunc converts an interface to a valid full param function. func convertBuilderFunc(fn interface{}) ParamEvaluatorBuilder { typFn := reflect.TypeOf(fn) if !goodParamFunc(typFn) { return nil } numFields := typFn.NumIn() return func(args []ast.ParamFuncArg) EvaluatorFunc { if len(args) != numFields { // no variadics support, for now. panic("args should be the same len as numFields") } var argValues []reflect.Value for i := 0; i < numFields; i++ { field := typFn.In(i) arg := args[i] if field.Kind() != reflect.TypeOf(arg).Kind() { panic("fields should have the same type") } argValues = append(argValues, reflect.ValueOf(arg)) } evalFn := reflect.ValueOf(fn).Call(argValues)[0].Interface() var evaluator EvaluatorFunc // check for typed and not typed if _v, ok := evalFn.(EvaluatorFunc); ok { evaluator = _v } else if _v, ok = evalFn.(func(string) bool); ok { evaluator = _v } return func(paramValue string) bool { return evaluator(paramValue) } } } type ( // Macro represents the parsed macro, // which holds // the evaluator (param type's evaluator + param functions evaluators) // and its param functions. // // Any type contains its own macro // instance, so an String type // contains its type evaluator // which is the "Evaluator" field // and it can register param functions // to that macro which maps to a parameter type. Macro struct { Evaluator EvaluatorFunc funcs []ParamFunc } // ParamEvaluatorBuilder is a func // which accepts a param function's arguments (values) // and returns an EvaluatorFunc, its job // is to make the macros to be registered // by user at the most generic possible way. ParamEvaluatorBuilder func([]ast.ParamFuncArg) EvaluatorFunc // ParamFunc represents the parsed // parameter function, it holds // the parameter's name // and the function which will build // the evaluator func. ParamFunc struct { Name string Func ParamEvaluatorBuilder } ) func newMacro(evaluator EvaluatorFunc) *Macro { return &Macro{Evaluator: evaluator} } // RegisterFunc registers a parameter function // to that macro. // Accepts the func name ("range") // and the function body, which should return an EvaluatorFunc // a bool (it will be converted to EvaluatorFunc later on), // i.e RegisterFunc("min", func(minValue int) func(paramValue string) bool){}) func (m *Macro) RegisterFunc(funcName string, fn interface{}) { fullFn := convertBuilderFunc(fn) m.registerFunc(funcName, fullFn) } func (m *Macro) registerFunc(funcName string, fullFn ParamEvaluatorBuilder) { if !goodParamFuncName(funcName) { return } for _, fn := range m.funcs { if fn.Name == funcName { fn.Func = fullFn return } } m.funcs = append(m.funcs, ParamFunc{ Name: funcName, Func: fullFn, }) } func (m *Macro) getFunc(funcName string) ParamEvaluatorBuilder { for _, fn := range m.funcs { if fn.Name == funcName { if fn.Func == nil { continue } return fn.Func } } return nil } // Map contains the default macros mapped to their types. // This is the manager which is used by the caller to register custom // parameter functions per param-type (String, Int, Alphabetical, File, Path). type Map struct { // string type // anything String *Macro // int type // only numbers (0-9) Int *Macro // alphabetical/letter type // letters only (upper or lowercase) Alphabetical *Macro // file type // letters (upper or lowercase) // numbers (0-9) // underscore (_) // dash (-) // point (.) // no spaces! or other character File *Macro // path type // anything, should be the last part Path *Macro } // NewMap returns a new macro Map with default // type evaluators. // // Learn more at: https://github.com/kataras/iris/tree/master/_examples/routing/dynamic-path func NewMap() *Map { return &Map{ // it allows everything, so no need for a regexp here. String: newMacro(func(string) bool { return true }), Int: newMacro(MustNewEvaluatorFromRegexp("^[0-9]+$")), Alphabetical: newMacro(MustNewEvaluatorFromRegexp("^[a-zA-Z ]+$")), File: newMacro(MustNewEvaluatorFromRegexp("^[a-zA-Z0-9_.-]*$")), // it allows everything, we have String and Path as different // types because I want to give the opportunity to the user // to organise the macro functions based on wildcard or single dynamic named path parameter. // Should be the last. Path: newMacro(func(string) bool { return true }), } } // Lookup returns the specific Macro from the map // based on the parameter type. // i.e if ast.ParamTypeInt then it will return the m.Int. // Returns the m.String if not matched. func (m *Map) Lookup(typ ast.ParamType) *Macro { switch typ { case ast.ParamTypeInt: return m.Int case ast.ParamTypeAlphabetical: return m.Alphabetical case ast.ParamTypeFile: return m.File case ast.ParamTypePath: return m.Path default: return m.String } }