package methodfunc import ( "errors" "fmt" "reflect" "strings" "github.com/kataras/iris/context" ) var posWords = map[int]string{ 0: "", 1: "first", 2: "second", 3: "third", 4: "forth", 5: "five", 6: "sixth", 7: "seventh", 8: "eighth", 9: "ninth", } func genParamKey(argIdx int) string { return "param" + posWords[argIdx] // paramfirst, paramsecond... } const ( paramTypeInt = "int" paramTypeLong = "long" paramTypeBoolean = "boolean" paramTypeString = "string" paramTypePath = "path" ) var macroTypes = map[string]string{ "int": paramTypeInt, "int64": paramTypeLong, "bool": paramTypeBoolean, "string": paramTypeString, // there is "path" param type but it's being captured "on-air" // "file" param type is not supported by the current implementation, yet // but if someone ask for it I'll implement it, it's easy. } type funcParser struct { info FuncInfo lexer *lexer } func newFuncParser(info FuncInfo) *funcParser { return &funcParser{ info: info, lexer: newLexer(info.Trailing), } } func (p *funcParser) parse() (*ast, error) { a := new(ast) funcArgPos := 0 for { w := p.lexer.next() if w == "" { break } if w == tokenBy { funcArgPos++ // starting with 1 because in typ.NumIn() the first is the struct receiver. // No need for these: // ByBy will act like /{param:type}/{param:type} as users expected // if func input arguments are there, else act By like normal path /by. // // if p.lexer.peekPrev() == tokenBy || typ.NumIn() == 1 { // ByBy, then act this second By like a path // a.relPath += "/" + strings.ToLower(w) // continue // } if err := p.parsePathParam(a, w, funcArgPos); err != nil { return nil, err } continue } a.relPath += "/" + strings.ToLower(w) } // This fixes a problem when developer misses to append the keyword `By` // to the method function when input arguments are declared (for path parameters binding). // We could just use it with `By` keyword but this is not a good practise // because what happens if we will become naive and declare something like // Get(id int) and GetBy(username string) or GetBy(id int) ? it's not working because of duplication of the path. // Docs are clear about that but we are humans, they may do a mistake by accident but // framework will not allow that. // So the best thing we can do to help prevent those errors is by printing that message // below to the developer. // Note: it should be at the end of the words loop because a.dynamic may be true later on. if numIn := p.info.Type.NumIn(); numIn > 1 && !a.dynamic { return nil, fmt.Errorf("found %d input arguments but keyword 'By' is missing from '%s'", // -1 because end-developer wants to know the actual input arguments, without the struct holder. numIn-1, p.info.Name) } return a, nil } func (p *funcParser) parsePathParam(a *ast, w string, funcArgPos int) error { typ := p.info.Type if typ.NumIn() <= funcArgPos { // old: // return nil, errors.New("keyword 'By' found but length of input receivers are not match for " + // p.info.Name) // By found but input arguments are not there, so act like /by path without restricts. a.relPath += "/" + strings.ToLower(w) return nil } var ( paramKey = genParamKey(funcArgPos) // paramfirst, paramsecond... paramType = paramTypeString // default string ) // string, int... goType := typ.In(funcArgPos).Name() nextWord := p.lexer.peekNext() if nextWord == tokenWildcard { p.lexer.skip() // skip the Wildcard word. paramType = paramTypePath } else if pType, ok := macroTypes[goType]; ok { // it's not wildcard, so check base on our available macro types. paramType = pType } else { return errors.New("invalid syntax for " + p.info.Name) } a.paramKeys = append(a.paramKeys, paramKey) a.paramTypes = append(a.paramTypes, paramType) // /{paramfirst:path}, /{paramfirst:long}... a.relPath += fmt.Sprintf("/{%s:%s}", paramKey, paramType) a.dynamic = true if nextWord == "" && typ.NumIn() > funcArgPos+1 { // By is the latest word but func is expected // more path parameters values, i.e: // GetBy(name string, age int) // The caller (parse) doesn't need to know // about the incremental funcArgPos because // it will not need it. return p.parsePathParam(a, nextWord, funcArgPos+1) } return nil } type ast struct { paramKeys []string // paramfirst, paramsecond... [0] paramTypes []string // string, int, long, path... [0] relPath string dynamic bool // when paramKeys (and paramTypes, are equal) > 0 } // moved to func_caller#buildMethodcall, it's bigger and with repeated code // than this, below function but it's faster. // func (a *ast) MethodCall(ctx context.Context, f reflect.Value) { // if a.dynamic { // f.Call(a.paramValues(ctx)) // return // } // // f.Interface().(func())() // } func (a *ast) paramValues(ctx context.Context) []reflect.Value { if !a.dynamic { return nil } l := len(a.paramKeys) values := make([]reflect.Value, l, l) for i := 0; i < l; i++ { paramKey := a.paramKeys[i] paramType := a.paramTypes[i] values[i] = getParamValueFromType(ctx, paramType, paramKey) } return values } func getParamValueFromType(ctx context.Context, paramType string, paramKey string) reflect.Value { if paramType == paramTypeInt { v, _ := ctx.Params().GetInt(paramKey) return reflect.ValueOf(v) } if paramType == paramTypeLong { v, _ := ctx.Params().GetInt64(paramKey) return reflect.ValueOf(v) } if paramType == paramTypeBoolean { v, _ := ctx.Params().GetBool(paramKey) return reflect.ValueOf(v) } // string, path... return reflect.ValueOf(ctx.Params().Get(paramKey)) }