package httptreemux import ( "fmt" "net/url" "strings" ) type Group struct { path string mux *TreeMux } // Add a sub-group to this group func (g *Group) NewGroup(path string) *Group { if len(path) < 1 { panic("Group path must not be empty") } checkPath(path) path = g.path + path //Don't want trailing slash as all sub-paths start with slash if path[len(path)-1] == '/' { path = path[:len(path)-1] } return &Group{path, g.mux} } // Path elements starting with : indicate a wildcard in the path. A wildcard will only match on a // single path segment. That is, the pattern `/post/:postid` will match on `/post/1` or `/post/1/`, // but not `/post/1/2`. // // A path element starting with * is a catch-all, whose value will be a string containing all text // in the URL matched by the wildcards. For example, with a pattern of `/images/*path` and a // requested URL `images/abc/def`, path would contain `abc/def`. // // # Routing Rule Priority // // The priority rules in the router are simple. // // 1. Static path segments take the highest priority. If a segment and its subtree are able to match the URL, that match is returned. // // 2. Wildcards take second priority. For a particular wildcard to match, that wildcard and its subtree must match the URL. // // 3. Finally, a catch-all rule will match when the earlier path segments have matched, and none of the static or wildcard conditions have matched. Catch-all rules must be at the end of a pattern. // // So with the following patterns, we'll see certain matches: // router = httptreemux.New() // router.GET("/:page", pageHandler) // router.GET("/:year/:month/:post", postHandler) // router.GET("/:year/:month", archiveHandler) // router.GET("/images/*path", staticHandler) // router.GET("/favicon.ico", staticHandler) // // /abc will match /:page // /2014/05 will match /:year/:month // /2014/05/really-great-blog-post will match /:year/:month/:post // /images/CoolImage.gif will match /images/*path // /images/2014/05/MayImage.jpg will also match /images/*path, with all the text after /images stored in the variable path. // /favicon.ico will match /favicon.ico // // # Trailing Slashes // // The router has special handling for paths with trailing slashes. If a pattern is added to the // router with a trailing slash, any matches on that pattern without a trailing slash will be // redirected to the version with the slash. If a pattern does not have a trailing slash, matches on // that pattern with a trailing slash will be redirected to the version without. // // The trailing slash flag is only stored once for a pattern. That is, if a pattern is added for a // method with a trailing slash, all other methods for that pattern will also be considered to have a // trailing slash, regardless of whether or not it is specified for those methods too. // // This behavior can be turned off by setting TreeMux.RedirectTrailingSlash to false. By // default it is set to true. The specifics of the redirect depend on RedirectBehavior. // // One exception to this rule is catch-all patterns. By default, trailing slash redirection is // disabled on catch-all patterns, since the structure of the entire URL and the desired patterns // can not be predicted. If trailing slash removal is desired on catch-all patterns, set // TreeMux.RemoveCatchAllTrailingSlash to true. // // router = httptreemux.New() // router.GET("/about", pageHandler) // router.GET("/posts/", postIndexHandler) // router.POST("/posts", postFormHandler) // // GET /about will match normally. // GET /about/ will redirect to /about. // GET /posts will redirect to /posts/. // GET /posts/ will match normally. // POST /posts will redirect to /posts/, because the GET method used a trailing slash. func (g *Group) Handle(method string, path string, handler HandlerFunc) { g.mux.mutex.Lock() defer g.mux.mutex.Unlock() addSlash := false addOne := func(thePath string) { node := g.mux.root.addPath(thePath[1:], nil, false) if addSlash { node.addSlash = true } node.setHandler(method, handler, false) if g.mux.HeadCanUseGet && method == "GET" && node.leafHandler["HEAD"] == nil { node.setHandler("HEAD", handler, true) } } checkPath(path) path = g.path + path if len(path) == 0 { panic("Cannot map an empty path") } if len(path) > 1 && path[len(path)-1] == '/' && g.mux.RedirectTrailingSlash { addSlash = true path = path[:len(path)-1] } if g.mux.EscapeAddedRoutes { u, err := url.ParseRequestURI(path) if err != nil { panic("URL parsing error " + err.Error() + " on url " + path) } escapedPath := unescapeSpecial(u.String()) if escapedPath != path { addOne(escapedPath) } } addOne(path) } // Syntactic sugar for Handle("GET", path, handler) func (g *Group) GET(path string, handler HandlerFunc) { g.Handle("GET", path, handler) } // Syntactic sugar for Handle("POST", path, handler) func (g *Group) POST(path string, handler HandlerFunc) { g.Handle("POST", path, handler) } // Syntactic sugar for Handle("PUT", path, handler) func (g *Group) PUT(path string, handler HandlerFunc) { g.Handle("PUT", path, handler) } // Syntactic sugar for Handle("DELETE", path, handler) func (g *Group) DELETE(path string, handler HandlerFunc) { g.Handle("DELETE", path, handler) } // Syntactic sugar for Handle("PATCH", path, handler) func (g *Group) PATCH(path string, handler HandlerFunc) { g.Handle("PATCH", path, handler) } // Syntactic sugar for Handle("HEAD", path, handler) func (g *Group) HEAD(path string, handler HandlerFunc) { g.Handle("HEAD", path, handler) } // Syntactic sugar for Handle("OPTIONS", path, handler) func (g *Group) OPTIONS(path string, handler HandlerFunc) { g.Handle("OPTIONS", path, handler) } func checkPath(path string) { // All non-empty paths must start with a slash if len(path) > 0 && path[0] != '/' { panic(fmt.Sprintf("Path %s must start with slash", path)) } } func unescapeSpecial(s string) string { // Look for sequences of \*, *, and \: that were escaped, and undo some of that escaping. // Unescape /* since it references a wildcard token. s = strings.Replace(s, "/%2A", "/*", -1) // Unescape /\: since it references a literal colon s = strings.Replace(s, "/%5C:", "/\\:", -1) // Replace escaped /\\: with /\: s = strings.Replace(s, "/%5C%5C:", "/%5C:", -1) // Replace escaped /\* with /* s = strings.Replace(s, "/%5C%2A", "/%2A", -1) // Replace escaped /\\* with /\* s = strings.Replace(s, "/%5C%5C%2A", "/%5C%2A", -1) return s }