mirror of
https://github.com/kataras/iris.git
synced 2025-01-23 18:51:03 +01:00
fb85ae15d5
Former-commit-id: 367bb53ed6656002c60c40e3ad30bda578de21c6
276 lines
7.0 KiB
Go
276 lines
7.0 KiB
Go
// Copyright 2017 Gerasimos Maropoulos, ΓΜ. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
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/beginner/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
|
|
}
|
|
}
|