tango/router.go

404 lines
8.6 KiB
Go

package tango
import (
"bytes"
"fmt"
"net/http"
"net/url"
"reflect"
"regexp"
"strings"
)
type RouteType int
const (
FuncRoute RouteType = iota + 1 // func ()
FuncHttpRoute // func (http.ResponseWriter, *http.Request)
FuncReqRoute // func (*http.Request)
FuncResponseRoute // func (http.ResponseWriter)
FuncCtxRoute // func (*tango.Context)
StructRoute // func (st) <Get>()
StructPtrRoute // func (*struct) <Get>()
)
type PathType int
const (
StaticPath PathType = iota + 1
NamedPath
RegexpPath
)
var (
SupportMethods = []string{
"GET",
"POST",
"HEAD",
"DELETE",
"PUT",
"OPTIONS",
"TRACE",
"PATCH",
}
PoolSize = 800
)
// Route
type Route struct {
path string //path string
regexp *regexp.Regexp //path regexp
pathType PathType
method reflect.Value
routeType RouteType
pool *pool
}
var specialBytes = []byte(`\+*?()|[]{}^$`)
func pathType(s string) PathType {
for i := 0; i < len(s); i++ {
if s[i] == ':' {
return NamedPath
}
if bytes.IndexByte(specialBytes, s[i]) >= 0 {
return RegexpPath
}
}
return StaticPath
}
func NewRoute(r string, t reflect.Type,
method reflect.Value, tp RouteType) *Route {
var cr *regexp.Regexp
var err error
var pathType = pathType(r)
if pathType == RegexpPath {
cr, err = regexp.Compile(r)
if err != nil {
panic("wrong route:" + err.Error())
return nil
}
}
var pool *pool
if tp == StructRoute || tp == StructPtrRoute {
pool = newPool(PoolSize, t)
}
return &Route{
path: r,
regexp: cr,
pathType: pathType,
method: method,
routeType: tp,
pool: pool,
}
}
func (r *Route) Method() reflect.Value {
return r.method
}
func (r *Route) PathType() PathType {
return r.pathType
}
func (r *Route) RouteType() RouteType {
return r.routeType
}
func (r *Route) IsStruct() bool {
return r.routeType == StructRoute || r.routeType == StructPtrRoute
}
func (r *Route) newAction() reflect.Value {
if !r.IsStruct() {
return r.method
}
return r.pool.New()
}
func (r *Route) try(path string) (url.Values, bool) {
p := make(url.Values)
var i, j int
for i < len(path) {
switch {
case j >= len(r.path):
if r.path != "/" && len(r.path) > 0 && r.path[len(r.path)-1] == '/' {
return p, true
}
return nil, false
case r.path[j] == ':':
var name, val string
var nextc byte
name, nextc, j = match(r.path, isAlnum, j+1)
val, _, i = match(path, matchPart(nextc), i)
p.Add(":"+name, val)
case path[i] == r.path[j]:
i++
j++
default:
return nil, false
}
}
if j != len(r.path) {
return nil, false
}
return p, true
}
type Router interface {
Route(methods interface{}, path string, handler interface{})
Match(requestPath, method string) (*Route, url.Values)
}
type router struct {
routes map[string][]*Route
routesEq map[string]map[string]*Route
routesName map[string][]*Route
}
func NewRouter() Router {
routesEq := make(map[string]map[string]*Route)
for _, m := range SupportMethods {
routesEq[m] = make(map[string]*Route)
}
routesName := make(map[string][]*Route)
for _, m := range SupportMethods {
routesName[m] = make([]*Route, 0)
}
routes := make(map[string][]*Route)
for _, m := range SupportMethods {
routes[m] = make([]*Route, 0)
}
return &router{
routesEq: routesEq,
routes: routes,
routesName: routesName,
}
}
func matchPart(b byte) func(byte) bool {
return func(c byte) bool {
return c != b && c != '/'
}
}
func match(s string, f func(byte) bool, i int) (matched string, next byte, j int) {
j = i
for j < len(s) && f(s[j]) {
j++
}
if j < len(s) {
next = s[j]
}
return s[i:j], next, j
}
func isAlpha(ch byte) bool {
return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || ch == '_'
}
func isDigit(ch byte) bool {
return '0' <= ch && ch <= '9'
}
func isAlnum(ch byte) bool {
return isAlpha(ch) || isDigit(ch)
}
func tail(pat, path string) string {
var i, j int
for i < len(path) {
switch {
case j >= len(pat):
if pat[len(pat)-1] == '/' {
return path[i:]
}
return ""
case pat[j] == ':':
var nextc byte
_, nextc, j = match(pat, isAlnum, j+1)
_, _, i = match(path, matchPart(nextc), i)
case path[i] == pat[j]:
i++
j++
default:
return ""
}
}
return ""
}
func removeStick(uri string) string {
uri = strings.TrimRight(uri, "/")
if uri == "" {
uri = "/"
}
return uri
}
func (router *router) Route(ms interface{}, url string, c interface{}) {
vc := reflect.ValueOf(c)
if vc.Kind() == reflect.Func {
switch ms.(type) {
case string:
router.addFunc([]string{ms.(string)}, url, c)
case []string:
router.addFunc(ms.([]string), url, c)
default:
panic("unknow methods format")
}
} else if vc.Kind() == reflect.Ptr && vc.Elem().Kind() == reflect.Struct {
var methods = make(map[string]string)
switch ms.(type) {
case string:
s := strings.Split(ms.(string), ":")
if len(s) == 1 {
methods[s[0]] = strings.Title(strings.ToLower(s[0]))
} else if len(s) == 2 {
methods[s[0]] = strings.TrimSpace(s[1])
} else {
panic("unknow methods format")
}
case []string:
for _, m := range ms.([]string) {
s := strings.Split(m, ":")
if len(s) == 1 {
methods[s[0]] = strings.Title(strings.ToLower(s[0]))
} else if len(s) == 2 {
methods[s[0]] = strings.TrimSpace(s[1])
} else {
panic("unknow format")
}
}
case map[string]string:
methods = ms.(map[string]string)
default:
panic("unsupported methods")
}
router.addStruct(methods, url, c)
} else {
panic("not support route type")
}
}
func (router *router) addRoute(m string, route *Route) {
switch route.pathType {
case StaticPath:
router.routesEq[m][route.path] = route
case NamedPath:
router.routesName[m] = append(router.routesName[m], route)
case RegexpPath:
router.routes[m] = append(router.routes[m], route)
default:
panic("should not here")
}
}
/*
Tango supports 5 form funcs
func()
func(*Context)
func(http.ResponseWriter, *http.Request)
func(http.ResponseWriter)
func(*http.Request)
it can has or has not return value
*/
func (router *router) addFunc(methods []string, url string, c interface{}) {
vc := reflect.ValueOf(c)
t := vc.Type()
var r *Route
if t.NumIn() == 0 {
r = NewRoute(removeStick(url), t, vc, FuncRoute)
} else if t.NumIn() == 1 {
if t.In(0) == reflect.TypeOf(new(Context)) {
r = NewRoute(removeStick(url), t, vc, FuncCtxRoute)
} else if t.In(0) == reflect.TypeOf(new(http.Request)) {
r = NewRoute(removeStick(url), t, vc, FuncReqRoute)
} else if t.In(0).Kind() == reflect.Interface && t.In(0).Name() == "ResponseWriter" &&
t.In(0).PkgPath() == "net/http" {
r = NewRoute(removeStick(url), t, vc, FuncResponseRoute)
} else {
panic("no support function type")
}
} else if t.NumIn() == 2 &&
(t.In(0).Kind() == reflect.Interface && t.In(0).Name() == "ResponseWriter" &&
t.In(0).PkgPath() == "net/http") &&
t.In(1) == reflect.TypeOf(new(http.Request)) {
r = NewRoute(removeStick(url), t, vc, FuncHttpRoute)
} else {
panic("no support function type")
}
for _, m := range methods {
router.addRoute(m, r)
}
}
func (router *router) addStruct(methods map[string]string, url string, c interface{}) {
vc := reflect.ValueOf(c)
t := vc.Type().Elem()
// added a default method Get, Post
for name, method := range methods {
if m, ok := t.MethodByName(method); ok {
router.addRoute(name, NewRoute(removeStick(url), t, m.Func, StructPtrRoute))
} else if m, ok := vc.Type().MethodByName(method); ok {
router.addRoute(name, NewRoute(removeStick(url), t, m.Func, StructRoute))
}
}
}
// when a request ask, then match the correct route
func (router *router) Match(reqPath, allowMethod string) (*Route, url.Values) {
// for non-regular path, search the map
if routes, ok := router.routesEq[allowMethod]; ok {
if route, ok := routes[reqPath]; ok {
return route, make(url.Values)
}
}
// name match
routes := router.routesName[allowMethod]
for _, r := range routes {
if args, ok := r.try(reqPath); ok {
return r, args
}
}
// regex match
routes = router.routes[allowMethod]
for _, r := range routes {
if !r.regexp.MatchString(reqPath) {
continue
}
match := r.regexp.FindStringSubmatch(reqPath)
if len(match[0]) != len(reqPath) {
continue
}
var args = make(url.Values)
// for regexp :0 -> first match param :1 -> the second
for i, arg := range match[1:] {
if name := r.regexp.SubexpNames()[i+1]; len(name) > 0 {
args.Add(":"+name, arg)
}
args.Add(fmt.Sprintf(":%d", i), arg)
}
return r, args
}
return nil, nil
}