|
- // Copyright 2012 The Gorilla Authors. All rights reserved.
- // Use of this source code is governed by a BSD-style
- // license that can be found in the LICENSE file.
-
- package mux
-
- import (
- "errors"
- "fmt"
- "net/http"
- "net/url"
- "regexp"
- "strings"
- )
-
- // Route stores information to match a request and build URLs.
- type Route struct {
- // Parent where the route was registered (a Router).
- parent parentRoute
- // Request handler for the route.
- handler http.Handler
- // List of matchers.
- matchers []matcher
- // Manager for the variables from host and path.
- regexp *routeRegexpGroup
- // If true, when the path pattern is "/path/", accessing "/path" will
- // redirect to the former and vice versa.
- strictSlash bool
- // If true, when the path pattern is "/path//to", accessing "/path//to"
- // will not redirect
- skipClean bool
- // If true, "/path/foo%2Fbar/to" will match the path "/path/{var}/to"
- useEncodedPath bool
- // The scheme used when building URLs.
- buildScheme string
- // If true, this route never matches: it is only used to build URLs.
- buildOnly bool
- // The name used to build URLs.
- name string
- // Error resulted from building a route.
- err error
-
- buildVarsFunc BuildVarsFunc
- }
-
- // SkipClean reports whether path cleaning is enabled for this route via
- // Router.SkipClean.
- func (r *Route) SkipClean() bool {
- return r.skipClean
- }
-
- // Match matches the route against the request.
- func (r *Route) Match(req *http.Request, match *RouteMatch) bool {
- if r.buildOnly || r.err != nil {
- return false
- }
-
- var matchErr error
-
- // Match everything.
- for _, m := range r.matchers {
- if matched := m.Match(req, match); !matched {
- if _, ok := m.(methodMatcher); ok {
- matchErr = ErrMethodMismatch
- continue
- }
- matchErr = nil
- return false
- }
- }
-
- if matchErr != nil {
- match.MatchErr = matchErr
- return false
- }
-
- if match.MatchErr == ErrMethodMismatch {
- // We found a route which matches request method, clear MatchErr
- match.MatchErr = nil
- // Then override the mis-matched handler
- match.Handler = r.handler
- }
-
- // Yay, we have a match. Let's collect some info about it.
- if match.Route == nil {
- match.Route = r
- }
- if match.Handler == nil {
- match.Handler = r.handler
- }
- if match.Vars == nil {
- match.Vars = make(map[string]string)
- }
-
- // Set variables.
- if r.regexp != nil {
- r.regexp.setMatch(req, match, r)
- }
- return true
- }
-
- // ----------------------------------------------------------------------------
- // Route attributes
- // ----------------------------------------------------------------------------
-
- // GetError returns an error resulted from building the route, if any.
- func (r *Route) GetError() error {
- return r.err
- }
-
- // BuildOnly sets the route to never match: it is only used to build URLs.
- func (r *Route) BuildOnly() *Route {
- r.buildOnly = true
- return r
- }
-
- // Handler --------------------------------------------------------------------
-
- // Handler sets a handler for the route.
- func (r *Route) Handler(handler http.Handler) *Route {
- if r.err == nil {
- r.handler = handler
- }
- return r
- }
-
- // HandlerFunc sets a handler function for the route.
- func (r *Route) HandlerFunc(f func(http.ResponseWriter, *http.Request)) *Route {
- return r.Handler(http.HandlerFunc(f))
- }
-
- // GetHandler returns the handler for the route, if any.
- func (r *Route) GetHandler() http.Handler {
- return r.handler
- }
-
- // Name -----------------------------------------------------------------------
-
- // Name sets the name for the route, used to build URLs.
- // If the name was registered already it will be overwritten.
- func (r *Route) Name(name string) *Route {
- if r.name != "" {
- r.err = fmt.Errorf("mux: route already has name %q, can't set %q",
- r.name, name)
- }
- if r.err == nil {
- r.name = name
- r.getNamedRoutes()[name] = r
- }
- return r
- }
-
- // GetName returns the name for the route, if any.
- func (r *Route) GetName() string {
- return r.name
- }
-
- // ----------------------------------------------------------------------------
- // Matchers
- // ----------------------------------------------------------------------------
-
- // matcher types try to match a request.
- type matcher interface {
- Match(*http.Request, *RouteMatch) bool
- }
-
- // addMatcher adds a matcher to the route.
- func (r *Route) addMatcher(m matcher) *Route {
- if r.err == nil {
- r.matchers = append(r.matchers, m)
- }
- return r
- }
-
- // addRegexpMatcher adds a host or path matcher and builder to a route.
- func (r *Route) addRegexpMatcher(tpl string, typ regexpType) error {
- if r.err != nil {
- return r.err
- }
- r.regexp = r.getRegexpGroup()
- if typ == regexpTypePath || typ == regexpTypePrefix {
- if len(tpl) > 0 && tpl[0] != '/' {
- return fmt.Errorf("mux: path must start with a slash, got %q", tpl)
- }
- if r.regexp.path != nil {
- tpl = strings.TrimRight(r.regexp.path.template, "/") + tpl
- }
- }
- rr, err := newRouteRegexp(tpl, typ, routeRegexpOptions{
- strictSlash: r.strictSlash,
- useEncodedPath: r.useEncodedPath,
- })
- if err != nil {
- return err
- }
- for _, q := range r.regexp.queries {
- if err = uniqueVars(rr.varsN, q.varsN); err != nil {
- return err
- }
- }
- if typ == regexpTypeHost {
- if r.regexp.path != nil {
- if err = uniqueVars(rr.varsN, r.regexp.path.varsN); err != nil {
- return err
- }
- }
- r.regexp.host = rr
- } else {
- if r.regexp.host != nil {
- if err = uniqueVars(rr.varsN, r.regexp.host.varsN); err != nil {
- return err
- }
- }
- if typ == regexpTypeQuery {
- r.regexp.queries = append(r.regexp.queries, rr)
- } else {
- r.regexp.path = rr
- }
- }
- r.addMatcher(rr)
- return nil
- }
-
- // Headers --------------------------------------------------------------------
-
- // headerMatcher matches the request against header values.
- type headerMatcher map[string]string
-
- func (m headerMatcher) Match(r *http.Request, match *RouteMatch) bool {
- return matchMapWithString(m, r.Header, true)
- }
-
- // Headers adds a matcher for request header values.
- // It accepts a sequence of key/value pairs to be matched. For example:
- //
- // r := mux.NewRouter()
- // r.Headers("Content-Type", "application/json",
- // "X-Requested-With", "XMLHttpRequest")
- //
- // The above route will only match if both request header values match.
- // If the value is an empty string, it will match any value if the key is set.
- func (r *Route) Headers(pairs ...string) *Route {
- if r.err == nil {
- var headers map[string]string
- headers, r.err = mapFromPairsToString(pairs...)
- return r.addMatcher(headerMatcher(headers))
- }
- return r
- }
-
- // headerRegexMatcher matches the request against the route given a regex for the header
- type headerRegexMatcher map[string]*regexp.Regexp
-
- func (m headerRegexMatcher) Match(r *http.Request, match *RouteMatch) bool {
- return matchMapWithRegex(m, r.Header, true)
- }
-
- // HeadersRegexp accepts a sequence of key/value pairs, where the value has regex
- // support. For example:
- //
- // r := mux.NewRouter()
- // r.HeadersRegexp("Content-Type", "application/(text|json)",
- // "X-Requested-With", "XMLHttpRequest")
- //
- // The above route will only match if both the request header matches both regular expressions.
- // If the value is an empty string, it will match any value if the key is set.
- // Use the start and end of string anchors (^ and $) to match an exact value.
- func (r *Route) HeadersRegexp(pairs ...string) *Route {
- if r.err == nil {
- var headers map[string]*regexp.Regexp
- headers, r.err = mapFromPairsToRegex(pairs...)
- return r.addMatcher(headerRegexMatcher(headers))
- }
- return r
- }
-
- // Host -----------------------------------------------------------------------
-
- // Host adds a matcher for the URL host.
- // It accepts a template with zero or more URL variables enclosed by {}.
- // Variables can define an optional regexp pattern to be matched:
- //
- // - {name} matches anything until the next dot.
- //
- // - {name:pattern} matches the given regexp pattern.
- //
- // For example:
- //
- // r := mux.NewRouter()
- // r.Host("www.example.com")
- // r.Host("{subdomain}.domain.com")
- // r.Host("{subdomain:[a-z]+}.domain.com")
- //
- // Variable names must be unique in a given route. They can be retrieved
- // calling mux.Vars(request).
- func (r *Route) Host(tpl string) *Route {
- r.err = r.addRegexpMatcher(tpl, regexpTypeHost)
- return r
- }
-
- // MatcherFunc ----------------------------------------------------------------
-
- // MatcherFunc is the function signature used by custom matchers.
- type MatcherFunc func(*http.Request, *RouteMatch) bool
-
- // Match returns the match for a given request.
- func (m MatcherFunc) Match(r *http.Request, match *RouteMatch) bool {
- return m(r, match)
- }
-
- // MatcherFunc adds a custom function to be used as request matcher.
- func (r *Route) MatcherFunc(f MatcherFunc) *Route {
- return r.addMatcher(f)
- }
-
- // Methods --------------------------------------------------------------------
-
- // methodMatcher matches the request against HTTP methods.
- type methodMatcher []string
-
- func (m methodMatcher) Match(r *http.Request, match *RouteMatch) bool {
- return matchInArray(m, r.Method)
- }
-
- // Methods adds a matcher for HTTP methods.
- // It accepts a sequence of one or more methods to be matched, e.g.:
- // "GET", "POST", "PUT".
- func (r *Route) Methods(methods ...string) *Route {
- for k, v := range methods {
- methods[k] = strings.ToUpper(v)
- }
- return r.addMatcher(methodMatcher(methods))
- }
-
- // Path -----------------------------------------------------------------------
-
- // Path adds a matcher for the URL path.
- // It accepts a template with zero or more URL variables enclosed by {}. The
- // template must start with a "/".
- // Variables can define an optional regexp pattern to be matched:
- //
- // - {name} matches anything until the next slash.
- //
- // - {name:pattern} matches the given regexp pattern.
- //
- // For example:
- //
- // r := mux.NewRouter()
- // r.Path("/products/").Handler(ProductsHandler)
- // r.Path("/products/{key}").Handler(ProductsHandler)
- // r.Path("/articles/{category}/{id:[0-9]+}").
- // Handler(ArticleHandler)
- //
- // Variable names must be unique in a given route. They can be retrieved
- // calling mux.Vars(request).
- func (r *Route) Path(tpl string) *Route {
- r.err = r.addRegexpMatcher(tpl, regexpTypePath)
- return r
- }
-
- // PathPrefix -----------------------------------------------------------------
-
- // PathPrefix adds a matcher for the URL path prefix. This matches if the given
- // template is a prefix of the full URL path. See Route.Path() for details on
- // the tpl argument.
- //
- // Note that it does not treat slashes specially ("/foobar/" will be matched by
- // the prefix "/foo") so you may want to use a trailing slash here.
- //
- // Also note that the setting of Router.StrictSlash() has no effect on routes
- // with a PathPrefix matcher.
- func (r *Route) PathPrefix(tpl string) *Route {
- r.err = r.addRegexpMatcher(tpl, regexpTypePrefix)
- return r
- }
-
- // Query ----------------------------------------------------------------------
-
- // Queries adds a matcher for URL query values.
- // It accepts a sequence of key/value pairs. Values may define variables.
- // For example:
- //
- // r := mux.NewRouter()
- // r.Queries("foo", "bar", "id", "{id:[0-9]+}")
- //
- // The above route will only match if the URL contains the defined queries
- // values, e.g.: ?foo=bar&id=42.
- //
- // It the value is an empty string, it will match any value if the key is set.
- //
- // Variables can define an optional regexp pattern to be matched:
- //
- // - {name} matches anything until the next slash.
- //
- // - {name:pattern} matches the given regexp pattern.
- func (r *Route) Queries(pairs ...string) *Route {
- length := len(pairs)
- if length%2 != 0 {
- r.err = fmt.Errorf(
- "mux: number of parameters must be multiple of 2, got %v", pairs)
- return nil
- }
- for i := 0; i < length; i += 2 {
- if r.err = r.addRegexpMatcher(pairs[i]+"="+pairs[i+1], regexpTypeQuery); r.err != nil {
- return r
- }
- }
-
- return r
- }
-
- // Schemes --------------------------------------------------------------------
-
- // schemeMatcher matches the request against URL schemes.
- type schemeMatcher []string
-
- func (m schemeMatcher) Match(r *http.Request, match *RouteMatch) bool {
- return matchInArray(m, r.URL.Scheme)
- }
-
- // Schemes adds a matcher for URL schemes.
- // It accepts a sequence of schemes to be matched, e.g.: "http", "https".
- func (r *Route) Schemes(schemes ...string) *Route {
- for k, v := range schemes {
- schemes[k] = strings.ToLower(v)
- }
- if r.buildScheme == "" && len(schemes) > 0 {
- r.buildScheme = schemes[0]
- }
- return r.addMatcher(schemeMatcher(schemes))
- }
-
- // BuildVarsFunc --------------------------------------------------------------
-
- // BuildVarsFunc is the function signature used by custom build variable
- // functions (which can modify route variables before a route's URL is built).
- type BuildVarsFunc func(map[string]string) map[string]string
-
- // BuildVarsFunc adds a custom function to be used to modify build variables
- // before a route's URL is built.
- func (r *Route) BuildVarsFunc(f BuildVarsFunc) *Route {
- r.buildVarsFunc = f
- return r
- }
-
- // Subrouter ------------------------------------------------------------------
-
- // Subrouter creates a subrouter for the route.
- //
- // It will test the inner routes only if the parent route matched. For example:
- //
- // r := mux.NewRouter()
- // s := r.Host("www.example.com").Subrouter()
- // s.HandleFunc("/products/", ProductsHandler)
- // s.HandleFunc("/products/{key}", ProductHandler)
- // s.HandleFunc("/articles/{category}/{id:[0-9]+}"), ArticleHandler)
- //
- // Here, the routes registered in the subrouter won't be tested if the host
- // doesn't match.
- func (r *Route) Subrouter() *Router {
- router := &Router{parent: r, strictSlash: r.strictSlash}
- r.addMatcher(router)
- return router
- }
-
- // ----------------------------------------------------------------------------
- // URL building
- // ----------------------------------------------------------------------------
-
- // URL builds a URL for the route.
- //
- // It accepts a sequence of key/value pairs for the route variables. For
- // example, given this route:
- //
- // r := mux.NewRouter()
- // r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler).
- // Name("article")
- //
- // ...a URL for it can be built using:
- //
- // url, err := r.Get("article").URL("category", "technology", "id", "42")
- //
- // ...which will return an url.URL with the following path:
- //
- // "/articles/technology/42"
- //
- // This also works for host variables:
- //
- // r := mux.NewRouter()
- // r.Host("{subdomain}.domain.com").
- // HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler).
- // Name("article")
- //
- // // url.String() will be "http://news.domain.com/articles/technology/42"
- // url, err := r.Get("article").URL("subdomain", "news",
- // "category", "technology",
- // "id", "42")
- //
- // All variables defined in the route are required, and their values must
- // conform to the corresponding patterns.
- func (r *Route) URL(pairs ...string) (*url.URL, error) {
- if r.err != nil {
- return nil, r.err
- }
- if r.regexp == nil {
- return nil, errors.New("mux: route doesn't have a host or path")
- }
- values, err := r.prepareVars(pairs...)
- if err != nil {
- return nil, err
- }
- var scheme, host, path string
- queries := make([]string, 0, len(r.regexp.queries))
- if r.regexp.host != nil {
- if host, err = r.regexp.host.url(values); err != nil {
- return nil, err
- }
- scheme = "http"
- if s := r.getBuildScheme(); s != "" {
- scheme = s
- }
- }
- if r.regexp.path != nil {
- if path, err = r.regexp.path.url(values); err != nil {
- return nil, err
- }
- }
- for _, q := range r.regexp.queries {
- var query string
- if query, err = q.url(values); err != nil {
- return nil, err
- }
- queries = append(queries, query)
- }
- return &url.URL{
- Scheme: scheme,
- Host: host,
- Path: path,
- RawQuery: strings.Join(queries, "&"),
- }, nil
- }
-
- // URLHost builds the host part of the URL for a route. See Route.URL().
- //
- // The route must have a host defined.
- func (r *Route) URLHost(pairs ...string) (*url.URL, error) {
- if r.err != nil {
- return nil, r.err
- }
- if r.regexp == nil || r.regexp.host == nil {
- return nil, errors.New("mux: route doesn't have a host")
- }
- values, err := r.prepareVars(pairs...)
- if err != nil {
- return nil, err
- }
- host, err := r.regexp.host.url(values)
- if err != nil {
- return nil, err
- }
- u := &url.URL{
- Scheme: "http",
- Host: host,
- }
- if s := r.getBuildScheme(); s != "" {
- u.Scheme = s
- }
- return u, nil
- }
-
- // URLPath builds the path part of the URL for a route. See Route.URL().
- //
- // The route must have a path defined.
- func (r *Route) URLPath(pairs ...string) (*url.URL, error) {
- if r.err != nil {
- return nil, r.err
- }
- if r.regexp == nil || r.regexp.path == nil {
- return nil, errors.New("mux: route doesn't have a path")
- }
- values, err := r.prepareVars(pairs...)
- if err != nil {
- return nil, err
- }
- path, err := r.regexp.path.url(values)
- if err != nil {
- return nil, err
- }
- return &url.URL{
- Path: path,
- }, nil
- }
-
- // GetPathTemplate returns the template used to build the
- // route match.
- // This is useful for building simple REST API documentation and for instrumentation
- // against third-party services.
- // An error will be returned if the route does not define a path.
- func (r *Route) GetPathTemplate() (string, error) {
- if r.err != nil {
- return "", r.err
- }
- if r.regexp == nil || r.regexp.path == nil {
- return "", errors.New("mux: route doesn't have a path")
- }
- return r.regexp.path.template, nil
- }
-
- // GetPathRegexp returns the expanded regular expression used to match route path.
- // This is useful for building simple REST API documentation and for instrumentation
- // against third-party services.
- // An error will be returned if the route does not define a path.
- func (r *Route) GetPathRegexp() (string, error) {
- if r.err != nil {
- return "", r.err
- }
- if r.regexp == nil || r.regexp.path == nil {
- return "", errors.New("mux: route does not have a path")
- }
- return r.regexp.path.regexp.String(), nil
- }
-
- // GetQueriesRegexp returns the expanded regular expressions used to match the
- // route queries.
- // This is useful for building simple REST API documentation and for instrumentation
- // against third-party services.
- // An error will be returned if the route does not have queries.
- func (r *Route) GetQueriesRegexp() ([]string, error) {
- if r.err != nil {
- return nil, r.err
- }
- if r.regexp == nil || r.regexp.queries == nil {
- return nil, errors.New("mux: route doesn't have queries")
- }
- var queries []string
- for _, query := range r.regexp.queries {
- queries = append(queries, query.regexp.String())
- }
- return queries, nil
- }
-
- // GetQueriesTemplates returns the templates used to build the
- // query matching.
- // This is useful for building simple REST API documentation and for instrumentation
- // against third-party services.
- // An error will be returned if the route does not define queries.
- func (r *Route) GetQueriesTemplates() ([]string, error) {
- if r.err != nil {
- return nil, r.err
- }
- if r.regexp == nil || r.regexp.queries == nil {
- return nil, errors.New("mux: route doesn't have queries")
- }
- var queries []string
- for _, query := range r.regexp.queries {
- queries = append(queries, query.template)
- }
- return queries, nil
- }
-
- // GetMethods returns the methods the route matches against
- // This is useful for building simple REST API documentation and for instrumentation
- // against third-party services.
- // An error will be returned if route does not have methods.
- func (r *Route) GetMethods() ([]string, error) {
- if r.err != nil {
- return nil, r.err
- }
- for _, m := range r.matchers {
- if methods, ok := m.(methodMatcher); ok {
- return []string(methods), nil
- }
- }
- return nil, errors.New("mux: route doesn't have methods")
- }
-
- // GetHostTemplate returns the template used to build the
- // route match.
- // This is useful for building simple REST API documentation and for instrumentation
- // against third-party services.
- // An error will be returned if the route does not define a host.
- func (r *Route) GetHostTemplate() (string, error) {
- if r.err != nil {
- return "", r.err
- }
- if r.regexp == nil || r.regexp.host == nil {
- return "", errors.New("mux: route doesn't have a host")
- }
- return r.regexp.host.template, nil
- }
-
- // prepareVars converts the route variable pairs into a map. If the route has a
- // BuildVarsFunc, it is invoked.
- func (r *Route) prepareVars(pairs ...string) (map[string]string, error) {
- m, err := mapFromPairsToString(pairs...)
- if err != nil {
- return nil, err
- }
- return r.buildVars(m), nil
- }
-
- func (r *Route) buildVars(m map[string]string) map[string]string {
- if r.parent != nil {
- m = r.parent.buildVars(m)
- }
- if r.buildVarsFunc != nil {
- m = r.buildVarsFunc(m)
- }
- return m
- }
-
- // ----------------------------------------------------------------------------
- // parentRoute
- // ----------------------------------------------------------------------------
-
- // parentRoute allows routes to know about parent host and path definitions.
- type parentRoute interface {
- getBuildScheme() string
- getNamedRoutes() map[string]*Route
- getRegexpGroup() *routeRegexpGroup
- buildVars(map[string]string) map[string]string
- }
-
- func (r *Route) getBuildScheme() string {
- if r.buildScheme != "" {
- return r.buildScheme
- }
- if r.parent != nil {
- return r.parent.getBuildScheme()
- }
- return ""
- }
-
- // getNamedRoutes returns the map where named routes are registered.
- func (r *Route) getNamedRoutes() map[string]*Route {
- if r.parent == nil {
- // During tests router is not always set.
- r.parent = NewRouter()
- }
- return r.parent.getNamedRoutes()
- }
-
- // getRegexpGroup returns regexp definitions from this route.
- func (r *Route) getRegexpGroup() *routeRegexpGroup {
- if r.regexp == nil {
- if r.parent == nil {
- // During tests router is not always set.
- r.parent = NewRouter()
- }
- regexp := r.parent.getRegexpGroup()
- if regexp == nil {
- r.regexp = new(routeRegexpGroup)
- } else {
- // Copy.
- r.regexp = &routeRegexpGroup{
- host: regexp.host,
- path: regexp.path,
- queries: regexp.queries,
- }
- }
- }
- return r.regexp
- }
|