From 7d706221c632905aa957e5542d7d5f23b0fbc016 Mon Sep 17 00:00:00 2001 From: limpo1989 Date: Fri, 15 Dec 2023 18:02:59 +0800 Subject: [PATCH] Refactor web router inspired by chi --- web/bind.go | 20 - web/context.go | 80 +- web/group.go | 280 -- web/internal/mux/.editorconfig | 20 - web/internal/mux/.gitignore | 1 - web/internal/mux/LICENSE | 27 - web/internal/mux/Makefile | 34 - web/internal/mux/README.md | 812 ----- web/internal/mux/bench_test.go | 81 - web/internal/mux/doc.go | 305 -- .../example_authentication_middleware_test.go | 46 - .../example_cors_method_middleware_test.go | 37 - web/internal/mux/example_route_test.go | 51 - web/internal/mux/example_route_vars_test.go | 36 - web/internal/mux/middleware.go | 74 - web/internal/mux/middleware_test.go | 622 ---- web/internal/mux/mux.go | 644 ---- web/internal/mux/mux_httpserver_test.go | 56 - web/internal/mux/mux_test.go | 3139 ----------------- web/internal/mux/old_test.go | 718 ---- web/internal/mux/regexp.go | 399 --- web/internal/mux/regexp_test.go | 91 - web/internal/mux/route.go | 762 ---- web/internal/mux/test_helpers.go | 19 - web/middleware.go | 62 + web/options.go | 19 +- web/router.go | 611 ++++ web/server.go | 30 +- web/tree.go | 872 +++++ web/tree_test.go | 508 +++ 30 files changed, 2145 insertions(+), 8311 deletions(-) delete mode 100644 web/group.go delete mode 100644 web/internal/mux/.editorconfig delete mode 100644 web/internal/mux/.gitignore delete mode 100644 web/internal/mux/LICENSE delete mode 100644 web/internal/mux/Makefile delete mode 100644 web/internal/mux/README.md delete mode 100644 web/internal/mux/bench_test.go delete mode 100644 web/internal/mux/doc.go delete mode 100644 web/internal/mux/example_authentication_middleware_test.go delete mode 100644 web/internal/mux/example_cors_method_middleware_test.go delete mode 100644 web/internal/mux/example_route_test.go delete mode 100644 web/internal/mux/example_route_vars_test.go delete mode 100644 web/internal/mux/middleware.go delete mode 100644 web/internal/mux/middleware_test.go delete mode 100644 web/internal/mux/mux.go delete mode 100644 web/internal/mux/mux_httpserver_test.go delete mode 100644 web/internal/mux/mux_test.go delete mode 100644 web/internal/mux/old_test.go delete mode 100644 web/internal/mux/regexp.go delete mode 100644 web/internal/mux/regexp_test.go delete mode 100644 web/internal/mux/route.go delete mode 100644 web/internal/mux/test_helpers.go create mode 100644 web/middleware.go create mode 100644 web/router.go create mode 100644 web/tree.go create mode 100644 web/tree_test.go diff --git a/web/bind.go b/web/bind.go index 6ae7e83c..513b4bf3 100644 --- a/web/bind.go +++ b/web/bind.go @@ -20,7 +20,6 @@ import ( "errors" "fmt" "net/http" - "path" "reflect" "go-spring.dev/spring/internal/utils" @@ -249,22 +248,3 @@ func defaultJsonRender(ctx *Context, err error, result interface{}) { ctx.JSON(http.StatusOK, response{Code: code, Message: message, Data: result}) } - -func lastChar(str string) uint8 { - if str == "" { - panic("The length of the string can't be 0") - } - return str[len(str)-1] -} - -func joinPaths(absolutePath, relativePath string) string { - if relativePath == "" { - return absolutePath - } - - finalPath := path.Join(absolutePath, relativePath) - if lastChar(relativePath) == '/' && lastChar(finalPath) != '/' { - return finalPath + "/" - } - return finalPath -} diff --git a/web/context.go b/web/context.go index 032021ff..33a8e745 100644 --- a/web/context.go +++ b/web/context.go @@ -29,7 +29,6 @@ import ( "unicode" "go-spring.dev/spring/web/binding" - "go-spring.dev/spring/web/internal/mux" "go-spring.dev/spring/web/render" ) @@ -55,9 +54,33 @@ type Context struct { // or to be sent by a client. Request *http.Request + routes Routes + // SameSite allows a server to define a cookie attribute making it impossible for // the browser to send this cookie along with cross-site requests. sameSite http.SameSite + + // URLParams are the stack of routeParams captured during the + // routing lifecycle across a stack of sub-routers. + urlParams RouteParams + + // routeParams matched for the current sub-router. It is + // intentionally unexported so it can't be tampered. + routeParams RouteParams + + // Routing path/method override used during the route search. + routePath string + routeMethod string + + // The endpoint routing pattern that matched the request URI path + // or `RoutePath` of the current sub-router. This value will update + // during the lifecycle of a request passing through a stack of + // sub-routers. + routePattern string + routePatterns []string + + methodNotAllowed bool + methodsAllowed []methodTyp } // Context returns the request's context. @@ -93,12 +116,7 @@ func (c *Context) Cookie(name string) (string, bool) { // PathParam returns the named variables in the request. func (c *Context) PathParam(name string) (string, bool) { - if params := mux.Vars(c.Request); nil != params { - if value, ok := params[name]; ok { - return value, true - } - } - return "", false + return c.urlParams.Get(name) } // QueryParam returns the named query in the request. @@ -297,6 +315,44 @@ func (c *Context) ClientIP() string { return remoteIP.String() } +// Reset context to initial state +func (c *Context) Reset() { + c.Writer = nil + c.Request = nil + c.sameSite = 0 + c.routes = nil + c.routePath = "" + c.routeMethod = "" + c.routePattern = "" + c.routePatterns = c.routePatterns[:0] + c.urlParams.Keys = c.urlParams.Keys[:0] + c.urlParams.Values = c.urlParams.Values[:0] + c.routeParams.Keys = c.routeParams.Keys[:0] + c.routeParams.Values = c.routeParams.Values[:0] + c.methodNotAllowed = false + c.methodsAllowed = c.methodsAllowed[:0] +} + +// RouteParams is a structure to track URL routing parameters efficiently. +type RouteParams struct { + Keys, Values []string +} + +// Add will append a URL parameter to the end of the route param +func (s *RouteParams) Add(key, value string) { + s.Keys = append(s.Keys, key) + s.Values = append(s.Values, value) +} + +func (s *RouteParams) Get(key string) (value string, ok bool) { + for index, k := range s.Keys { + if key == k { + return s.Values[index], true + } + } + return "", false +} + // https://stackoverflow.com/questions/53069040/checking-a-string-contains-only-ascii-characters func isASCII(s string) bool { for i := 0; i < len(s); i++ { @@ -325,3 +381,13 @@ func bodyAllowedForStatus(status int) bool { } return true } + +func notFound() http.Handler { + return http.NotFoundHandler() +} + +func notAllowed() http.Handler { + return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + http.Error(writer, "405 method not allowed", http.StatusMethodNotAllowed) + }) +} diff --git a/web/group.go b/web/group.go deleted file mode 100644 index 6b7bfa35..00000000 --- a/web/group.go +++ /dev/null @@ -1,280 +0,0 @@ -/* - * Copyright 2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package web - -import ( - "net/http" - - "go-spring.dev/spring/web/internal/mux" -) - -// MiddlewareFunc is a function which receives an http.Handler and returns another http.Handler. -type MiddlewareFunc = mux.MiddlewareFunc - -type RouterGroup interface { - // Handler dispatches the handler registered in the matched route. - http.Handler - - // BasePath returns the base path of router group. - BasePath() string - - // Use appends a MiddlewareFunc to the chain. - Use(mwf ...MiddlewareFunc) - // Renderer to be used Response renderer in default. - Renderer(renderer Renderer) - - // NotFound to be used when no route matches. - NotFound(handler http.Handler) - // MethodNotAllowed to be used when the request method does not match the route. - MethodNotAllowed(handler http.Handler) - - // StrictSlash defines the trailing slash behavior for new routes. The initial - // value is false. - StrictSlash(value bool) - // SkipClean defines the path cleaning behaviour for new routes. The initial - // value is false. Users should be careful about which routes are not cleaned - // - // When true, if the route path is "/path//to", it will remain with the double - // slash. This is helpful if you have a route like: /fetch/http://xkcd.com/534/ - // - // When false, the path will be cleaned, so /fetch/http://xkcd.com/534/ will - // become /fetch/http/xkcd.com/534 - SkipClean(value bool) - // UseEncodedPath tells the router to match the encoded original path - // to the routes. - // For eg. "/path/foo%2Fbar/to" will match the path "/path/{var}/to". - // - // If not called, the router will match the unencoded path to the routes. - // For eg. "/path/foo%2Fbar/to" will match the path "/path/foo/bar/to" - UseEncodedPath() - - // Group creates a new router group. - Group(path string) RouterGroup - - // Any registers a route that matches all the HTTP methods. - // GET, POST, PUT, PATCH, HEAD, OPTIONS, DELETE, CONNECT, TRACE. - Any(path string, handler interface{}, r ...Renderer) - // Get registers a new GET route with a matcher for the URL path of the get method. - Get(path string, handler interface{}, r ...Renderer) - // Head registers a new HEAD route with a matcher for the URL path of the get method. - Head(path string, handler interface{}, r ...Renderer) - // Post registers a new POST route with a matcher for the URL path of the get method. - Post(path string, handler interface{}, r ...Renderer) - // Put registers a new PUT route with a matcher for the URL path of the get method. - Put(path string, handler interface{}, r ...Renderer) - // Patch registers a new PATCH route with a matcher for the URL path of the get method. - Patch(path string, handler interface{}, r ...Renderer) - // Delete registers a new DELETE route with a matcher for the URL path of the get method. - Delete(path string, handler interface{}, r ...Renderer) - // Connect registers a new CONNECT route with a matcher for the URL path of the get method. - Connect(path string, handler interface{}, r ...Renderer) - // Options registers a new OPTIONS route with a matcher for the URL path of the get method. - Options(path string, handler interface{}, r ...Renderer) - // Trace registers a new TRACE route with a matcher for the URL path of the get method. - Trace(path string, handler interface{}, r ...Renderer) -} - -type routerGroup struct { - basePath string - router *mux.Router - renderer Renderer -} - -// BasePath returns the base path of router group. -func (s *routerGroup) BasePath() string { - return s.basePath -} - -// NotFound to be used when no route matches. -// This can be used to render your own 404 Not Found errors. -func (s *routerGroup) NotFound(handler http.Handler) { - s.router.NotFoundHandler = handler -} - -// MethodNotAllowed to be used when the request method does not match the route. -// This can be used to render your own 405 Method Not Allowed errors. -func (s *routerGroup) MethodNotAllowed(handler http.Handler) { - s.router.MethodNotAllowedHandler = handler -} - -// Renderer to be used Response renderer in default. -func (s *routerGroup) Renderer(renderer Renderer) { - s.renderer = renderer -} - -// StrictSlash defines the trailing slash behavior for new routes. The initial -// value is false. -// -// When true, if the route path is "/path/", accessing "/path" will perform a redirect -// to the former and vice versa. In other words, your application will always -// see the path as specified in the route. -// -// When false, if the route path is "/path", accessing "/path/" will not match -// this route and vice versa. -// -// The re-direct is a HTTP 301 (Moved Permanently). Note that when this is set for -// routes with a non-idempotent method (e.g. POST, PUT), the subsequent re-directed -// request will be made as a GET by most clients. Use middleware or client settings -// to modify this behaviour as needed. -// -// Special case: when a route sets a path prefix using the PathPrefix() method, -// strict slash is ignored for that route because the redirect behavior can't -// be determined from a prefix alone. However, any subrouters created from that -// route inherit the original StrictSlash setting. -func (s *routerGroup) StrictSlash(value bool) { - s.router.StrictSlash(value) -} - -// SkipClean defines the path cleaning behaviour for new routes. The initial -// value is false. Users should be careful about which routes are not cleaned -// -// When true, if the route path is "/path//to", it will remain with the double -// slash. This is helpful if you have a route like: /fetch/http://xkcd.com/534/ -// -// When false, the path will be cleaned, so /fetch/http://xkcd.com/534/ will -// become /fetch/http/xkcd.com/534 -func (s *routerGroup) SkipClean(value bool) { - s.router.SkipClean(value) -} - -// UseEncodedPath tells the router to match the encoded original path -// to the routes. -// For eg. "/path/foo%2Fbar/to" will match the path "/path/{var}/to". -// -// If not called, the router will match the unencoded path to the routes. -// For eg. "/path/foo%2Fbar/to" will match the path "/path/foo/bar/to" -func (s *routerGroup) UseEncodedPath() { - s.router.UseEncodedPath() -} - -// ServeHTTP dispatches the handler registered in the matched route. -// -// When there is a match, the route variables can be retrieved calling -// Vars(request). -func (s *routerGroup) ServeHTTP(w http.ResponseWriter, req *http.Request) { - s.router.ServeHTTP(w, req) -} - -// Use appends a MiddlewareFunc to the chain. -// Middleware can be used to intercept or otherwise modify requests and/or responses, and are executed in the order that they are applied to the Router. -func (s *routerGroup) Use(mwf ...MiddlewareFunc) { - s.router.Use(mwf...) -} - -// Group creates a new router group. -func (s *routerGroup) Group(path string) RouterGroup { - group := &routerGroup{ - basePath: joinPaths(s.basePath, path), - router: s.router.PathPrefix(path).Subrouter(), - renderer: s.renderer, - } - group.router.NotFoundHandler = s.router.NotFoundHandler - group.router.MethodNotAllowedHandler = s.router.MethodNotAllowedHandler - return group -} - -// Bind registers a new route with a matcher for the URL path. -// Automatic binding request to handler input params, following functions: -// -// func(ctx context.Context) -// -// func(ctx context.Context) R -// -// func(ctx context.Context) error -// -// func(ctx context.Context, req T) R -// -// func(ctx context.Context, req T) error -// -// func(ctx context.Context, req T) (R, error) -func (s *routerGroup) Bind(path string, handler interface{}, r ...Renderer) *mux.Route { - var renderer = s.renderer - if len(r) > 0 { - renderer = r[0] - } - return s.Handle(path, Bind(handler, renderer)) -} - -// Handle registers a new route with a matcher for the URL path. -// See Route.Path() and Route.Handler(). -func (s *routerGroup) Handle(path string, handler http.Handler) *mux.Route { - return s.router.Handle(path, handler) -} - -// HandleFunc registers a new route with a matcher for the URL path. -// See Route.Path() and Route.HandlerFunc(). -func (s *routerGroup) HandleFunc(path string, f func(http.ResponseWriter, *http.Request)) *mux.Route { - return s.router.HandleFunc(path, f) -} - -// Walk walks the router and all its sub-routers, calling walkFn for each route -// in the tree. The routes are walked in the order they were added. Sub-routers -// are explored depth-first. -func (s *routerGroup) Walk(walkFn mux.WalkFunc) error { - return s.router.Walk(walkFn) -} - -// Any registers a route that matches all the HTTP methods. -// GET, POST, PUT, PATCH, HEAD, OPTIONS, DELETE, CONNECT, TRACE. -func (s *routerGroup) Any(path string, handler interface{}, r ...Renderer) { - s.Bind(path, handler, r...) -} - -// Get registers a new GET route with a matcher for the URL path of the get method. -func (s *routerGroup) Get(path string, handler interface{}, r ...Renderer) { - s.Bind(path, handler, r...).Methods(http.MethodGet) -} - -// Head registers a new HEAD route with a matcher for the URL path of the get method. -func (s *routerGroup) Head(path string, handler interface{}, r ...Renderer) { - s.Bind(path, handler, r...).Methods(http.MethodHead) -} - -// Post registers a new POST route with a matcher for the URL path of the get method. -func (s *routerGroup) Post(path string, handler interface{}, r ...Renderer) { - s.Bind(path, handler, r...).Methods(http.MethodPost) -} - -// Put registers a new PUT route with a matcher for the URL path of the get method. -func (s *routerGroup) Put(path string, handler interface{}, r ...Renderer) { - s.Bind(path, handler, r...).Methods(http.MethodPut) -} - -// Patch registers a new PATCH route with a matcher for the URL path of the get method. -func (s *routerGroup) Patch(path string, handler interface{}, r ...Renderer) { - s.Bind(path, handler, r...).Methods(http.MethodPatch) -} - -// Delete registers a new DELETE route with a matcher for the URL path of the get method. -func (s *routerGroup) Delete(path string, handler interface{}, r ...Renderer) { - s.Bind(path, handler, r...).Methods(http.MethodDelete) -} - -// Connect registers a new CONNECT route with a matcher for the URL path of the get method. -func (s *routerGroup) Connect(path string, handler interface{}, r ...Renderer) { - s.Bind(path, handler, r...).Methods(http.MethodConnect) -} - -// Options registers a new OPTIONS route with a matcher for the URL path of the get method. -func (s *routerGroup) Options(path string, handler interface{}, r ...Renderer) { - s.Bind(path, handler, r...).Methods(http.MethodOptions) -} - -// Trace registers a new TRACE route with a matcher for the URL path of the get method. -func (s *routerGroup) Trace(path string, handler interface{}, r ...Renderer) { - s.Bind(path, handler, r...).Methods(http.MethodTrace) -} diff --git a/web/internal/mux/.editorconfig b/web/internal/mux/.editorconfig deleted file mode 100644 index c6b74c3e..00000000 --- a/web/internal/mux/.editorconfig +++ /dev/null @@ -1,20 +0,0 @@ -; https://editorconfig.org/ - -root = true - -[*] -insert_final_newline = true -charset = utf-8 -trim_trailing_whitespace = true -indent_style = space -indent_size = 2 - -[{Makefile,go.mod,go.sum,*.go,.gitmodules}] -indent_style = tab -indent_size = 4 - -[*.md] -indent_size = 4 -trim_trailing_whitespace = false - -eclint_indent_style = unset \ No newline at end of file diff --git a/web/internal/mux/.gitignore b/web/internal/mux/.gitignore deleted file mode 100644 index 84039fec..00000000 --- a/web/internal/mux/.gitignore +++ /dev/null @@ -1 +0,0 @@ -coverage.coverprofile diff --git a/web/internal/mux/LICENSE b/web/internal/mux/LICENSE deleted file mode 100644 index bb9d80bc..00000000 --- a/web/internal/mux/LICENSE +++ /dev/null @@ -1,27 +0,0 @@ -Copyright (c) 2023 The Gorilla Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/web/internal/mux/Makefile b/web/internal/mux/Makefile deleted file mode 100644 index 98f5ab75..00000000 --- a/web/internal/mux/Makefile +++ /dev/null @@ -1,34 +0,0 @@ -GO_LINT=$(shell which golangci-lint 2> /dev/null || echo '') -GO_LINT_URI=github.com/golangci/golangci-lint/cmd/golangci-lint@latest - -GO_SEC=$(shell which gosec 2> /dev/null || echo '') -GO_SEC_URI=github.com/securego/gosec/v2/cmd/gosec@latest - -GO_VULNCHECK=$(shell which govulncheck 2> /dev/null || echo '') -GO_VULNCHECK_URI=golang.org/x/vuln/cmd/govulncheck@latest - -.PHONY: golangci-lint -golangci-lint: - $(if $(GO_LINT), ,go install $(GO_LINT_URI)) - @echo "##### Running golangci-lint" - golangci-lint run -v - -.PHONY: gosec -gosec: - $(if $(GO_SEC), ,go install $(GO_SEC_URI)) - @echo "##### Running gosec" - gosec ./... - -.PHONY: govulncheck -govulncheck: - $(if $(GO_VULNCHECK), ,go install $(GO_VULNCHECK_URI)) - @echo "##### Running govulncheck" - govulncheck ./... - -.PHONY: verify -verify: golangci-lint gosec govulncheck - -.PHONY: test -test: - @echo "##### Running tests" - go test -race -cover -coverprofile=coverage.coverprofile -covermode=atomic -v ./... \ No newline at end of file diff --git a/web/internal/mux/README.md b/web/internal/mux/README.md deleted file mode 100644 index 382513d5..00000000 --- a/web/internal/mux/README.md +++ /dev/null @@ -1,812 +0,0 @@ -# gorilla/mux - -![testing](https://github.com/gorilla/mux/actions/workflows/test.yml/badge.svg) -[![codecov](https://codecov.io/github/gorilla/mux/branch/main/graph/badge.svg)](https://codecov.io/github/gorilla/mux) -[![godoc](https://godoc.org/github.com/gorilla/mux?status.svg)](https://godoc.org/github.com/gorilla/mux) -[![sourcegraph](https://sourcegraph.com/github.com/gorilla/mux/-/badge.svg)](https://sourcegraph.com/github.com/gorilla/mux?badge) - - -![Gorilla Logo](https://github.com/gorilla/.github/assets/53367916/d92caabf-98e0-473e-bfbf-ab554ba435e5) - -Package `gorilla/mux` implements a request router and dispatcher for matching incoming requests to -their respective handler. - -The name mux stands for "HTTP request multiplexer". Like the standard `http.ServeMux`, `mux.Router` matches incoming requests against a list of registered routes and calls a handler for the route that matches the URL or other conditions. The main features are: - -* It implements the `http.Handler` interface so it is compatible with the standard `http.ServeMux`. -* Requests can be matched based on URL host, path, path prefix, schemes, header and query values, HTTP methods or using custom matchers. -* URL hosts, paths and query values can have variables with an optional regular expression. -* Registered URLs can be built, or "reversed", which helps maintaining references to resources. -* Routes can be used as subrouters: nested routes are only tested if the parent route matches. This is useful to define groups of routes that share common conditions like a host, a path prefix or other repeated attributes. As a bonus, this optimizes request matching. - ---- - -* [Install](#install) -* [Examples](#examples) -* [Matching Routes](#matching-routes) -* [Static Files](#static-files) -* [Serving Single Page Applications](#serving-single-page-applications) (e.g. React, Vue, Ember.js, etc.) -* [Registered URLs](#registered-urls) -* [Walking Routes](#walking-routes) -* [Graceful Shutdown](#graceful-shutdown) -* [Middleware](#middleware) -* [Handling CORS Requests](#handling-cors-requests) -* [Testing Handlers](#testing-handlers) -* [Full Example](#full-example) - ---- - -## Install - -With a [correctly configured](https://golang.org/doc/install#testing) Go toolchain: - -```sh -go get -u github.com/gorilla/mux -``` - -## Examples - -Let's start registering a couple of URL paths and handlers: - -```go -func main() { - r := mux.NewRouter() - r.HandleFunc("/", HomeHandler) - r.HandleFunc("/products", ProductsHandler) - r.HandleFunc("/articles", ArticlesHandler) - http.Handle("/", r) -} -``` - -Here we register three routes mapping URL paths to handlers. This is equivalent to how `http.HandleFunc()` works: if an incoming request URL matches one of the paths, the corresponding handler is called passing (`http.ResponseWriter`, `*http.Request`) as parameters. - -Paths can have variables. They are defined using the format `{name}` or `{name:pattern}`. If a regular expression pattern is not defined, the matched variable will be anything until the next slash. For example: - -```go -r := mux.NewRouter() -r.HandleFunc("/products/{key}", ProductHandler) -r.HandleFunc("/articles/{category}/", ArticlesCategoryHandler) -r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler) -``` - -The names are used to create a map of route variables which can be retrieved calling `mux.Vars()`: - -```go -func ArticlesCategoryHandler(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - w.WriteHeader(http.StatusOK) - fmt.Fprintf(w, "Category: %v\n", vars["category"]) -} -``` - -And this is all you need to know about the basic usage. More advanced options are explained below. - -### Matching Routes - -Routes can also be restricted to a domain or subdomain. Just define a host pattern to be matched. They can also have variables: - -```go -r := mux.NewRouter() -// Only matches if domain is "www.example.com". -r.Host("www.example.com") -// Matches a dynamic subdomain. -r.Host("{subdomain:[a-z]+}.example.com") -``` - -There are several other matchers that can be added. To match path prefixes: - -```go -r.PathPrefix("/products/") -``` - -...or HTTP methods: - -```go -r.Methods("GET", "POST") -``` - -...or URL schemes: - -```go -r.Schemes("https") -``` - -...or header values: - -```go -r.Headers("X-Requested-With", "XMLHttpRequest") -``` - -...or query values: - -```go -r.Queries("key", "value") -``` - -...or to use a custom matcher function: - -```go -r.MatcherFunc(func(r *http.Request, rm *RouteMatch) bool { - return r.ProtoMajor == 0 -}) -``` - -...and finally, it is possible to combine several matchers in a single route: - -```go -r.HandleFunc("/products", ProductsHandler). - Host("www.example.com"). - Methods("GET"). - Schemes("http") -``` - -Routes are tested in the order they were added to the router. If two routes match, the first one wins: - -```go -r := mux.NewRouter() -r.HandleFunc("/specific", specificHandler) -r.PathPrefix("/").Handler(catchAllHandler) -``` - -Setting the same matching conditions again and again can be boring, so we have a way to group several routes that share the same requirements. We call it "subrouting". - -For example, let's say we have several URLs that should only match when the host is `www.example.com`. Create a route for that host and get a "subrouter" from it: - -```go -r := mux.NewRouter() -s := r.Host("www.example.com").Subrouter() -``` - -Then register routes in the subrouter: - -```go -s.HandleFunc("/products/", ProductsHandler) -s.HandleFunc("/products/{key}", ProductHandler) -s.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler) -``` - -The three URL paths we registered above will only be tested if the domain is `www.example.com`, because the subrouter is tested first. This is not only convenient, but also optimizes request matching. You can create subrouters combining any attribute matchers accepted by a route. - -Subrouters can be used to create domain or path "namespaces": you define subrouters in a central place and then parts of the app can register its paths relatively to a given subrouter. - -There's one more thing about subroutes. When a subrouter has a path prefix, the inner routes use it as base for their paths: - -```go -r := mux.NewRouter() -s := r.PathPrefix("/products").Subrouter() -// "/products/" -s.HandleFunc("/", ProductsHandler) -// "/products/{key}/" -s.HandleFunc("/{key}/", ProductHandler) -// "/products/{key}/details" -s.HandleFunc("/{key}/details", ProductDetailsHandler) -``` - - -### Static Files - -Note that the path provided to `PathPrefix()` represents a "wildcard": calling -`PathPrefix("/static/").Handler(...)` means that the handler will be passed any -request that matches "/static/\*". This makes it easy to serve static files with mux: - -```go -func main() { - var dir string - - flag.StringVar(&dir, "dir", ".", "the directory to serve files from. Defaults to the current dir") - flag.Parse() - r := mux.NewRouter() - - // This will serve files under http://localhost:8000/static/ - r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(dir)))) - - srv := &http.Server{ - Handler: r, - Addr: "127.0.0.1:8000", - // Good practice: enforce timeouts for servers you create! - WriteTimeout: 15 * time.Second, - ReadTimeout: 15 * time.Second, - } - - log.Fatal(srv.ListenAndServe()) -} -``` - -### Serving Single Page Applications - -Most of the time it makes sense to serve your SPA on a separate web server from your API, -but sometimes it's desirable to serve them both from one place. It's possible to write a simple -handler for serving your SPA (for use with React Router's [BrowserRouter](https://reacttraining.com/react-router/web/api/BrowserRouter) for example), and leverage -mux's powerful routing for your API endpoints. - -```go -package main - -import ( - "encoding/json" - "log" - "net/http" - "os" - "path/filepath" - "time" - - "github.com/gorilla/mux" -) - -// spaHandler implements the http.Handler interface, so we can use it -// to respond to HTTP requests. The path to the static directory and -// path to the index file within that static directory are used to -// serve the SPA in the given static directory. -type spaHandler struct { - staticPath string - indexPath string -} - -// ServeHTTP inspects the URL path to locate a file within the static dir -// on the SPA handler. If a file is found, it will be served. If not, the -// file located at the index path on the SPA handler will be served. This -// is suitable behavior for serving an SPA (single page application). -func (h spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - // Join internally call path.Clean to prevent directory traversal - path := filepath.Join(h.staticPath, r.URL.Path) - - // check whether a file exists or is a directory at the given path - fi, err := os.Stat(path) - if os.IsNotExist(err) || fi.IsDir() { - // file does not exist or path is a directory, serve index.html - http.ServeFile(w, r, filepath.Join(h.staticPath, h.indexPath)) - return - } - - if err != nil { - // if we got an error (that wasn't that the file doesn't exist) stating the - // file, return a 500 internal server error and stop - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - // otherwise, use http.FileServer to serve the static file - http.FileServer(http.Dir(h.staticPath)).ServeHTTP(w, r) -} - -func main() { - router := mux.NewRouter() - - router.HandleFunc("/api/health", func(w http.ResponseWriter, r *http.Request) { - // an example API handler - json.NewEncoder(w).Encode(map[string]bool{"ok": true}) - }) - - spa := spaHandler{staticPath: "build", indexPath: "index.html"} - router.PathPrefix("/").Handler(spa) - - srv := &http.Server{ - Handler: router, - Addr: "127.0.0.1:8000", - // Good practice: enforce timeouts for servers you create! - WriteTimeout: 15 * time.Second, - ReadTimeout: 15 * time.Second, - } - - log.Fatal(srv.ListenAndServe()) -} -``` - -### Registered URLs - -Now let's see how to build registered URLs. - -Routes can be named. All routes that define a name can have their URLs built, or "reversed". We define a name calling `Name()` on a route. For example: - -```go -r := mux.NewRouter() -r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler). - Name("article") -``` - -To build a URL, get the route and call the `URL()` method, passing a sequence of key/value pairs for the route variables. For the previous route, we would do: - -```go -url, err := r.Get("article").URL("category", "technology", "id", "42") -``` - -...and the result will be a `url.URL` with the following path: - -``` -"/articles/technology/42" -``` - -This also works for host and query value variables: - -```go -r := mux.NewRouter() -r.Host("{subdomain}.example.com"). - Path("/articles/{category}/{id:[0-9]+}"). - Queries("filter", "{filter}"). - HandlerFunc(ArticleHandler). - Name("article") - -// url.String() will be "http://news.example.com/articles/technology/42?filter=gorilla" -url, err := r.Get("article").URL("subdomain", "news", - "category", "technology", - "id", "42", - "filter", "gorilla") -``` - -All variables defined in the route are required, and their values must conform to the corresponding patterns. These requirements guarantee that a generated URL will always match a registered route -- the only exception is for explicitly defined "build-only" routes which never match. - -Regex support also exists for matching Headers within a route. For example, we could do: - -```go -r.HeadersRegexp("Content-Type", "application/(text|json)") -``` - -...and the route will match both requests with a Content-Type of `application/json` as well as `application/text` - -There's also a way to build only the URL host or path for a route: use the methods `URLHost()` or `URLPath()` instead. For the previous route, we would do: - -```go -// "http://news.example.com/" -host, err := r.Get("article").URLHost("subdomain", "news") - -// "/articles/technology/42" -path, err := r.Get("article").URLPath("category", "technology", "id", "42") -``` - -And if you use subrouters, host and path defined separately can be built as well: - -```go -r := mux.NewRouter() -s := r.Host("{subdomain}.example.com").Subrouter() -s.Path("/articles/{category}/{id:[0-9]+}"). - HandlerFunc(ArticleHandler). - Name("article") - -// "http://news.example.com/articles/technology/42" -url, err := r.Get("article").URL("subdomain", "news", - "category", "technology", - "id", "42") -``` - -To find all the required variables for a given route when calling `URL()`, the method `GetVarNames()` is available: -```go -r := mux.NewRouter() -r.Host("{domain}"). - Path("/{group}/{item_id}"). - Queries("some_data1", "{some_data1}"). - Queries("some_data2", "{some_data2}"). - Name("article") - -// Will print [domain group item_id some_data1 some_data2] -fmt.Println(r.Get("article").GetVarNames()) - -``` -### Walking Routes - -The `Walk` function on `mux.Router` can be used to visit all of the routes that are registered on a router. For example, -the following prints all of the registered routes: - -```go -package main - -import ( - "fmt" - "net/http" - "strings" - - "github.com/gorilla/mux" -) - -func handler(w http.ResponseWriter, r *http.Request) { - return -} - -func main() { - r := mux.NewRouter() - r.HandleFunc("/", handler) - r.HandleFunc("/products", handler).Methods("POST") - r.HandleFunc("/articles", handler).Methods("GET") - r.HandleFunc("/articles/{id}", handler).Methods("GET", "PUT") - r.HandleFunc("/authors", handler).Queries("surname", "{surname}") - err := r.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error { - pathTemplate, err := route.GetPathTemplate() - if err == nil { - fmt.Println("ROUTE:", pathTemplate) - } - pathRegexp, err := route.GetPathRegexp() - if err == nil { - fmt.Println("Path regexp:", pathRegexp) - } - queriesTemplates, err := route.GetQueriesTemplates() - if err == nil { - fmt.Println("Queries templates:", strings.Join(queriesTemplates, ",")) - } - queriesRegexps, err := route.GetQueriesRegexp() - if err == nil { - fmt.Println("Queries regexps:", strings.Join(queriesRegexps, ",")) - } - methods, err := route.GetMethods() - if err == nil { - fmt.Println("Methods:", strings.Join(methods, ",")) - } - fmt.Println() - return nil - }) - - if err != nil { - fmt.Println(err) - } - - http.Handle("/", r) -} -``` - -### Graceful Shutdown - -Go 1.8 introduced the ability to [gracefully shutdown](https://golang.org/doc/go1.8#http_shutdown) a `*http.Server`. Here's how to do that alongside `mux`: - -```go -package main - -import ( - "context" - "flag" - "log" - "net/http" - "os" - "os/signal" - "time" - - "github.com/gorilla/mux" -) - -func main() { - var wait time.Duration - flag.DurationVar(&wait, "graceful-timeout", time.Second * 15, "the duration for which the server gracefully wait for existing connections to finish - e.g. 15s or 1m") - flag.Parse() - - r := mux.NewRouter() - // Add your routes as needed - - srv := &http.Server{ - Addr: "0.0.0.0:8080", - // Good practice to set timeouts to avoid Slowloris attacks. - WriteTimeout: time.Second * 15, - ReadTimeout: time.Second * 15, - IdleTimeout: time.Second * 60, - Handler: r, // Pass our instance of gorilla/mux in. - } - - // Run our server in a goroutine so that it doesn't block. - go func() { - if err := srv.ListenAndServe(); err != nil { - log.Println(err) - } - }() - - c := make(chan os.Signal, 1) - // We'll accept graceful shutdowns when quit via SIGINT (Ctrl+C) - // SIGKILL, SIGQUIT or SIGTERM (Ctrl+/) will not be caught. - signal.Notify(c, os.Interrupt) - - // Block until we receive our signal. - <-c - - // Create a deadline to wait for. - ctx, cancel := context.WithTimeout(context.Background(), wait) - defer cancel() - // Doesn't block if no connections, but will otherwise wait - // until the timeout deadline. - srv.Shutdown(ctx) - // Optionally, you could run srv.Shutdown in a goroutine and block on - // <-ctx.Done() if your application should wait for other services - // to finalize based on context cancellation. - log.Println("shutting down") - os.Exit(0) -} -``` - -### Middleware - -Mux supports the addition of middlewares to a [Router](https://godoc.org/github.com/gorilla/mux#Router), which are executed in the order they are added if a match is found, including its subrouters. -Middlewares are (typically) small pieces of code which take one request, do something with it, and pass it down to another middleware or the final handler. Some common use cases for middleware are request logging, header manipulation, or `ResponseWriter` hijacking. - -Mux middlewares are defined using the de facto standard type: - -```go -type MiddlewareFunc func(http.Handler) http.Handler -``` - -Typically, the returned handler is a closure which does something with the http.ResponseWriter and http.Request passed to it, and then calls the handler passed as parameter to the MiddlewareFunc. This takes advantage of closures being able access variables from the context where they are created, while retaining the signature enforced by the receivers. - -A very basic middleware which logs the URI of the request being handled could be written as: - -```go -func loggingMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Do stuff here - log.Println(r.RequestURI) - // Call the next handler, which can be another middleware in the chain, or the final handler. - next.ServeHTTP(w, r) - }) -} -``` - -Middlewares can be added to a router using `Router.Use()`: - -```go -r := mux.NewRouter() -r.HandleFunc("/", handler) -r.Use(loggingMiddleware) -``` - -A more complex authentication middleware, which maps session token to users, could be written as: - -```go -// Define our struct -type authenticationMiddleware struct { - tokenUsers map[string]string -} - -// Initialize it somewhere -func (amw *authenticationMiddleware) Populate() { - amw.tokenUsers["00000000"] = "user0" - amw.tokenUsers["aaaaaaaa"] = "userA" - amw.tokenUsers["05f717e5"] = "randomUser" - amw.tokenUsers["deadbeef"] = "user0" -} - -// Middleware function, which will be called for each request -func (amw *authenticationMiddleware) Middleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - token := r.Header.Get("X-Session-Token") - - if user, found := amw.tokenUsers[token]; found { - // We found the token in our map - log.Printf("Authenticated user %s\n", user) - // Pass down the request to the next middleware (or final handler) - next.ServeHTTP(w, r) - } else { - // Write an error and stop the handler chain - http.Error(w, "Forbidden", http.StatusForbidden) - } - }) -} -``` - -```go -r := mux.NewRouter() -r.HandleFunc("/", handler) - -amw := authenticationMiddleware{tokenUsers: make(map[string]string)} -amw.Populate() - -r.Use(amw.Middleware) -``` - -Note: The handler chain will be stopped if your middleware doesn't call `next.ServeHTTP()` with the corresponding parameters. This can be used to abort a request if the middleware writer wants to. Middlewares _should_ write to `ResponseWriter` if they _are_ going to terminate the request, and they _should not_ write to `ResponseWriter` if they _are not_ going to terminate it. - -### Handling CORS Requests - -[CORSMethodMiddleware](https://godoc.org/github.com/gorilla/mux#CORSMethodMiddleware) intends to make it easier to strictly set the `Access-Control-Allow-Methods` response header. - -* You will still need to use your own CORS handler to set the other CORS headers such as `Access-Control-Allow-Origin` -* The middleware will set the `Access-Control-Allow-Methods` header to all the method matchers (e.g. `r.Methods(http.MethodGet, http.MethodPut, http.MethodOptions)` -> `Access-Control-Allow-Methods: GET,PUT,OPTIONS`) on a route -* If you do not specify any methods, then: -> _Important_: there must be an `OPTIONS` method matcher for the middleware to set the headers. - -Here is an example of using `CORSMethodMiddleware` along with a custom `OPTIONS` handler to set all the required CORS headers: - -```go -package main - -import ( - "net/http" - "github.com/gorilla/mux" -) - -func main() { - r := mux.NewRouter() - - // IMPORTANT: you must specify an OPTIONS method matcher for the middleware to set CORS headers - r.HandleFunc("/foo", fooHandler).Methods(http.MethodGet, http.MethodPut, http.MethodPatch, http.MethodOptions) - r.Use(mux.CORSMethodMiddleware(r)) - - http.ListenAndServe(":8080", r) -} - -func fooHandler(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Access-Control-Allow-Origin", "*") - if r.Method == http.MethodOptions { - return - } - - w.Write([]byte("foo")) -} -``` - -And an request to `/foo` using something like: - -```bash -curl localhost:8080/foo -v -``` - -Would look like: - -```bash -* Trying ::1... -* TCP_NODELAY set -* Connected to localhost (::1) port 8080 (#0) -> GET /foo HTTP/1.1 -> Host: localhost:8080 -> User-Agent: curl/7.59.0 -> Accept: */* -> -< HTTP/1.1 200 OK -< Access-Control-Allow-Methods: GET,PUT,PATCH,OPTIONS -< Access-Control-Allow-Origin: * -< Date: Fri, 28 Jun 2019 20:13:30 GMT -< Content-Length: 3 -< Content-Type: text/plain; charset=utf-8 -< -* Connection #0 to host localhost left intact -foo -``` - -### Testing Handlers - -Testing handlers in a Go web application is straightforward, and _mux_ doesn't complicate this any further. Given two files: `endpoints.go` and `endpoints_test.go`, here's how we'd test an application using _mux_. - -First, our simple HTTP handler: - -```go -// endpoints.go -package main - -func HealthCheckHandler(w http.ResponseWriter, r *http.Request) { - // A very simple health check. - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - - // In the future we could report back on the status of our DB, or our cache - // (e.g. Redis) by performing a simple PING, and include them in the response. - io.WriteString(w, `{"alive": true}`) -} - -func main() { - r := mux.NewRouter() - r.HandleFunc("/health", HealthCheckHandler) - - log.Fatal(http.ListenAndServe("localhost:8080", r)) -} -``` - -Our test code: - -```go -// endpoints_test.go -package main - -import ( - "net/http" - "net/http/httptest" - "testing" -) - -func TestHealthCheckHandler(t *testing.T) { - // Create a request to pass to our handler. We don't have any query parameters for now, so we'll - // pass 'nil' as the third parameter. - req, err := http.NewRequest("GET", "/health", nil) - if err != nil { - t.Fatal(err) - } - - // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. - rr := httptest.NewRecorder() - handler := http.HandlerFunc(HealthCheckHandler) - - // Our handlers satisfy http.Handler, so we can call their ServeHTTP method - // directly and pass in our Request and ResponseRecorder. - handler.ServeHTTP(rr, req) - - // Check the status code is what we expect. - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got %v want %v", - status, http.StatusOK) - } - - // Check the response body is what we expect. - expected := `{"alive": true}` - if rr.Body.String() != expected { - t.Errorf("handler returned unexpected body: got %v want %v", - rr.Body.String(), expected) - } -} -``` - -In the case that our routes have [variables](#examples), we can pass those in the request. We could write -[table-driven tests](https://dave.cheney.net/2013/06/09/writing-table-driven-tests-in-go) to test multiple -possible route variables as needed. - -```go -// endpoints.go -func main() { - r := mux.NewRouter() - // A route with a route variable: - r.HandleFunc("/metrics/{type}", MetricsHandler) - - log.Fatal(http.ListenAndServe("localhost:8080", r)) -} -``` - -Our test file, with a table-driven test of `routeVariables`: - -```go -// endpoints_test.go -func TestMetricsHandler(t *testing.T) { - tt := []struct{ - routeVariable string - shouldPass bool - }{ - {"goroutines", true}, - {"heap", true}, - {"counters", true}, - {"queries", true}, - {"adhadaeqm3k", false}, - } - - for _, tc := range tt { - path := fmt.Sprintf("/metrics/%s", tc.routeVariable) - req, err := http.NewRequest("GET", path, nil) - if err != nil { - t.Fatal(err) - } - - rr := httptest.NewRecorder() - - // To add the vars to the context, - // we need to create a router through which we can pass the request. - router := mux.NewRouter() - router.HandleFunc("/metrics/{type}", MetricsHandler) - router.ServeHTTP(rr, req) - - // In this case, our MetricsHandler returns a non-200 response - // for a route variable it doesn't know about. - if rr.Code == http.StatusOK && !tc.shouldPass { - t.Errorf("handler should have failed on routeVariable %s: got %v want %v", - tc.routeVariable, rr.Code, http.StatusOK) - } - } -} -``` - -## Full Example - -Here's a complete, runnable example of a small `mux` based server: - -```go -package main - -import ( - "net/http" - "log" - "github.com/gorilla/mux" -) - -func YourHandler(w http.ResponseWriter, r *http.Request) { - w.Write([]byte("Gorilla!\n")) -} - -func main() { - r := mux.NewRouter() - // Routes consist of a path and a handler function. - r.HandleFunc("/", YourHandler) - - // Bind to a port and pass our router in - log.Fatal(http.ListenAndServe(":8000", r)) -} -``` - -## License - -BSD licensed. See the LICENSE file for details. diff --git a/web/internal/mux/bench_test.go b/web/internal/mux/bench_test.go deleted file mode 100644 index c1f86ca5..00000000 --- a/web/internal/mux/bench_test.go +++ /dev/null @@ -1,81 +0,0 @@ -// 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 ( - "net/http" - "net/http/httptest" - "testing" -) - -func BenchmarkMux(b *testing.B) { - router := new(Router) - handler := func(w http.ResponseWriter, r *http.Request) {} - router.HandleFunc("/v1/{v1}", handler) - - request, _ := http.NewRequest("GET", "/v1/anything", nil) - for i := 0; i < b.N; i++ { - router.ServeHTTP(nil, request) - } -} - -func BenchmarkMuxSimple(b *testing.B) { - router := new(Router) - handler := func(w http.ResponseWriter, r *http.Request) {} - router.HandleFunc("/status", handler) - - testCases := []struct { - name string - omitRouteFromContext bool - }{ - { - name: "default", - omitRouteFromContext: false, - }, - { - name: "omit route from ctx", - omitRouteFromContext: true, - }, - } - for _, tc := range testCases { - b.Run(tc.name, func(b *testing.B) { - router.OmitRouteFromContext(tc.omitRouteFromContext) - - request, _ := http.NewRequest("GET", "/status", nil) - b.ReportAllocs() - b.ResetTimer() - for i := 0; i < b.N; i++ { - router.ServeHTTP(nil, request) - } - }) - } -} - -func BenchmarkMuxAlternativeInRegexp(b *testing.B) { - router := new(Router) - handler := func(w http.ResponseWriter, r *http.Request) {} - router.HandleFunc("/v1/{v1:(?:a|b)}", handler) - - requestA, _ := http.NewRequest("GET", "/v1/a", nil) - requestB, _ := http.NewRequest("GET", "/v1/b", nil) - for i := 0; i < b.N; i++ { - router.ServeHTTP(nil, requestA) - router.ServeHTTP(nil, requestB) - } -} - -func BenchmarkManyPathVariables(b *testing.B) { - router := new(Router) - handler := func(w http.ResponseWriter, r *http.Request) {} - router.HandleFunc("/v1/{v1}/{v2}/{v3}/{v4}/{v5}", handler) - - matchingRequest, _ := http.NewRequest("GET", "/v1/1/2/3/4/5", nil) - notMatchingRequest, _ := http.NewRequest("GET", "/v1/1/2/3/4", nil) - recorder := httptest.NewRecorder() - for i := 0; i < b.N; i++ { - router.ServeHTTP(nil, matchingRequest) - router.ServeHTTP(recorder, notMatchingRequest) - } -} diff --git a/web/internal/mux/doc.go b/web/internal/mux/doc.go deleted file mode 100644 index 80601351..00000000 --- a/web/internal/mux/doc.go +++ /dev/null @@ -1,305 +0,0 @@ -// 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 implements a request router and dispatcher. - -The name mux stands for "HTTP request multiplexer". Like the standard -http.ServeMux, mux.Router matches incoming requests against a list of -registered routes and calls a handler for the route that matches the URL -or other conditions. The main features are: - - - Requests can be matched based on URL host, path, path prefix, schemes, - header and query values, HTTP methods or using custom matchers. - - URL hosts, paths and query values can have variables with an optional - regular expression. - - Registered URLs can be built, or "reversed", which helps maintaining - references to resources. - - Routes can be used as subrouters: nested routes are only tested if the - parent route matches. This is useful to define groups of routes that - share common conditions like a host, a path prefix or other repeated - attributes. As a bonus, this optimizes request matching. - - It implements the http.Handler interface so it is compatible with the - standard http.ServeMux. - -Let's start registering a couple of URL paths and handlers: - - func main() { - r := mux.NewRouter() - r.HandleFunc("/", HomeHandler) - r.HandleFunc("/products", ProductsHandler) - r.HandleFunc("/articles", ArticlesHandler) - http.Handle("/", r) - } - -Here we register three routes mapping URL paths to handlers. This is -equivalent to how http.HandleFunc() works: if an incoming request URL matches -one of the paths, the corresponding handler is called passing -(http.ResponseWriter, *http.Request) as parameters. - -Paths can have variables. They are defined using the format {name} or -{name:pattern}. If a regular expression pattern is not defined, the matched -variable will be anything until the next slash. For example: - - r := mux.NewRouter() - r.HandleFunc("/products/{key}", ProductHandler) - r.HandleFunc("/articles/{category}/", ArticlesCategoryHandler) - r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler) - -Groups can be used inside patterns, as long as they are non-capturing (?:re). For example: - - r.HandleFunc("/articles/{category}/{sort:(?:asc|desc|new)}", ArticlesCategoryHandler) - -The names are used to create a map of route variables which can be retrieved -calling mux.Vars(): - - vars := mux.Vars(request) - category := vars["category"] - -Note that if any capturing groups are present, mux will panic() during parsing. To prevent -this, convert any capturing groups to non-capturing, e.g. change "/{sort:(asc|desc)}" to -"/{sort:(?:asc|desc)}". This is a change from prior versions which behaved unpredictably -when capturing groups were present. - -And this is all you need to know about the basic usage. More advanced options -are explained below. - -Routes can also be restricted to a domain or subdomain. Just define a host -pattern to be matched. They can also have variables: - - r := mux.NewRouter() - // Only matches if domain is "www.example.com". - r.Host("www.example.com") - // Matches a dynamic subdomain. - r.Host("{subdomain:[a-z]+}.domain.com") - -There are several other matchers that can be added. To match path prefixes: - - r.PathPrefix("/products/") - -...or HTTP methods: - - r.Methods("GET", "POST") - -...or URL schemes: - - r.Schemes("https") - -...or header values: - - r.Headers("X-Requested-With", "XMLHttpRequest") - -...or query values: - - r.Queries("key", "value") - -...or to use a custom matcher function: - - r.MatcherFunc(func(r *http.Request, rm *RouteMatch) bool { - return r.ProtoMajor == 0 - }) - -...and finally, it is possible to combine several matchers in a single route: - - r.HandleFunc("/products", ProductsHandler). - Host("www.example.com"). - Methods("GET"). - Schemes("http") - -Setting the same matching conditions again and again can be boring, so we have -a way to group several routes that share the same requirements. -We call it "subrouting". - -For example, let's say we have several URLs that should only match when the -host is "www.example.com". Create a route for that host and get a "subrouter" -from it: - - r := mux.NewRouter() - s := r.Host("www.example.com").Subrouter() - -Then register routes in the subrouter: - - s.HandleFunc("/products/", ProductsHandler) - s.HandleFunc("/products/{key}", ProductHandler) - s.HandleFunc("/articles/{category}/{id:[0-9]+}"), ArticleHandler) - -The three URL paths we registered above will only be tested if the domain is -"www.example.com", because the subrouter is tested first. This is not -only convenient, but also optimizes request matching. You can create -subrouters combining any attribute matchers accepted by a route. - -Subrouters can be used to create domain or path "namespaces": you define -subrouters in a central place and then parts of the app can register its -paths relatively to a given subrouter. - -There's one more thing about subroutes. When a subrouter has a path prefix, -the inner routes use it as base for their paths: - - r := mux.NewRouter() - s := r.PathPrefix("/products").Subrouter() - // "/products/" - s.HandleFunc("/", ProductsHandler) - // "/products/{key}/" - s.HandleFunc("/{key}/", ProductHandler) - // "/products/{key}/details" - s.HandleFunc("/{key}/details", ProductDetailsHandler) - -Note that the path provided to PathPrefix() represents a "wildcard": calling -PathPrefix("/static/").Handler(...) means that the handler will be passed any -request that matches "/static/*". This makes it easy to serve static files with mux: - - func main() { - var dir string - - flag.StringVar(&dir, "dir", ".", "the directory to serve files from. Defaults to the current dir") - flag.Parse() - r := mux.NewRouter() - - // This will serve files under http://localhost:8000/static/ - r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(dir)))) - - srv := &http.Server{ - Handler: r, - Addr: "127.0.0.1:8000", - // Good practice: enforce timeouts for servers you create! - WriteTimeout: 15 * time.Second, - ReadTimeout: 15 * time.Second, - } - - log.Fatal(srv.ListenAndServe()) - } - -Now let's see how to build registered URLs. - -Routes can be named. All routes that define a name can have their URLs built, -or "reversed". We define a name calling Name() on a route. For example: - - r := mux.NewRouter() - r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler). - Name("article") - -To build a URL, get the route and call the URL() method, passing a sequence of -key/value pairs for the route variables. For the previous route, we would do: - - url, err := r.Get("article").URL("category", "technology", "id", "42") - -...and the result will be a url.URL with the following path: - - "/articles/technology/42" - -This also works for host and query value variables: - - r := mux.NewRouter() - r.Host("{subdomain}.domain.com"). - Path("/articles/{category}/{id:[0-9]+}"). - Queries("filter", "{filter}"). - HandlerFunc(ArticleHandler). - Name("article") - - // url.String() will be "http://news.domain.com/articles/technology/42?filter=gorilla" - url, err := r.Get("article").URL("subdomain", "news", - "category", "technology", - "id", "42", - "filter", "gorilla") - -All variables defined in the route are required, and their values must -conform to the corresponding patterns. These requirements guarantee that a -generated URL will always match a registered route -- the only exception is -for explicitly defined "build-only" routes which never match. - -Regex support also exists for matching Headers within a route. For example, we could do: - - r.HeadersRegexp("Content-Type", "application/(text|json)") - -...and the route will match both requests with a Content-Type of `application/json` as well as -`application/text` - -There's also a way to build only the URL host or path for a route: -use the methods URLHost() or URLPath() instead. For the previous route, -we would do: - - // "http://news.domain.com/" - host, err := r.Get("article").URLHost("subdomain", "news") - - // "/articles/technology/42" - path, err := r.Get("article").URLPath("category", "technology", "id", "42") - -And if you use subrouters, host and path defined separately can be built -as well: - - r := mux.NewRouter() - s := r.Host("{subdomain}.domain.com").Subrouter() - s.Path("/articles/{category}/{id:[0-9]+}"). - HandlerFunc(ArticleHandler). - Name("article") - - // "http://news.domain.com/articles/technology/42" - url, err := r.Get("article").URL("subdomain", "news", - "category", "technology", - "id", "42") - -Mux supports the addition of middlewares to a Router, which are executed in the order they are added if a match is found, including its subrouters. Middlewares are (typically) small pieces of code which take one request, do something with it, and pass it down to another middleware or the final handler. Some common use cases for middleware are request logging, header manipulation, or ResponseWriter hijacking. - - type MiddlewareFunc func(http.Handler) http.Handler - -Typically, the returned handler is a closure which does something with the http.ResponseWriter and http.Request passed to it, and then calls the handler passed as parameter to the MiddlewareFunc (closures can access variables from the context where they are created). - -A very basic middleware which logs the URI of the request being handled could be written as: - - func simpleMw(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Do stuff here - log.Println(r.RequestURI) - // Call the next handler, which can be another middleware in the chain, or the final handler. - next.ServeHTTP(w, r) - }) - } - -Middlewares can be added to a router using `Router.Use()`: - - r := mux.NewRouter() - r.HandleFunc("/", handler) - r.Use(simpleMw) - -A more complex authentication middleware, which maps session token to users, could be written as: - - // Define our struct - type authenticationMiddleware struct { - tokenUsers map[string]string - } - - // Initialize it somewhere - func (amw *authenticationMiddleware) Populate() { - amw.tokenUsers["00000000"] = "user0" - amw.tokenUsers["aaaaaaaa"] = "userA" - amw.tokenUsers["05f717e5"] = "randomUser" - amw.tokenUsers["deadbeef"] = "user0" - } - - // Middleware function, which will be called for each request - func (amw *authenticationMiddleware) Middleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - token := r.Header.Get("X-Session-Token") - - if user, found := amw.tokenUsers[token]; found { - // We found the token in our map - log.Printf("Authenticated user %s\n", user) - next.ServeHTTP(w, r) - } else { - http.Error(w, "Forbidden", http.StatusForbidden) - } - }) - } - - r := mux.NewRouter() - r.HandleFunc("/", handler) - - amw := authenticationMiddleware{tokenUsers: make(map[string]string)} - amw.Populate() - - r.Use(amw.Middleware) - -Note: The handler chain will be stopped if your middleware doesn't call `next.ServeHTTP()` with the corresponding parameters. This can be used to abort a request if the middleware writer wants to. -*/ -package mux diff --git a/web/internal/mux/example_authentication_middleware_test.go b/web/internal/mux/example_authentication_middleware_test.go deleted file mode 100644 index f6878a95..00000000 --- a/web/internal/mux/example_authentication_middleware_test.go +++ /dev/null @@ -1,46 +0,0 @@ -package mux_test - -import ( - "log" - "net/http" - - "go-spring.dev/spring/web/internal/mux" -) - -// Define our struct -type authenticationMiddleware struct { - tokenUsers map[string]string -} - -// Initialize it somewhere -func (amw *authenticationMiddleware) Populate() { - amw.tokenUsers["00000000"] = "user0" - amw.tokenUsers["aaaaaaaa"] = "userA" - amw.tokenUsers["05f717e5"] = "randomUser" - amw.tokenUsers["deadbeef"] = "user0" -} - -// Middleware function, which will be called for each request -func (amw *authenticationMiddleware) Middleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - token := r.Header.Get("X-Session-Token") - - if user, found := amw.tokenUsers[token]; found { - // We found the token in our map - log.Printf("Authenticated user %s\n", user) - next.ServeHTTP(w, r) - } else { - http.Error(w, "Forbidden", http.StatusForbidden) - } - }) -} - -func Example_authenticationMiddleware() { - r := mux.NewRouter() - r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - // Do something here - }) - amw := authenticationMiddleware{make(map[string]string)} - amw.Populate() - r.Use(amw.Middleware) -} diff --git a/web/internal/mux/example_cors_method_middleware_test.go b/web/internal/mux/example_cors_method_middleware_test.go deleted file mode 100644 index acd51fac..00000000 --- a/web/internal/mux/example_cors_method_middleware_test.go +++ /dev/null @@ -1,37 +0,0 @@ -package mux_test - -import ( - "fmt" - "net/http" - "net/http/httptest" - - "go-spring.dev/spring/web/internal/mux" -) - -func ExampleCORSMethodMiddleware() { - r := mux.NewRouter() - - r.HandleFunc("/foo", func(w http.ResponseWriter, r *http.Request) { - // Handle the request - }).Methods(http.MethodGet, http.MethodPut, http.MethodPatch) - r.HandleFunc("/foo", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Access-Control-Allow-Origin", "http://example.com") - w.Header().Set("Access-Control-Max-Age", "86400") - }).Methods(http.MethodOptions) - - r.Use(mux.CORSMethodMiddleware(r)) - - rw := httptest.NewRecorder() - req, _ := http.NewRequest("OPTIONS", "/foo", nil) // needs to be OPTIONS - req.Header.Set("Access-Control-Request-Method", "POST") // needs to be non-empty - req.Header.Set("Access-Control-Request-Headers", "Authorization") // needs to be non-empty - req.Header.Set("Origin", "http://example.com") // needs to be non-empty - - r.ServeHTTP(rw, req) - - fmt.Println(rw.Header().Get("Access-Control-Allow-Methods")) - fmt.Println(rw.Header().Get("Access-Control-Allow-Origin")) - // Output: - // GET,PUT,PATCH,OPTIONS - // http://example.com -} diff --git a/web/internal/mux/example_route_test.go b/web/internal/mux/example_route_test.go deleted file mode 100644 index 5d80db22..00000000 --- a/web/internal/mux/example_route_test.go +++ /dev/null @@ -1,51 +0,0 @@ -package mux_test - -import ( - "fmt" - "net/http" - - "go-spring.dev/spring/web/internal/mux" -) - -// This example demonstrates setting a regular expression matcher for -// the header value. A plain word will match any value that contains a -// matching substring as if the pattern was wrapped with `.*`. -func ExampleRoute_HeadersRegexp() { - r := mux.NewRouter() - route := r.NewRoute().HeadersRegexp("Accept", "html") - - req1, _ := http.NewRequest("GET", "example.com", nil) - req1.Header.Add("Accept", "text/plain") - req1.Header.Add("Accept", "text/html") - - req2, _ := http.NewRequest("GET", "example.com", nil) - req2.Header.Set("Accept", "application/xhtml+xml") - - matchInfo := &mux.RouteMatch{} - fmt.Printf("Match: %v %q\n", route.Match(req1, matchInfo), req1.Header["Accept"]) - fmt.Printf("Match: %v %q\n", route.Match(req2, matchInfo), req2.Header["Accept"]) - // Output: - // Match: true ["text/plain" "text/html"] - // Match: true ["application/xhtml+xml"] -} - -// This example demonstrates setting a strict regular expression matcher -// for the header value. Using the start and end of string anchors, the -// value must be an exact match. -func ExampleRoute_HeadersRegexp_exactMatch() { - r := mux.NewRouter() - route := r.NewRoute().HeadersRegexp("Origin", "^https://example.co$") - - yes, _ := http.NewRequest("GET", "example.co", nil) - yes.Header.Set("Origin", "https://example.co") - - no, _ := http.NewRequest("GET", "example.co.uk", nil) - no.Header.Set("Origin", "https://example.co.uk") - - matchInfo := &mux.RouteMatch{} - fmt.Printf("Match: %v %q\n", route.Match(yes, matchInfo), yes.Header["Origin"]) - fmt.Printf("Match: %v %q\n", route.Match(no, matchInfo), no.Header["Origin"]) - // Output: - // Match: true ["https://example.co"] - // Match: false ["https://example.co.uk"] -} diff --git a/web/internal/mux/example_route_vars_test.go b/web/internal/mux/example_route_vars_test.go deleted file mode 100644 index ee750cf8..00000000 --- a/web/internal/mux/example_route_vars_test.go +++ /dev/null @@ -1,36 +0,0 @@ -package mux_test - -import ( - "fmt" - - "go-spring.dev/spring/web/internal/mux" -) - -// This example demonstrates building a dynamic URL using -// required vars and values retrieve from another source -func ExampleRoute_GetVarNames() { - r := mux.NewRouter() - - route := r.Host("{domain}"). - Path("/{group}/{item_id}"). - Queries("some_data1", "{some_data1}"). - Queries("some_data2_and_3", "{some_data2}.{some_data3}") - - dataSource := func(key string) string { - return "my_value_for_" + key - } - - varNames, _ := route.GetVarNames() - - pairs := make([]string, 0, len(varNames)*2) - - for _, varName := range varNames { - pairs = append(pairs, varName, dataSource(varName)) - } - - url, err := route.URL(pairs...) - if err != nil { - panic(err) - } - fmt.Println(url.String()) -} diff --git a/web/internal/mux/middleware.go b/web/internal/mux/middleware.go deleted file mode 100644 index cb51c565..00000000 --- a/web/internal/mux/middleware.go +++ /dev/null @@ -1,74 +0,0 @@ -package mux - -import ( - "net/http" - "strings" -) - -// MiddlewareFunc is a function which receives an http.Handler and returns another http.Handler. -// Typically, the returned handler is a closure which does something with the http.ResponseWriter and http.Request passed -// to it, and then calls the handler passed as parameter to the MiddlewareFunc. -type MiddlewareFunc func(http.Handler) http.Handler - -// middleware interface is anything which implements a MiddlewareFunc named Middleware. -type middleware interface { - Middleware(handler http.Handler) http.Handler -} - -// Middleware allows MiddlewareFunc to implement the middleware interface. -func (mw MiddlewareFunc) Middleware(handler http.Handler) http.Handler { - return mw(handler) -} - -// Use appends a MiddlewareFunc to the chain. Middleware can be used to intercept or otherwise modify requests and/or responses, and are executed in the order that they are applied to the Router. -func (r *Router) Use(mwf ...MiddlewareFunc) { - for _, fn := range mwf { - r.middlewares = append(r.middlewares, fn) - } -} - -// useInterface appends a middleware to the chain. Middleware can be used to intercept or otherwise modify requests and/or responses, and are executed in the order that they are applied to the Router. -func (r *Router) useInterface(mw middleware) { - r.middlewares = append(r.middlewares, mw) -} - -// CORSMethodMiddleware automatically sets the Access-Control-Allow-Methods response header -// on requests for routes that have an OPTIONS method matcher to all the method matchers on -// the route. Routes that do not explicitly handle OPTIONS requests will not be processed -// by the middleware. See examples for usage. -func CORSMethodMiddleware(r *Router) MiddlewareFunc { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - allMethods, err := getAllMethodsForRoute(r, req) - if err == nil { - for _, v := range allMethods { - if v == http.MethodOptions { - w.Header().Set("Access-Control-Allow-Methods", strings.Join(allMethods, ",")) - } - } - } - - next.ServeHTTP(w, req) - }) - } -} - -// getAllMethodsForRoute returns all the methods from method matchers matching a given -// request. -func getAllMethodsForRoute(r *Router, req *http.Request) ([]string, error) { - var allMethods []string - - for _, route := range r.routes { - var match RouteMatch - if route.Match(req, &match) || match.MatchErr == ErrMethodMismatch { - methods, err := route.GetMethods() - if err != nil { - return nil, err - } - - allMethods = append(allMethods, methods...) - } - } - - return allMethods, nil -} diff --git a/web/internal/mux/middleware_test.go b/web/internal/mux/middleware_test.go deleted file mode 100644 index 5300dbca..00000000 --- a/web/internal/mux/middleware_test.go +++ /dev/null @@ -1,622 +0,0 @@ -package mux - -import ( - "bytes" - "net/http" - "testing" -) - -type testMiddleware struct { - timesCalled uint -} - -func (tm *testMiddleware) Middleware(h http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - tm.timesCalled++ - h.ServeHTTP(w, r) - }) -} - -func dummyHandler(w http.ResponseWriter, r *http.Request) {} - -func TestMiddlewareAdd(t *testing.T) { - router := NewRouter() - router.HandleFunc("/", dummyHandler).Methods("GET") - - mw := &testMiddleware{} - - router.useInterface(mw) - if len(router.middlewares) != 1 || router.middlewares[0] != mw { - t.Fatal("Middleware interface was not added correctly") - } - - router.Use(mw.Middleware) - if len(router.middlewares) != 2 { - t.Fatal("Middleware method was not added correctly") - } - - banalMw := func(handler http.Handler) http.Handler { - return handler - } - router.Use(banalMw) - if len(router.middlewares) != 3 { - t.Fatal("Middleware function was not added correctly") - } -} - -func TestMiddleware(t *testing.T) { - router := NewRouter() - router.HandleFunc("/", dummyHandler).Methods("GET") - - mw := &testMiddleware{} - router.useInterface(mw) - - rw := NewRecorder() - req := newRequest("GET", "/") - - t.Run("regular middleware call", func(t *testing.T) { - router.ServeHTTP(rw, req) - if mw.timesCalled != 1 { - t.Fatalf("Expected %d calls, but got only %d", 1, mw.timesCalled) - } - }) - - t.Run("not called for 404", func(t *testing.T) { - req = newRequest("GET", "/not/found") - router.ServeHTTP(rw, req) - if mw.timesCalled != 1 { - t.Fatalf("Expected %d calls, but got only %d", 1, mw.timesCalled) - } - }) - - t.Run("not called for method mismatch", func(t *testing.T) { - req = newRequest("POST", "/") - router.ServeHTTP(rw, req) - if mw.timesCalled != 1 { - t.Fatalf("Expected %d calls, but got only %d", 1, mw.timesCalled) - } - }) - - t.Run("regular call using function middleware", func(t *testing.T) { - router.Use(mw.Middleware) - req = newRequest("GET", "/") - router.ServeHTTP(rw, req) - if mw.timesCalled != 3 { - t.Fatalf("Expected %d calls, but got only %d", 3, mw.timesCalled) - } - }) -} - -func TestMiddlewareSubrouter(t *testing.T) { - router := NewRouter() - router.HandleFunc("/", dummyHandler).Methods("GET") - - subrouter := router.PathPrefix("/sub").Subrouter() - subrouter.HandleFunc("/x", dummyHandler).Methods("GET") - - mw := &testMiddleware{} - subrouter.useInterface(mw) - - rw := NewRecorder() - req := newRequest("GET", "/") - - t.Run("not called for route outside subrouter", func(t *testing.T) { - router.ServeHTTP(rw, req) - if mw.timesCalled != 0 { - t.Fatalf("Expected %d calls, but got only %d", 0, mw.timesCalled) - } - }) - - t.Run("not called for subrouter root 404", func(t *testing.T) { - req = newRequest("GET", "/sub/") - router.ServeHTTP(rw, req) - if mw.timesCalled != 0 { - t.Fatalf("Expected %d calls, but got only %d", 0, mw.timesCalled) - } - }) - - t.Run("called once for route inside subrouter", func(t *testing.T) { - req = newRequest("GET", "/sub/x") - router.ServeHTTP(rw, req) - if mw.timesCalled != 1 { - t.Fatalf("Expected %d calls, but got only %d", 1, mw.timesCalled) - } - }) - - t.Run("not called for 404 inside subrouter", func(t *testing.T) { - req = newRequest("GET", "/sub/not/found") - router.ServeHTTP(rw, req) - if mw.timesCalled != 1 { - t.Fatalf("Expected %d calls, but got only %d", 1, mw.timesCalled) - } - }) - - t.Run("middleware added to router", func(t *testing.T) { - router.useInterface(mw) - - t.Run("called once for route outside subrouter", func(t *testing.T) { - req = newRequest("GET", "/") - router.ServeHTTP(rw, req) - if mw.timesCalled != 2 { - t.Fatalf("Expected %d calls, but got only %d", 2, mw.timesCalled) - } - }) - - t.Run("called twice for route inside subrouter", func(t *testing.T) { - req = newRequest("GET", "/sub/x") - router.ServeHTTP(rw, req) - if mw.timesCalled != 4 { - t.Fatalf("Expected %d calls, but got only %d", 4, mw.timesCalled) - } - }) - }) -} - -func TestMiddlewareExecution(t *testing.T) { - mwStr := []byte("Middleware\n") - handlerStr := []byte("Logic\n") - - router := NewRouter() - router.HandleFunc("/", func(w http.ResponseWriter, e *http.Request) { - _, err := w.Write(handlerStr) - if err != nil { - t.Fatalf("Failed writing HTTP response: %v", err) - } - }) - - t.Run("responds normally without middleware", func(t *testing.T) { - rw := NewRecorder() - req := newRequest("GET", "/") - - router.ServeHTTP(rw, req) - - if !bytes.Equal(rw.Body.Bytes(), handlerStr) { - t.Fatal("Handler response is not what it should be") - } - }) - - t.Run("responds with handler and middleware response", func(t *testing.T) { - rw := NewRecorder() - req := newRequest("GET", "/") - - router.Use(func(h http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, err := w.Write(mwStr) - if err != nil { - t.Fatalf("Failed writing HTTP response: %v", err) - } - h.ServeHTTP(w, r) - }) - }) - - router.ServeHTTP(rw, req) - if !bytes.Equal(rw.Body.Bytes(), append(mwStr, handlerStr...)) { - t.Fatal("Middleware + handler response is not what it should be") - } - }) -} - -func TestMiddlewareNotFound(t *testing.T) { - mwStr := []byte("Middleware\n") - handlerStr := []byte("Logic\n") - - router := NewRouter() - router.HandleFunc("/", func(w http.ResponseWriter, e *http.Request) { - _, err := w.Write(handlerStr) - if err != nil { - t.Fatalf("Failed writing HTTP response: %v", err) - } - }) - router.Use(func(h http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, err := w.Write(mwStr) - if err != nil { - t.Fatalf("Failed writing HTTP response: %v", err) - } - h.ServeHTTP(w, r) - }) - }) - - // Test not found call with default handler - t.Run("not called", func(t *testing.T) { - rw := NewRecorder() - req := newRequest("GET", "/notfound") - - router.ServeHTTP(rw, req) - if bytes.Contains(rw.Body.Bytes(), mwStr) { - t.Fatal("Middleware was called for a 404") - } - }) - - t.Run("not called with custom not found handler", func(t *testing.T) { - rw := NewRecorder() - req := newRequest("GET", "/notfound") - - router.NotFoundHandler = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - _, err := rw.Write([]byte("Custom 404 handler")) - if err != nil { - t.Fatalf("Failed writing HTTP response: %v", err) - } - }) - router.ServeHTTP(rw, req) - - if !bytes.Contains(rw.Body.Bytes(), mwStr) { - t.Fatal("Middleware not called for a custom 404") - } - }) -} - -func TestMiddlewareMethodMismatch(t *testing.T) { - mwStr := []byte("Middleware\n") - handlerStr := []byte("Logic\n") - - router := NewRouter() - router.HandleFunc("/", func(w http.ResponseWriter, e *http.Request) { - _, err := w.Write(handlerStr) - if err != nil { - t.Fatalf("Failed writing HTTP response: %v", err) - } - }).Methods("GET") - - router.Use(func(h http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, err := w.Write(mwStr) - if err != nil { - t.Fatalf("Failed writing HTTP response: %v", err) - } - h.ServeHTTP(w, r) - }) - }) - - t.Run("not called", func(t *testing.T) { - rw := NewRecorder() - req := newRequest("POST", "/") - - router.ServeHTTP(rw, req) - if bytes.Contains(rw.Body.Bytes(), mwStr) { - t.Fatal("Middleware was called for a method mismatch") - } - }) - - t.Run("not called with custom method not allowed handler", func(t *testing.T) { - rw := NewRecorder() - req := newRequest("POST", "/") - - router.MethodNotAllowedHandler = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - _, err := rw.Write([]byte("Method not allowed")) - if err != nil { - t.Fatalf("Failed writing HTTP response: %v", err) - } - }) - router.ServeHTTP(rw, req) - - if !bytes.Contains(rw.Body.Bytes(), mwStr) { - t.Fatal("Middleware not called for a method mismatch") - } - }) -} - -func TestMiddlewareNotFoundSubrouter(t *testing.T) { - mwStr := []byte("Middleware\n") - handlerStr := []byte("Logic\n") - - router := NewRouter() - router.HandleFunc("/", func(w http.ResponseWriter, e *http.Request) { - _, err := w.Write(handlerStr) - if err != nil { - t.Fatalf("Failed writing HTTP response: %v", err) - } - }) - - subrouter := router.PathPrefix("/sub/").Subrouter() - subrouter.HandleFunc("/", func(w http.ResponseWriter, e *http.Request) { - _, err := w.Write(handlerStr) - if err != nil { - t.Fatalf("Failed writing HTTP response: %v", err) - } - }) - - router.Use(func(h http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, err := w.Write(mwStr) - if err != nil { - t.Fatalf("Failed writing HTTP response: %v", err) - } - h.ServeHTTP(w, r) - }) - }) - - t.Run("not called", func(t *testing.T) { - rw := NewRecorder() - req := newRequest("GET", "/sub/notfound") - - router.ServeHTTP(rw, req) - if bytes.Contains(rw.Body.Bytes(), mwStr) { - t.Fatal("Middleware was called for a 404") - } - }) - - t.Run("not called with custom not found handler", func(t *testing.T) { - rw := NewRecorder() - req := newRequest("GET", "/sub/notfound") - - subrouter.NotFoundHandler = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - _, err := rw.Write([]byte("Custom 404 handler")) - if err != nil { - t.Fatalf("Failed writing HTTP response: %v", err) - } - }) - router.ServeHTTP(rw, req) - - if !bytes.Contains(rw.Body.Bytes(), mwStr) { - t.Fatal("Middleware not called for a custom 404") - } - }) -} - -func TestMiddlewareMethodMismatchSubrouter(t *testing.T) { - mwStr := []byte("Middleware\n") - handlerStr := []byte("Logic\n") - - router := NewRouter() - router.HandleFunc("/", func(w http.ResponseWriter, e *http.Request) { - _, err := w.Write(handlerStr) - if err != nil { - t.Fatalf("Failed writing HTTP response: %v", err) - } - }) - - subrouter := router.PathPrefix("/sub/").Subrouter() - subrouter.HandleFunc("/", func(w http.ResponseWriter, e *http.Request) { - _, err := w.Write(handlerStr) - if err != nil { - t.Fatalf("Failed writing HTTP response: %v", err) - } - }).Methods("GET") - - router.Use(func(h http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, err := w.Write(mwStr) - if err != nil { - t.Fatalf("Failed writing HTTP response: %v", err) - } - h.ServeHTTP(w, r) - }) - }) - - t.Run("not called", func(t *testing.T) { - rw := NewRecorder() - req := newRequest("POST", "/sub/") - - router.ServeHTTP(rw, req) - if bytes.Contains(rw.Body.Bytes(), mwStr) { - t.Fatal("Middleware was called for a method mismatch") - } - }) - - t.Run("not called with custom method not allowed handler", func(t *testing.T) { - rw := NewRecorder() - req := newRequest("POST", "/sub/") - - router.MethodNotAllowedHandler = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - _, err := rw.Write([]byte("Method not allowed")) - if err != nil { - t.Fatalf("Failed writing HTTP response: %v", err) - } - }) - router.ServeHTTP(rw, req) - - if !bytes.Contains(rw.Body.Bytes(), mwStr) { - t.Fatal("Middleware not called for a method mismatch") - } - }) -} - -func TestCORSMethodMiddleware(t *testing.T) { - testCases := []struct { - name string - registerRoutes func(r *Router) - requestHeader http.Header - requestMethod string - requestPath string - expectedAccessControlAllowMethodsHeader string - expectedResponse string - }{ - { - name: "does not set without OPTIONS matcher", - registerRoutes: func(r *Router) { - r.HandleFunc("/foo", stringHandler("a")).Methods(http.MethodGet, http.MethodPut, http.MethodPatch) - }, - requestMethod: "GET", - requestPath: "/foo", - expectedAccessControlAllowMethodsHeader: "", - expectedResponse: "a", - }, - { - name: "sets on non OPTIONS", - registerRoutes: func(r *Router) { - r.HandleFunc("/foo", stringHandler("a")).Methods(http.MethodGet, http.MethodPut, http.MethodPatch) - r.HandleFunc("/foo", stringHandler("b")).Methods(http.MethodOptions) - }, - requestMethod: "GET", - requestPath: "/foo", - expectedAccessControlAllowMethodsHeader: "GET,PUT,PATCH,OPTIONS", - expectedResponse: "a", - }, - { - name: "sets without preflight headers", - registerRoutes: func(r *Router) { - r.HandleFunc("/foo", stringHandler("a")).Methods(http.MethodGet, http.MethodPut, http.MethodPatch) - r.HandleFunc("/foo", stringHandler("b")).Methods(http.MethodOptions) - }, - requestMethod: "OPTIONS", - requestPath: "/foo", - expectedAccessControlAllowMethodsHeader: "GET,PUT,PATCH,OPTIONS", - expectedResponse: "b", - }, - { - name: "does not set on error", - registerRoutes: func(r *Router) { - r.HandleFunc("/foo", stringHandler("a")) - }, - requestMethod: "OPTIONS", - requestPath: "/foo", - expectedAccessControlAllowMethodsHeader: "", - expectedResponse: "a", - }, - { - name: "sets header on valid preflight", - registerRoutes: func(r *Router) { - r.HandleFunc("/foo", stringHandler("a")).Methods(http.MethodGet, http.MethodPut, http.MethodPatch) - r.HandleFunc("/foo", stringHandler("b")).Methods(http.MethodOptions) - }, - requestMethod: "OPTIONS", - requestPath: "/foo", - requestHeader: http.Header{ - "Access-Control-Request-Method": []string{"GET"}, - "Access-Control-Request-Headers": []string{"Authorization"}, - "Origin": []string{"http://example.com"}, - }, - expectedAccessControlAllowMethodsHeader: "GET,PUT,PATCH,OPTIONS", - expectedResponse: "b", - }, - { - name: "does not set methods from unmatching routes", - registerRoutes: func(r *Router) { - r.HandleFunc("/foo", stringHandler("c")).Methods(http.MethodDelete) - r.HandleFunc("/foo/bar", stringHandler("a")).Methods(http.MethodGet, http.MethodPut, http.MethodPatch) - r.HandleFunc("/foo/bar", stringHandler("b")).Methods(http.MethodOptions) - }, - requestMethod: "OPTIONS", - requestPath: "/foo/bar", - requestHeader: http.Header{ - "Access-Control-Request-Method": []string{"GET"}, - "Access-Control-Request-Headers": []string{"Authorization"}, - "Origin": []string{"http://example.com"}, - }, - expectedAccessControlAllowMethodsHeader: "GET,PUT,PATCH,OPTIONS", - expectedResponse: "b", - }, - } - - for _, tt := range testCases { - t.Run(tt.name, func(t *testing.T) { - router := NewRouter() - - tt.registerRoutes(router) - - router.Use(CORSMethodMiddleware(router)) - - rw := NewRecorder() - req := newRequest(tt.requestMethod, tt.requestPath) - req.Header = tt.requestHeader - - router.ServeHTTP(rw, req) - - actualMethodsHeader := rw.Header().Get("Access-Control-Allow-Methods") - if actualMethodsHeader != tt.expectedAccessControlAllowMethodsHeader { - t.Fatalf("Expected Access-Control-Allow-Methods to equal %s but got %s", tt.expectedAccessControlAllowMethodsHeader, actualMethodsHeader) - } - - actualResponse := rw.Body.String() - if actualResponse != tt.expectedResponse { - t.Fatalf("Expected response to equal %s but got %s", tt.expectedResponse, actualResponse) - } - }) - } -} - -func TestCORSMethodMiddlewareSubrouter(t *testing.T) { - router := NewRouter().StrictSlash(true) - - subrouter := router.PathPrefix("/test").Subrouter() - subrouter.HandleFunc("/hello", stringHandler("a")).Methods(http.MethodGet, http.MethodOptions, http.MethodPost) - subrouter.HandleFunc("/hello/{name}", stringHandler("b")).Methods(http.MethodGet, http.MethodOptions) - - subrouter.Use(CORSMethodMiddleware(subrouter)) - - rw := NewRecorder() - req := newRequest("GET", "/test/hello/asdf") - router.ServeHTTP(rw, req) - - actualMethods := rw.Header().Get("Access-Control-Allow-Methods") - expectedMethods := "GET,OPTIONS" - if actualMethods != expectedMethods { - t.Fatalf("expected methods %q but got: %q", expectedMethods, actualMethods) - } -} - -func TestMiddlewareOnMultiSubrouter(t *testing.T) { - first := "first" - second := "second" - notFound := "404 not found" - - router := NewRouter() - firstSubRouter := router.PathPrefix("/").Subrouter() - secondSubRouter := router.PathPrefix("/").Subrouter() - - router.NotFoundHandler = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - _, err := rw.Write([]byte(notFound)) - if err != nil { - t.Fatalf("Failed writing HTTP response: %v", err) - } - }) - - firstSubRouter.HandleFunc("/first", func(w http.ResponseWriter, r *http.Request) { - - }) - - secondSubRouter.HandleFunc("/second", func(w http.ResponseWriter, r *http.Request) { - - }) - - firstSubRouter.Use(func(h http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, err := w.Write([]byte(first)) - if err != nil { - t.Fatalf("Failed writing HTTP response: %v", err) - } - h.ServeHTTP(w, r) - }) - }) - - secondSubRouter.Use(func(h http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, err := w.Write([]byte(second)) - if err != nil { - t.Fatalf("Failed writing HTTP response: %v", err) - } - h.ServeHTTP(w, r) - }) - }) - - t.Run("/first uses first middleware", func(t *testing.T) { - rw := NewRecorder() - req := newRequest("GET", "/first") - - router.ServeHTTP(rw, req) - if rw.Body.String() != first { - t.Fatalf("Middleware did not run: expected %s middleware to write a response (got %s)", first, rw.Body.String()) - } - }) - - t.Run("/second uses second middleware", func(t *testing.T) { - rw := NewRecorder() - req := newRequest("GET", "/second") - - router.ServeHTTP(rw, req) - if rw.Body.String() != second { - t.Fatalf("Middleware did not run: expected %s middleware to write a response (got %s)", second, rw.Body.String()) - } - }) - - t.Run("uses not found handler", func(t *testing.T) { - rw := NewRecorder() - req := newRequest("GET", "/second/not-exist") - - router.ServeHTTP(rw, req) - if rw.Body.String() != notFound { - t.Fatalf("Notfound handler did not run: expected %s for not-exist, (got %s)", notFound, rw.Body.String()) - } - }) -} diff --git a/web/internal/mux/mux.go b/web/internal/mux/mux.go deleted file mode 100644 index 2a3a3d33..00000000 --- a/web/internal/mux/mux.go +++ /dev/null @@ -1,644 +0,0 @@ -// 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 ( - "context" - "errors" - "fmt" - "net/http" - "net/url" - "path" - "regexp" -) - -var ( - // ErrMethodMismatch is returned when the method in the request does not match - // the method defined against the route. - ErrMethodMismatch = errors.New("method is not allowed") - // ErrNotFound is returned when no route match is found. - ErrNotFound = errors.New("no matching route was found") -) - -// NewRouter returns a new router instance. -func NewRouter() *Router { - return &Router{namedRoutes: make(map[string]*Route)} -} - -// Router registers routes to be matched and dispatches a handler. -// -// It implements the http.Handler interface, so it can be registered to serve -// requests: -// -// var router = mux.NewRouter() -// -// func main() { -// http.Handle("/", router) -// } -// -// Or, for Google App Engine, register it in a init() function: -// -// func init() { -// http.Handle("/", router) -// } -// -// This will send all incoming requests to the router. -type Router struct { - // Configurable Handler to be used when no route matches. - // This can be used to render your own 404 Not Found errors. - NotFoundHandler http.Handler - - // Configurable Handler to be used when the request method does not match the route. - // This can be used to render your own 405 Method Not Allowed errors. - MethodNotAllowedHandler http.Handler - - // Routes to be matched, in order. - routes []*Route - - // Routes by name for URL building. - namedRoutes map[string]*Route - - // If true, do not clear the request context after handling the request. - // - // Deprecated: No effect, since the context is stored on the request itself. - KeepContext bool - - // Slice of middlewares to be called after a match is found - middlewares []middleware - - // configuration shared with `Route` - routeConf -} - -// common route configuration shared between `Router` and `Route` -type routeConf struct { - // If true, "/path/foo%2Fbar/to" will match the path "/path/{var}/to" - useEncodedPath bool - - // 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, the http.Request context will not contain the Route. - omitRouteFromContext bool - - // Manager for the variables from host and path. - regexp routeRegexpGroup - - // List of matchers. - matchers []matcher - - // The scheme used when building URLs. - buildScheme string - - buildVarsFunc BuildVarsFunc -} - -// returns an effective deep copy of `routeConf` -func copyRouteConf(r routeConf) routeConf { - c := r - - if r.regexp.path != nil { - c.regexp.path = copyRouteRegexp(r.regexp.path) - } - - if r.regexp.host != nil { - c.regexp.host = copyRouteRegexp(r.regexp.host) - } - - c.regexp.queries = make([]*routeRegexp, 0, len(r.regexp.queries)) - for _, q := range r.regexp.queries { - c.regexp.queries = append(c.regexp.queries, copyRouteRegexp(q)) - } - - c.matchers = make([]matcher, len(r.matchers)) - copy(c.matchers, r.matchers) - - return c -} - -func copyRouteRegexp(r *routeRegexp) *routeRegexp { - c := *r - return &c -} - -// Match attempts to match the given request against the router's registered routes. -// -// If the request matches a route of this router or one of its subrouters the Route, -// Handler, and Vars fields of the the match argument are filled and this function -// returns true. -// -// If the request does not match any of this router's or its subrouters' routes -// then this function returns false. If available, a reason for the match failure -// will be filled in the match argument's MatchErr field. If the match failure type -// (eg: not found) has a registered handler, the handler is assigned to the Handler -// field of the match argument. -func (r *Router) Match(req *http.Request, match *RouteMatch) bool { - for _, route := range r.routes { - if route.Match(req, match) { - for i := len(r.middlewares) - 1; i >= 0; i-- { - match.Handler = r.middlewares[i].Middleware(match.Handler) - } - return true - } - } - - if match.MatchErr == ErrMethodMismatch { - if r.MethodNotAllowedHandler != nil { - match.Handler = r.MethodNotAllowedHandler - for i := len(r.middlewares) - 1; i >= 0; i-- { - match.Handler = r.middlewares[i].Middleware(match.Handler) - } - return true - } - - return false - } - - // Closest match for a router (includes sub-routers) - if r.NotFoundHandler != nil { - match.MatchErr = ErrNotFound - match.Handler = r.NotFoundHandler - for i := len(r.middlewares) - 1; i >= 0; i-- { - match.Handler = r.middlewares[i].Middleware(match.Handler) - } - return true - } - - match.MatchErr = ErrNotFound - return false -} - -// ServeHTTP dispatches the handler registered in the matched route. -// -// When there is a match, the route variables can be retrieved calling -// mux.Vars(request). -func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { - if !r.skipClean { - path := req.URL.Path - if r.useEncodedPath { - path = req.URL.EscapedPath() - } - // Clean path to canonical form and redirect. - if p := cleanPath(path); p != path { - w.Header().Set("Location", replaceURLPath(req.URL, p)) - w.WriteHeader(http.StatusMovedPermanently) - return - } - } - var match RouteMatch - var handler http.Handler - if r.Match(req, &match) { - handler = match.Handler - if handler != nil { - // Populate context for custom handlers - if r.omitRouteFromContext { - // Only populate the match vars (if any) into the context. - req = requestWithVars(req, match.Vars) - } else { - req = requestWithRouteAndVars(req, match.Route, match.Vars) - } - } - } - - if handler == nil && match.MatchErr == ErrMethodMismatch { - handler = methodNotAllowedHandler() - } - - if handler == nil { - handler = http.NotFoundHandler() - } - - handler.ServeHTTP(w, req) -} - -// Get returns a route registered with the given name. -func (r *Router) Get(name string) *Route { - return r.namedRoutes[name] -} - -// GetRoute returns a route registered with the given name. This method -// was renamed to Get() and remains here for backwards compatibility. -func (r *Router) GetRoute(name string) *Route { - return r.namedRoutes[name] -} - -// StrictSlash defines the trailing slash behavior for new routes. The initial -// value is false. -// -// When true, if the route path is "/path/", accessing "/path" will perform a redirect -// to the former and vice versa. In other words, your application will always -// see the path as specified in the route. -// -// When false, if the route path is "/path", accessing "/path/" will not match -// this route and vice versa. -// -// The redirect is a HTTP 301 (Moved Permanently). Note that when this is set for -// routes with a non-idempotent method (e.g. POST, PUT), the subsequent redirected -// request will be made as a GET by most clients. Use middleware or client settings -// to modify this behaviour as needed. -// -// Special case: when a route sets a path prefix using the PathPrefix() method, -// strict slash is ignored for that route because the redirect behavior can't -// be determined from a prefix alone. However, any subrouters created from that -// route inherit the original StrictSlash setting. -func (r *Router) StrictSlash(value bool) *Router { - r.strictSlash = value - return r -} - -// SkipClean defines the path cleaning behaviour for new routes. The initial -// value is false. Users should be careful about which routes are not cleaned -// -// When true, if the route path is "/path//to", it will remain with the double -// slash. This is helpful if you have a route like: /fetch/http://xkcd.com/534/ -// -// When false, the path will be cleaned, so /fetch/http://xkcd.com/534/ will -// become /fetch/http/xkcd.com/534 -func (r *Router) SkipClean(value bool) *Router { - r.skipClean = value - return r -} - -// OmitRouteFromContext defines the behavior of omitting the Route from the -// -// http.Request context. -// -// CurrentRoute will yield nil with this option. -func (r *Router) OmitRouteFromContext(value bool) *Router { - r.omitRouteFromContext = value - return r -} - -// UseEncodedPath tells the router to match the encoded original path -// to the routes. -// For eg. "/path/foo%2Fbar/to" will match the path "/path/{var}/to". -// -// If not called, the router will match the unencoded path to the routes. -// For eg. "/path/foo%2Fbar/to" will match the path "/path/foo/bar/to" -func (r *Router) UseEncodedPath() *Router { - r.useEncodedPath = true - return r -} - -// ---------------------------------------------------------------------------- -// Route factories -// ---------------------------------------------------------------------------- - -// NewRoute registers an empty route. -func (r *Router) NewRoute() *Route { - // initialize a route with a copy of the parent router's configuration - route := &Route{routeConf: copyRouteConf(r.routeConf), namedRoutes: r.namedRoutes} - r.routes = append(r.routes, route) - return route -} - -// Name registers a new route with a name. -// See Route.Name(). -func (r *Router) Name(name string) *Route { - return r.NewRoute().Name(name) -} - -// Handle registers a new route with a matcher for the URL path. -// See Route.Path() and Route.Handler(). -func (r *Router) Handle(path string, handler http.Handler) *Route { - return r.NewRoute().Path(path).Handler(handler) -} - -// HandleFunc registers a new route with a matcher for the URL path. -// See Route.Path() and Route.HandlerFunc(). -func (r *Router) HandleFunc(path string, f func(http.ResponseWriter, - *http.Request)) *Route { - return r.NewRoute().Path(path).HandlerFunc(f) -} - -// Headers registers a new route with a matcher for request header values. -// See Route.Headers(). -func (r *Router) Headers(pairs ...string) *Route { - return r.NewRoute().Headers(pairs...) -} - -// Host registers a new route with a matcher for the URL host. -// See Route.Host(). -func (r *Router) Host(tpl string) *Route { - return r.NewRoute().Host(tpl) -} - -// MatcherFunc registers a new route with a custom matcher function. -// See Route.MatcherFunc(). -func (r *Router) MatcherFunc(f MatcherFunc) *Route { - return r.NewRoute().MatcherFunc(f) -} - -// Methods registers a new route with a matcher for HTTP methods. -// See Route.Methods(). -func (r *Router) Methods(methods ...string) *Route { - return r.NewRoute().Methods(methods...) -} - -// Path registers a new route with a matcher for the URL path. -// See Route.Path(). -func (r *Router) Path(tpl string) *Route { - return r.NewRoute().Path(tpl) -} - -// PathPrefix registers a new route with a matcher for the URL path prefix. -// See Route.PathPrefix(). -func (r *Router) PathPrefix(tpl string) *Route { - return r.NewRoute().PathPrefix(tpl) -} - -// Queries registers a new route with a matcher for URL query values. -// See Route.Queries(). -func (r *Router) Queries(pairs ...string) *Route { - return r.NewRoute().Queries(pairs...) -} - -// Schemes registers a new route with a matcher for URL schemes. -// See Route.Schemes(). -func (r *Router) Schemes(schemes ...string) *Route { - return r.NewRoute().Schemes(schemes...) -} - -// BuildVarsFunc registers a new route with a custom function for modifying -// route variables before building a URL. -func (r *Router) BuildVarsFunc(f BuildVarsFunc) *Route { - return r.NewRoute().BuildVarsFunc(f) -} - -// Walk walks the router and all its sub-routers, calling walkFn for each route -// in the tree. The routes are walked in the order they were added. Sub-routers -// are explored depth-first. -func (r *Router) Walk(walkFn WalkFunc) error { - return r.walk(walkFn, []*Route{}) -} - -// SkipRouter is used as a return value from WalkFuncs to indicate that the -// router that walk is about to descend down to should be skipped. -var SkipRouter = errors.New("skip this router") - -// WalkFunc is the type of the function called for each route visited by Walk. -// At every invocation, it is given the current route, and the current router, -// and a list of ancestor routes that lead to the current route. -type WalkFunc func(route *Route, router *Router, ancestors []*Route) error - -func (r *Router) walk(walkFn WalkFunc, ancestors []*Route) error { - for _, t := range r.routes { - err := walkFn(t, r, ancestors) - if err == SkipRouter { - continue - } - if err != nil { - return err - } - for _, sr := range t.matchers { - if h, ok := sr.(*Router); ok { - ancestors = append(ancestors, t) - err := h.walk(walkFn, ancestors) - if err != nil { - return err - } - ancestors = ancestors[:len(ancestors)-1] - } - } - if h, ok := t.handler.(*Router); ok { - ancestors = append(ancestors, t) - err := h.walk(walkFn, ancestors) - if err != nil { - return err - } - ancestors = ancestors[:len(ancestors)-1] - } - } - return nil -} - -// ---------------------------------------------------------------------------- -// Context -// ---------------------------------------------------------------------------- - -// RouteMatch stores information about a matched route. -type RouteMatch struct { - Route *Route - Handler http.Handler - Vars map[string]string - - // MatchErr is set to appropriate matching error - // It is set to ErrMethodMismatch if there is a mismatch in - // the request method and route method - MatchErr error -} - -type contextKey int - -const ( - varsKey contextKey = iota - routeKey -) - -// Vars returns the route variables for the current request, if any. -func Vars(r *http.Request) map[string]string { - if rv := r.Context().Value(varsKey); rv != nil { - return rv.(map[string]string) - } - return nil -} - -// CurrentRoute returns the matched route for the current request, if any. -// This only works when called inside the handler of the matched route -// because the matched route is stored in the request context which is cleared -// after the handler returns. -func CurrentRoute(r *http.Request) *Route { - if rv := r.Context().Value(routeKey); rv != nil { - return rv.(*Route) - } - return nil -} - -// requestWithRouteAndVars adds the matched vars to the request ctx. -// It shortcuts the operation when the vars are empty. -func requestWithVars(r *http.Request, vars map[string]string) *http.Request { - if len(vars) == 0 { - return r - } - ctx := context.WithValue(r.Context(), varsKey, vars) - return r.WithContext(ctx) -} - -// requestWithRouteAndVars adds the matched route and vars to the request ctx. -// It saves extra allocations in cloning the request once and skipping the -// -// population of empty vars, which in turn mux.Vars can handle gracefully. -func requestWithRouteAndVars(r *http.Request, route *Route, vars map[string]string) *http.Request { - ctx := context.WithValue(r.Context(), routeKey, route) - if len(vars) > 0 { - ctx = context.WithValue(ctx, varsKey, vars) - } - return r.WithContext(ctx) -} - -// ---------------------------------------------------------------------------- -// Helpers -// ---------------------------------------------------------------------------- - -// cleanPath returns the canonical path for p, eliminating . and .. elements. -// Borrowed from the net/http package. -func cleanPath(p string) string { - if p == "" { - return "/" - } - if p[0] != '/' { - p = "/" + p - } - np := path.Clean(p) - // path.Clean removes trailing slash except for root; - // put the trailing slash back if necessary. - if p[len(p)-1] == '/' && np != "/" { - np += "/" - } - - return np -} - -// replaceURLPath prints an url.URL with a different path. -func replaceURLPath(u *url.URL, p string) string { - // Operate on a copy of the request url. - u2 := *u - u2.Path = p - return u2.String() -} - -// uniqueVars returns an error if two slices contain duplicated strings. -func uniqueVars(s1, s2 []string) error { - for _, v1 := range s1 { - for _, v2 := range s2 { - if v1 == v2 { - return fmt.Errorf("mux: duplicated route variable %q", v2) - } - } - } - return nil -} - -// checkPairs returns the count of strings passed in, and an error if -// the count is not an even number. -func checkPairs(pairs ...string) (int, error) { - length := len(pairs) - if length%2 != 0 { - return length, fmt.Errorf( - "mux: number of parameters must be multiple of 2, got %v", pairs) - } - return length, nil -} - -// mapFromPairsToString converts variadic string parameters to a -// string to string map. -func mapFromPairsToString(pairs ...string) (map[string]string, error) { - length, err := checkPairs(pairs...) - if err != nil { - return nil, err - } - m := make(map[string]string, length/2) - for i := 0; i < length; i += 2 { - m[pairs[i]] = pairs[i+1] - } - return m, nil -} - -// mapFromPairsToRegex converts variadic string parameters to a -// string to regex map. -func mapFromPairsToRegex(pairs ...string) (map[string]*regexp.Regexp, error) { - length, err := checkPairs(pairs...) - if err != nil { - return nil, err - } - m := make(map[string]*regexp.Regexp, length/2) - for i := 0; i < length; i += 2 { - regex, err := regexp.Compile(pairs[i+1]) - if err != nil { - return nil, err - } - m[pairs[i]] = regex - } - return m, nil -} - -// matchInArray returns true if the given string value is in the array. -func matchInArray(arr []string, value string) bool { - for _, v := range arr { - if v == value { - return true - } - } - return false -} - -// matchMapWithString returns true if the given key/value pairs exist in a given map. -func matchMapWithString(toCheck map[string]string, toMatch map[string][]string, canonicalKey bool) bool { - for k, v := range toCheck { - // Check if key exists. - if canonicalKey { - k = http.CanonicalHeaderKey(k) - } - if values := toMatch[k]; values == nil { - return false - } else if v != "" { - // If value was defined as an empty string we only check that the - // key exists. Otherwise we also check for equality. - valueExists := false - for _, value := range values { - if v == value { - valueExists = true - break - } - } - if !valueExists { - return false - } - } - } - return true -} - -// matchMapWithRegex returns true if the given key/value pairs exist in a given map compiled against -// the given regex -func matchMapWithRegex(toCheck map[string]*regexp.Regexp, toMatch map[string][]string, canonicalKey bool) bool { - for k, v := range toCheck { - // Check if key exists. - if canonicalKey { - k = http.CanonicalHeaderKey(k) - } - if values := toMatch[k]; values == nil { - return false - } else if v != nil { - // If value was defined as an empty string we only check that the - // key exists. Otherwise we also check for equality. - valueExists := false - for _, value := range values { - if v.MatchString(value) { - valueExists = true - break - } - } - if !valueExists { - return false - } - } - } - return true -} - -// methodNotAllowed replies to the request with an HTTP status code 405. -func methodNotAllowed(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusMethodNotAllowed) -} - -// methodNotAllowedHandler returns a simple request handler -// that replies to each request with a status code 405. -func methodNotAllowedHandler() http.Handler { return http.HandlerFunc(methodNotAllowed) } diff --git a/web/internal/mux/mux_httpserver_test.go b/web/internal/mux/mux_httpserver_test.go deleted file mode 100644 index f55a2de3..00000000 --- a/web/internal/mux/mux_httpserver_test.go +++ /dev/null @@ -1,56 +0,0 @@ -//go:build go1.9 -// +build go1.9 - -package mux - -import ( - "bytes" - "io" - "net/http" - "net/http/httptest" - "testing" -) - -func TestSchemeMatchers(t *testing.T) { - router := NewRouter() - router.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) { - _, err := rw.Write([]byte("hello http world")) - if err != nil { - t.Fatalf("Failed writing HTTP response: %v", err) - } - }).Schemes("http") - router.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) { - _, err := rw.Write([]byte("hello https world")) - if err != nil { - t.Fatalf("Failed writing HTTP response: %v", err) - } - }).Schemes("https") - - assertResponseBody := func(t *testing.T, s *httptest.Server, expectedBody string) { - resp, err := s.Client().Get(s.URL) - if err != nil { - t.Fatalf("unexpected error getting from server: %v", err) - } - if resp.StatusCode != 200 { - t.Fatalf("expected a status code of 200, got %v", resp.StatusCode) - } - body, err := io.ReadAll(resp.Body) - if err != nil { - t.Fatalf("unexpected error reading body: %v", err) - } - if !bytes.Equal(body, []byte(expectedBody)) { - t.Fatalf("response should be hello world, was: %q", string(body)) - } - } - - t.Run("httpServer", func(t *testing.T) { - s := httptest.NewServer(router) - defer s.Close() - assertResponseBody(t, s, "hello http world") - }) - t.Run("httpsServer", func(t *testing.T) { - s := httptest.NewTLSServer(router) - defer s.Close() - assertResponseBody(t, s, "hello https world") - }) -} diff --git a/web/internal/mux/mux_test.go b/web/internal/mux/mux_test.go deleted file mode 100644 index cb3bbe0a..00000000 --- a/web/internal/mux/mux_test.go +++ /dev/null @@ -1,3139 +0,0 @@ -// 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 ( - "bufio" - "bytes" - "context" - "errors" - "fmt" - "io" - "log" - "net/http" - "net/http/httptest" - "net/url" - "reflect" - "strings" - "testing" - "time" -) - -func (r *Route) GoString() string { - matchers := make([]string, len(r.matchers)) - for i, m := range r.matchers { - matchers[i] = fmt.Sprintf("%#v", m) - } - return fmt.Sprintf("&Route{matchers:[]matcher{%s}}", strings.Join(matchers, ", ")) -} - -func (r *routeRegexp) GoString() string { - return fmt.Sprintf("&routeRegexp{template: %q, regexpType: %v, options: %v, regexp: regexp.MustCompile(%q), reverse: %q, varsN: %v, varsR: %v", r.template, r.regexpType, r.options, r.regexp.String(), r.reverse, r.varsN, r.varsR) -} - -type routeTest struct { - title string // title of the test - route *Route // the route being tested - request *http.Request // a request to test the route - vars map[string]string // the expected vars of the match - scheme string // the expected scheme of the built URL - host string // the expected host of the built URL - path string // the expected path of the built URL - query string // the expected query string of the built URL - pathTemplate string // the expected path template of the route - hostTemplate string // the expected host template of the route - queriesTemplate string // the expected query template of the route - methods []string // the expected route methods - pathRegexp string // the expected path regexp - queriesRegexp string // the expected query regexp - shouldMatch bool // whether the request is expected to match the route at all - shouldRedirect bool // whether the request should result in a redirect -} - -func TestHost(t *testing.T) { - - tests := []routeTest{ - { - title: "Host route match", - route: new(Route).Host("aaa.bbb.ccc"), - request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), - vars: nil, - host: "aaa.bbb.ccc", - path: "", - shouldMatch: true, - }, - { - title: "Host route, wrong host in request URL", - route: new(Route).Host("aaa.bbb.ccc"), - request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), - vars: nil, - host: "aaa.bbb.ccc", - path: "", - shouldMatch: false, - }, - { - title: "Host route with port, match", - route: new(Route).Host("aaa.bbb.ccc:1234"), - request: newRequest("GET", "http://aaa.bbb.ccc:1234/111/222/333"), - vars: nil, - host: "aaa.bbb.ccc:1234", - path: "", - shouldMatch: true, - }, - { - title: "Host route with port, wrong port in request URL", - route: new(Route).Host("aaa.bbb.ccc:1234"), - request: newRequest("GET", "http://aaa.bbb.ccc:9999/111/222/333"), - vars: nil, - host: "aaa.bbb.ccc:1234", - path: "", - shouldMatch: false, - }, - { - title: "Host route, match with host in request header", - route: new(Route).Host("aaa.bbb.ccc"), - request: newRequestHost("GET", "/111/222/333", "aaa.bbb.ccc"), - vars: nil, - host: "aaa.bbb.ccc", - path: "", - shouldMatch: true, - }, - { - title: "Host route, wrong host in request header", - route: new(Route).Host("aaa.bbb.ccc"), - request: newRequestHost("GET", "/111/222/333", "aaa.222.ccc"), - vars: nil, - host: "aaa.bbb.ccc", - path: "", - shouldMatch: false, - }, - { - title: "Host route with port, match with request header", - route: new(Route).Host("aaa.bbb.ccc:1234"), - request: newRequestHost("GET", "/111/222/333", "aaa.bbb.ccc:1234"), - vars: nil, - host: "aaa.bbb.ccc:1234", - path: "", - shouldMatch: true, - }, - { - title: "Host route with port, wrong host in request header", - route: new(Route).Host("aaa.bbb.ccc:1234"), - request: newRequestHost("GET", "/111/222/333", "aaa.bbb.ccc:9999"), - vars: nil, - host: "aaa.bbb.ccc:1234", - path: "", - shouldMatch: false, - }, - { - title: "Host route with pattern, match with request header", - route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc:1{v2:(?:23|4)}"), - request: newRequestHost("GET", "/111/222/333", "aaa.bbb.ccc:123"), - vars: map[string]string{"v1": "bbb", "v2": "23"}, - host: "aaa.bbb.ccc:123", - path: "", - hostTemplate: `aaa.{v1:[a-z]{3}}.ccc:1{v2:(?:23|4)}`, - shouldMatch: true, - }, - { - title: "Host route with pattern, match", - route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc"), - request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), - vars: map[string]string{"v1": "bbb"}, - host: "aaa.bbb.ccc", - path: "", - hostTemplate: `aaa.{v1:[a-z]{3}}.ccc`, - shouldMatch: true, - }, - { - title: "Host route with pattern, additional capturing group, match", - route: new(Route).Host("aaa.{v1:[a-z]{2}(?:b|c)}.ccc"), - request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), - vars: map[string]string{"v1": "bbb"}, - host: "aaa.bbb.ccc", - path: "", - hostTemplate: `aaa.{v1:[a-z]{2}(?:b|c)}.ccc`, - shouldMatch: true, - }, - { - title: "Host route with pattern, wrong host in request URL", - route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc"), - request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), - vars: map[string]string{"v1": "bbb"}, - host: "aaa.bbb.ccc", - path: "", - hostTemplate: `aaa.{v1:[a-z]{3}}.ccc`, - shouldMatch: false, - }, - { - title: "Host route with multiple patterns, match", - route: new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}"), - request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), - vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc"}, - host: "aaa.bbb.ccc", - path: "", - hostTemplate: `{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}`, - shouldMatch: true, - }, - { - title: "Host route with multiple patterns, wrong host in request URL", - route: new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}"), - request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), - vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc"}, - host: "aaa.bbb.ccc", - path: "", - hostTemplate: `{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}`, - shouldMatch: false, - }, - { - title: "Host route with hyphenated name and pattern, match", - route: new(Route).Host("aaa.{v-1:[a-z]{3}}.ccc"), - request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), - vars: map[string]string{"v-1": "bbb"}, - host: "aaa.bbb.ccc", - path: "", - hostTemplate: `aaa.{v-1:[a-z]{3}}.ccc`, - shouldMatch: true, - }, - { - title: "Host route with hyphenated name and pattern, additional capturing group, match", - route: new(Route).Host("aaa.{v-1:[a-z]{2}(?:b|c)}.ccc"), - request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), - vars: map[string]string{"v-1": "bbb"}, - host: "aaa.bbb.ccc", - path: "", - hostTemplate: `aaa.{v-1:[a-z]{2}(?:b|c)}.ccc`, - shouldMatch: true, - }, - { - title: "Host route with multiple hyphenated names and patterns, match", - route: new(Route).Host("{v-1:[a-z]{3}}.{v-2:[a-z]{3}}.{v-3:[a-z]{3}}"), - request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), - vars: map[string]string{"v-1": "aaa", "v-2": "bbb", "v-3": "ccc"}, - host: "aaa.bbb.ccc", - path: "", - hostTemplate: `{v-1:[a-z]{3}}.{v-2:[a-z]{3}}.{v-3:[a-z]{3}}`, - shouldMatch: true, - }, - } - for _, test := range tests { - t.Run(test.title, func(t *testing.T) { - testRoute(t, test) - testTemplate(t, test) - }) - } -} - -func TestPath(t *testing.T) { - tests := []routeTest{ - { - title: "Path route, match", - route: new(Route).Path("/111/222/333"), - request: newRequest("GET", "http://localhost/111/222/333"), - vars: nil, - host: "", - path: "/111/222/333", - shouldMatch: true, - }, - { - title: "Path route, match with trailing slash in request and path", - route: new(Route).Path("/111/"), - request: newRequest("GET", "http://localhost/111/"), - vars: nil, - host: "", - path: "/111/", - shouldMatch: true, - }, - { - title: "Path route, do not match with trailing slash in path", - route: new(Route).Path("/111/"), - request: newRequest("GET", "http://localhost/111"), - vars: nil, - host: "", - path: "/111", - pathTemplate: `/111/`, - pathRegexp: `^/111/$`, - shouldMatch: false, - }, - { - title: "Path route, do not match with trailing slash in request", - route: new(Route).Path("/111"), - request: newRequest("GET", "http://localhost/111/"), - vars: nil, - host: "", - path: "/111/", - pathTemplate: `/111`, - shouldMatch: false, - }, - { - title: "Path route, match root with no host", - route: new(Route).Path("/"), - request: newRequest("GET", "/"), - vars: nil, - host: "", - path: "/", - pathTemplate: `/`, - pathRegexp: `^/$`, - shouldMatch: true, - }, - { - title: "Path route, match root with no host, App Engine format", - route: new(Route).Path("/"), - request: func() *http.Request { - r := newRequest("GET", "http://localhost/") - r.RequestURI = "/" - return r - }(), - vars: nil, - host: "", - path: "/", - pathTemplate: `/`, - shouldMatch: true, - }, - { - title: "Path route, wrong path in request in request URL", - route: new(Route).Path("/111/222/333"), - request: newRequest("GET", "http://localhost/1/2/3"), - vars: nil, - host: "", - path: "/111/222/333", - shouldMatch: false, - }, - { - title: "Path route with pattern, match", - route: new(Route).Path("/111/{v1:[0-9]{3}}/333"), - request: newRequest("GET", "http://localhost/111/222/333"), - vars: map[string]string{"v1": "222"}, - host: "", - path: "/111/222/333", - pathTemplate: `/111/{v1:[0-9]{3}}/333`, - shouldMatch: true, - }, - { - title: "Path route with pattern, URL in request does not match", - route: new(Route).Path("/111/{v1:[0-9]{3}}/333"), - request: newRequest("GET", "http://localhost/111/aaa/333"), - vars: map[string]string{"v1": "222"}, - host: "", - path: "/111/222/333", - pathTemplate: `/111/{v1:[0-9]{3}}/333`, - pathRegexp: `^/111/(?P[0-9]{3})/333$`, - shouldMatch: false, - }, - { - title: "Path route with multiple patterns, match", - route: new(Route).Path("/{v1:[0-9]{3}}/{v2:[0-9]{3}}/{v3:[0-9]{3}}"), - request: newRequest("GET", "http://localhost/111/222/333"), - vars: map[string]string{"v1": "111", "v2": "222", "v3": "333"}, - host: "", - path: "/111/222/333", - pathTemplate: `/{v1:[0-9]{3}}/{v2:[0-9]{3}}/{v3:[0-9]{3}}`, - pathRegexp: `^/(?P[0-9]{3})/(?P[0-9]{3})/(?P[0-9]{3})$`, - shouldMatch: true, - }, - { - title: "Path route with multiple patterns, URL in request does not match", - route: new(Route).Path("/{v1:[0-9]{3}}/{v2:[0-9]{3}}/{v3:[0-9]{3}}"), - request: newRequest("GET", "http://localhost/111/aaa/333"), - vars: map[string]string{"v1": "111", "v2": "222", "v3": "333"}, - host: "", - path: "/111/222/333", - pathTemplate: `/{v1:[0-9]{3}}/{v2:[0-9]{3}}/{v3:[0-9]{3}}`, - pathRegexp: `^/(?P[0-9]{3})/(?P[0-9]{3})/(?P[0-9]{3})$`, - shouldMatch: false, - }, - { - title: "Path route with multiple patterns with pipe, match", - route: new(Route).Path("/{category:a|(?:b/c)}/{product}/{id:[0-9]+}"), - request: newRequest("GET", "http://localhost/a/product_name/1"), - vars: map[string]string{"category": "a", "product": "product_name", "id": "1"}, - host: "", - path: "/a/product_name/1", - pathTemplate: `/{category:a|(?:b/c)}/{product}/{id:[0-9]+}`, - pathRegexp: `^/(?Pa|(?:b/c))/(?P[^/]+)/(?P[0-9]+)$`, - shouldMatch: true, - }, - { - title: "Path route with hyphenated name and pattern, match", - route: new(Route).Path("/111/{v-1:[0-9]{3}}/333"), - request: newRequest("GET", "http://localhost/111/222/333"), - vars: map[string]string{"v-1": "222"}, - host: "", - path: "/111/222/333", - pathTemplate: `/111/{v-1:[0-9]{3}}/333`, - pathRegexp: `^/111/(?P[0-9]{3})/333$`, - shouldMatch: true, - }, - { - title: "Path route with multiple hyphenated names and patterns, match", - route: new(Route).Path("/{v-1:[0-9]{3}}/{v-2:[0-9]{3}}/{v-3:[0-9]{3}}"), - request: newRequest("GET", "http://localhost/111/222/333"), - vars: map[string]string{"v-1": "111", "v-2": "222", "v-3": "333"}, - host: "", - path: "/111/222/333", - pathTemplate: `/{v-1:[0-9]{3}}/{v-2:[0-9]{3}}/{v-3:[0-9]{3}}`, - pathRegexp: `^/(?P[0-9]{3})/(?P[0-9]{3})/(?P[0-9]{3})$`, - shouldMatch: true, - }, - { - title: "Path route with multiple hyphenated names and patterns with pipe, match", - route: new(Route).Path("/{product-category:a|(?:b/c)}/{product-name}/{product-id:[0-9]+}"), - request: newRequest("GET", "http://localhost/a/product_name/1"), - vars: map[string]string{"product-category": "a", "product-name": "product_name", "product-id": "1"}, - host: "", - path: "/a/product_name/1", - pathTemplate: `/{product-category:a|(?:b/c)}/{product-name}/{product-id:[0-9]+}`, - pathRegexp: `^/(?Pa|(?:b/c))/(?P[^/]+)/(?P[0-9]+)$`, - shouldMatch: true, - }, - { - title: "Path route with multiple hyphenated names and patterns with pipe and case insensitive, match", - route: new(Route).Path("/{type:(?i:daily|mini|variety)}-{date:\\d{4,4}-\\d{2,2}-\\d{2,2}}"), - request: newRequest("GET", "http://localhost/daily-2016-01-01"), - vars: map[string]string{"type": "daily", "date": "2016-01-01"}, - host: "", - path: "/daily-2016-01-01", - pathTemplate: `/{type:(?i:daily|mini|variety)}-{date:\d{4,4}-\d{2,2}-\d{2,2}}`, - pathRegexp: `^/(?P(?i:daily|mini|variety))-(?P\d{4,4}-\d{2,2}-\d{2,2})$`, - shouldMatch: true, - }, - { - title: "Path route with empty match right after other match", - route: new(Route).Path(`/{v1:[0-9]*}{v2:[a-z]*}/{v3:[0-9]*}`), - request: newRequest("GET", "http://localhost/111/222"), - vars: map[string]string{"v1": "111", "v2": "", "v3": "222"}, - host: "", - path: "/111/222", - pathTemplate: `/{v1:[0-9]*}{v2:[a-z]*}/{v3:[0-9]*}`, - pathRegexp: `^/(?P[0-9]*)(?P[a-z]*)/(?P[0-9]*)$`, - shouldMatch: true, - }, - { - title: "Path route with single pattern with pipe, match", - route: new(Route).Path("/{category:a|b/c}"), - request: newRequest("GET", "http://localhost/a"), - vars: map[string]string{"category": "a"}, - host: "", - path: "/a", - pathTemplate: `/{category:a|b/c}`, - shouldMatch: true, - }, - { - title: "Path route with single pattern with pipe, match", - route: new(Route).Path("/{category:a|b/c}"), - request: newRequest("GET", "http://localhost/b/c"), - vars: map[string]string{"category": "b/c"}, - host: "", - path: "/b/c", - pathTemplate: `/{category:a|b/c}`, - shouldMatch: true, - }, - { - title: "Path route with multiple patterns with pipe, match", - route: new(Route).Path("/{category:a|b/c}/{product}/{id:[0-9]+}"), - request: newRequest("GET", "http://localhost/a/product_name/1"), - vars: map[string]string{"category": "a", "product": "product_name", "id": "1"}, - host: "", - path: "/a/product_name/1", - pathTemplate: `/{category:a|b/c}/{product}/{id:[0-9]+}`, - shouldMatch: true, - }, - { - title: "Path route with multiple patterns with pipe, match", - route: new(Route).Path("/{category:a|b/c}/{product}/{id:[0-9]+}"), - request: newRequest("GET", "http://localhost/b/c/product_name/1"), - vars: map[string]string{"category": "b/c", "product": "product_name", "id": "1"}, - host: "", - path: "/b/c/product_name/1", - pathTemplate: `/{category:a|b/c}/{product}/{id:[0-9]+}`, - shouldMatch: true, - }, - } - - for _, test := range tests { - t.Run(test.title, func(t *testing.T) { - testRoute(t, test) - testTemplate(t, test) - testUseEscapedRoute(t, test) - testRegexp(t, test) - }) - } -} - -func TestPathPrefix(t *testing.T) { - tests := []routeTest{ - { - title: "PathPrefix route, match", - route: new(Route).PathPrefix("/111"), - request: newRequest("GET", "http://localhost/111/222/333"), - vars: nil, - host: "", - path: "/111", - shouldMatch: true, - }, - { - title: "PathPrefix route, match substring", - route: new(Route).PathPrefix("/1"), - request: newRequest("GET", "http://localhost/111/222/333"), - vars: nil, - host: "", - path: "/1", - shouldMatch: true, - }, - { - title: "PathPrefix route, URL prefix in request does not match", - route: new(Route).PathPrefix("/111"), - request: newRequest("GET", "http://localhost/1/2/3"), - vars: nil, - host: "", - path: "/111", - shouldMatch: false, - }, - { - title: "PathPrefix route with pattern, match", - route: new(Route).PathPrefix("/111/{v1:[0-9]{3}}"), - request: newRequest("GET", "http://localhost/111/222/333"), - vars: map[string]string{"v1": "222"}, - host: "", - path: "/111/222", - pathTemplate: `/111/{v1:[0-9]{3}}`, - shouldMatch: true, - }, - { - title: "PathPrefix route with pattern, URL prefix in request does not match", - route: new(Route).PathPrefix("/111/{v1:[0-9]{3}}"), - request: newRequest("GET", "http://localhost/111/aaa/333"), - vars: map[string]string{"v1": "222"}, - host: "", - path: "/111/222", - pathTemplate: `/111/{v1:[0-9]{3}}`, - shouldMatch: false, - }, - { - title: "PathPrefix route with multiple patterns, match", - route: new(Route).PathPrefix("/{v1:[0-9]{3}}/{v2:[0-9]{3}}"), - request: newRequest("GET", "http://localhost/111/222/333"), - vars: map[string]string{"v1": "111", "v2": "222"}, - host: "", - path: "/111/222", - pathTemplate: `/{v1:[0-9]{3}}/{v2:[0-9]{3}}`, - shouldMatch: true, - }, - { - title: "PathPrefix route with multiple patterns, URL prefix in request does not match", - route: new(Route).PathPrefix("/{v1:[0-9]{3}}/{v2:[0-9]{3}}"), - request: newRequest("GET", "http://localhost/111/aaa/333"), - vars: map[string]string{"v1": "111", "v2": "222"}, - host: "", - path: "/111/222", - pathTemplate: `/{v1:[0-9]{3}}/{v2:[0-9]{3}}`, - shouldMatch: false, - }, - } - - for _, test := range tests { - t.Run(test.title, func(t *testing.T) { - testRoute(t, test) - testTemplate(t, test) - testUseEscapedRoute(t, test) - }) - } -} - -func TestSchemeHostPath(t *testing.T) { - tests := []routeTest{ - { - title: "Host and Path route, match", - route: new(Route).Host("aaa.bbb.ccc").Path("/111/222/333"), - request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), - vars: nil, - scheme: "http", - host: "aaa.bbb.ccc", - path: "/111/222/333", - pathTemplate: `/111/222/333`, - hostTemplate: `aaa.bbb.ccc`, - shouldMatch: true, - }, - { - title: "Scheme, Host, and Path route, match", - route: new(Route).Schemes("https").Host("aaa.bbb.ccc").Path("/111/222/333"), - request: newRequest("GET", "https://aaa.bbb.ccc/111/222/333"), - vars: nil, - scheme: "https", - host: "aaa.bbb.ccc", - path: "/111/222/333", - pathTemplate: `/111/222/333`, - hostTemplate: `aaa.bbb.ccc`, - shouldMatch: true, - }, - { - title: "Host and Path route, wrong host in request URL", - route: new(Route).Host("aaa.bbb.ccc").Path("/111/222/333"), - request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), - vars: nil, - scheme: "http", - host: "aaa.bbb.ccc", - path: "/111/222/333", - pathTemplate: `/111/222/333`, - hostTemplate: `aaa.bbb.ccc`, - shouldMatch: false, - }, - { - title: "Host and Path route with pattern, match", - route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc").Path("/111/{v2:[0-9]{3}}/333"), - request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), - vars: map[string]string{"v1": "bbb", "v2": "222"}, - scheme: "http", - host: "aaa.bbb.ccc", - path: "/111/222/333", - pathTemplate: `/111/{v2:[0-9]{3}}/333`, - hostTemplate: `aaa.{v1:[a-z]{3}}.ccc`, - shouldMatch: true, - }, - { - title: "Scheme, Host, and Path route with host and path patterns, match", - route: new(Route).Schemes("ftp", "ssss").Host("aaa.{v1:[a-z]{3}}.ccc").Path("/111/{v2:[0-9]{3}}/333"), - request: newRequest("GET", "ssss://aaa.bbb.ccc/111/222/333"), - vars: map[string]string{"v1": "bbb", "v2": "222"}, - scheme: "ftp", - host: "aaa.bbb.ccc", - path: "/111/222/333", - pathTemplate: `/111/{v2:[0-9]{3}}/333`, - hostTemplate: `aaa.{v1:[a-z]{3}}.ccc`, - shouldMatch: true, - }, - { - title: "Host and Path route with pattern, URL in request does not match", - route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc").Path("/111/{v2:[0-9]{3}}/333"), - request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), - vars: map[string]string{"v1": "bbb", "v2": "222"}, - scheme: "http", - host: "aaa.bbb.ccc", - path: "/111/222/333", - pathTemplate: `/111/{v2:[0-9]{3}}/333`, - hostTemplate: `aaa.{v1:[a-z]{3}}.ccc`, - shouldMatch: false, - }, - { - title: "Host and Path route with multiple patterns, match", - route: new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}").Path("/{v4:[0-9]{3}}/{v5:[0-9]{3}}/{v6:[0-9]{3}}"), - request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), - vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc", "v4": "111", "v5": "222", "v6": "333"}, - scheme: "http", - host: "aaa.bbb.ccc", - path: "/111/222/333", - pathTemplate: `/{v4:[0-9]{3}}/{v5:[0-9]{3}}/{v6:[0-9]{3}}`, - hostTemplate: `{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}`, - shouldMatch: true, - }, - { - title: "Host and Path route with multiple patterns, URL in request does not match", - route: new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}").Path("/{v4:[0-9]{3}}/{v5:[0-9]{3}}/{v6:[0-9]{3}}"), - request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), - vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc", "v4": "111", "v5": "222", "v6": "333"}, - scheme: "http", - host: "aaa.bbb.ccc", - path: "/111/222/333", - pathTemplate: `/{v4:[0-9]{3}}/{v5:[0-9]{3}}/{v6:[0-9]{3}}`, - hostTemplate: `{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}`, - shouldMatch: false, - }, - } - - for _, test := range tests { - t.Run(test.title, func(t *testing.T) { - testRoute(t, test) - testTemplate(t, test) - testUseEscapedRoute(t, test) - }) - } -} - -func TestHeaders(t *testing.T) { - // newRequestHeaders creates a new request with a method, url, and headers - newRequestHeaders := func(method, url string, headers map[string]string) *http.Request { - req, err := http.NewRequest(method, url, nil) - if err != nil { - panic(err) - } - for k, v := range headers { - req.Header.Add(k, v) - } - return req - } - - tests := []routeTest{ - { - title: "Headers route, match", - route: new(Route).Headers("foo", "bar", "baz", "ding"), - request: newRequestHeaders("GET", "http://localhost", map[string]string{"foo": "bar", "baz": "ding"}), - vars: nil, - host: "", - path: "", - shouldMatch: true, - }, - { - title: "Headers route, bad header values", - route: new(Route).Headers("foo", "bar", "baz", "ding"), - request: newRequestHeaders("GET", "http://localhost", map[string]string{"foo": "bar", "baz": "dong"}), - vars: nil, - host: "", - path: "", - shouldMatch: false, - }, - { - title: "Headers route, regex header values to match", - route: new(Route).HeadersRegexp("foo", "ba[zr]"), - request: newRequestHeaders("GET", "http://localhost", map[string]string{"foo": "baw"}), - vars: nil, - host: "", - path: "", - shouldMatch: false, - }, - { - title: "Headers route, regex header values to match", - route: new(Route).HeadersRegexp("foo", "ba[zr]"), - request: newRequestHeaders("GET", "http://localhost", map[string]string{"foo": "baz"}), - vars: nil, - host: "", - path: "", - shouldMatch: true, - }, - } - - for _, test := range tests { - t.Run(test.title, func(t *testing.T) { - testRoute(t, test) - testTemplate(t, test) - }) - } -} - -func TestMethods(t *testing.T) { - tests := []routeTest{ - { - title: "Methods route, match GET", - route: new(Route).Methods("GET", "POST"), - request: newRequest("GET", "http://localhost"), - vars: nil, - host: "", - path: "", - methods: []string{"GET", "POST"}, - shouldMatch: true, - }, - { - title: "Methods route, match POST", - route: new(Route).Methods("GET", "POST"), - request: newRequest("POST", "http://localhost"), - vars: nil, - host: "", - path: "", - methods: []string{"GET", "POST"}, - shouldMatch: true, - }, - { - title: "Methods route, bad method", - route: new(Route).Methods("GET", "POST"), - request: newRequest("PUT", "http://localhost"), - vars: nil, - host: "", - path: "", - methods: []string{"GET", "POST"}, - shouldMatch: false, - }, - { - title: "Route without methods", - route: new(Route), - request: newRequest("PUT", "http://localhost"), - vars: nil, - host: "", - path: "", - methods: []string{}, - shouldMatch: true, - }, - } - - for _, test := range tests { - t.Run(test.title, func(t *testing.T) { - testRoute(t, test) - testTemplate(t, test) - testMethods(t, test) - }) - } -} - -func TestQueries(t *testing.T) { - tests := []routeTest{ - { - title: "Queries route, match", - route: new(Route).Queries("foo", "bar", "baz", "ding"), - request: newRequest("GET", "http://localhost?foo=bar&baz=ding"), - vars: nil, - host: "", - path: "", - query: "foo=bar&baz=ding", - queriesTemplate: "foo=bar,baz=ding", - queriesRegexp: "^foo=bar$,^baz=ding$", - shouldMatch: true, - }, - { - title: "Queries route, match with a query string", - route: new(Route).Host("www.example.com").Path("/api").Queries("foo", "bar", "baz", "ding"), - request: newRequest("GET", "http://www.example.com/api?foo=bar&baz=ding"), - vars: nil, - host: "", - path: "", - query: "foo=bar&baz=ding", - pathTemplate: `/api`, - hostTemplate: `www.example.com`, - queriesTemplate: "foo=bar,baz=ding", - queriesRegexp: "^foo=bar$,^baz=ding$", - shouldMatch: true, - }, - { - title: "Queries route, match with a query string out of order", - route: new(Route).Host("www.example.com").Path("/api").Queries("foo", "bar", "baz", "ding"), - request: newRequest("GET", "http://www.example.com/api?baz=ding&foo=bar"), - vars: nil, - host: "", - path: "", - query: "foo=bar&baz=ding", - pathTemplate: `/api`, - hostTemplate: `www.example.com`, - queriesTemplate: "foo=bar,baz=ding", - queriesRegexp: "^foo=bar$,^baz=ding$", - shouldMatch: true, - }, - { - title: "Queries route, bad query", - route: new(Route).Queries("foo", "bar", "baz", "ding"), - request: newRequest("GET", "http://localhost?foo=bar&baz=dong"), - vars: nil, - host: "", - path: "", - queriesTemplate: "foo=bar,baz=ding", - queriesRegexp: "^foo=bar$,^baz=ding$", - shouldMatch: false, - }, - { - title: "Queries route with pattern, match", - route: new(Route).Queries("foo", "{v1}"), - request: newRequest("GET", "http://localhost?foo=bar"), - vars: map[string]string{"v1": "bar"}, - host: "", - path: "", - query: "foo=bar", - queriesTemplate: "foo={v1}", - queriesRegexp: "^foo=(?P.*)$", - shouldMatch: true, - }, - { - title: "Queries route with multiple patterns, match", - route: new(Route).Queries("foo", "{v1}", "baz", "{v2}"), - request: newRequest("GET", "http://localhost?foo=bar&baz=ding"), - vars: map[string]string{"v1": "bar", "v2": "ding"}, - host: "", - path: "", - query: "foo=bar&baz=ding", - queriesTemplate: "foo={v1},baz={v2}", - queriesRegexp: "^foo=(?P.*)$,^baz=(?P.*)$", - shouldMatch: true, - }, - { - title: "Queries route with regexp pattern, match", - route: new(Route).Queries("foo", "{v1:[0-9]+}"), - request: newRequest("GET", "http://localhost?foo=10"), - vars: map[string]string{"v1": "10"}, - host: "", - path: "", - query: "foo=10", - queriesTemplate: "foo={v1:[0-9]+}", - queriesRegexp: "^foo=(?P[0-9]+)$", - shouldMatch: true, - }, - { - title: "Queries route with regexp pattern, regexp does not match", - route: new(Route).Queries("foo", "{v1:[0-9]+}"), - request: newRequest("GET", "http://localhost?foo=a"), - vars: nil, - host: "", - path: "", - queriesTemplate: "foo={v1:[0-9]+}", - queriesRegexp: "^foo=(?P[0-9]+)$", - shouldMatch: false, - }, - { - title: "Queries route with regexp pattern with quantifier, match", - route: new(Route).Queries("foo", "{v1:[0-9]{1}}"), - request: newRequest("GET", "http://localhost?foo=1"), - vars: map[string]string{"v1": "1"}, - host: "", - path: "", - query: "foo=1", - queriesTemplate: "foo={v1:[0-9]{1}}", - queriesRegexp: "^foo=(?P[0-9]{1})$", - shouldMatch: true, - }, - { - title: "Queries route with regexp pattern with quantifier, additional variable in query string, match", - route: new(Route).Queries("foo", "{v1:[0-9]{1}}"), - request: newRequest("GET", "http://localhost?bar=2&foo=1"), - vars: map[string]string{"v1": "1"}, - host: "", - path: "", - query: "foo=1", - queriesTemplate: "foo={v1:[0-9]{1}}", - queriesRegexp: "^foo=(?P[0-9]{1})$", - shouldMatch: true, - }, - { - title: "Queries route with regexp pattern with quantifier, regexp does not match", - route: new(Route).Queries("foo", "{v1:[0-9]{1}}"), - request: newRequest("GET", "http://localhost?foo=12"), - vars: nil, - host: "", - path: "", - queriesTemplate: "foo={v1:[0-9]{1}}", - queriesRegexp: "^foo=(?P[0-9]{1})$", - shouldMatch: false, - }, - { - title: "Queries route with regexp pattern with quantifier, additional capturing group", - route: new(Route).Queries("foo", "{v1:[0-9]{1}(?:a|b)}"), - request: newRequest("GET", "http://localhost?foo=1a"), - vars: map[string]string{"v1": "1a"}, - host: "", - path: "", - query: "foo=1a", - queriesTemplate: "foo={v1:[0-9]{1}(?:a|b)}", - queriesRegexp: "^foo=(?P[0-9]{1}(?:a|b))$", - shouldMatch: true, - }, - { - title: "Queries route with regexp pattern with quantifier, additional variable in query string, regexp does not match", - route: new(Route).Queries("foo", "{v1:[0-9]{1}}"), - request: newRequest("GET", "http://localhost?foo=12"), - vars: nil, - host: "", - path: "", - queriesTemplate: "foo={v1:[0-9]{1}}", - queriesRegexp: "^foo=(?P[0-9]{1})$", - shouldMatch: false, - }, - { - title: "Queries route with hyphenated name, match", - route: new(Route).Queries("foo", "{v-1}"), - request: newRequest("GET", "http://localhost?foo=bar"), - vars: map[string]string{"v-1": "bar"}, - host: "", - path: "", - query: "foo=bar", - queriesTemplate: "foo={v-1}", - queriesRegexp: "^foo=(?P.*)$", - shouldMatch: true, - }, - { - title: "Queries route with multiple hyphenated names, match", - route: new(Route).Queries("foo", "{v-1}", "baz", "{v-2}"), - request: newRequest("GET", "http://localhost?foo=bar&baz=ding"), - vars: map[string]string{"v-1": "bar", "v-2": "ding"}, - host: "", - path: "", - query: "foo=bar&baz=ding", - queriesTemplate: "foo={v-1},baz={v-2}", - queriesRegexp: "^foo=(?P.*)$,^baz=(?P.*)$", - shouldMatch: true, - }, - { - title: "Queries route with hyphenate name and pattern, match", - route: new(Route).Queries("foo", "{v-1:[0-9]+}"), - request: newRequest("GET", "http://localhost?foo=10"), - vars: map[string]string{"v-1": "10"}, - host: "", - path: "", - query: "foo=10", - queriesTemplate: "foo={v-1:[0-9]+}", - queriesRegexp: "^foo=(?P[0-9]+)$", - shouldMatch: true, - }, - { - title: "Queries route with hyphenated name and pattern with quantifier, additional capturing group", - route: new(Route).Queries("foo", "{v-1:[0-9]{1}(?:a|b)}"), - request: newRequest("GET", "http://localhost?foo=1a"), - vars: map[string]string{"v-1": "1a"}, - host: "", - path: "", - query: "foo=1a", - queriesTemplate: "foo={v-1:[0-9]{1}(?:a|b)}", - queriesRegexp: "^foo=(?P[0-9]{1}(?:a|b))$", - shouldMatch: true, - }, - { - title: "Queries route with empty value, should match", - route: new(Route).Queries("foo", ""), - request: newRequest("GET", "http://localhost?foo=bar"), - vars: nil, - host: "", - path: "", - query: "foo=", - queriesTemplate: "foo=", - queriesRegexp: "^foo=.*$", - shouldMatch: true, - }, - { - title: "Queries route with empty value and no parameter in request, should not match", - route: new(Route).Queries("foo", ""), - request: newRequest("GET", "http://localhost"), - vars: nil, - host: "", - path: "", - queriesTemplate: "foo=", - queriesRegexp: "^foo=.*$", - shouldMatch: false, - }, - { - title: "Queries route with empty value and empty parameter in request, should match", - route: new(Route).Queries("foo", ""), - request: newRequest("GET", "http://localhost?foo="), - vars: nil, - host: "", - path: "", - query: "foo=", - queriesTemplate: "foo=", - queriesRegexp: "^foo=.*$", - shouldMatch: true, - }, - { - title: "Queries route with overlapping value, should not match", - route: new(Route).Queries("foo", "bar"), - request: newRequest("GET", "http://localhost?foo=barfoo"), - vars: nil, - host: "", - path: "", - queriesTemplate: "foo=bar", - queriesRegexp: "^foo=bar$", - shouldMatch: false, - }, - { - title: "Queries route with no parameter in request, should not match", - route: new(Route).Queries("foo", "{bar}"), - request: newRequest("GET", "http://localhost"), - vars: nil, - host: "", - path: "", - queriesTemplate: "foo={bar}", - queriesRegexp: "^foo=(?P.*)$", - shouldMatch: false, - }, - { - title: "Queries route with empty parameter in request, should match", - route: new(Route).Queries("foo", "{bar}"), - request: newRequest("GET", "http://localhost?foo="), - vars: map[string]string{"foo": ""}, - host: "", - path: "", - query: "foo=", - queriesTemplate: "foo={bar}", - queriesRegexp: "^foo=(?P.*)$", - shouldMatch: true, - }, - { - title: "Queries route, bad submatch", - route: new(Route).Queries("foo", "bar", "baz", "ding"), - request: newRequest("GET", "http://localhost?fffoo=bar&baz=dingggg"), - vars: nil, - host: "", - path: "", - queriesTemplate: "foo=bar,baz=ding", - queriesRegexp: "^foo=bar$,^baz=ding$", - shouldMatch: false, - }, - { - title: "Queries route with pattern, match, escaped value", - route: new(Route).Queries("foo", "{v1}"), - request: newRequest("GET", "http://localhost?foo=%25bar%26%20%2F%3D%3F"), - vars: map[string]string{"v1": "%bar& /=?"}, - host: "", - path: "", - query: "foo=%25bar%26+%2F%3D%3F", - queriesTemplate: "foo={v1}", - queriesRegexp: "^foo=(?P.*)$", - shouldMatch: true, - }, - } - - for _, test := range tests { - t.Run(test.title, func(t *testing.T) { - testTemplate(t, test) - testQueriesTemplates(t, test) - testUseEscapedRoute(t, test) - testQueriesRegexp(t, test) - }) - } -} - -func TestSchemes(t *testing.T) { - tests := []routeTest{ - // Schemes - { - title: "Schemes route, default scheme, match http, build http", - route: new(Route).Host("localhost"), - request: newRequest("GET", "http://localhost"), - scheme: "http", - host: "localhost", - shouldMatch: true, - }, - { - title: "Schemes route, match https, build https", - route: new(Route).Schemes("https", "ftp").Host("localhost"), - request: newRequest("GET", "https://localhost"), - scheme: "https", - host: "localhost", - shouldMatch: true, - }, - { - title: "Schemes route, match ftp, build https", - route: new(Route).Schemes("https", "ftp").Host("localhost"), - request: newRequest("GET", "ftp://localhost"), - scheme: "https", - host: "localhost", - shouldMatch: true, - }, - { - title: "Schemes route, match ftp, build ftp", - route: new(Route).Schemes("ftp", "https").Host("localhost"), - request: newRequest("GET", "ftp://localhost"), - scheme: "ftp", - host: "localhost", - shouldMatch: true, - }, - { - title: "Schemes route, bad scheme", - route: new(Route).Schemes("https", "ftp").Host("localhost"), - request: newRequest("GET", "http://localhost"), - scheme: "https", - host: "localhost", - shouldMatch: false, - }, - } - for _, test := range tests { - t.Run(test.title, func(t *testing.T) { - testRoute(t, test) - testTemplate(t, test) - }) - } -} - -func TestMatcherFunc(t *testing.T) { - m := func(r *http.Request, m *RouteMatch) bool { - return r.URL.Host == "aaa.bbb.ccc" - } - - tests := []routeTest{ - { - title: "MatchFunc route, match", - route: new(Route).MatcherFunc(m), - request: newRequest("GET", "http://aaa.bbb.ccc"), - vars: nil, - host: "", - path: "", - shouldMatch: true, - }, - { - title: "MatchFunc route, non-match", - route: new(Route).MatcherFunc(m), - request: newRequest("GET", "http://aaa.222.ccc"), - vars: nil, - host: "", - path: "", - shouldMatch: false, - }, - } - - for _, test := range tests { - t.Run(test.title, func(t *testing.T) { - testRoute(t, test) - testTemplate(t, test) - }) - } -} - -func TestBuildVarsFunc(t *testing.T) { - tests := []routeTest{ - { - title: "BuildVarsFunc set on route", - route: new(Route).Path(`/111/{v1:\d}{v2:.*}`).BuildVarsFunc(func(vars map[string]string) map[string]string { - vars["v1"] = "3" - vars["v2"] = "a" - return vars - }), - request: newRequest("GET", "http://localhost/111/2"), - path: "/111/3a", - pathTemplate: `/111/{v1:\d}{v2:.*}`, - shouldMatch: true, - }, - { - title: "BuildVarsFunc set on route and parent route", - route: new(Route).PathPrefix(`/{v1:\d}`).BuildVarsFunc(func(vars map[string]string) map[string]string { - vars["v1"] = "2" - return vars - }).Subrouter().Path(`/{v2:\w}`).BuildVarsFunc(func(vars map[string]string) map[string]string { - vars["v2"] = "b" - return vars - }), - request: newRequest("GET", "http://localhost/1/a"), - path: "/2/b", - pathTemplate: `/{v1:\d}/{v2:\w}`, - shouldMatch: true, - }, - } - - for _, test := range tests { - t.Run(test.title, func(t *testing.T) { - testRoute(t, test) - testTemplate(t, test) - }) - } -} - -func TestSubRouter(t *testing.T) { - subrouter1 := new(Route).Host("{v1:[a-z]+}.google.com").Subrouter() - subrouter2 := new(Route).PathPrefix("/foo/{v1}").Subrouter() - subrouter3 := new(Route).PathPrefix("/foo").Subrouter() - subrouter4 := new(Route).PathPrefix("/foo/bar").Subrouter() - subrouter5 := new(Route).PathPrefix("/{category}").Subrouter() - tests := []routeTest{ - { - route: subrouter1.Path("/{v2:[a-z]+}"), - request: newRequest("GET", "http://aaa.google.com/bbb"), - vars: map[string]string{"v1": "aaa", "v2": "bbb"}, - host: "aaa.google.com", - path: "/bbb", - pathTemplate: `/{v2:[a-z]+}`, - hostTemplate: `{v1:[a-z]+}.google.com`, - shouldMatch: true, - }, - { - route: subrouter1.Path("/{v2:[a-z]+}"), - request: newRequest("GET", "http://111.google.com/111"), - vars: map[string]string{"v1": "aaa", "v2": "bbb"}, - host: "aaa.google.com", - path: "/bbb", - pathTemplate: `/{v2:[a-z]+}`, - hostTemplate: `{v1:[a-z]+}.google.com`, - shouldMatch: false, - }, - { - route: subrouter2.Path("/baz/{v2}"), - request: newRequest("GET", "http://localhost/foo/bar/baz/ding"), - vars: map[string]string{"v1": "bar", "v2": "ding"}, - host: "", - path: "/foo/bar/baz/ding", - pathTemplate: `/foo/{v1}/baz/{v2}`, - shouldMatch: true, - }, - { - route: subrouter2.Path("/baz/{v2}"), - request: newRequest("GET", "http://localhost/foo/bar"), - vars: map[string]string{"v1": "bar", "v2": "ding"}, - host: "", - path: "/foo/bar/baz/ding", - pathTemplate: `/foo/{v1}/baz/{v2}`, - shouldMatch: false, - }, - { - route: subrouter3.Path("/"), - request: newRequest("GET", "http://localhost/foo/"), - vars: nil, - host: "", - path: "/foo/", - pathTemplate: `/foo/`, - shouldMatch: true, - }, - { - route: subrouter3.Path(""), - request: newRequest("GET", "http://localhost/foo"), - vars: nil, - host: "", - path: "/foo", - pathTemplate: `/foo`, - shouldMatch: true, - }, - - { - route: subrouter4.Path("/"), - request: newRequest("GET", "http://localhost/foo/bar/"), - vars: nil, - host: "", - path: "/foo/bar/", - pathTemplate: `/foo/bar/`, - shouldMatch: true, - }, - { - route: subrouter4.Path(""), - request: newRequest("GET", "http://localhost/foo/bar"), - vars: nil, - host: "", - path: "/foo/bar", - pathTemplate: `/foo/bar`, - shouldMatch: true, - }, - { - route: subrouter5.Path("/"), - request: newRequest("GET", "http://localhost/baz/"), - vars: map[string]string{"category": "baz"}, - host: "", - path: "/baz/", - pathTemplate: `/{category}/`, - shouldMatch: true, - }, - { - route: subrouter5.Path(""), - request: newRequest("GET", "http://localhost/baz"), - vars: map[string]string{"category": "baz"}, - host: "", - path: "/baz", - pathTemplate: `/{category}`, - shouldMatch: true, - }, - { - title: "Mismatch method specified on parent route", - route: new(Route).Methods("POST").PathPrefix("/foo").Subrouter().Path("/"), - request: newRequest("GET", "http://localhost/foo/"), - vars: nil, - host: "", - path: "/foo/", - pathTemplate: `/foo/`, - shouldMatch: false, - }, - { - title: "Match method specified on parent route", - route: new(Route).Methods("POST").PathPrefix("/foo").Subrouter().Path("/"), - request: newRequest("POST", "http://localhost/foo/"), - vars: nil, - host: "", - path: "/foo/", - pathTemplate: `/foo/`, - shouldMatch: true, - }, - { - title: "Mismatch scheme specified on parent route", - route: new(Route).Schemes("https").Subrouter().PathPrefix("/"), - request: newRequest("GET", "http://localhost/"), - vars: nil, - host: "", - path: "/", - pathTemplate: `/`, - shouldMatch: false, - }, - { - title: "Match scheme specified on parent route", - route: new(Route).Schemes("http").Subrouter().PathPrefix("/"), - request: newRequest("GET", "http://localhost/"), - vars: nil, - host: "", - path: "/", - pathTemplate: `/`, - shouldMatch: true, - }, - { - title: "No match header specified on parent route", - route: new(Route).Headers("X-Forwarded-Proto", "https").Subrouter().PathPrefix("/"), - request: newRequest("GET", "http://localhost/"), - vars: nil, - host: "", - path: "/", - pathTemplate: `/`, - shouldMatch: false, - }, - { - title: "Header mismatch value specified on parent route", - route: new(Route).Headers("X-Forwarded-Proto", "https").Subrouter().PathPrefix("/"), - request: newRequestWithHeaders("GET", "http://localhost/", "X-Forwarded-Proto", "http"), - vars: nil, - host: "", - path: "/", - pathTemplate: `/`, - shouldMatch: false, - }, - { - title: "Header match value specified on parent route", - route: new(Route).Headers("X-Forwarded-Proto", "https").Subrouter().PathPrefix("/"), - request: newRequestWithHeaders("GET", "http://localhost/", "X-Forwarded-Proto", "https"), - vars: nil, - host: "", - path: "/", - pathTemplate: `/`, - shouldMatch: true, - }, - { - title: "Query specified on parent route not present", - route: new(Route).Headers("key", "foobar").Subrouter().PathPrefix("/"), - request: newRequest("GET", "http://localhost/"), - vars: nil, - host: "", - path: "/", - pathTemplate: `/`, - shouldMatch: false, - }, - { - title: "Query mismatch value specified on parent route", - route: new(Route).Queries("key", "foobar").Subrouter().PathPrefix("/"), - request: newRequest("GET", "http://localhost/?key=notfoobar"), - vars: nil, - host: "", - path: "/", - pathTemplate: `/`, - shouldMatch: false, - }, - { - title: "Query match value specified on subroute", - route: new(Route).Queries("key", "foobar").Subrouter().PathPrefix("/"), - request: newRequest("GET", "http://localhost/?key=foobar"), - vars: nil, - host: "", - path: "/", - pathTemplate: `/`, - shouldMatch: true, - }, - { - title: "Build with scheme on parent router", - route: new(Route).Schemes("ftp").Host("google.com").Subrouter().Path("/"), - request: newRequest("GET", "ftp://google.com/"), - scheme: "ftp", - host: "google.com", - path: "/", - pathTemplate: `/`, - hostTemplate: `google.com`, - shouldMatch: true, - }, - { - title: "Prefer scheme on child route when building URLs", - route: new(Route).Schemes("https", "ftp").Host("google.com").Subrouter().Schemes("ftp").Path("/"), - request: newRequest("GET", "ftp://google.com/"), - scheme: "ftp", - host: "google.com", - path: "/", - pathTemplate: `/`, - hostTemplate: `google.com`, - shouldMatch: true, - }, - } - - for _, test := range tests { - t.Run(test.title, func(t *testing.T) { - testRoute(t, test) - testTemplate(t, test) - testUseEscapedRoute(t, test) - }) - } -} - -func TestNamedRoutes(t *testing.T) { - r1 := NewRouter() - r1.NewRoute().Name("a") - r1.NewRoute().Name("b") - r1.NewRoute().Name("c") - - r2 := r1.NewRoute().Subrouter() - r2.NewRoute().Name("d") - r2.NewRoute().Name("e") - r2.NewRoute().Name("f") - - r3 := r2.NewRoute().Subrouter() - r3.NewRoute().Name("g") - r3.NewRoute().Name("h") - r3.NewRoute().Name("i") - r3.Name("j") - - if r1.namedRoutes == nil || len(r1.namedRoutes) != 10 { - t.Errorf("Expected 10 named routes, got %v", r1.namedRoutes) - } else if r1.Get("j") == nil { - t.Errorf("Subroute name not registered") - } -} - -func TestNameMultipleCalls(t *testing.T) { - r1 := NewRouter() - rt := r1.NewRoute().Name("foo").Name("bar") - err := rt.GetError() - if err == nil { - t.Errorf("Expected an error") - } -} - -func TestStrictSlash(t *testing.T) { - r := NewRouter() - r.StrictSlash(true) - - tests := []routeTest{ - { - title: "Redirect path without slash", - route: r.NewRoute().Path("/111/"), - request: newRequest("GET", "http://localhost/111"), - vars: nil, - host: "", - path: "/111/", - shouldMatch: true, - shouldRedirect: true, - }, - { - title: "Do not redirect path with slash", - route: r.NewRoute().Path("/111/"), - request: newRequest("GET", "http://localhost/111/"), - vars: nil, - host: "", - path: "/111/", - shouldMatch: true, - shouldRedirect: false, - }, - { - title: "Redirect path with slash", - route: r.NewRoute().Path("/111"), - request: newRequest("GET", "http://localhost/111/"), - vars: nil, - host: "", - path: "/111", - shouldMatch: true, - shouldRedirect: true, - }, - { - title: "Do not redirect path without slash", - route: r.NewRoute().Path("/111"), - request: newRequest("GET", "http://localhost/111"), - vars: nil, - host: "", - path: "/111", - shouldMatch: true, - shouldRedirect: false, - }, - { - title: "Propagate StrictSlash to subrouters", - route: r.NewRoute().PathPrefix("/static/").Subrouter().Path("/images/"), - request: newRequest("GET", "http://localhost/static/images"), - vars: nil, - host: "", - path: "/static/images/", - shouldMatch: true, - shouldRedirect: true, - }, - { - title: "Ignore StrictSlash for path prefix", - route: r.NewRoute().PathPrefix("/static/"), - request: newRequest("GET", "http://localhost/static/logo.png"), - vars: nil, - host: "", - path: "/static/", - shouldMatch: true, - shouldRedirect: false, - }, - } - - for _, test := range tests { - t.Run(test.title, func(t *testing.T) { - testRoute(t, test) - testTemplate(t, test) - testUseEscapedRoute(t, test) - }) - } -} - -func TestUseEncodedPath(t *testing.T) { - r := NewRouter() - r.UseEncodedPath() - - tests := []routeTest{ - { - title: "Router with useEncodedPath, URL with encoded slash does match", - route: r.NewRoute().Path("/v1/{v1}/v2"), - request: newRequest("GET", "http://localhost/v1/1%2F2/v2"), - vars: map[string]string{"v1": "1%2F2"}, - host: "", - path: "/v1/1%2F2/v2", - pathTemplate: `/v1/{v1}/v2`, - shouldMatch: true, - }, - { - title: "Router with useEncodedPath, URL with encoded slash doesn't match", - route: r.NewRoute().Path("/v1/1/2/v2"), - request: newRequest("GET", "http://localhost/v1/1%2F2/v2"), - vars: map[string]string{"v1": "1%2F2"}, - host: "", - path: "/v1/1%2F2/v2", - pathTemplate: `/v1/1/2/v2`, - shouldMatch: false, - }, - } - - for _, test := range tests { - t.Run(test.title, func(t *testing.T) { - testRoute(t, test) - testTemplate(t, test) - }) - } -} - -func TestWalkSingleDepth(t *testing.T) { - r0 := NewRouter() - r1 := NewRouter() - r2 := NewRouter() - - r0.Path("/g") - r0.Path("/o") - r0.Path("/d").Handler(r1) - r0.Path("/r").Handler(r2) - r0.Path("/a") - - r1.Path("/z") - r1.Path("/i") - r1.Path("/l") - r1.Path("/l") - - r2.Path("/i") - r2.Path("/l") - r2.Path("/l") - - paths := []string{"g", "o", "r", "i", "l", "l", "a"} - depths := []int{0, 0, 0, 1, 1, 1, 0} - i := 0 - err := r0.Walk(func(route *Route, router *Router, ancestors []*Route) error { - matcher := route.matchers[0].(*routeRegexp) - if matcher.template == "/d" { - return SkipRouter - } - if len(ancestors) != depths[i] { - t.Errorf(`Expected depth of %d at i = %d; got "%d"`, depths[i], i, len(ancestors)) - } - if matcher.template != "/"+paths[i] { - t.Errorf(`Expected "/%s" at i = %d; got "%s"`, paths[i], i, matcher.template) - } - i++ - return nil - }) - if err != nil { - panic(err) - } - if i != len(paths) { - t.Errorf("Expected %d routes, found %d", len(paths), i) - } -} - -func TestWalkNested(t *testing.T) { - router := NewRouter() - - routeSubrouter := func(r *Route) (*Route, *Router) { - return r, r.Subrouter() - } - - gRoute, g := routeSubrouter(router.Path("/g")) - oRoute, o := routeSubrouter(g.PathPrefix("/o")) - rRoute, r := routeSubrouter(o.PathPrefix("/r")) - iRoute, i := routeSubrouter(r.PathPrefix("/i")) - l1Route, l1 := routeSubrouter(i.PathPrefix("/l")) - l2Route, l2 := routeSubrouter(l1.PathPrefix("/l")) - l2.Path("/a") - - testCases := []struct { - path string - ancestors []*Route - }{ - {"/g", []*Route{}}, - {"/g/o", []*Route{gRoute}}, - {"/g/o/r", []*Route{gRoute, oRoute}}, - {"/g/o/r/i", []*Route{gRoute, oRoute, rRoute}}, - {"/g/o/r/i/l", []*Route{gRoute, oRoute, rRoute, iRoute}}, - {"/g/o/r/i/l/l", []*Route{gRoute, oRoute, rRoute, iRoute, l1Route}}, - {"/g/o/r/i/l/l/a", []*Route{gRoute, oRoute, rRoute, iRoute, l1Route, l2Route}}, - } - - idx := 0 - err := router.Walk(func(route *Route, router *Router, ancestors []*Route) error { - path := testCases[idx].path - tpl := route.regexp.path.template - if tpl != path { - t.Errorf(`Expected %s got %s`, path, tpl) - } - currWantAncestors := testCases[idx].ancestors - if !reflect.DeepEqual(currWantAncestors, ancestors) { - t.Errorf(`Expected %+v got %+v`, currWantAncestors, ancestors) - } - idx++ - return nil - }) - if err != nil { - panic(err) - } - if idx != len(testCases) { - t.Errorf("Expected %d routes, found %d", len(testCases), idx) - } -} - -func TestWalkSubrouters(t *testing.T) { - router := NewRouter() - - g := router.Path("/g").Subrouter() - o := g.PathPrefix("/o").Subrouter() - o.Methods("GET") - o.Methods("PUT") - - // all 4 routes should be matched - paths := []string{"/g", "/g/o", "/g/o", "/g/o"} - idx := 0 - err := router.Walk(func(route *Route, router *Router, ancestors []*Route) error { - path := paths[idx] - tpl, _ := route.GetPathTemplate() - if tpl != path { - t.Errorf(`Expected %s got %s`, path, tpl) - } - idx++ - return nil - }) - if err != nil { - panic(err) - } - if idx != len(paths) { - t.Errorf("Expected %d routes, found %d", len(paths), idx) - } -} - -func TestWalkErrorRoute(t *testing.T) { - router := NewRouter() - router.Path("/g") - expectedError := errors.New("error") - err := router.Walk(func(route *Route, router *Router, ancestors []*Route) error { - return expectedError - }) - if err != expectedError { - t.Errorf("Expected %v routes, found %v", expectedError, err) - } -} - -func TestWalkErrorMatcher(t *testing.T) { - router := NewRouter() - expectedError := router.Path("/g").Subrouter().Path("").GetError() - err := router.Walk(func(route *Route, router *Router, ancestors []*Route) error { - return route.GetError() - }) - if err != expectedError { - t.Errorf("Expected %v routes, found %v", expectedError, err) - } -} - -func TestWalkErrorHandler(t *testing.T) { - handler := NewRouter() - expectedError := handler.Path("/path").Subrouter().Path("").GetError() - router := NewRouter() - router.Path("/g").Handler(handler) - err := router.Walk(func(route *Route, router *Router, ancestors []*Route) error { - return route.GetError() - }) - if err != expectedError { - t.Errorf("Expected %v routes, found %v", expectedError, err) - } -} - -func TestSubrouterErrorHandling(t *testing.T) { - superRouterCalled := false - subRouterCalled := false - - router := NewRouter() - router.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - superRouterCalled = true - }) - subRouter := router.PathPrefix("/bign8").Subrouter() - subRouter.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - subRouterCalled = true - }) - - req, _ := http.NewRequest("GET", "http://localhost/bign8/was/here", nil) - router.ServeHTTP(NewRecorder(), req) - - if superRouterCalled { - t.Error("Super router 404 handler called when sub-router 404 handler is available.") - } - if !subRouterCalled { - t.Error("Sub-router 404 handler was not called.") - } -} - -// See: https://github.com/gorilla/mux/issues/200 -func TestPanicOnCapturingGroups(t *testing.T) { - defer func() { - if recover() == nil { - t.Errorf("(Test that capturing groups now fail fast) Expected panic, however test completed successfully.\n") - } - }() - NewRouter().NewRoute().Path("/{type:(promo|special)}/{promoId}.json") -} - -// ---------------------------------------------------------------------------- -// Helpers -// ---------------------------------------------------------------------------- - -func getRouteTemplate(route *Route) string { - host, err := route.GetHostTemplate() - if err != nil { - host = "none" - } - path, err := route.GetPathTemplate() - if err != nil { - path = "none" - } - return fmt.Sprintf("Host: %v, Path: %v", host, path) -} - -func testRoute(t *testing.T, test routeTest) { - request := test.request - route := test.route - vars := test.vars - shouldMatch := test.shouldMatch - query := test.query - shouldRedirect := test.shouldRedirect - uri := url.URL{ - Scheme: test.scheme, - Host: test.host, - Path: test.path, - } - if uri.Scheme == "" { - uri.Scheme = "http" - } - - var match RouteMatch - ok := route.Match(request, &match) - if ok != shouldMatch { - msg := "Should match" - if !shouldMatch { - msg = "Should not match" - } - t.Errorf("(%v) %v:\nRoute: %#v\nRequest: %#v\nVars: %v\n", test.title, msg, route, request, vars) - return - } - if shouldMatch { - if vars != nil && !stringMapEqual(vars, match.Vars) { - t.Errorf("(%v) Vars not equal: expected %v, got %v", test.title, vars, match.Vars) - return - } - if test.scheme != "" { - u, err := route.URL(mapToPairs(match.Vars)...) - if err != nil { - t.Fatalf("(%v) URL error: %v -- %v", test.title, err, getRouteTemplate(route)) - } - if uri.Scheme != u.Scheme { - t.Errorf("(%v) URLScheme not equal: expected %v, got %v", test.title, uri.Scheme, u.Scheme) - return - } - } - if test.host != "" { - u, err := test.route.URLHost(mapToPairs(match.Vars)...) - if err != nil { - t.Fatalf("(%v) URLHost error: %v -- %v", test.title, err, getRouteTemplate(route)) - } - if uri.Scheme != u.Scheme { - t.Errorf("(%v) URLHost scheme not equal: expected %v, got %v -- %v", test.title, uri.Scheme, u.Scheme, getRouteTemplate(route)) - return - } - if uri.Host != u.Host { - t.Errorf("(%v) URLHost host not equal: expected %v, got %v -- %v", test.title, uri.Host, u.Host, getRouteTemplate(route)) - return - } - } - if test.path != "" { - u, err := route.URLPath(mapToPairs(match.Vars)...) - if err != nil { - t.Fatalf("(%v) URLPath error: %v -- %v", test.title, err, getRouteTemplate(route)) - } - if uri.Path != u.Path { - t.Errorf("(%v) URLPath not equal: expected %v, got %v -- %v", test.title, uri.Path, u.Path, getRouteTemplate(route)) - return - } - } - if test.host != "" && test.path != "" { - u, err := route.URL(mapToPairs(match.Vars)...) - if err != nil { - t.Fatalf("(%v) URL error: %v -- %v", test.title, err, getRouteTemplate(route)) - } - if expected, got := uri.String(), u.String(); expected != got { - t.Errorf("(%v) URL not equal: expected %v, got %v -- %v", test.title, expected, got, getRouteTemplate(route)) - return - } - } - if query != "" { - u, err := route.URL(mapToPairs(match.Vars)...) - if err != nil { - t.Errorf("(%v) erred while creating url: %v", test.title, err) - return - } - if query != u.RawQuery { - t.Errorf("(%v) URL query not equal: expected %v, got %v", test.title, query, u.RawQuery) - return - } - } - if shouldRedirect && match.Handler == nil { - t.Errorf("(%v) Did not redirect", test.title) - return - } - if !shouldRedirect && match.Handler != nil { - t.Errorf("(%v) Unexpected redirect", test.title) - return - } - } -} - -func testUseEscapedRoute(t *testing.T, test routeTest) { - test.route.useEncodedPath = true - testRoute(t, test) -} - -func testTemplate(t *testing.T, test routeTest) { - route := test.route - pathTemplate := test.pathTemplate - if len(pathTemplate) == 0 { - pathTemplate = test.path - } - hostTemplate := test.hostTemplate - if len(hostTemplate) == 0 { - hostTemplate = test.host - } - - routePathTemplate, pathErr := route.GetPathTemplate() - if pathErr == nil && routePathTemplate != pathTemplate { - t.Errorf("(%v) GetPathTemplate not equal: expected %v, got %v", test.title, pathTemplate, routePathTemplate) - } - - routeHostTemplate, hostErr := route.GetHostTemplate() - if hostErr == nil && routeHostTemplate != hostTemplate { - t.Errorf("(%v) GetHostTemplate not equal: expected %v, got %v", test.title, hostTemplate, routeHostTemplate) - } -} - -func testMethods(t *testing.T, test routeTest) { - route := test.route - methods, _ := route.GetMethods() - if strings.Join(methods, ",") != strings.Join(test.methods, ",") { - t.Errorf("(%v) GetMethods not equal: expected %v, got %v", test.title, test.methods, methods) - } -} - -func testRegexp(t *testing.T, test routeTest) { - route := test.route - routePathRegexp, regexpErr := route.GetPathRegexp() - if test.pathRegexp != "" && regexpErr == nil && routePathRegexp != test.pathRegexp { - t.Errorf("(%v) GetPathRegexp not equal: expected %v, got %v", test.title, test.pathRegexp, routePathRegexp) - } -} - -func testQueriesRegexp(t *testing.T, test routeTest) { - route := test.route - queries, queriesErr := route.GetQueriesRegexp() - gotQueries := strings.Join(queries, ",") - if test.queriesRegexp != "" && queriesErr == nil && gotQueries != test.queriesRegexp { - t.Errorf("(%v) GetQueriesRegexp not equal: expected %v, got %v", test.title, test.queriesRegexp, gotQueries) - } -} - -func testQueriesTemplates(t *testing.T, test routeTest) { - route := test.route - queries, queriesErr := route.GetQueriesTemplates() - gotQueries := strings.Join(queries, ",") - if test.queriesTemplate != "" && queriesErr == nil && gotQueries != test.queriesTemplate { - t.Errorf("(%v) GetQueriesTemplates not equal: expected %v, got %v", test.title, test.queriesTemplate, gotQueries) - } -} - -type TestA301ResponseWriter struct { - hh http.Header - status int -} - -func (ho *TestA301ResponseWriter) Header() http.Header { - return ho.hh -} - -func (ho *TestA301ResponseWriter) Write(b []byte) (int, error) { - return 0, nil -} - -func (ho *TestA301ResponseWriter) WriteHeader(code int) { - ho.status = code -} - -func Test301Redirect(t *testing.T) { - m := make(http.Header) - - func1 := func(w http.ResponseWriter, r *http.Request) {} - func2 := func(w http.ResponseWriter, r *http.Request) {} - - r := NewRouter() - r.HandleFunc("/api/", func2).Name("func2") - r.HandleFunc("/", func1).Name("func1") - - req, _ := http.NewRequest("GET", "http://localhost//api/?abc=def", nil) - - res := TestA301ResponseWriter{ - hh: m, - status: 0, - } - r.ServeHTTP(&res, req) - - if "http://localhost/api/?abc=def" != res.hh["Location"][0] { - t.Errorf("Should have complete URL with query string") - } -} - -func TestSkipClean(t *testing.T) { - func1 := func(w http.ResponseWriter, r *http.Request) {} - func2 := func(w http.ResponseWriter, r *http.Request) {} - - r := NewRouter() - r.SkipClean(true) - r.HandleFunc("/api/", func2).Name("func2") - r.HandleFunc("/", func1).Name("func1") - - req, _ := http.NewRequest("GET", "http://localhost//api/?abc=def", nil) - res := NewRecorder() - r.ServeHTTP(res, req) - - if len(res.HeaderMap["Location"]) != 0 { - t.Errorf("Shouldn't redirect since skip clean is disabled") - } -} - -// https://plus.google.com/101022900381697718949/posts/eWy6DjFJ6uW -func TestSubrouterHeader(t *testing.T) { - expected := "func1 response" - func1 := func(w http.ResponseWriter, r *http.Request) { - fmt.Fprint(w, expected) - } - func2 := func(http.ResponseWriter, *http.Request) {} - - r := NewRouter() - s := r.Headers("SomeSpecialHeader", "").Subrouter() - s.HandleFunc("/", func1).Name("func1") - r.HandleFunc("/", func2).Name("func2") - - req, _ := http.NewRequest("GET", "http://localhost/", nil) - req.Header.Add("SomeSpecialHeader", "foo") - match := new(RouteMatch) - matched := r.Match(req, match) - if !matched { - t.Errorf("Should match request") - } - if match.Route.GetName() != "func1" { - t.Errorf("Expecting func1 handler, got %s", match.Route.GetName()) - } - resp := NewRecorder() - match.Handler.ServeHTTP(resp, req) - if resp.Body.String() != expected { - t.Errorf("Expecting %q", expected) - } -} - -func TestNoMatchMethodErrorHandler(t *testing.T) { - func1 := func(w http.ResponseWriter, r *http.Request) {} - - r := NewRouter() - r.HandleFunc("/", func1).Methods("GET", "POST") - - req, _ := http.NewRequest("PUT", "http://localhost/", nil) - match := new(RouteMatch) - matched := r.Match(req, match) - - if matched { - t.Error("Should not have matched route for methods") - } - - if match.MatchErr != ErrMethodMismatch { - t.Error("Should get ErrMethodMismatch error") - } - - resp := NewRecorder() - r.ServeHTTP(resp, req) - if resp.Code != http.StatusMethodNotAllowed { - t.Errorf("Expecting code %v", 405) - } - - // Add matching route - r.HandleFunc("/", func1).Methods("PUT") - - match = new(RouteMatch) - matched = r.Match(req, match) - - if !matched { - t.Error("Should have matched route for methods") - } - - if match.MatchErr != nil { - t.Error("Should not have any matching error. Found:", match.MatchErr) - } -} - -func TestMultipleDefinitionOfSamePathWithDifferentMethods(t *testing.T) { - emptyHandler := func(w http.ResponseWriter, r *http.Request) {} - - r := NewRouter() - r.HandleFunc("/api", emptyHandler).Methods("POST") - r.HandleFunc("/api", emptyHandler).Queries("time", "{time:[0-9]+}").Methods("GET") - - t.Run("Post Method should be matched properly", func(t *testing.T) { - req, _ := http.NewRequest("POST", "http://localhost/api", nil) - match := new(RouteMatch) - matched := r.Match(req, match) - if !matched { - t.Error("Should have matched route for methods") - } - if match.MatchErr != nil { - t.Error("Should not have any matching error. Found:", match.MatchErr) - } - }) - - t.Run("Get Method with invalid query value should not match", func(t *testing.T) { - req, _ := http.NewRequest("GET", "http://localhost/api?time=-4", nil) - match := new(RouteMatch) - matched := r.Match(req, match) - if matched { - t.Error("Should not have matched route for methods") - } - if match.MatchErr != ErrNotFound { - t.Error("Should have ErrNotFound error. Found:", match.MatchErr) - } - }) - - t.Run("A mismach method of a valid path should return ErrMethodMismatch", func(t *testing.T) { - r := NewRouter() - r.HandleFunc("/api2", emptyHandler).Methods("POST") - req, _ := http.NewRequest("GET", "http://localhost/api2", nil) - match := new(RouteMatch) - matched := r.Match(req, match) - if matched { - t.Error("Should not have matched route for methods") - } - if match.MatchErr != ErrMethodMismatch { - t.Error("Should have ErrMethodMismatch error. Found:", match.MatchErr) - } - }) - -} - -func TestErrMatchNotFound(t *testing.T) { - emptyHandler := func(w http.ResponseWriter, r *http.Request) {} - - r := NewRouter() - r.HandleFunc("/", emptyHandler) - s := r.PathPrefix("/sub/").Subrouter() - s.HandleFunc("/", emptyHandler) - - // Regular 404 not found - req, _ := http.NewRequest("GET", "/sub/whatever", nil) - match := new(RouteMatch) - matched := r.Match(req, match) - - if matched { - t.Errorf("Subrouter should not have matched that, got %v", match.Route) - } - // Even without a custom handler, MatchErr is set to ErrNotFound - if match.MatchErr != ErrNotFound { - t.Errorf("Expected ErrNotFound MatchErr, but was %v", match.MatchErr) - } - - // Now lets add a 404 handler to subrouter - s.NotFoundHandler = http.NotFoundHandler() - req, _ = http.NewRequest("GET", "/sub/whatever", nil) - - // Test the subrouter first - match = new(RouteMatch) - matched = s.Match(req, match) - // Now we should get a match - if !matched { - t.Errorf("Subrouter should have matched %s", req.RequestURI) - } - // But MatchErr should be set to ErrNotFound anyway - if match.MatchErr != ErrNotFound { - t.Errorf("Expected ErrNotFound MatchErr, but was %v", match.MatchErr) - } - - // Now test the parent (MatchErr should propagate) - match = new(RouteMatch) - matched = r.Match(req, match) - - // Now we should get a match - if !matched { - t.Errorf("Router should have matched %s via subrouter", req.RequestURI) - } - // But MatchErr should be set to ErrNotFound anyway - if match.MatchErr != ErrNotFound { - t.Errorf("Expected ErrNotFound MatchErr, but was %v", match.MatchErr) - } -} - -// methodsSubrouterTest models the data necessary for testing handler -// matching for subrouters created after HTTP methods matcher registration. -type methodsSubrouterTest struct { - title string - wantCode int - router *Router - // method is the input into the request and expected response - method string - // input request path - path string - // redirectTo is the expected location path for strict-slash matches - redirectTo string -} - -// methodHandler writes the method string in response. -func methodHandler(method string) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - _, err := w.Write([]byte(method)) - if err != nil { - log.Printf("Failed writing HTTP response: %v", err) - } - } -} - -// TestMethodsSubrouterCatchall matches handlers for subrouters where a -// catchall handler is set for a mis-matching method. -func TestMethodsSubrouterCatchall(t *testing.T) { - t.Parallel() - - router := NewRouter() - router.Methods("PATCH").Subrouter().PathPrefix("/").HandlerFunc(methodHandler("PUT")) - router.Methods("GET").Subrouter().HandleFunc("/foo", methodHandler("GET")) - router.Methods("POST").Subrouter().HandleFunc("/foo", methodHandler("POST")) - router.Methods("DELETE").Subrouter().HandleFunc("/foo", methodHandler("DELETE")) - - tests := []methodsSubrouterTest{ - { - title: "match GET handler", - router: router, - path: "http://localhost/foo", - method: "GET", - wantCode: http.StatusOK, - }, - { - title: "match POST handler", - router: router, - method: "POST", - path: "http://localhost/foo", - wantCode: http.StatusOK, - }, - { - title: "match DELETE handler", - router: router, - method: "DELETE", - path: "http://localhost/foo", - wantCode: http.StatusOK, - }, - { - title: "disallow PUT method", - router: router, - method: "PUT", - path: "http://localhost/foo", - wantCode: http.StatusMethodNotAllowed, - }, - } - - for _, test := range tests { - t.Run(test.title, func(t *testing.T) { - testMethodsSubrouter(t, test) - }) - } -} - -// TestMethodsSubrouterStrictSlash matches handlers on subrouters with -// strict-slash matchers. -func TestMethodsSubrouterStrictSlash(t *testing.T) { - t.Parallel() - - router := NewRouter() - sub := router.PathPrefix("/").Subrouter() - sub.StrictSlash(true).Path("/foo").Methods("GET").Subrouter().HandleFunc("", methodHandler("GET")) - sub.StrictSlash(true).Path("/foo/").Methods("PUT").Subrouter().HandleFunc("/", methodHandler("PUT")) - sub.StrictSlash(true).Path("/foo/").Methods("POST").Subrouter().HandleFunc("/", methodHandler("POST")) - - tests := []methodsSubrouterTest{ - { - title: "match POST handler", - router: router, - method: "POST", - path: "http://localhost/foo/", - wantCode: http.StatusOK, - }, - { - title: "match GET handler", - router: router, - method: "GET", - path: "http://localhost/foo", - wantCode: http.StatusOK, - }, - { - title: "match POST handler, redirect strict-slash", - router: router, - method: "POST", - path: "http://localhost/foo", - redirectTo: "http://localhost/foo/", - wantCode: http.StatusMovedPermanently, - }, - { - title: "match GET handler, redirect strict-slash", - router: router, - method: "GET", - path: "http://localhost/foo/", - redirectTo: "http://localhost/foo", - wantCode: http.StatusMovedPermanently, - }, - { - title: "disallow DELETE method", - router: router, - method: "DELETE", - path: "http://localhost/foo", - wantCode: http.StatusMethodNotAllowed, - }, - } - - for _, test := range tests { - t.Run(test.title, func(t *testing.T) { - testMethodsSubrouter(t, test) - }) - } -} - -// TestMethodsSubrouterPathPrefix matches handlers on subrouters created -// on a router with a path prefix matcher and method matcher. -func TestMethodsSubrouterPathPrefix(t *testing.T) { - t.Parallel() - - router := NewRouter() - router.PathPrefix("/1").Methods("POST").Subrouter().HandleFunc("/2", methodHandler("POST")) - router.PathPrefix("/1").Methods("DELETE").Subrouter().HandleFunc("/2", methodHandler("DELETE")) - router.PathPrefix("/1").Methods("PUT").Subrouter().HandleFunc("/2", methodHandler("PUT")) - router.PathPrefix("/1").Methods("POST").Subrouter().HandleFunc("/2", methodHandler("POST2")) - - tests := []methodsSubrouterTest{ - { - title: "match first POST handler", - router: router, - method: "POST", - path: "http://localhost/1/2", - wantCode: http.StatusOK, - }, - { - title: "match DELETE handler", - router: router, - method: "DELETE", - path: "http://localhost/1/2", - wantCode: http.StatusOK, - }, - { - title: "match PUT handler", - router: router, - method: "PUT", - path: "http://localhost/1/2", - wantCode: http.StatusOK, - }, - { - title: "disallow PATCH method", - router: router, - method: "PATCH", - path: "http://localhost/1/2", - wantCode: http.StatusMethodNotAllowed, - }, - } - - for _, test := range tests { - t.Run(test.title, func(t *testing.T) { - testMethodsSubrouter(t, test) - }) - } -} - -// TestMethodsSubrouterSubrouter matches handlers on subrouters produced -// from method matchers registered on a root subrouter. -func TestMethodsSubrouterSubrouter(t *testing.T) { - t.Parallel() - - router := NewRouter() - sub := router.PathPrefix("/1").Subrouter() - sub.Methods("POST").Subrouter().HandleFunc("/2", methodHandler("POST")) - sub.Methods("GET").Subrouter().HandleFunc("/2", methodHandler("GET")) - sub.Methods("PATCH").Subrouter().HandleFunc("/2", methodHandler("PATCH")) - sub.HandleFunc("/2", methodHandler("PUT")).Subrouter().Methods("PUT") - sub.HandleFunc("/2", methodHandler("POST2")).Subrouter().Methods("POST") - - tests := []methodsSubrouterTest{ - { - title: "match first POST handler", - router: router, - method: "POST", - path: "http://localhost/1/2", - wantCode: http.StatusOK, - }, - { - title: "match GET handler", - router: router, - method: "GET", - path: "http://localhost/1/2", - wantCode: http.StatusOK, - }, - { - title: "match PATCH handler", - router: router, - method: "PATCH", - path: "http://localhost/1/2", - wantCode: http.StatusOK, - }, - { - title: "match PUT handler", - router: router, - method: "PUT", - path: "http://localhost/1/2", - wantCode: http.StatusOK, - }, - { - title: "disallow DELETE method", - router: router, - method: "DELETE", - path: "http://localhost/1/2", - wantCode: http.StatusMethodNotAllowed, - }, - } - - for _, test := range tests { - t.Run(test.title, func(t *testing.T) { - testMethodsSubrouter(t, test) - }) - } -} - -// TestMethodsSubrouterPathVariable matches handlers on matching paths -// with path variables in them. -func TestMethodsSubrouterPathVariable(t *testing.T) { - t.Parallel() - - router := NewRouter() - router.Methods("GET").Subrouter().HandleFunc("/foo", methodHandler("GET")) - router.Methods("POST").Subrouter().HandleFunc("/{any}", methodHandler("POST")) - router.Methods("DELETE").Subrouter().HandleFunc("/1/{any}", methodHandler("DELETE")) - router.Methods("PUT").Subrouter().HandleFunc("/1/{any}", methodHandler("PUT")) - - tests := []methodsSubrouterTest{ - { - title: "match GET handler", - router: router, - method: "GET", - path: "http://localhost/foo", - wantCode: http.StatusOK, - }, - { - title: "match POST handler", - router: router, - method: "POST", - path: "http://localhost/foo", - wantCode: http.StatusOK, - }, - { - title: "match DELETE handler", - router: router, - method: "DELETE", - path: "http://localhost/1/foo", - wantCode: http.StatusOK, - }, - { - title: "match PUT handler", - router: router, - method: "PUT", - path: "http://localhost/1/foo", - wantCode: http.StatusOK, - }, - { - title: "disallow PATCH method", - router: router, - method: "PATCH", - path: "http://localhost/1/foo", - wantCode: http.StatusMethodNotAllowed, - }, - } - - for _, test := range tests { - t.Run(test.title, func(t *testing.T) { - testMethodsSubrouter(t, test) - }) - } -} - -func ExampleSetURLVars() { - req, _ := http.NewRequest("GET", "/foo", nil) - req = SetURLVars(req, map[string]string{"foo": "bar"}) - - fmt.Println(Vars(req)["foo"]) - - // Output: bar -} - -// testMethodsSubrouter runs an individual methodsSubrouterTest. -func testMethodsSubrouter(t *testing.T, test methodsSubrouterTest) { - // Execute request - req, _ := http.NewRequest(test.method, test.path, nil) - resp := NewRecorder() - test.router.ServeHTTP(resp, req) - - switch test.wantCode { - case http.StatusMethodNotAllowed: - if resp.Code != http.StatusMethodNotAllowed { - t.Errorf(`(%s) Expected "405 Method Not Allowed", but got %d code`, test.title, resp.Code) - } else if matchedMethod := resp.Body.String(); matchedMethod != "" { - t.Errorf(`(%s) Expected "405 Method Not Allowed", but %q handler was called`, test.title, matchedMethod) - } - - case http.StatusMovedPermanently: - if gotLocation := resp.HeaderMap.Get("Location"); gotLocation != test.redirectTo { - t.Errorf("(%s) Expected %q route-match to redirect to %q, but got %q", test.title, test.method, test.redirectTo, gotLocation) - } - - case http.StatusOK: - if matchedMethod := resp.Body.String(); matchedMethod != test.method { - t.Errorf("(%s) Expected %q handler to be called, but %q handler was called", test.title, test.method, matchedMethod) - } - - default: - expectedCodes := []int{http.StatusMethodNotAllowed, http.StatusMovedPermanently, http.StatusOK} - t.Errorf("(%s) Expected wantCode to be one of: %v, but got %d", test.title, expectedCodes, test.wantCode) - } -} - -func TestSubrouterMatching(t *testing.T) { - const ( - none, stdOnly, subOnly uint8 = 0, 1 << 0, 1 << 1 - both = subOnly | stdOnly - ) - - type request struct { - Name string - Request *http.Request - Flags uint8 - } - - cases := []struct { - Name string - Standard, Subrouter func(*Router) - Requests []request - }{ - { - "pathPrefix", - func(r *Router) { - r.PathPrefix("/before").PathPrefix("/after") - }, - func(r *Router) { - r.PathPrefix("/before").Subrouter().PathPrefix("/after") - }, - []request{ - {"no match final path prefix", newRequest("GET", "/after"), none}, - {"no match parent path prefix", newRequest("GET", "/before"), none}, - {"matches append", newRequest("GET", "/before/after"), both}, - {"matches as prefix", newRequest("GET", "/before/after/1234"), both}, - }, - }, - { - "path", - func(r *Router) { - r.Path("/before").Path("/after") - }, - func(r *Router) { - r.Path("/before").Subrouter().Path("/after") - }, - []request{ - {"no match subroute path", newRequest("GET", "/after"), none}, - {"no match parent path", newRequest("GET", "/before"), none}, - {"no match as prefix", newRequest("GET", "/before/after/1234"), none}, - {"no match append", newRequest("GET", "/before/after"), none}, - }, - }, - { - "host", - func(r *Router) { - r.Host("before.com").Host("after.com") - }, - func(r *Router) { - r.Host("before.com").Subrouter().Host("after.com") - }, - []request{ - {"no match before", newRequestHost("GET", "/", "before.com"), none}, - {"no match other", newRequestHost("GET", "/", "other.com"), none}, - {"matches after", newRequestHost("GET", "/", "after.com"), none}, - }, - }, - { - "queries variant keys", - func(r *Router) { - r.Queries("foo", "bar").Queries("cricket", "baseball") - }, - func(r *Router) { - r.Queries("foo", "bar").Subrouter().Queries("cricket", "baseball") - }, - []request{ - {"matches with all", newRequest("GET", "/?foo=bar&cricket=baseball"), both}, - {"matches with more", newRequest("GET", "/?foo=bar&cricket=baseball&something=else"), both}, - {"no match with none", newRequest("GET", "/"), none}, - {"no match with some", newRequest("GET", "/?cricket=baseball"), none}, - }, - }, - { - "queries overlapping keys", - func(r *Router) { - r.Queries("foo", "bar").Queries("foo", "baz") - }, - func(r *Router) { - r.Queries("foo", "bar").Subrouter().Queries("foo", "baz") - }, - []request{ - {"no match old value", newRequest("GET", "/?foo=bar"), none}, - {"no match diff value", newRequest("GET", "/?foo=bak"), none}, - {"no match with none", newRequest("GET", "/"), none}, - {"matches override", newRequest("GET", "/?foo=baz"), none}, - }, - }, - { - "header variant keys", - func(r *Router) { - r.Headers("foo", "bar").Headers("cricket", "baseball") - }, - func(r *Router) { - r.Headers("foo", "bar").Subrouter().Headers("cricket", "baseball") - }, - []request{ - { - "matches with all", - newRequestWithHeaders("GET", "/", "foo", "bar", "cricket", "baseball"), - both, - }, - { - "matches with more", - newRequestWithHeaders("GET", "/", "foo", "bar", "cricket", "baseball", "something", "else"), - both, - }, - {"no match with none", newRequest("GET", "/"), none}, - {"no match with some", newRequestWithHeaders("GET", "/", "cricket", "baseball"), none}, - }, - }, - { - "header overlapping keys", - func(r *Router) { - r.Headers("foo", "bar").Headers("foo", "baz") - }, - func(r *Router) { - r.Headers("foo", "bar").Subrouter().Headers("foo", "baz") - }, - []request{ - {"no match old value", newRequestWithHeaders("GET", "/", "foo", "bar"), none}, - {"no match diff value", newRequestWithHeaders("GET", "/", "foo", "bak"), none}, - {"no match with none", newRequest("GET", "/"), none}, - {"matches override", newRequestWithHeaders("GET", "/", "foo", "baz"), none}, - }, - }, - { - "method", - func(r *Router) { - r.Methods("POST").Methods("GET") - }, - func(r *Router) { - r.Methods("POST").Subrouter().Methods("GET") - }, - []request{ - {"matches before", newRequest("POST", "/"), none}, - {"no match other", newRequest("HEAD", "/"), none}, - {"matches override", newRequest("GET", "/"), none}, - }, - }, - { - "schemes", - func(r *Router) { - r.Schemes("http").Schemes("https") - }, - func(r *Router) { - r.Schemes("http").Subrouter().Schemes("https") - }, - []request{ - {"matches overrides", newRequest("GET", "https://www.example.com/"), none}, - {"matches original", newRequest("GET", "http://www.example.com/"), none}, - {"no match other", newRequest("GET", "ftp://www.example.com/"), none}, - }, - }, - } - - // case -> request -> router - for _, c := range cases { - t.Run(c.Name, func(t *testing.T) { - for _, req := range c.Requests { - t.Run(req.Name, func(t *testing.T) { - for _, v := range []struct { - Name string - Config func(*Router) - Expected bool - }{ - {"subrouter", c.Subrouter, (req.Flags & subOnly) != 0}, - {"standard", c.Standard, (req.Flags & stdOnly) != 0}, - } { - r := NewRouter() - v.Config(r) - if r.Match(req.Request, &RouteMatch{}) != v.Expected { - if v.Expected { - t.Errorf("expected %v match", v.Name) - } else { - t.Errorf("expected %v no match", v.Name) - } - } - } - }) - } - }) - } -} - -// verify that copyRouteConf copies fields as expected. -func Test_copyRouteConf(t *testing.T) { - var ( - m MatcherFunc = func(*http.Request, *RouteMatch) bool { - return true - } - b BuildVarsFunc = func(i map[string]string) map[string]string { - return i - } - r, _ = newRouteRegexp("hi", regexpTypeHost, routeRegexpOptions{}) - ) - - tests := []struct { - name string - args routeConf - want routeConf - }{ - { - "empty", - routeConf{}, - routeConf{}, - }, - { - "full", - routeConf{ - useEncodedPath: true, - strictSlash: true, - skipClean: true, - regexp: routeRegexpGroup{host: r, path: r, queries: []*routeRegexp{r}}, - matchers: []matcher{m}, - buildScheme: "https", - buildVarsFunc: b, - }, - routeConf{ - useEncodedPath: true, - strictSlash: true, - skipClean: true, - regexp: routeRegexpGroup{host: r, path: r, queries: []*routeRegexp{r}}, - matchers: []matcher{m}, - buildScheme: "https", - buildVarsFunc: b, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // special case some incomparable fields of routeConf before delegating to reflect.DeepEqual - got := copyRouteConf(tt.args) - - // funcs not comparable, just compare length of slices - if len(got.matchers) != len(tt.want.matchers) { - t.Errorf("matchers different lengths: %v %v", len(got.matchers), len(tt.want.matchers)) - } - got.matchers, tt.want.matchers = nil, nil - - // deep equal treats nil slice differently to empty slice so check for zero len first - { - bothZero := len(got.regexp.queries) == 0 && len(tt.want.regexp.queries) == 0 - if !bothZero && !reflect.DeepEqual(got.regexp.queries, tt.want.regexp.queries) { - t.Errorf("queries unequal: %v %v", got.regexp.queries, tt.want.regexp.queries) - } - got.regexp.queries, tt.want.regexp.queries = nil, nil - } - - // funcs not comparable, just compare nullity - if (got.buildVarsFunc == nil) != (tt.want.buildVarsFunc == nil) { - t.Errorf("build vars funcs unequal: %v %v", got.buildVarsFunc == nil, tt.want.buildVarsFunc == nil) - } - got.buildVarsFunc, tt.want.buildVarsFunc = nil, nil - - // finish the deal - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("route confs unequal: %v %v", got, tt.want) - } - }) - } -} - -func TestMethodNotAllowed(t *testing.T) { - handler := func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } - router := NewRouter() - router.HandleFunc("/thing", handler).Methods(http.MethodGet) - router.HandleFunc("/something", handler).Methods(http.MethodGet) - - w := NewRecorder() - req := newRequest(http.MethodPut, "/thing") - - router.ServeHTTP(w, req) - - if w.Code != http.StatusMethodNotAllowed { - t.Fatalf("Expected status code 405 (got %d)", w.Code) - } -} - -type customMethodNotAllowedHandler struct { - msg string -} - -func (h customMethodNotAllowedHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusMethodNotAllowed) - fmt.Fprint(w, h.msg) -} - -func TestSubrouterCustomMethodNotAllowed(t *testing.T) { - handler := func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } - - router := NewRouter() - router.HandleFunc("/test", handler).Methods(http.MethodGet) - router.MethodNotAllowedHandler = customMethodNotAllowedHandler{msg: "custom router handler"} - - subrouter := router.PathPrefix("/sub").Subrouter() - subrouter.HandleFunc("/test", handler).Methods(http.MethodGet) - subrouter.MethodNotAllowedHandler = customMethodNotAllowedHandler{msg: "custom sub router handler"} - - testCases := map[string]struct { - path string - expMsg string - }{ - "router method not allowed": { - path: "/test", - expMsg: "custom router handler", - }, - "subrouter method not allowed": { - path: "/sub/test", - expMsg: "custom sub router handler", - }, - } - - for name, tc := range testCases { - t.Run(name, func(tt *testing.T) { - w := NewRecorder() - req := newRequest(http.MethodPut, tc.path) - - router.ServeHTTP(w, req) - - if w.Code != http.StatusMethodNotAllowed { - tt.Errorf("Expected status code 405 (got %d)", w.Code) - } - - b, err := io.ReadAll(w.Body) - if err != nil { - tt.Errorf("failed to read body: %v", err) - } - - if string(b) != tc.expMsg { - tt.Errorf("expected msg %q, got %q", tc.expMsg, string(b)) - } - }) - } -} - -func TestSubrouterNotFound(t *testing.T) { - handler := func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } - router := NewRouter() - router.Path("/a").Subrouter().HandleFunc("/thing", handler).Methods(http.MethodGet) - router.Path("/b").Subrouter().HandleFunc("/something", handler).Methods(http.MethodGet) - - w := NewRecorder() - req := newRequest(http.MethodPut, "/not-present") - - router.ServeHTTP(w, req) - - if w.Code != http.StatusNotFound { - t.Fatalf("Expected status code 404 (got %d)", w.Code) - } -} - -func TestContextMiddleware(t *testing.T) { - withTimeout := func(h http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ctx, cancel := context.WithTimeout(r.Context(), time.Minute) - defer cancel() - h.ServeHTTP(w, r.WithContext(ctx)) - }) - } - - r := NewRouter() - r.Handle("/path/{foo}", withTimeout(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - vars := Vars(r) - if vars["foo"] != "bar" { - t.Fatal("Expected foo var to be set") - } - }))) - - rec := NewRecorder() - req := newRequest("GET", "/path/bar") - r.ServeHTTP(rec, req) -} - -func TestGetVarNames(t *testing.T) { - r := NewRouter() - - route := r.Host("{domain}"). - Path("/{group}/{item_id}"). - Queries("some_data1", "{some_data1}"). - Queries("some_data2_and_3", "{some_data2}.{some_data3}") - - // Order of vars in the slice is not guaranteed, so just check for existence - expected := map[string]bool{ - "domain": true, - "group": true, - "item_id": true, - "some_data1": true, - "some_data2": true, - "some_data3": true, - } - - varNames, err := route.GetVarNames() - if err != nil { - t.Fatal(err) - } - - if len(varNames) != len(expected) { - t.Fatalf("expected %d names, got %d", len(expected), len(varNames)) - } - - for _, varName := range varNames { - if !expected[varName] { - t.Fatalf("got unexpected %s", varName) - } - } -} - -func getPopulateContextTestCases() []struct { - name string - path string - omitRouteFromContext bool - wantVar string - wantStaticRoute bool - wantDynamicRoute bool -} { - return []struct { - name string - path string - omitRouteFromContext bool - wantVar string - wantStaticRoute bool - wantDynamicRoute bool - }{ - { - name: "no populated vars", - path: "/static", - wantVar: "", - wantStaticRoute: true, - }, - { - name: "empty var", - path: "/dynamic/", - wantVar: "", - wantDynamicRoute: true, - }, - { - name: "populated vars", - path: "/dynamic/foo", - wantVar: "foo", - wantDynamicRoute: true, - }, - { - name: "omit route /static", - path: "/static", - omitRouteFromContext: true, - wantVar: "", - wantStaticRoute: false, - }, - { - name: "omit route /dynamic", - path: "/dynamic/", - omitRouteFromContext: true, - wantVar: "", - wantDynamicRoute: false, - }, - } -} - -func TestPopulateContext(t *testing.T) { - testCases := getPopulateContextTestCases() - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - matched := false - r := NewRouter() - r.OmitRouteFromContext(tc.omitRouteFromContext) - var static *Route - var dynamic *Route - fn := func(w http.ResponseWriter, r *http.Request) { - matched = true - if got := Vars(r)["x"]; got != tc.wantVar { - t.Fatalf("wantVar=%q, got=%q", tc.wantVar, got) - } - switch { - case tc.wantDynamicRoute: - r2 := CurrentRoute(r) - if r2 != dynamic || r2.GetName() != "dynamic" { - t.Fatalf("expected dynmic route in ctx, got %v", r2) - } - case tc.wantStaticRoute: - r2 := CurrentRoute(r) - if r2 != static || r2.GetName() != "static" { - t.Fatalf("expected static route in ctx, got %v", r2) - } - default: - if r2 := CurrentRoute(r); r2 != nil { - t.Fatalf("expected no route in ctx, got %v", r2) - } - } - w.WriteHeader(http.StatusNoContent) - } - static = r.Name("static").Path("/static").HandlerFunc(fn) - dynamic = r.Name("dynamic").Path("/dynamic/{x:.*}").HandlerFunc(fn) - req := newRequest(http.MethodGet, "http://localhost"+tc.path) - rec := NewRecorder() - r.ServeHTTP(rec, req) - if !matched { - t.Fatal("Expected route to match") - } - }) - } -} - -func BenchmarkPopulateContext(b *testing.B) { - testCases := getPopulateContextTestCases() - for _, tc := range testCases { - b.Run(tc.name, func(b *testing.B) { - matched := false - r := NewRouter() - r.OmitRouteFromContext(tc.omitRouteFromContext) - fn := func(w http.ResponseWriter, r *http.Request) { - matched = true - w.WriteHeader(http.StatusNoContent) - } - r.Name("static").Path("/static").HandlerFunc(fn) - r.Name("dynamic").Path("/dynamic/{x:.*}").HandlerFunc(fn) - req := newRequest(http.MethodGet, "http://localhost"+tc.path) - rec := NewRecorder() - b.ReportAllocs() - b.ResetTimer() - for i := 0; i < b.N; i++ { - r.ServeHTTP(rec, req) - } - if !matched { - b.Fatal("Expected route to match") - } - }) - } -} - -// mapToPairs converts a string map to a slice of string pairs -func mapToPairs(m map[string]string) []string { - var i int - p := make([]string, len(m)*2) - for k, v := range m { - p[i] = k - p[i+1] = v - i += 2 - } - return p -} - -// stringMapEqual checks the equality of two string maps -func stringMapEqual(m1, m2 map[string]string) bool { - nil1 := m1 == nil - nil2 := m2 == nil - if nil1 != nil2 || len(m1) != len(m2) { - return false - } - for k, v := range m1 { - if v != m2[k] { - return false - } - } - return true -} - -// stringHandler returns a handler func that writes a message 's' to the -// http.ResponseWriter. -func stringHandler(s string) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - _, err := w.Write([]byte(s)) - if err != nil { - log.Printf("Failed writing HTTP response: %v", err) - } - } -} - -// newRequest is a helper function to create a new request with a method and url. -// The request returned is a 'server' request as opposed to a 'client' one through -// simulated write onto the wire and read off of the wire. -// The differences between requests are detailed in the net/http package. -func newRequest(method, url string) *http.Request { - req, err := http.NewRequest(method, url, nil) - if err != nil { - panic(err) - } - // extract the escaped original host+path from url - // http://localhost/path/here?v=1#frag -> //localhost/path/here - opaque := "" - if i := len(req.URL.Scheme); i > 0 { - opaque = url[i+1:] - } - - if i := strings.LastIndex(opaque, "?"); i > -1 { - opaque = opaque[:i] - } - if i := strings.LastIndex(opaque, "#"); i > -1 { - opaque = opaque[:i] - } - - // Escaped host+path workaround as detailed in https://golang.org/pkg/net/url/#URL - // for < 1.5 client side workaround - req.URL.Opaque = opaque - - // Simulate writing to wire - var buff bytes.Buffer - err = req.Write(&buff) - if err != nil { - log.Printf("Failed writing HTTP request: %v", err) - } - ioreader := bufio.NewReader(&buff) - - // Parse request off of 'wire' - req, err = http.ReadRequest(ioreader) - if err != nil { - panic(err) - } - return req -} - -// create a new request with the provided headers -func newRequestWithHeaders(method, url string, headers ...string) *http.Request { - req := newRequest(method, url) - - if len(headers)%2 != 0 { - panic(fmt.Sprintf("Expected headers length divisible by 2 but got %v", len(headers))) - } - - for i := 0; i < len(headers); i += 2 { - req.Header.Set(headers[i], headers[i+1]) - } - - return req -} - -// newRequestHost a new request with a method, url, and host header -func newRequestHost(method, url, host string) *http.Request { - req := httptest.NewRequest(method, url, nil) - req.Host = host - return req -} diff --git a/web/internal/mux/old_test.go b/web/internal/mux/old_test.go deleted file mode 100644 index 96dbe337..00000000 --- a/web/internal/mux/old_test.go +++ /dev/null @@ -1,718 +0,0 @@ -// Old tests ported to Go1. This is a mess. Want to drop it one day. - -// Copyright 2011 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 ( - "bytes" - "net/http" - "testing" -) - -// ---------------------------------------------------------------------------- -// ResponseRecorder -// ---------------------------------------------------------------------------- -// Copyright 2009 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// ResponseRecorder is an implementation of http.ResponseWriter that -// records its mutations for later inspection in tests. -type ResponseRecorder struct { - Code int // the HTTP response code from WriteHeader - HeaderMap http.Header // the HTTP response headers - Body *bytes.Buffer // if non-nil, the bytes.Buffer to append written data to - Flushed bool -} - -// NewRecorder returns an initialized ResponseRecorder. -func NewRecorder() *ResponseRecorder { - return &ResponseRecorder{ - HeaderMap: make(http.Header), - Body: new(bytes.Buffer), - } -} - -// Header returns the response headers. -func (rw *ResponseRecorder) Header() http.Header { - return rw.HeaderMap -} - -// Write always succeeds and writes to rw.Body, if not nil. -func (rw *ResponseRecorder) Write(buf []byte) (int, error) { - if rw.Body != nil { - rw.Body.Write(buf) - } - if rw.Code == 0 { - rw.Code = http.StatusOK - } - return len(buf), nil -} - -// WriteHeader sets rw.Code. -func (rw *ResponseRecorder) WriteHeader(code int) { - rw.Code = code -} - -// Flush sets rw.Flushed to true. -func (rw *ResponseRecorder) Flush() { - rw.Flushed = true -} - -// ---------------------------------------------------------------------------- - -func TestRouteMatchers(t *testing.T) { - var scheme, host, path, query, method string - var headers map[string]string - var resultVars map[bool]map[string]string - - router := NewRouter() - router.NewRoute().Host("{var1}.google.com"). - Path("/{var2:[a-z]+}/{var3:[0-9]+}"). - Queries("foo", "bar"). - Methods("GET"). - Schemes("https"). - Headers("x-requested-with", "XMLHttpRequest") - router.NewRoute().Host("www.{var4}.com"). - PathPrefix("/foo/{var5:[a-z]+}/{var6:[0-9]+}"). - Queries("baz", "ding"). - Methods("POST"). - Schemes("http"). - Headers("Content-Type", "application/json") - - reset := func() { - // Everything match. - scheme = "https" - host = "www.google.com" - path = "/product/42" - query = "?foo=bar" - method = "GET" - headers = map[string]string{"X-Requested-With": "XMLHttpRequest"} - resultVars = map[bool]map[string]string{ - true: {"var1": "www", "var2": "product", "var3": "42"}, - false: {}, - } - } - - reset2 := func() { - // Everything match. - scheme = "http" - host = "www.google.com" - path = "/foo/product/42/path/that/is/ignored" - query = "?baz=ding" - method = "POST" - headers = map[string]string{"Content-Type": "application/json"} - resultVars = map[bool]map[string]string{ - true: {"var4": "google", "var5": "product", "var6": "42"}, - false: {}, - } - } - - match := func(shouldMatch bool) { - url := scheme + "://" + host + path + query - request, _ := http.NewRequest(method, url, nil) - for key, value := range headers { - request.Header.Add(key, value) - } - - var routeMatch RouteMatch - matched := router.Match(request, &routeMatch) - if matched != shouldMatch { - t.Errorf("Expected: %v\nGot: %v\nRequest: %v %v", shouldMatch, matched, request.Method, url) - } - - if matched { - currentRoute := routeMatch.Route - if currentRoute == nil { - t.Errorf("Expected a current route.") - } - vars := routeMatch.Vars - expectedVars := resultVars[shouldMatch] - if len(vars) != len(expectedVars) { - t.Errorf("Expected vars: %v Got: %v.", expectedVars, vars) - } - for name, value := range vars { - if expectedVars[name] != value { - t.Errorf("Expected vars: %v Got: %v.", expectedVars, vars) - } - } - } - } - - // 1st route -------------------------------------------------------------- - - // Everything match. - reset() - match(true) - - // Scheme doesn't match. - reset() - scheme = "http" - match(false) - - // Host doesn't match. - reset() - host = "www.mygoogle.com" - match(false) - - // Path doesn't match. - reset() - path = "/product/notdigits" - match(false) - - // Query doesn't match. - reset() - query = "?foo=baz" - match(false) - - // Method doesn't match. - reset() - method = "POST" - match(false) - - // Header doesn't match. - reset() - headers = map[string]string{} - match(false) - - // Everything match, again. - reset() - match(true) - - // 2nd route -------------------------------------------------------------- - // Everything match. - reset2() - match(true) - - // Scheme doesn't match. - reset2() - scheme = "https" - match(false) - - // Host doesn't match. - reset2() - host = "sub.google.com" - match(false) - - // Path doesn't match. - reset2() - path = "/bar/product/42" - match(false) - - // Query doesn't match. - reset2() - query = "?foo=baz" - match(false) - - // Method doesn't match. - reset2() - method = "GET" - match(false) - - // Header doesn't match. - reset2() - headers = map[string]string{} - match(false) - - // Everything match, again. - reset2() - match(true) -} - -type headerMatcherTest struct { - matcher headerMatcher - headers map[string]string - result bool -} - -var headerMatcherTests = []headerMatcherTest{ - { - matcher: headerMatcher(map[string]string{"x-requested-with": "XMLHttpRequest"}), - headers: map[string]string{"X-Requested-With": "XMLHttpRequest"}, - result: true, - }, - { - matcher: headerMatcher(map[string]string{"x-requested-with": ""}), - headers: map[string]string{"X-Requested-With": "anything"}, - result: true, - }, - { - matcher: headerMatcher(map[string]string{"x-requested-with": "XMLHttpRequest"}), - headers: map[string]string{}, - result: false, - }, -} - -type hostMatcherTest struct { - matcher *Route - url string - vars map[string]string - result bool -} - -var hostMatcherTests = []hostMatcherTest{ - { - matcher: NewRouter().NewRoute().Host("{foo:[a-z][a-z][a-z]}.{bar:[a-z][a-z][a-z]}.{baz:[a-z][a-z][a-z]}"), - url: "http://abc.def.ghi/", - vars: map[string]string{"foo": "abc", "bar": "def", "baz": "ghi"}, - result: true, - }, - { - matcher: NewRouter().NewRoute().Host("{foo:[a-z][a-z][a-z]}.{bar:[a-z][a-z][a-z]}.{baz:[a-z][a-z][a-z]}:{port:.*}"), - url: "http://abc.def.ghi:65535/", - vars: map[string]string{"foo": "abc", "bar": "def", "baz": "ghi", "port": "65535"}, - result: true, - }, - { - matcher: NewRouter().NewRoute().Host("{foo:[a-z][a-z][a-z]}.{bar:[a-z][a-z][a-z]}.{baz:[a-z][a-z][a-z]}"), - url: "http://abc.def.ghi:65535/", - vars: map[string]string{"foo": "abc", "bar": "def", "baz": "ghi"}, - result: true, - }, - { - matcher: NewRouter().NewRoute().Host("{foo:[a-z][a-z][a-z]}.{bar:[a-z][a-z][a-z]}.{baz:[a-z][a-z][a-z]}"), - url: "http://a.b.c/", - vars: map[string]string{"foo": "abc", "bar": "def", "baz": "ghi"}, - result: false, - }, -} - -type methodMatcherTest struct { - matcher methodMatcher - method string - result bool -} - -var methodMatcherTests = []methodMatcherTest{ - { - matcher: methodMatcher([]string{"GET", "POST", "PUT"}), - method: "GET", - result: true, - }, - { - matcher: methodMatcher([]string{"GET", "POST", "PUT"}), - method: "POST", - result: true, - }, - { - matcher: methodMatcher([]string{"GET", "POST", "PUT"}), - method: "PUT", - result: true, - }, - { - matcher: methodMatcher([]string{"GET", "POST", "PUT"}), - method: "DELETE", - result: false, - }, -} - -type pathMatcherTest struct { - matcher *Route - url string - vars map[string]string - result bool -} - -var pathMatcherTests = []pathMatcherTest{ - { - matcher: NewRouter().NewRoute().Path("/{foo:[0-9][0-9][0-9]}/{bar:[0-9][0-9][0-9]}/{baz:[0-9][0-9][0-9]}"), - url: "http://localhost:8080/123/456/789", - vars: map[string]string{"foo": "123", "bar": "456", "baz": "789"}, - result: true, - }, - { - matcher: NewRouter().NewRoute().Path("/{foo:[0-9][0-9][0-9]}/{bar:[0-9][0-9][0-9]}/{baz:[0-9][0-9][0-9]}"), - url: "http://localhost:8080/1/2/3", - vars: map[string]string{"foo": "123", "bar": "456", "baz": "789"}, - result: false, - }, -} - -type schemeMatcherTest struct { - matcher schemeMatcher - url string - result bool -} - -var schemeMatcherTests = []schemeMatcherTest{ - { - matcher: schemeMatcher([]string{"http", "https"}), - url: "http://localhost:8080/", - result: true, - }, - { - matcher: schemeMatcher([]string{"http", "https"}), - url: "https://localhost:8080/", - result: true, - }, - { - matcher: schemeMatcher([]string{"https"}), - url: "http://localhost:8080/", - result: false, - }, - { - matcher: schemeMatcher([]string{"http"}), - url: "https://localhost:8080/", - result: false, - }, -} - -type urlBuildingTest struct { - route *Route - vars []string - url string -} - -var urlBuildingTests = []urlBuildingTest{ - { - route: new(Route).Host("foo.domain.com"), - vars: []string{}, - url: "http://foo.domain.com", - }, - { - route: new(Route).Host("{subdomain}.domain.com"), - vars: []string{"subdomain", "bar"}, - url: "http://bar.domain.com", - }, - { - route: new(Route).Host("{subdomain}.domain.com:{port:.*}"), - vars: []string{"subdomain", "bar", "port", "65535"}, - url: "http://bar.domain.com:65535", - }, - { - route: new(Route).Host("foo.domain.com").Path("/articles"), - vars: []string{}, - url: "http://foo.domain.com/articles", - }, - { - route: new(Route).Path("/articles"), - vars: []string{}, - url: "/articles", - }, - { - route: new(Route).Path("/articles/{category}/{id:[0-9]+}"), - vars: []string{"category", "technology", "id", "42"}, - url: "/articles/technology/42", - }, - { - route: new(Route).Host("{subdomain}.domain.com").Path("/articles/{category}/{id:[0-9]+}"), - vars: []string{"subdomain", "foo", "category", "technology", "id", "42"}, - url: "http://foo.domain.com/articles/technology/42", - }, - { - route: new(Route).Host("example.com").Schemes("https", "http"), - vars: []string{}, - url: "https://example.com", - }, -} - -func TestHeaderMatcher(t *testing.T) { - for _, v := range headerMatcherTests { - request, _ := http.NewRequest("GET", "http://localhost:8080/", nil) - for key, value := range v.headers { - request.Header.Add(key, value) - } - var routeMatch RouteMatch - result := v.matcher.Match(request, &routeMatch) - if result != v.result { - if v.result { - t.Errorf("%#v: should match %v.", v.matcher, request.Header) - } else { - t.Errorf("%#v: should not match %v.", v.matcher, request.Header) - } - } - } -} - -func TestHostMatcher(t *testing.T) { - for _, v := range hostMatcherTests { - request, err := http.NewRequest("GET", v.url, nil) - if err != nil { - t.Errorf("http.NewRequest failed %#v", err) - continue - } - var routeMatch RouteMatch - result := v.matcher.Match(request, &routeMatch) - vars := routeMatch.Vars - if result != v.result { - if v.result { - t.Errorf("%#v: should match %v.", v.matcher, v.url) - } else { - t.Errorf("%#v: should not match %v.", v.matcher, v.url) - } - } - if result { - if len(vars) != len(v.vars) { - t.Errorf("%#v: vars length should be %v, got %v.", v.matcher, len(v.vars), len(vars)) - } - for name, value := range vars { - if v.vars[name] != value { - t.Errorf("%#v: expected value %v for key %v, got %v.", v.matcher, v.vars[name], name, value) - } - } - } else { - if len(vars) != 0 { - t.Errorf("%#v: vars length should be 0, got %v.", v.matcher, len(vars)) - } - } - } -} - -func TestMethodMatcher(t *testing.T) { - for _, v := range methodMatcherTests { - request, _ := http.NewRequest(v.method, "http://localhost:8080/", nil) - var routeMatch RouteMatch - result := v.matcher.Match(request, &routeMatch) - if result != v.result { - if v.result { - t.Errorf("%#v: should match %v.", v.matcher, v.method) - } else { - t.Errorf("%#v: should not match %v.", v.matcher, v.method) - } - } - } -} - -func TestPathMatcher(t *testing.T) { - for _, v := range pathMatcherTests { - request, _ := http.NewRequest("GET", v.url, nil) - var routeMatch RouteMatch - result := v.matcher.Match(request, &routeMatch) - vars := routeMatch.Vars - if result != v.result { - if v.result { - t.Errorf("%#v: should match %v.", v.matcher, v.url) - } else { - t.Errorf("%#v: should not match %v.", v.matcher, v.url) - } - } - if result { - if len(vars) != len(v.vars) { - t.Errorf("%#v: vars length should be %v, got %v.", v.matcher, len(v.vars), len(vars)) - } - for name, value := range vars { - if v.vars[name] != value { - t.Errorf("%#v: expected value %v for key %v, got %v.", v.matcher, v.vars[name], name, value) - } - } - } else { - if len(vars) != 0 { - t.Errorf("%#v: vars length should be 0, got %v.", v.matcher, len(vars)) - } - } - } -} - -func TestSchemeMatcher(t *testing.T) { - for _, v := range schemeMatcherTests { - request, _ := http.NewRequest("GET", v.url, nil) - var routeMatch RouteMatch - result := v.matcher.Match(request, &routeMatch) - if result != v.result { - if v.result { - t.Errorf("%#v: should match %v.", v.matcher, v.url) - } else { - t.Errorf("%#v: should not match %v.", v.matcher, v.url) - } - } - } -} - -func TestUrlBuilding(t *testing.T) { - - for _, v := range urlBuildingTests { - u, _ := v.route.URL(v.vars...) - url := u.String() - if url != v.url { - t.Errorf("expected %v, got %v", v.url, url) - } - } - - ArticleHandler := func(w http.ResponseWriter, r *http.Request) { - } - - router := NewRouter() - router.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler).Name("article") - - url, _ := router.Get("article").URL("category", "technology", "id", "42") - expected := "/articles/technology/42" - if url.String() != expected { - t.Errorf("Expected %v, got %v", expected, url.String()) - } -} - -func TestMatchedRouteName(t *testing.T) { - routeName := "stock" - router := NewRouter() - route := router.NewRoute().Path("/products/").Name(routeName) - - url := "http://www.example.com/products/" - request, _ := http.NewRequest("GET", url, nil) - var rv RouteMatch - ok := router.Match(request, &rv) - - if !ok || rv.Route != route { - t.Errorf("Expected same route, got %+v.", rv.Route) - } - - retName := rv.Route.GetName() - if retName != routeName { - t.Errorf("Expected %q, got %q.", routeName, retName) - } -} - -func TestSubRouting(t *testing.T) { - // Example from docs. - router := NewRouter() - subrouter := router.NewRoute().Host("www.example.com").Subrouter() - route := subrouter.NewRoute().Path("/products/").Name("products") - - url := "http://www.example.com/products/" - request, _ := http.NewRequest("GET", url, nil) - var rv RouteMatch - ok := router.Match(request, &rv) - - if !ok || rv.Route != route { - t.Errorf("Expected same route, got %+v.", rv.Route) - } - - u, _ := router.Get("products").URL() - builtURL := u.String() - // Yay, subroute aware of the domain when building! - if builtURL != url { - t.Errorf("Expected %q, got %q.", url, builtURL) - } -} - -func TestVariableNames(t *testing.T) { - route := new(Route).Host("{arg1}.domain.com").Path("/{arg1}/{arg2:[0-9]+}") - if route.err == nil { - t.Errorf("Expected error for duplicated variable names") - } -} - -func TestRedirectSlash(t *testing.T) { - var route *Route - var routeMatch RouteMatch - r := NewRouter() - - r.StrictSlash(false) - route = r.NewRoute() - if route.strictSlash != false { - t.Errorf("Expected false redirectSlash.") - } - - r.StrictSlash(true) - route = r.NewRoute() - if route.strictSlash != true { - t.Errorf("Expected true redirectSlash.") - } - - route = new(Route) - route.strictSlash = true - route.Path("/{arg1}/{arg2:[0-9]+}/") - request, _ := http.NewRequest("GET", "http://localhost/foo/123", nil) - routeMatch = RouteMatch{} - _ = route.Match(request, &routeMatch) - vars := routeMatch.Vars - if vars["arg1"] != "foo" { - t.Errorf("Expected foo.") - } - if vars["arg2"] != "123" { - t.Errorf("Expected 123.") - } - rsp := NewRecorder() - routeMatch.Handler.ServeHTTP(rsp, request) - if rsp.HeaderMap.Get("Location") != "http://localhost/foo/123/" { - t.Errorf("Expected redirect header.") - } - - route = new(Route) - route.strictSlash = true - route.Path("/{arg1}/{arg2:[0-9]+}") - request, _ = http.NewRequest("GET", "http://localhost/foo/123/", nil) - routeMatch = RouteMatch{} - _ = route.Match(request, &routeMatch) - vars = routeMatch.Vars - if vars["arg1"] != "foo" { - t.Errorf("Expected foo.") - } - if vars["arg2"] != "123" { - t.Errorf("Expected 123.") - } - rsp = NewRecorder() - routeMatch.Handler.ServeHTTP(rsp, request) - if rsp.HeaderMap.Get("Location") != "http://localhost/foo/123" { - t.Errorf("Expected redirect header.") - } -} - -// Test for the new regexp library, still not available in stable Go. -func TestNewRegexp(t *testing.T) { - var p *routeRegexp - var matches []string - - tests := map[string]map[string][]string{ - "/{foo:a{2}}": { - "/a": nil, - "/aa": {"aa"}, - "/aaa": nil, - "/aaaa": nil, - }, - "/{foo:a{2,}}": { - "/a": nil, - "/aa": {"aa"}, - "/aaa": {"aaa"}, - "/aaaa": {"aaaa"}, - }, - "/{foo:a{2,3}}": { - "/a": nil, - "/aa": {"aa"}, - "/aaa": {"aaa"}, - "/aaaa": nil, - }, - "/{foo:[a-z]{3}}/{bar:[a-z]{2}}": { - "/a": nil, - "/ab": nil, - "/abc": nil, - "/abcd": nil, - "/abc/ab": {"abc", "ab"}, - "/abc/abc": nil, - "/abcd/ab": nil, - }, - `/{foo:\w{3,}}/{bar:\d{2,}}`: { - "/a": nil, - "/ab": nil, - "/abc": nil, - "/abc/1": nil, - "/abc/12": {"abc", "12"}, - "/abcd/12": {"abcd", "12"}, - "/abcd/123": {"abcd", "123"}, - }, - } - - for pattern, paths := range tests { - p, _ = newRouteRegexp(pattern, regexpTypePath, routeRegexpOptions{}) - for path, result := range paths { - matches = p.regexp.FindStringSubmatch(path) - if result == nil { - if matches != nil { - t.Errorf("%v should not match %v.", pattern, path) - } - } else { - if len(matches) != len(result)+1 { - t.Errorf("Expected %v matches, got %v.", len(result)+1, len(matches)) - } else { - for k, v := range result { - if matches[k+1] != v { - t.Errorf("Expected %v, got %v.", v, matches[k+1]) - } - } - } - } - } - } -} diff --git a/web/internal/mux/regexp.go b/web/internal/mux/regexp.go deleted file mode 100644 index b577073f..00000000 --- a/web/internal/mux/regexp.go +++ /dev/null @@ -1,399 +0,0 @@ -// 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 ( - "bytes" - "fmt" - "net/http" - "net/url" - "regexp" - "strconv" - "strings" -) - -type routeRegexpOptions struct { - strictSlash bool - useEncodedPath bool -} - -type regexpType int - -const ( - regexpTypePath regexpType = iota - regexpTypeHost - regexpTypePrefix - regexpTypeQuery -) - -// newRouteRegexp parses a route template and returns a routeRegexp, -// used to match a host, a path or a query string. -// -// It will extract named variables, assemble a regexp to be matched, create -// a "reverse" template to build URLs and compile regexps to validate variable -// values used in URL building. -// -// Previously we accepted only Python-like identifiers for variable -// names ([a-zA-Z_][a-zA-Z0-9_]*), but currently the only restriction is that -// name and pattern can't be empty, and names can't contain a colon. -func newRouteRegexp(tpl string, typ regexpType, options routeRegexpOptions) (*routeRegexp, error) { - // Check if it is well-formed. - idxs, errBraces := braceIndices(tpl) - if errBraces != nil { - return nil, errBraces - } - // Backup the original. - template := tpl - // Now let's parse it. - defaultPattern := "[^/]+" - if typ == regexpTypeQuery { - defaultPattern = ".*" - } else if typ == regexpTypeHost { - defaultPattern = "[^.]+" - } - // Only match strict slash if not matching - if typ != regexpTypePath { - options.strictSlash = false - } - // Set a flag for strictSlash. - endSlash := false - if options.strictSlash && strings.HasSuffix(tpl, "/") { - tpl = tpl[:len(tpl)-1] - endSlash = true - } - varsN := make([]string, len(idxs)/2) - varsR := make([]*regexp.Regexp, len(idxs)/2) - pattern := bytes.NewBufferString("") - pattern.WriteByte('^') - reverse := bytes.NewBufferString("") - var end int - var err error - for i := 0; i < len(idxs); i += 2 { - // Set all values we are interested in. - raw := tpl[end:idxs[i]] - end = idxs[i+1] - parts := strings.SplitN(tpl[idxs[i]+1:end-1], ":", 2) - name := parts[0] - patt := defaultPattern - if len(parts) == 2 { - patt = parts[1] - } - // Name or pattern can't be empty. - if name == "" || patt == "" { - return nil, fmt.Errorf("mux: missing name or pattern in %q", - tpl[idxs[i]:end]) - } - // Build the regexp pattern. - fmt.Fprintf(pattern, "%s(?P<%s>%s)", regexp.QuoteMeta(raw), varGroupName(i/2), patt) - - // Build the reverse template. - fmt.Fprintf(reverse, "%s%%s", raw) - - // Append variable name and compiled pattern. - varsN[i/2] = name - varsR[i/2], err = regexp.Compile(fmt.Sprintf("^%s$", patt)) - if err != nil { - return nil, err - } - } - // Add the remaining. - raw := tpl[end:] - pattern.WriteString(regexp.QuoteMeta(raw)) - if options.strictSlash { - pattern.WriteString("[/]?") - } - if typ == regexpTypeQuery { - // Add the default pattern if the query value is empty - if queryVal := strings.SplitN(template, "=", 2)[1]; queryVal == "" { - pattern.WriteString(defaultPattern) - } - } - if typ != regexpTypePrefix { - pattern.WriteByte('$') - } - - var wildcardHostPort bool - if typ == regexpTypeHost { - if !strings.Contains(pattern.String(), ":") { - wildcardHostPort = true - } - } - reverse.WriteString(raw) - if endSlash { - reverse.WriteByte('/') - } - // Compile full regexp. - reg, errCompile := regexp.Compile(pattern.String()) - if errCompile != nil { - return nil, errCompile - } - - // Check for capturing groups which used to work in older versions - if reg.NumSubexp() != len(idxs)/2 { - panic(fmt.Sprintf("route %s contains capture groups in its regexp. ", template) + - "Only non-capturing groups are accepted: e.g. (?:pattern) instead of (pattern)") - } - - // Done! - return &routeRegexp{ - template: template, - regexpType: typ, - options: options, - regexp: reg, - reverse: reverse.String(), - varsN: varsN, - varsR: varsR, - wildcardHostPort: wildcardHostPort, - }, nil -} - -// routeRegexp stores a regexp to match a host or path and information to -// collect and validate route variables. -type routeRegexp struct { - // The unmodified template. - template string - // The type of match - regexpType regexpType - // Options for matching - options routeRegexpOptions - // Expanded regexp. - regexp *regexp.Regexp - // Reverse template. - reverse string - // Variable names. - varsN []string - // Variable regexps (validators). - varsR []*regexp.Regexp - // Wildcard host-port (no strict port match in hostname) - wildcardHostPort bool -} - -// Match matches the regexp against the URL host or path. -func (r *routeRegexp) Match(req *http.Request, match *RouteMatch) bool { - if r.regexpType == regexpTypeHost { - host := getHost(req) - if r.wildcardHostPort { - // Don't be strict on the port match - if i := strings.Index(host, ":"); i != -1 { - host = host[:i] - } - } - return r.regexp.MatchString(host) - } - - if r.regexpType == regexpTypeQuery { - return r.matchQueryString(req) - } - path := req.URL.Path - if r.options.useEncodedPath { - path = req.URL.EscapedPath() - } - return r.regexp.MatchString(path) -} - -// url builds a URL part using the given values. -func (r *routeRegexp) url(values map[string]string) (string, error) { - urlValues := make([]interface{}, len(r.varsN)) - for k, v := range r.varsN { - value, ok := values[v] - if !ok { - return "", fmt.Errorf("mux: missing route variable %q", v) - } - if r.regexpType == regexpTypeQuery { - value = url.QueryEscape(value) - } - urlValues[k] = value - } - rv := fmt.Sprintf(r.reverse, urlValues...) - if !r.regexp.MatchString(rv) { - // The URL is checked against the full regexp, instead of checking - // individual variables. This is faster but to provide a good error - // message, we check individual regexps if the URL doesn't match. - for k, v := range r.varsN { - if !r.varsR[k].MatchString(values[v]) { - return "", fmt.Errorf( - "mux: variable %q doesn't match, expected %q", values[v], - r.varsR[k].String()) - } - } - } - return rv, nil -} - -// getURLQuery returns a single query parameter from a request URL. -// For a URL with foo=bar&baz=ding, we return only the relevant key -// value pair for the routeRegexp. -func (r *routeRegexp) getURLQuery(req *http.Request) string { - if r.regexpType != regexpTypeQuery { - return "" - } - templateKey := strings.SplitN(r.template, "=", 2)[0] - val, ok := findFirstQueryKey(req.URL.RawQuery, templateKey) - if ok { - return templateKey + "=" + val - } - return "" -} - -// findFirstQueryKey returns the same result as (*url.URL).Query()[key][0]. -// If key was not found, empty string and false is returned. -func findFirstQueryKey(rawQuery, key string) (value string, ok bool) { - query := []byte(rawQuery) - for len(query) > 0 { - foundKey := query - if i := bytes.IndexAny(foundKey, "&;"); i >= 0 { - foundKey, query = foundKey[:i], foundKey[i+1:] - } else { - query = query[:0] - } - if len(foundKey) == 0 { - continue - } - var value []byte - if i := bytes.IndexByte(foundKey, '='); i >= 0 { - foundKey, value = foundKey[:i], foundKey[i+1:] - } - if len(foundKey) < len(key) { - // Cannot possibly be key. - continue - } - keyString, err := url.QueryUnescape(string(foundKey)) - if err != nil { - continue - } - if keyString != key { - continue - } - valueString, err := url.QueryUnescape(string(value)) - if err != nil { - continue - } - return valueString, true - } - return "", false -} - -func (r *routeRegexp) matchQueryString(req *http.Request) bool { - return r.regexp.MatchString(r.getURLQuery(req)) -} - -// braceIndices returns the first level curly brace indices from a string. -// It returns an error in case of unbalanced braces. -func braceIndices(s string) ([]int, error) { - var level, idx int - var idxs []int - for i := 0; i < len(s); i++ { - switch s[i] { - case '{': - if level++; level == 1 { - idx = i - } - case '}': - if level--; level == 0 { - idxs = append(idxs, idx, i+1) - } else if level < 0 { - return nil, fmt.Errorf("mux: unbalanced braces in %q", s) - } - } - } - if level != 0 { - return nil, fmt.Errorf("mux: unbalanced braces in %q", s) - } - return idxs, nil -} - -// varGroupName builds a capturing group name for the indexed variable. -func varGroupName(idx int) string { - return "v" + strconv.Itoa(idx) -} - -// ---------------------------------------------------------------------------- -// routeRegexpGroup -// ---------------------------------------------------------------------------- - -// routeRegexpGroup groups the route matchers that carry variables. -type routeRegexpGroup struct { - host *routeRegexp - path *routeRegexp - queries []*routeRegexp -} - -// setMatch extracts the variables from the URL once a route matches. -func (v routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route) { - // Store host variables. - if v.host != nil { - if len(v.host.varsN) > 0 { - host := getHost(req) - if v.host.wildcardHostPort { - // Don't be strict on the port match - if i := strings.Index(host, ":"); i != -1 { - host = host[:i] - } - } - matches := v.host.regexp.FindStringSubmatchIndex(host) - if len(matches) > 0 { - m.Vars = extractVars(host, matches, v.host.varsN, m.Vars) - } - } - } - path := req.URL.Path - if r.useEncodedPath { - path = req.URL.EscapedPath() - } - // Store path variables. - if v.path != nil { - if len(v.path.varsN) > 0 { - matches := v.path.regexp.FindStringSubmatchIndex(path) - if len(matches) > 0 { - m.Vars = extractVars(path, matches, v.path.varsN, m.Vars) - } - } - // Check if we should redirect. - if v.path.options.strictSlash { - p1 := strings.HasSuffix(path, "/") - p2 := strings.HasSuffix(v.path.template, "/") - if p1 != p2 { - p := req.URL.Path - if p1 { - p = p[:len(p)-1] - } else { - p += "/" - } - u := replaceURLPath(req.URL, p) - m.Handler = http.RedirectHandler(u, http.StatusMovedPermanently) - } - } - } - // Store query string variables. - for _, q := range v.queries { - if len(q.varsN) > 0 { - queryURL := q.getURLQuery(req) - matches := q.regexp.FindStringSubmatchIndex(queryURL) - if len(matches) > 0 { - m.Vars = extractVars(queryURL, matches, q.varsN, m.Vars) - } - } - } -} - -// getHost tries its best to return the request host. -// According to section 14.23 of RFC 2616 the Host header -// can include the port number if the default value of 80 is not used. -func getHost(r *http.Request) string { - if r.URL.IsAbs() { - return r.URL.Host - } - return r.Host -} - -func extractVars(input string, matches []int, names []string, output map[string]string) map[string]string { - for i, name := range names { - if output == nil { - output = make(map[string]string, len(names)) - } - output[name] = input[matches[2*i+2]:matches[2*i+3]] - } - return output -} diff --git a/web/internal/mux/regexp_test.go b/web/internal/mux/regexp_test.go deleted file mode 100644 index d7518f3e..00000000 --- a/web/internal/mux/regexp_test.go +++ /dev/null @@ -1,91 +0,0 @@ -package mux - -import ( - "net/url" - "reflect" - "strconv" - "testing" -) - -func Test_findFirstQueryKey(t *testing.T) { - tests := []string{ - "a=1&b=2", - "a=1&a=2&a=banana", - "ascii=%3Ckey%3A+0x90%3E", - "a=1;b=2", - "a=1&a=2;a=banana", - "a==", - "a=%2", - "a=20&%20%3F&=%23+%25%21%3C%3E%23%22%7B%7D%7C%5C%5E%5B%5D%60%E2%98%BA%09:%2F@$%27%28%29%2A%2C%3B&a=30", - "a=1& ?&=#+%!<>#\"{}|\\^[]`☺\t:/@$'()*,;&a=5", - "a=xxxxxxxxxxxxxxxx&b=YYYYYYYYYYYYYYY&c=ppppppppppppppppppp&f=ttttttttttttttttt&a=uuuuuuuuuuuuu", - } - for _, query := range tests { - t.Run(query, func(t *testing.T) { - // Check against url.ParseQuery, ignoring the error. - all, _ := url.ParseQuery(query) - for key, want := range all { - t.Run(key, func(t *testing.T) { - got, ok := findFirstQueryKey(query, key) - if !ok { - t.Error("Did not get expected key", key) - } - if !reflect.DeepEqual(got, want[0]) { - t.Errorf("findFirstQueryKey(%s,%s) = %v, want %v", query, key, got, want[0]) - } - }) - } - }) - } -} - -func Benchmark_findQueryKey(b *testing.B) { - tests := []string{ - "a=1&b=2", - "ascii=%3Ckey%3A+0x90%3E", - "a=20&%20%3F&=%23+%25%21%3C%3E%23%22%7B%7D%7C%5C%5E%5B%5D%60%E2%98%BA%09:%2F@$%27%28%29%2A%2C%3B&a=30", - "a=xxxxxxxxxxxxxxxx&bbb=YYYYYYYYYYYYYYY&cccc=ppppppppppppppppppp&ddddd=ttttttttttttttttt&a=uuuuuuuuuuuuu", - "a=;b=;c=;d=;e=;f=;g=;h=;i=,j=;k=", - } - for i, query := range tests { - b.Run(strconv.Itoa(i), func(b *testing.B) { - // Check against url.ParseQuery, ignoring the error. - all, _ := url.ParseQuery(query) - b.ReportAllocs() - b.ResetTimer() - for i := 0; i < b.N; i++ { - for key := range all { - _, _ = findFirstQueryKey(query, key) - } - } - }) - } -} - -func Benchmark_findQueryKeyGoLib(b *testing.B) { - tests := []string{ - "a=1&b=2", - "ascii=%3Ckey%3A+0x90%3E", - "a=20&%20%3F&=%23+%25%21%3C%3E%23%22%7B%7D%7C%5C%5E%5B%5D%60%E2%98%BA%09:%2F@$%27%28%29%2A%2C%3B&a=30", - "a=xxxxxxxxxxxxxxxx&bbb=YYYYYYYYYYYYYYY&cccc=ppppppppppppppppppp&ddddd=ttttttttttttttttt&a=uuuuuuuuuuuuu", - "a=;b=;c=;d=;e=;f=;g=;h=;i=,j=;k=", - } - for i, query := range tests { - b.Run(strconv.Itoa(i), func(b *testing.B) { - // Check against url.ParseQuery, ignoring the error. - all, _ := url.ParseQuery(query) - var u url.URL - u.RawQuery = query - b.ReportAllocs() - b.ResetTimer() - for i := 0; i < b.N; i++ { - for key := range all { - v := u.Query()[key] - if len(v) > 0 { - _ = v[0] - } - } - } - }) - } -} diff --git a/web/internal/mux/route.go b/web/internal/mux/route.go deleted file mode 100644 index 8a9e754a..00000000 --- a/web/internal/mux/route.go +++ /dev/null @@ -1,762 +0,0 @@ -// 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 { - // Request handler for the route. - handler http.Handler - // 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 - - // "global" reference to all named routes - namedRoutes map[string]*Route - - // config possibly passed in from `Router` - routeConf -} - -// 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 - } - - // Ignore ErrNotFound errors. These errors arise from match call - // to Subrouters. - // - // This prevents subsequent matching subrouters from failing to - // run middleware. If not ignored, the middleware would see a - // non-nil MatchErr and be skipped, even when there was a - // matching route. - if match.MatchErr == ErrNotFound { - match.MatchErr = nil - } - - matchErr = nil // nolint:ineffassign - return false - } else { - // Multiple routes may share the same path but use different HTTP methods. For instance: - // Route 1: POST "/users/{id}". - // Route 2: GET "/users/{id}", parameters: "id": "[0-9]+". - // - // The router must handle these cases correctly. For a GET request to "/users/abc" with "id" as "-2", - // The router should return a "Not Found" error as no route fully matches this request. - if match.MatchErr == ErrMethodMismatch { - match.MatchErr = nil - } - } - } - - if matchErr != nil { - match.MatchErr = matchErr - return false - } - - if match.MatchErr == ErrMethodMismatch && r.handler != nil { - // 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 - } - - // Set variables. - 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. -// It is an error to call Name more than once on a route. -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.namedRoutes[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 - } - 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().NewRoute() -// 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().NewRoute() -// 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().NewRoute() -// 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().NewRoute() -// 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().NewRoute() -// 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. -// -// If 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 { - scheme := r.URL.Scheme - // https://golang.org/pkg/net/http/#Request - // "For [most] server requests, fields other than Path and RawQuery will be - // empty." - // Since we're an http muxer, the scheme is either going to be http or https - // though, so we can just set it based on the tls termination state. - if scheme == "" { - if r.TLS == nil { - scheme = "http" - } else { - scheme = "https" - } - } - return matchInArray(m, scheme) -} - -// Schemes adds a matcher for URL schemes. -// It accepts a sequence of schemes to be matched, e.g.: "http", "https". -// If the request's URL has a scheme set, it will be matched against. -// Generally, the URL scheme will only be set if a previous handler set it, -// such as the ProxyHeaders handler from gorilla/handlers. -// If unset, the scheme will be determined based on the request's TLS -// termination state. -// The first argument to Schemes will be used when constructing a route URL. -func (r *Route) Schemes(schemes ...string) *Route { - for k, v := range schemes { - schemes[k] = strings.ToLower(v) - } - if 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 { - if r.buildVarsFunc != nil { - // compose the old and new functions - old := r.buildVarsFunc - r.buildVarsFunc = func(m map[string]string) map[string]string { - return f(old(m)) - } - } else { - 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().NewRoute() -// 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 { - // initialize a subrouter with a copy of the parent route's configuration - router := &Router{routeConf: copyRouteConf(r.routeConf), namedRoutes: r.namedRoutes} - 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.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler). -// Host("{subdomain}.domain.com"). -// 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") -// -// The scheme of the resulting url will be the first argument that was passed to Schemes: -// -// // url.String() will be "https://example.com" -// r := mux.NewRouter().NewRoute() -// url, err := r.Host("example.com") -// .Schemes("https", "http").URL() -// -// 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 - } - 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 r.buildScheme != "" { - scheme = r.buildScheme - } - } - 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.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 r.buildScheme != "" { - u.Scheme = r.buildScheme - } - 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.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.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.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.queries == nil { - return nil, errors.New("mux: route doesn't have queries") - } - queries := make([]string, 0, len(r.regexp.queries)) - 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.queries == nil { - return nil, errors.New("mux: route doesn't have queries") - } - queries := make([]string, 0, len(r.regexp.queries)) - 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.host == nil { - return "", errors.New("mux: route doesn't have a host") - } - return r.regexp.host.template, nil -} - -// GetVarNames returns the names of all variables added by regexp matchers -// These can be used to know which route variables should be passed into r.URL() -func (r *Route) GetVarNames() ([]string, error) { - if r.err != nil { - return nil, r.err - } - var varNames []string - if r.regexp.host != nil { - varNames = append(varNames, r.regexp.host.varsN...) - } - if r.regexp.path != nil { - varNames = append(varNames, r.regexp.path.varsN...) - } - for _, regx := range r.regexp.queries { - varNames = append(varNames, regx.varsN...) - } - return varNames, 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.buildVarsFunc != nil { - m = r.buildVarsFunc(m) - } - return m -} diff --git a/web/internal/mux/test_helpers.go b/web/internal/mux/test_helpers.go deleted file mode 100644 index 5f5c496d..00000000 --- a/web/internal/mux/test_helpers.go +++ /dev/null @@ -1,19 +0,0 @@ -// 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 "net/http" - -// SetURLVars sets the URL variables for the given request, to be accessed via -// mux.Vars for testing route behaviour. Arguments are not modified, a shallow -// copy is returned. -// -// This API should only be used for testing purposes; it provides a way to -// inject variables into the request context. Alternatively, URL variables -// can be set by making a route that captures the required variables, -// starting a server and sending the request to that server. -func SetURLVars(r *http.Request, val map[string]string) *http.Request { - return requestWithVars(r, val) -} diff --git a/web/middleware.go b/web/middleware.go new file mode 100644 index 00000000..28386b2a --- /dev/null +++ b/web/middleware.go @@ -0,0 +1,62 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package web + +import "net/http" + +// MiddlewareFunc is a function which receives an http.Handler and returns another http.Handler. +// Typically, the returned handler is a closure which does something with the http.ResponseWriter and http.Request passed +// to it, and then calls the handler passed as parameter to the MiddlewareFunc. +type MiddlewareFunc = func(next http.Handler) http.Handler + +// Middlewares type is a slice of standard middleware handlers with methods +// to compose middleware chains and http.Handler's. +type Middlewares []MiddlewareFunc + +// Handler builds and returns a http.Handler from the chain of middlewares, +// with `h http.Handler` as the final handler. +func (mws Middlewares) Handler(h http.Handler) http.Handler { + return &chainHandler{Endpoint: h, chain: mws.chain(h), Middlewares: mws} +} + +// HandlerFunc builds and returns a http.Handler from the chain of middlewares, +// with `h http.Handler` as the final handler. +func (mws Middlewares) HandlerFunc(h http.HandlerFunc) http.Handler { + return &chainHandler{Endpoint: h, chain: mws.chain(h), Middlewares: mws} +} + +// Build a http.Handler composed of an inline middlewares. +func (mws Middlewares) chain(handler http.Handler) http.Handler { + if 0 == len(mws) { + return handler + } + + for i := len(mws) - 1; i >= 0; i-- { + handler = mws[i](handler) + } + return handler +} + +type chainHandler struct { + Endpoint http.Handler + chain http.Handler + Middlewares Middlewares +} + +func (c *chainHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + c.chain.ServeHTTP(w, r) +} diff --git a/web/options.go b/web/options.go index e926dcb2..7b9758f6 100644 --- a/web/options.go +++ b/web/options.go @@ -26,7 +26,7 @@ type Options struct { // in the form "host:port". If empty, ":http" (port 8080) is used. // The service names are defined in RFC 6335 and assigned by IANA. // See net.Dial for details of the address format. - Addr string `value:"${addr:=}"` + Addr string `json:"addr" value:"${addr:=}"` // CertFile containing a certificate and matching private key for the // server must be provided if neither the Server's @@ -34,10 +34,10 @@ type Options struct { // If the certificate is signed by a certificate authority, the // certFile should be the concatenation of the server's certificate, // any intermediates, and the CA's certificate. - CertFile string `value:"${cert-file:=}"` + CertFile string `json:"cert-file" value:"${cert-file:=}"` // KeyFile containing a private key file. - KeyFile string `value:"${key-file:=}"` + KeyFile string `json:"key-file" value:"${key-file:=}"` // ReadTimeout is the maximum duration for reading the entire // request, including the body. A zero or negative value means @@ -47,7 +47,7 @@ type Options struct { // decisions on each request body's acceptable deadline or // upload rate, most users will prefer to use // ReadHeaderTimeout. It is valid to use them both. - ReadTimeout time.Duration `value:"${read-timeout:=0s}"` + ReadTimeout time.Duration `json:"read-timeout" value:"${read-timeout:=0s}"` // ReadHeaderTimeout is the amount of time allowed to read // request headers. The connection's read deadline is reset @@ -55,27 +55,30 @@ type Options struct { // is considered too slow for the body. If ReadHeaderTimeout // is zero, the value of ReadTimeout is used. If both are // zero, there is no timeout. - ReadHeaderTimeout time.Duration `value:"${read-header-timeout:=0s}"` + ReadHeaderTimeout time.Duration `json:"read-header-timeout" value:"${read-header-timeout:=0s}"` // WriteTimeout is the maximum duration before timing out // writes of the response. It is reset whenever a new // request's header is read. Like ReadTimeout, it does not // let Handlers make decisions on a per-request basis. // A zero or negative value means there will be no timeout. - WriteTimeout time.Duration `value:"${write-timeout:=0s}"` + WriteTimeout time.Duration `json:"write-timeout" value:"${write-timeout:=0s}"` // IdleTimeout is the maximum amount of time to wait for the // next request when keep-alives are enabled. If IdleTimeout // is zero, the value of ReadTimeout is used. If both are // zero, there is no timeout. - IdleTimeout time.Duration `value:"${idle-timeout:=0s}"` + IdleTimeout time.Duration `json:"idle-timeout" value:"${idle-timeout:=0s}"` // MaxHeaderBytes controls the maximum number of bytes the // server will read parsing the request header's keys and // values, including the request line. It does not limit the // size of the request body. // If zero, DefaultMaxHeaderBytes is used. - MaxHeaderBytes int `value:"${max-header-bytes:=0}"` + MaxHeaderBytes int `json:"max-header-bytes" value:"${max-header-bytes:=0}"` + + // Router optionally specifies an external router. + Router Router `json:"-"` } func (options Options) IsTls() bool { diff --git a/web/router.go b/web/router.go new file mode 100644 index 00000000..db260d17 --- /dev/null +++ b/web/router.go @@ -0,0 +1,611 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package web + +import ( + "fmt" + "net/http" + "sync" +) + +// Router registers routes to be matched and dispatches a handler. +// +// It implements the http.Handler interface, so it can be registered to serve +// requests: +// +// var router = web.NewRouter() +// +// func main() { +// http.Handle("/", router) +// } +// +// Registers a new route with a matcher for the URL pattern. +// Automatic binding request to handler input params and validate params, following functions: +// +// This will send all incoming requests to the router. +type Router interface { + // Handler dispatches the handler registered in the matched route. + http.Handler + + // Use appends a MiddlewareFunc to the chain. + Use(mwf ...MiddlewareFunc) + + // Renderer to be used Response renderer in default. + Renderer(renderer Renderer) + + // Group creates a new router group. + Group(pattern string) Router + + // Handle registers a new route with a matcher for the URL pattern. + Handle(pattern string, handler http.Handler) + + // HandleFunc registers a new route with a matcher for the URL pattern. + HandleFunc(pattern string, handler http.HandlerFunc) + + // Any registers a route that matches all the HTTP methods. + // GET, POST, PUT, PATCH, HEAD, OPTIONS, DELETE, CONNECT, TRACE. + // + // The handler accepts the following functional signatures: + // + // func(ctx context.Context) + // + // func(ctx context.Context) R + // + // func(ctx context.Context) error + // + // func(ctx context.Context, req T) R + // + // func(ctx context.Context, req T) error + // + // func(ctx context.Context, req T) (R, error) + // + Any(pattern string, handler interface{}) + + // Get registers a new GET route with a matcher for the URL path of the get method. + // + // The handler accepts the following functional signatures: + // + // func(ctx context.Context) + // + // func(ctx context.Context) R + // + // func(ctx context.Context) error + // + // func(ctx context.Context, req T) R + // + // func(ctx context.Context, req T) error + // + // func(ctx context.Context, req T) (R, error) + // + Get(pattern string, handler interface{}) + + // Head registers a new HEAD route with a matcher for the URL path of the head method. + // + // The handler accepts the following functional signatures: + // + // func(ctx context.Context) + // + // func(ctx context.Context) R + // + // func(ctx context.Context) error + // + // func(ctx context.Context, req T) R + // + // func(ctx context.Context, req T) error + // + // func(ctx context.Context, req T) (R, error) + // + Head(pattern string, handler interface{}) + + // Post registers a new POST route with a matcher for the URL path of the post method. + // + // The handler accepts the following functional signatures: + // + // func(ctx context.Context) + // + // func(ctx context.Context) R + // + // func(ctx context.Context) error + // + // func(ctx context.Context, req T) R + // + // func(ctx context.Context, req T) error + // + // func(ctx context.Context, req T) (R, error) + // + Post(pattern string, handler interface{}) + + // Put registers a new PUT route with a matcher for the URL path of the put method. + // + // The handler accepts the following functional signatures: + // + // func(ctx context.Context) + // + // func(ctx context.Context) R + // + // func(ctx context.Context) error + // + // func(ctx context.Context, req T) R + // + // func(ctx context.Context, req T) error + // + // func(ctx context.Context, req T) (R, error) + // + Put(pattern string, handler interface{}) + + // Patch registers a new PATCH route with a matcher for the URL path of the patch method. + // + // The handler accepts the following functional signatures: + // + // func(ctx context.Context) + // + // func(ctx context.Context) R + // + // func(ctx context.Context) error + // + // func(ctx context.Context, req T) R + // + // func(ctx context.Context, req T) error + // + // func(ctx context.Context, req T) (R, error) + // + Patch(pattern string, handler interface{}) + + // Delete registers a new DELETE route with a matcher for the URL path of the delete method. + // + // The handler accepts the following functional signatures: + // + // func(ctx context.Context) + // + // func(ctx context.Context) R + // + // func(ctx context.Context) error + // + // func(ctx context.Context, req T) R + // + // func(ctx context.Context, req T) error + // + // func(ctx context.Context, req T) (R, error) + // + Delete(pattern string, handler interface{}) + + // Connect registers a new CONNECT route with a matcher for the URL path of the connect method. + // + // The handler accepts the following functional signatures: + // + // func(ctx context.Context) + // + // func(ctx context.Context) R + // + // func(ctx context.Context) error + // + // func(ctx context.Context, req T) R + // + // func(ctx context.Context, req T) error + // + // func(ctx context.Context, req T) (R, error) + // + Connect(pattern string, handler interface{}) + + // Options registers a new OPTIONS route with a matcher for the URL path of the options method. + // + // The handler accepts the following functional signatures: + // + // func(ctx context.Context) + // + // func(ctx context.Context) R + // + // func(ctx context.Context) error + // + // func(ctx context.Context, req T) R + // + // func(ctx context.Context, req T) error + // + // func(ctx context.Context, req T) (R, error) + // + Options(pattern string, handler interface{}) + + // Trace registers a new TRACE route with a matcher for the URL path of the trace method. + // + // The handler accepts the following functional signatures: + // + // func(ctx context.Context) + // + // func(ctx context.Context) R + // + // func(ctx context.Context) error + // + // func(ctx context.Context, req T) R + // + // func(ctx context.Context, req T) error + // + // func(ctx context.Context, req T) (R, error) + // + Trace(pattern string, handler interface{}) + + // NotFound to be used when no route matches. + NotFound(handler http.HandlerFunc) + + // MethodNotAllowed to be used when the request method does not match the route. + MethodNotAllowed(handler http.HandlerFunc) +} + +type Routes interface { + // Routes returns the routing tree in an easily traversable structure. + Routes() []Route + + // Middlewares returns the list of middlewares in use by the router. + Middlewares() Middlewares + + // Match searches the routing tree for a handler that matches + // the method/path - similar to routing a http request, but without + // executing the handler thereafter. + Match(webCtx *Context, method, path string) bool +} + +// NewRouter returns a new router instance. +func NewRouter() Router { + return &routerGroup{ + tree: &node{}, + renderer: RendererFunc(defaultJsonRender), + pool: &sync.Pool{New: func() interface{} { return &Context{} }}, + } +} + +type routerGroup struct { + handler http.Handler + inline bool + tree *node + parent *routerGroup + middlewares Middlewares + renderer Renderer + notFoundHandler http.HandlerFunc + notAllowedHandler http.HandlerFunc + pool *sync.Pool +} + +// Use appends a MiddlewareFunc to the chain. +// Middleware can be used to intercept or otherwise modify requests and/or responses, and are executed in the order that they are applied to the Router. +func (rg *routerGroup) Use(mwf ...MiddlewareFunc) { + rg.middlewares = append(rg.middlewares, mwf...) +} + +// Renderer to be used Response renderer in default. +func (rg *routerGroup) Renderer(renderer Renderer) { + rg.renderer = renderer +} + +func (rg *routerGroup) NotFoundHandler() http.Handler { + if rg.notFoundHandler != nil { + return rg.notFoundHandler + } + return notFound() +} + +func (rg *routerGroup) NotAllowedHandler() http.Handler { + if rg.notAllowedHandler != nil { + return rg.notAllowedHandler + } + return notAllowed() +} + +// ServeHTTP dispatches the handler registered in the matched route. +func (rg *routerGroup) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if nil == rg.handler { + rg.NotFoundHandler().ServeHTTP(w, r) + return + } + + webCtx := FromContext(r.Context()) + if nil != webCtx { + rg.handler.ServeHTTP(w, r) + return + } + + // get context from pool + webCtx = rg.pool.Get().(*Context) + webCtx.Writer = w + webCtx.Request = r + webCtx.routes = rg + + // with context + r = r.WithContext(WithContext(r.Context(), webCtx)) + rg.handler.ServeHTTP(w, r) + + // put context to pool + webCtx.Reset() + rg.pool.Put(webCtx) + +} + +// Recursively update data on child routers. +func (rg *routerGroup) updateSubRoutes(fn func(subMux *routerGroup)) { + for _, r := range rg.tree.routes() { + subMux, ok := r.SubRoutes.(*routerGroup) + if !ok { + continue + } + fn(subMux) + } +} + +func (rg *routerGroup) nextRoutePath(webCtx *Context) string { + routePath := "/" + nx := len(webCtx.routeParams.Keys) - 1 // index of last param in list + if nx >= 0 && webCtx.routeParams.Keys[nx] == "*" && len(webCtx.routeParams.Values) > nx { + routePath = "/" + webCtx.routeParams.Values[nx] + } + return routePath +} + +// routeHTTP routes a http.Request through the routing tree to serve +// the matching handler for a particular http method. +func (rg *routerGroup) routeHTTP(w http.ResponseWriter, r *http.Request) { + // Grab the route context object + webCtx := FromContext(r.Context()) + + // The request routing path + routePath := webCtx.routePath + if routePath == "" { + if r.URL.RawPath != "" { + routePath = r.URL.RawPath + } else { + routePath = r.URL.Path + } + if routePath == "" { + routePath = "/" + } + } + + if webCtx.routeMethod == "" { + webCtx.routeMethod = r.Method + } + + method, ok := methodMap[webCtx.routeMethod] + if !ok { + rg.NotAllowedHandler().ServeHTTP(w, r) + return + } + + // Find the route + if _, _, h := rg.tree.FindRoute(webCtx, method, routePath); h != nil { + h.ServeHTTP(w, r) + return + } + if webCtx.methodNotAllowed { + rg.NotAllowedHandler().ServeHTTP(w, r) + } else { + rg.NotFoundHandler().ServeHTTP(w, r) + } +} + +// Group creates a new router group. +func (rg *routerGroup) Group(pattern string) Router { + subRouter := &routerGroup{tree: &node{}, renderer: rg.renderer, pool: rg.pool} + rg.Mount(pattern, subRouter) + return subRouter +} + +// Mount attaches another http.Handler or RouterGroup as a subrouter along a routing +// path. It's very useful to split up a large API as many independent routers and +// compose them as a single service using Mount. +func (rg *routerGroup) Mount(pattern string, handler http.Handler) { + if handler == nil { + panic(fmt.Sprintf("attempting to Mount() a nil handler on '%s'", pattern)) + } + + // Provide runtime safety for ensuring a pattern isn't mounted on an existing + // routing pattern. + if rg.tree.findPattern(pattern+"*") || rg.tree.findPattern(pattern+"/*") { + panic(fmt.Sprintf("attempting to Mount() a handler on an existing path, '%s'", pattern)) + } + + // Assign sub-Router'rg with the parent not found & method not allowed handler if not specified. + subr, ok := handler.(*routerGroup) + if ok && subr.notFoundHandler == nil && rg.notFoundHandler != nil { + subr.NotFound(rg.notFoundHandler) + } + if ok && subr.notAllowedHandler == nil && rg.notAllowedHandler != nil { + subr.MethodNotAllowed(rg.notAllowedHandler) + } + + mountHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + webCtx := FromContext(r.Context()) + + // shift the url path past the previous subrouter + webCtx.routePath = rg.nextRoutePath(webCtx) + + // reset the wildcard URLParam which connects the subrouter + n := len(webCtx.urlParams.Keys) - 1 + if n >= 0 && webCtx.urlParams.Keys[n] == "*" && len(webCtx.urlParams.Values) > n { + webCtx.urlParams.Values[n] = "" + } + + handler.ServeHTTP(w, r) + }) + + if pattern == "" || pattern[len(pattern)-1] != '/' { + rg.handle(mALL|mSTUB, pattern, mountHandler) + rg.handle(mALL|mSTUB, pattern+"/", mountHandler) + pattern += "/" + } + + method := mALL + subroutes, _ := handler.(Routes) + if subroutes != nil { + method |= mSTUB + } + n := rg.handle(method, pattern+"*", mountHandler) + + if subroutes != nil { + n.subroutes = subroutes + } +} + +// bind a new route with a matcher for the URL pattern. +// Automatic binding request to handler input params and validate params. +func (rg *routerGroup) bind(method methodTyp, pattern string, handler interface{}) *node { + return rg.handle(method, pattern, Bind(handler, rg.renderer)) +} + +func (rg *routerGroup) handle(method methodTyp, pattern string, handler http.Handler) *node { + if len(pattern) == 0 || pattern[0] != '/' { + panic(fmt.Sprintf("routing pattern must begin with '/' in '%s'", pattern)) + } + if !rg.inline && rg.handler == nil { + rg.handler = rg.middlewares.HandlerFunc(rg.routeHTTP) + } + + if rg.inline { + rg.handler = http.HandlerFunc(rg.routeHTTP) + handler = rg.middlewares.Handler(handler) + } + + // Add the endpoint to the tree + return rg.tree.InsertRoute(method, pattern, handler) +} + +// Handle registers a new route with a matcher for the URL pattern. +func (rg *routerGroup) Handle(pattern string, handler http.Handler) { + rg.handle(mALL, pattern, handler) +} + +// HandleFunc registers a new route with a matcher for the URL pattern. +func (rg *routerGroup) HandleFunc(pattern string, handler http.HandlerFunc) { + rg.handle(mALL, pattern, handler) +} + +// Any registers a route that matches all the HTTP methods. +// GET, POST, PUT, PATCH, HEAD, OPTIONS, DELETE, CONNECT, TRACE. +func (rg *routerGroup) Any(pattern string, handler interface{}) { + rg.bind(mALL, pattern, handler) +} + +// Get registers a new GET route with a matcher for the URL pattern of the get method. +func (rg *routerGroup) Get(pattern string, handler interface{}) { + rg.bind(mGET, pattern, handler) +} + +// Head registers a new HEAD route with a matcher for the URL pattern of the get method. +func (rg *routerGroup) Head(pattern string, handler interface{}) { + rg.bind(mHEAD, pattern, handler) +} + +// Post registers a new POST route with a matcher for the URL pattern of the get method. +func (rg *routerGroup) Post(pattern string, handler interface{}) { + rg.bind(mPOST, pattern, handler) +} + +// Put registers a new PUT route with a matcher for the URL pattern of the get method. +func (rg *routerGroup) Put(pattern string, handler interface{}) { + rg.bind(mPUT, pattern, handler) +} + +// Patch registers a new PATCH route with a matcher for the URL pattern of the get method. +func (rg *routerGroup) Patch(pattern string, handler interface{}) { + rg.bind(mPATCH, pattern, handler) +} + +// Delete registers a new DELETE route with a matcher for the URL pattern of the get method. +func (rg *routerGroup) Delete(pattern string, handler interface{}) { + rg.bind(mDELETE, pattern, handler) +} + +// Connect registers a new CONNECT route with a matcher for the URL pattern of the get method. +func (rg *routerGroup) Connect(pattern string, handler interface{}) { + rg.bind(mCONNECT, pattern, handler) +} + +// Options registers a new OPTIONS route with a matcher for the URL pattern of the get method. +func (rg *routerGroup) Options(pattern string, handler interface{}) { + rg.bind(mOPTIONS, pattern, handler) +} + +// Trace registers a new TRACE route with a matcher for the URL pattern of the get method. +func (rg *routerGroup) Trace(pattern string, handler interface{}) { + rg.bind(mTRACE, pattern, handler) +} + +// NotFound to be used when no route matches. +// This can be used to render your own 404 Not Found errors. +func (rg *routerGroup) NotFound(handler http.HandlerFunc) { + // Build NotFound handler chain + m := rg + hFn := handler + if rg.inline && rg.parent != nil { + m = rg.parent + hFn = rg.middlewares.HandlerFunc(hFn).ServeHTTP + } + + // Update the notFoundHandler from this point forward + m.notFoundHandler = hFn + m.updateSubRoutes(func(subMux *routerGroup) { + if subMux.notFoundHandler == nil { + subMux.NotFound(hFn) + } + }) +} + +// MethodNotAllowed to be used when the request method does not match the route. +// This can be used to render your own 405 Method Not Allowed errors. +func (rg *routerGroup) MethodNotAllowed(handler http.HandlerFunc) { + // Build MethodNotAllowed handler chain + m := rg + hFn := handler + if rg.inline && rg.parent != nil { + m = rg.parent + hFn = rg.middlewares.HandlerFunc(hFn).ServeHTTP + } + + // Update the methodNotAllowedHandler from this point forward + m.notAllowedHandler = hFn + m.updateSubRoutes(func(subMux *routerGroup) { + if subMux.notAllowedHandler == nil { + subMux.MethodNotAllowed(hFn) + } + }) +} + +// Routes returns a slice of routing information from the tree, +// useful for traversing available routes of a router. +func (rg *routerGroup) Routes() []Route { + return rg.tree.routes() +} + +// Middlewares returns a slice of middleware handler functions. +func (rg *routerGroup) Middlewares() Middlewares { + return rg.middlewares +} + +// Match searches the routing tree for a handler that matches the method/path. +// It's similar to routing a http request, but without executing the handler +// thereafter. +func (rg *routerGroup) Match(webCtx *Context, method, path string) bool { + m, ok := methodMap[method] + if !ok { + return false + } + + node, _, h := rg.tree.FindRoute(webCtx, m, path) + + if node != nil && node.subroutes != nil { + webCtx.routePath = rg.nextRoutePath(webCtx) + return node.subroutes.Match(webCtx, method, webCtx.routePath) + } + + return h != nil +} diff --git a/web/server.go b/web/server.go index 898933f7..8893c432 100644 --- a/web/server.go +++ b/web/server.go @@ -19,16 +19,13 @@ package web import ( "context" "net/http" - - "go-spring.dev/spring/web/internal/mux" ) // A Server defines parameters for running an HTTP server. type Server struct { options Options httpSvr *http.Server - - RouterGroup + Router } // NewServer returns a new server instance. @@ -39,10 +36,16 @@ func NewServer(options Options) *Server { addr = ":8080" // default port: 8080 } + var router = options.Router + if nil == router { + router = NewRouter() + } + svr := &Server{ options: options, httpSvr: &http.Server{ Addr: addr, + Handler: router, TLSConfig: options.TlsConfig(), ReadTimeout: options.ReadTimeout, ReadHeaderTimeout: options.ReadHeaderTimeout, @@ -50,15 +53,9 @@ func NewServer(options Options) *Server { IdleTimeout: options.IdleTimeout, MaxHeaderBytes: options.MaxHeaderBytes, }, - RouterGroup: &routerGroup{ - basePath: "/", - router: mux.NewRouter(), - renderer: RendererFunc(defaultJsonRender), - }, + Router: router, } - svr.NotFound(notFound()) - svr.MethodNotAllowed(notAllowed()) return svr } @@ -71,7 +68,6 @@ func (s *Server) Addr() string { // calls Serve to handle requests on incoming connections. // Accepted connections are configured to enable TCP keep-alives. func (s *Server) Run() error { - s.httpSvr.Handler = s if nil != s.httpSvr.TLSConfig { return s.httpSvr.ListenAndServeTLS(s.options.CertFile, s.options.KeyFile) } @@ -88,13 +84,3 @@ func (s *Server) Run() error { func (s *Server) Shutdown(ctx context.Context) error { return s.httpSvr.Shutdown(ctx) } - -func notFound() http.Handler { - return http.NotFoundHandler() -} - -func notAllowed() http.Handler { - return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { - http.Error(writer, "405 method not allowed", http.StatusMethodNotAllowed) - }) -} diff --git a/web/tree.go b/web/tree.go new file mode 100644 index 00000000..b29ec0a7 --- /dev/null +++ b/web/tree.go @@ -0,0 +1,872 @@ +package web + +// Radix tree implementation below is a based on the original work by +// Armon Dadgar in https://github.com/armon/go-radix/blob/master/radix.go +// (MIT licensed). It's been heavily modified for use as a HTTP routing tree. + +import ( + "fmt" + "net/http" + "regexp" + "sort" + "strings" +) + +type methodTyp uint + +const ( + mSTUB methodTyp = 1 << iota + mCONNECT + mDELETE + mGET + mHEAD + mOPTIONS + mPATCH + mPOST + mPUT + mTRACE + + mALL = mCONNECT | mDELETE | mGET | mHEAD | + mOPTIONS | mPATCH | mPOST | mPUT | mTRACE +) + +var methodMap = map[string]methodTyp{ + http.MethodConnect: mCONNECT, + http.MethodDelete: mDELETE, + http.MethodGet: mGET, + http.MethodHead: mHEAD, + http.MethodOptions: mOPTIONS, + http.MethodPatch: mPATCH, + http.MethodPost: mPOST, + http.MethodPut: mPUT, + http.MethodTrace: mTRACE, +} + +var reverseMethodMap = map[methodTyp]string{ + mCONNECT: http.MethodConnect, + mDELETE: http.MethodDelete, + mGET: http.MethodGet, + mHEAD: http.MethodHead, + mOPTIONS: http.MethodOptions, + mPATCH: http.MethodPatch, + mPOST: http.MethodPost, + mPUT: http.MethodPut, + mTRACE: http.MethodTrace, +} + +type nodeTyp uint8 + +const ( + ntStatic nodeTyp = iota // /home + ntRegexp // /{id:[0-9]+} + ntParam // /{user} + ntCatchAll // /api/v1/* +) + +type node struct { + // subroutes on the leaf node + subroutes Routes + + // regexp matcher for regexp nodes + rex *regexp.Regexp + + // HTTP handler endpoints on the leaf node + endpoints endpoints + + // prefix is the common prefix we ignore + prefix string + + // child nodes should be stored in-order for iteration, + // in groups of the node type. + children [ntCatchAll + 1]nodes + + // first byte of the child prefix + tail byte + + // node type: static, regexp, param, catchAll + typ nodeTyp + + // first byte of the prefix + label byte +} + +// endpoints is a mapping of http method constants to handlers +// for a given route. +type endpoints map[methodTyp]*endpoint + +type endpoint struct { + // endpoint handler + handler http.Handler + + // pattern is the routing pattern for handler nodes + pattern string + + // parameter keys recorded on handler nodes + paramKeys []string +} + +func (s endpoints) Value(method methodTyp) *endpoint { + mh, ok := s[method] + if !ok { + mh = &endpoint{} + s[method] = mh + } + return mh +} + +func (n *node) InsertRoute(method methodTyp, pattern string, handler http.Handler) *node { + var parent *node + search := pattern + + for { + // Handle key exhaustion + if len(search) == 0 { + // Insert or update the node's leaf handler + n.setEndpoint(method, handler, pattern) + return n + } + + // We're going to be searching for a wild node next, + // in this case, we need to get the tail + var label = search[0] + var segTail byte + var segEndIdx int + var segTyp nodeTyp + var segRexpat string + if label == '{' || label == '*' { + segTyp, _, segRexpat, segTail, _, segEndIdx = patNextSegment(search) + } + + var prefix string + if segTyp == ntRegexp { + prefix = segRexpat + } + + // Look for the edge to attach to + parent = n + n = n.getEdge(segTyp, label, segTail, prefix) + + // No edge, create one + if n == nil { + child := &node{label: label, tail: segTail, prefix: search} + hn := parent.addChild(child, search) + hn.setEndpoint(method, handler, pattern) + + return hn + } + + // Found an edge to match the pattern + + if n.typ > ntStatic { + // We found a param node, trim the param from the search path and continue. + // This param/wild pattern segment would already be on the tree from a previous + // call to addChild when creating a new node. + search = search[segEndIdx:] + continue + } + + // Static nodes fall below here. + // Determine longest prefix of the search key on match. + commonPrefix := longestPrefix(search, n.prefix) + if commonPrefix == len(n.prefix) { + // the common prefix is as long as the current node's prefix we're attempting to insert. + // keep the search going. + search = search[commonPrefix:] + continue + } + + // Split the node + child := &node{ + typ: ntStatic, + prefix: search[:commonPrefix], + } + parent.replaceChild(search[0], segTail, child) + + // Restore the existing node + n.label = n.prefix[commonPrefix] + n.prefix = n.prefix[commonPrefix:] + child.addChild(n, n.prefix) + + // If the new key is a subset, set the method/handler on this node and finish. + search = search[commonPrefix:] + if len(search) == 0 { + child.setEndpoint(method, handler, pattern) + return child + } + + // Create a new edge for the node + subchild := &node{ + typ: ntStatic, + label: search[0], + prefix: search, + } + hn := child.addChild(subchild, search) + hn.setEndpoint(method, handler, pattern) + return hn + } +} + +// addChild appends the new `child` node to the tree using the `pattern` as the trie key. +// For a URL router, we split the static, param, regexp and wildcard segments +// into different nodes. In addition, addChild will recursively call itself until every +// pattern segment is added to the url pattern tree as individual nodes, depending on type. +func (n *node) addChild(child *node, prefix string) *node { + search := prefix + + // handler leaf node added to the tree is the child. + // this may be overridden later down the flow + hn := child + + // Parse next segment + segTyp, _, segRexpat, segTail, segStartIdx, segEndIdx := patNextSegment(search) + + // Add child depending on next up segment + switch segTyp { + + case ntStatic: + // Search prefix is all static (that is, has no params in path) + // noop + + default: + // Search prefix contains a param, regexp or wildcard + + if segTyp == ntRegexp { + rex, err := regexp.Compile(segRexpat) + if err != nil { + panic(fmt.Sprintf("invalid regexp pattern '%s' in route param", segRexpat)) + } + child.prefix = segRexpat + child.rex = rex + } + + if segStartIdx == 0 { + // Route starts with a param + child.typ = segTyp + + if segTyp == ntCatchAll { + segStartIdx = -1 + } else { + segStartIdx = segEndIdx + } + if segStartIdx < 0 { + segStartIdx = len(search) + } + child.tail = segTail // for params, we set the tail + + if segStartIdx != len(search) { + // add static edge for the remaining part, split the end. + // its not possible to have adjacent param nodes, so its certainly + // going to be a static node next. + + search = search[segStartIdx:] // advance search position + + nn := &node{ + typ: ntStatic, + label: search[0], + prefix: search, + } + hn = child.addChild(nn, search) + } + + } else if segStartIdx > 0 { + // Route has some param + + // starts with a static segment + child.typ = ntStatic + child.prefix = search[:segStartIdx] + child.rex = nil + + // add the param edge node + search = search[segStartIdx:] + + nn := &node{ + typ: segTyp, + label: search[0], + tail: segTail, + } + hn = child.addChild(nn, search) + + } + } + + n.children[child.typ] = append(n.children[child.typ], child) + n.children[child.typ].Sort() + return hn +} + +func (n *node) replaceChild(label, tail byte, child *node) { + for i := 0; i < len(n.children[child.typ]); i++ { + if n.children[child.typ][i].label == label && n.children[child.typ][i].tail == tail { + n.children[child.typ][i] = child + n.children[child.typ][i].label = label + n.children[child.typ][i].tail = tail + return + } + } + panic("replacing missing child") +} + +func (n *node) getEdge(ntyp nodeTyp, label, tail byte, prefix string) *node { + nds := n.children[ntyp] + for i := 0; i < len(nds); i++ { + if nds[i].label == label && nds[i].tail == tail { + if ntyp == ntRegexp && nds[i].prefix != prefix { + continue + } + return nds[i] + } + } + return nil +} + +func (n *node) setEndpoint(method methodTyp, handler http.Handler, pattern string) { + // Set the handler for the method type on the node + if n.endpoints == nil { + n.endpoints = make(endpoints) + } + + paramKeys := patParamKeys(pattern) + + if method&mSTUB == mSTUB { + n.endpoints.Value(mSTUB).handler = handler + } + if method&mALL == mALL { + h := n.endpoints.Value(mALL) + h.handler = handler + h.pattern = pattern + h.paramKeys = paramKeys + for _, m := range methodMap { + h := n.endpoints.Value(m) + h.handler = handler + h.pattern = pattern + h.paramKeys = paramKeys + } + } else { + h := n.endpoints.Value(method) + h.handler = handler + h.pattern = pattern + h.paramKeys = paramKeys + } +} + +func (n *node) FindRoute(rctx *Context, method methodTyp, path string) (*node, endpoints, http.Handler) { + // Reset the context routing pattern and params + rctx.routePattern = "" + rctx.routeParams.Keys = rctx.routeParams.Keys[:0] + rctx.routeParams.Values = rctx.routeParams.Values[:0] + + // Find the routing handlers for the path + rn := n.findRoute(rctx, method, path) + if rn == nil { + return nil, nil, nil + } + + // Record the routing params in the request lifecycle + rctx.urlParams.Keys = append(rctx.urlParams.Keys, rctx.routeParams.Keys...) + rctx.urlParams.Values = append(rctx.urlParams.Values, rctx.routeParams.Values...) + + // Record the routing pattern in the request lifecycle + if rn.endpoints[method].pattern != "" { + rctx.routePattern = rn.endpoints[method].pattern + rctx.routePatterns = append(rctx.routePatterns, rctx.routePattern) + } + + return rn, rn.endpoints, rn.endpoints[method].handler +} + +// Recursive edge traversal by checking all nodeTyp groups along the way. +// It's like searching through a multi-dimensional radix trie. +func (n *node) findRoute(rctx *Context, method methodTyp, path string) *node { + nn := n + search := path + + for t, nds := range nn.children { + ntyp := nodeTyp(t) + if len(nds) == 0 { + continue + } + + var xn *node + xsearch := search + + var label byte + if search != "" { + label = search[0] + } + + switch ntyp { + case ntStatic: + xn = nds.findEdge(label) + if xn == nil || !strings.HasPrefix(xsearch, xn.prefix) { + continue + } + xsearch = xsearch[len(xn.prefix):] + + case ntParam, ntRegexp: + // short-circuit and return no matching route for empty param values + if xsearch == "" { + continue + } + + // serially loop through each node grouped by the tail delimiter + for idx := 0; idx < len(nds); idx++ { + xn = nds[idx] + + // label for param nodes is the delimiter byte + p := strings.IndexByte(xsearch, xn.tail) + + if p < 0 { + if xn.tail == '/' { + p = len(xsearch) + } else { + continue + } + } else if ntyp == ntRegexp && p == 0 { + continue + } + + if ntyp == ntRegexp && xn.rex != nil { + if !xn.rex.MatchString(xsearch[:p]) { + continue + } + } else if strings.IndexByte(xsearch[:p], '/') != -1 { + // avoid a match across path segments + continue + } + + prevlen := len(rctx.routeParams.Values) + rctx.routeParams.Values = append(rctx.routeParams.Values, xsearch[:p]) + xsearch = xsearch[p:] + + if len(xsearch) == 0 { + if xn.isLeaf() { + h := xn.endpoints[method] + if h != nil && h.handler != nil { + rctx.routeParams.Keys = append(rctx.routeParams.Keys, h.paramKeys...) + return xn + } + + for endpoints := range xn.endpoints { + if endpoints == mALL || endpoints == mSTUB { + continue + } + rctx.methodsAllowed = append(rctx.methodsAllowed, endpoints) + } + + // flag that the routing context found a route, but not a corresponding + // supported method + rctx.methodNotAllowed = true + } + } + + // recursively find the next node on this branch + fin := xn.findRoute(rctx, method, xsearch) + if fin != nil { + return fin + } + + // not found on this branch, reset vars + rctx.routeParams.Values = rctx.routeParams.Values[:prevlen] + xsearch = search + } + + rctx.routeParams.Values = append(rctx.routeParams.Values, "") + + default: + // catch-all nodes + rctx.routeParams.Values = append(rctx.routeParams.Values, search) + xn = nds[0] + xsearch = "" + } + + if xn == nil { + continue + } + + // did we find it yet? + if len(xsearch) == 0 { + if xn.isLeaf() { + h := xn.endpoints[method] + if h != nil && h.handler != nil { + rctx.routeParams.Keys = append(rctx.routeParams.Keys, h.paramKeys...) + return xn + } + + for endpoints := range xn.endpoints { + if endpoints == mALL || endpoints == mSTUB { + continue + } + rctx.methodsAllowed = append(rctx.methodsAllowed, endpoints) + } + + // flag that the routing context found a route, but not a corresponding + // supported method + rctx.methodNotAllowed = true + } + } + + // recursively find the next node.. + fin := xn.findRoute(rctx, method, xsearch) + if fin != nil { + return fin + } + + // Did not find final handler, let's remove the param here if it was set + if xn.typ > ntStatic { + if len(rctx.routeParams.Values) > 0 { + rctx.routeParams.Values = rctx.routeParams.Values[:len(rctx.routeParams.Values)-1] + } + } + + } + + return nil +} + +func (n *node) findEdge(ntyp nodeTyp, label byte) *node { + nds := n.children[ntyp] + num := len(nds) + idx := 0 + + switch ntyp { + case ntStatic, ntParam, ntRegexp: + i, j := 0, num-1 + for i <= j { + idx = i + (j-i)/2 + if label > nds[idx].label { + i = idx + 1 + } else if label < nds[idx].label { + j = idx - 1 + } else { + i = num // breaks cond + } + } + if nds[idx].label != label { + return nil + } + return nds[idx] + + default: // catch all + return nds[idx] + } +} + +func (n *node) isLeaf() bool { + return n.endpoints != nil +} + +func (n *node) findPattern(pattern string) bool { + nn := n + for _, nds := range nn.children { + if len(nds) == 0 { + continue + } + + n = nn.findEdge(nds[0].typ, pattern[0]) + if n == nil { + continue + } + + var idx int + var xpattern string + + switch n.typ { + case ntStatic: + idx = longestPrefix(pattern, n.prefix) + if idx < len(n.prefix) { + continue + } + + case ntParam, ntRegexp: + idx = strings.IndexByte(pattern, '}') + 1 + + case ntCatchAll: + idx = longestPrefix(pattern, "*") + + default: + panic("unknown node type") + } + + xpattern = pattern[idx:] + if len(xpattern) == 0 { + return true + } + + return n.findPattern(xpattern) + } + return false +} + +func (n *node) routes() []Route { + rts := []Route{} + + n.walk(func(eps endpoints, subroutes Routes) bool { + if eps[mSTUB] != nil && eps[mSTUB].handler != nil && subroutes == nil { + return false + } + + // Group methodHandlers by unique patterns + pats := make(map[string]endpoints) + + for mt, h := range eps { + if h.pattern == "" { + continue + } + p, ok := pats[h.pattern] + if !ok { + p = endpoints{} + pats[h.pattern] = p + } + p[mt] = h + } + + for p, mh := range pats { + hs := make(map[string]http.Handler) + if mh[mALL] != nil && mh[mALL].handler != nil { + hs["*"] = mh[mALL].handler + } + + for mt, h := range mh { + if h.handler == nil { + continue + } + m := methodTypString(mt) + if m == "" { + continue + } + hs[m] = h.handler + } + + rt := Route{subroutes, hs, p} + rts = append(rts, rt) + } + + return false + }) + + return rts +} + +func (n *node) walk(fn func(eps endpoints, subroutes Routes) bool) bool { + // Visit the leaf values if any + if (n.endpoints != nil || n.subroutes != nil) && fn(n.endpoints, n.subroutes) { + return true + } + + // Recurse on the children + for _, ns := range n.children { + for _, cn := range ns { + if cn.walk(fn) { + return true + } + } + } + return false +} + +// patNextSegment returns the next segment details from a pattern: +// node type, param key, regexp string, param tail byte, param starting index, param ending index +func patNextSegment(pattern string) (nodeTyp, string, string, byte, int, int) { + ps := strings.Index(pattern, "{") + ws := strings.Index(pattern, "*") + + if ps < 0 && ws < 0 { + return ntStatic, "", "", 0, 0, len(pattern) // we return the entire thing + } + + // Sanity check + if ps >= 0 && ws >= 0 && ws < ps { + panic("wildcard '*' must be the last pattern in a route, otherwise use a '{param}'") + } + + var tail byte = '/' // Default endpoint tail to / byte + + if ps >= 0 { + // Param/Regexp pattern is next + nt := ntParam + + // Read to closing } taking into account opens and closes in curl count (cc) + cc := 0 + pe := ps + for i, c := range pattern[ps:] { + if c == '{' { + cc++ + } else if c == '}' { + cc-- + if cc == 0 { + pe = ps + i + break + } + } + } + if pe == ps { + panic("route param closing delimiter '}' is missing") + } + + key := pattern[ps+1 : pe] + pe++ // set end to next position + + if pe < len(pattern) { + tail = pattern[pe] + } + + var rexpat string + if idx := strings.Index(key, ":"); idx >= 0 { + nt = ntRegexp + rexpat = key[idx+1:] + key = key[:idx] + } + + if len(rexpat) > 0 { + if rexpat[0] != '^' { + rexpat = "^" + rexpat + } + if rexpat[len(rexpat)-1] != '$' { + rexpat += "$" + } + } + + return nt, key, rexpat, tail, ps, pe + } + + // Wildcard pattern as finale + if ws < len(pattern)-1 { + panic("wildcard '*' must be the last value in a route. trim trailing text or use a '{param}' instead") + } + return ntCatchAll, "*", "", 0, ws, len(pattern) +} + +func patParamKeys(pattern string) []string { + pat := pattern + paramKeys := []string{} + for { + ptyp, paramKey, _, _, _, e := patNextSegment(pat) + if ptyp == ntStatic { + return paramKeys + } + for i := 0; i < len(paramKeys); i++ { + if paramKeys[i] == paramKey { + panic(fmt.Sprintf("routing pattern '%s' contains duplicate param key, '%s'", pattern, paramKey)) + } + } + paramKeys = append(paramKeys, paramKey) + pat = pat[e:] + } +} + +// longestPrefix finds the length of the shared prefix +// of two strings +func longestPrefix(k1, k2 string) int { + max := len(k1) + if l := len(k2); l < max { + max = l + } + var i int + for i = 0; i < max; i++ { + if k1[i] != k2[i] { + break + } + } + return i +} + +func methodTypString(method methodTyp) string { + for s, t := range methodMap { + if method == t { + return s + } + } + return "" +} + +type nodes []*node + +// Sort the list of nodes by label +func (ns nodes) Sort() { sort.Sort(ns); ns.tailSort() } +func (ns nodes) Len() int { return len(ns) } +func (ns nodes) Swap(i, j int) { ns[i], ns[j] = ns[j], ns[i] } +func (ns nodes) Less(i, j int) bool { return ns[i].label < ns[j].label } + +// tailSort pushes nodes with '/' as the tail to the end of the list for param nodes. +// The list order determines the traversal order. +func (ns nodes) tailSort() { + for i := len(ns) - 1; i >= 0; i-- { + if ns[i].typ > ntStatic && ns[i].tail == '/' { + ns.Swap(i, len(ns)-1) + return + } + } +} + +func (ns nodes) findEdge(label byte) *node { + num := len(ns) + idx := 0 + i, j := 0, num-1 + for i <= j { + idx = i + (j-i)/2 + if label > ns[idx].label { + i = idx + 1 + } else if label < ns[idx].label { + j = idx - 1 + } else { + i = num // breaks cond + } + } + if ns[idx].label != label { + return nil + } + return ns[idx] +} + +// Route describes the details of a routing handler. +// Handlers map key is an HTTP method +type Route struct { + SubRoutes Routes + Handlers map[string]http.Handler + Pattern string +} + +// WalkFunc is the type of the function called for each method and route visited by Walk. +type WalkFunc func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error + +// Walk walks any router tree that implements Routes interface. +func Walk(r Routes, walkFn WalkFunc) error { + return walk(r, walkFn, "") +} + +func walk(r Routes, walkFn WalkFunc, parentRoute string, parentMw ...func(http.Handler) http.Handler) error { + for _, route := range r.Routes() { + mws := make([]func(http.Handler) http.Handler, len(parentMw)) + copy(mws, parentMw) + mws = append(mws, r.Middlewares()...) + + if route.SubRoutes != nil { + if err := walk(route.SubRoutes, walkFn, parentRoute+route.Pattern, mws...); err != nil { + return err + } + continue + } + + for method, handler := range route.Handlers { + if method == "*" { + // Ignore a "catchAll" method, since we pass down all the specific methods for each route. + continue + } + + fullRoute := parentRoute + route.Pattern + fullRoute = strings.Replace(fullRoute, "/*/", "/", -1) + + if chain, ok := handler.(*chainHandler); ok { + if err := walkFn(method, fullRoute, chain.Endpoint, append(mws, chain.Middlewares...)...); err != nil { + return err + } + } else { + if err := walkFn(method, fullRoute, handler, mws...); err != nil { + return err + } + } + } + } + + return nil +} diff --git a/web/tree_test.go b/web/tree_test.go new file mode 100644 index 00000000..edf698eb --- /dev/null +++ b/web/tree_test.go @@ -0,0 +1,508 @@ +package web + +import ( + "fmt" + "log" + "net/http" + "testing" +) + +func TestTree(t *testing.T) { + hStub := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + hIndex := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + hFavicon := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + hArticleList := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + hArticleNear := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + hArticleShow := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + hArticleShowRelated := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + hArticleShowOpts := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + hArticleSlug := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + hArticleByUser := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + hUserList := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + hUserShow := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + hAdminCatchall := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + hAdminAppShow := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + hAdminAppShowCatchall := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + hUserProfile := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + hUserSuper := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + hUserAll := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + hHubView1 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + hHubView2 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + hHubView3 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + + tr := &node{} + + tr.InsertRoute(mGET, "/", hIndex) + tr.InsertRoute(mGET, "/favicon.ico", hFavicon) + + tr.InsertRoute(mGET, "/pages/*", hStub) + + tr.InsertRoute(mGET, "/article", hArticleList) + tr.InsertRoute(mGET, "/article/", hArticleList) + + tr.InsertRoute(mGET, "/article/near", hArticleNear) + tr.InsertRoute(mGET, "/article/{id}", hStub) + tr.InsertRoute(mGET, "/article/{id}", hArticleShow) + tr.InsertRoute(mGET, "/article/{id}", hArticleShow) // duplicate will have no effect + tr.InsertRoute(mGET, "/article/@{user}", hArticleByUser) + + tr.InsertRoute(mGET, "/article/{sup}/{opts}", hArticleShowOpts) + tr.InsertRoute(mGET, "/article/{id}/{opts}", hArticleShowOpts) // overwrite above route, latest wins + + tr.InsertRoute(mGET, "/article/{iffd}/edit", hStub) + tr.InsertRoute(mGET, "/article/{id}//related", hArticleShowRelated) + tr.InsertRoute(mGET, "/article/slug/{month}/-/{day}/{year}", hArticleSlug) + + tr.InsertRoute(mGET, "/admin/user", hUserList) + tr.InsertRoute(mGET, "/admin/user/", hStub) // will get replaced by next route + tr.InsertRoute(mGET, "/admin/user/", hUserList) + + tr.InsertRoute(mGET, "/admin/user//{id}", hUserShow) + tr.InsertRoute(mGET, "/admin/user/{id}", hUserShow) + + tr.InsertRoute(mGET, "/admin/apps/{id}", hAdminAppShow) + tr.InsertRoute(mGET, "/admin/apps/{id}/*", hAdminAppShowCatchall) + + tr.InsertRoute(mGET, "/admin/*", hStub) // catchall segment will get replaced by next route + tr.InsertRoute(mGET, "/admin/*", hAdminCatchall) + + tr.InsertRoute(mGET, "/users/{userID}/profile", hUserProfile) + tr.InsertRoute(mGET, "/users/super/*", hUserSuper) + tr.InsertRoute(mGET, "/users/*", hUserAll) + + tr.InsertRoute(mGET, "/hubs/{hubID}/view", hHubView1) + tr.InsertRoute(mGET, "/hubs/{hubID}/view/*", hHubView2) + sr := &routerGroup{tree: &node{}} + sr.Get("/users", hHubView3) + tr.InsertRoute(mGET, "/hubs/{hubID}/*", sr) + tr.InsertRoute(mGET, "/hubs/{hubID}/users", hHubView3) + + tests := []struct { + r string // input request path + h http.Handler // output matched handler + k []string // output param keys + v []string // output param values + }{ + {r: "/", h: hIndex, k: []string{}, v: []string{}}, + {r: "/favicon.ico", h: hFavicon, k: []string{}, v: []string{}}, + + {r: "/pages", h: nil, k: []string{}, v: []string{}}, + {r: "/pages/", h: hStub, k: []string{"*"}, v: []string{""}}, + {r: "/pages/yes", h: hStub, k: []string{"*"}, v: []string{"yes"}}, + + {r: "/article", h: hArticleList, k: []string{}, v: []string{}}, + {r: "/article/", h: hArticleList, k: []string{}, v: []string{}}, + {r: "/article/near", h: hArticleNear, k: []string{}, v: []string{}}, + {r: "/article/neard", h: hArticleShow, k: []string{"id"}, v: []string{"neard"}}, + {r: "/article/123", h: hArticleShow, k: []string{"id"}, v: []string{"123"}}, + {r: "/article/123/456", h: hArticleShowOpts, k: []string{"id", "opts"}, v: []string{"123", "456"}}, + {r: "/article/@peter", h: hArticleByUser, k: []string{"user"}, v: []string{"peter"}}, + {r: "/article/22//related", h: hArticleShowRelated, k: []string{"id"}, v: []string{"22"}}, + {r: "/article/111/edit", h: hStub, k: []string{"iffd"}, v: []string{"111"}}, + {r: "/article/slug/sept/-/4/2015", h: hArticleSlug, k: []string{"month", "day", "year"}, v: []string{"sept", "4", "2015"}}, + {r: "/article/:id", h: hArticleShow, k: []string{"id"}, v: []string{":id"}}, + + {r: "/admin/user", h: hUserList, k: []string{}, v: []string{}}, + {r: "/admin/user/", h: hUserList, k: []string{}, v: []string{}}, + {r: "/admin/user/1", h: hUserShow, k: []string{"id"}, v: []string{"1"}}, + {r: "/admin/user//1", h: hUserShow, k: []string{"id"}, v: []string{"1"}}, + {r: "/admin/hi", h: hAdminCatchall, k: []string{"*"}, v: []string{"hi"}}, + {r: "/admin/lots/of/:fun", h: hAdminCatchall, k: []string{"*"}, v: []string{"lots/of/:fun"}}, + {r: "/admin/apps/333", h: hAdminAppShow, k: []string{"id"}, v: []string{"333"}}, + {r: "/admin/apps/333/woot", h: hAdminAppShowCatchall, k: []string{"id", "*"}, v: []string{"333", "woot"}}, + + {r: "/hubs/123/view", h: hHubView1, k: []string{"hubID"}, v: []string{"123"}}, + {r: "/hubs/123/view/index.html", h: hHubView2, k: []string{"hubID", "*"}, v: []string{"123", "index.html"}}, + {r: "/hubs/123/users", h: hHubView3, k: []string{"hubID"}, v: []string{"123"}}, + + {r: "/users/123/profile", h: hUserProfile, k: []string{"userID"}, v: []string{"123"}}, + {r: "/users/super/123/okay/yes", h: hUserSuper, k: []string{"*"}, v: []string{"123/okay/yes"}}, + {r: "/users/123/okay/yes", h: hUserAll, k: []string{"*"}, v: []string{"123/okay/yes"}}, + } + + // log.Println("~~~~~~~~~") + // log.Println("~~~~~~~~~") + // debugPrintTree(0, 0, tr, 0) + // log.Println("~~~~~~~~~") + // log.Println("~~~~~~~~~") + + for i, tt := range tests { + rctx := &Context{} + + _, handlers, _ := tr.FindRoute(rctx, mGET, tt.r) + + var handler http.Handler + if methodHandler, ok := handlers[mGET]; ok { + handler = methodHandler.handler + } + + paramKeys := rctx.routeParams.Keys + paramValues := rctx.routeParams.Values + + if fmt.Sprintf("%v", tt.h) != fmt.Sprintf("%v", handler) { + t.Errorf("input [%d]: find '%s' expecting handler:%v , got:%v", i, tt.r, tt.h, handler) + } + if !stringSliceEqual(tt.k, paramKeys) { + t.Errorf("input [%d]: find '%s' expecting paramKeys:(%d)%v , got:(%d)%v", i, tt.r, len(tt.k), tt.k, len(paramKeys), paramKeys) + } + if !stringSliceEqual(tt.v, paramValues) { + t.Errorf("input [%d]: find '%s' expecting paramValues:(%d)%v , got:(%d)%v", i, tt.r, len(tt.v), tt.v, len(paramValues), paramValues) + } + } +} + +func TestTreeMoar(t *testing.T) { + hStub := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + hStub1 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + hStub2 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + hStub3 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + hStub4 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + hStub5 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + hStub6 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + hStub7 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + hStub8 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + hStub9 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + hStub10 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + hStub11 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + hStub12 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + hStub13 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + hStub14 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + hStub15 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + hStub16 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + + // TODO: panic if we see {id}{x} because we're missing a delimiter, its not possible. + // also {:id}* is not possible. + + tr := &node{} + + tr.InsertRoute(mGET, "/articlefun", hStub5) + tr.InsertRoute(mGET, "/articles/{id}", hStub) + tr.InsertRoute(mDELETE, "/articles/{slug}", hStub8) + tr.InsertRoute(mGET, "/articles/search", hStub1) + tr.InsertRoute(mGET, "/articles/{id}:delete", hStub8) + tr.InsertRoute(mGET, "/articles/{iidd}!sup", hStub4) + tr.InsertRoute(mGET, "/articles/{id}:{op}", hStub3) + tr.InsertRoute(mGET, "/articles/{id}:{op}", hStub2) // this route sets a new handler for the above route + tr.InsertRoute(mGET, "/articles/{slug:^[a-z]+}/posts", hStub) // up to tail '/' will only match if contents match the rex + tr.InsertRoute(mGET, "/articles/{id}/posts/{pid}", hStub6) // /articles/123/posts/1 + tr.InsertRoute(mGET, "/articles/{id}/posts/{month}/{day}/{year}/{slug}", hStub7) // /articles/123/posts/09/04/1984/juice + tr.InsertRoute(mGET, "/articles/{id}.json", hStub10) + tr.InsertRoute(mGET, "/articles/{id}/data.json", hStub11) + tr.InsertRoute(mGET, "/articles/files/{file}.{ext}", hStub12) + tr.InsertRoute(mPUT, "/articles/me", hStub13) + + // TODO: make a separate test case for this one.. + // tr.InsertRoute(mGET, "/articles/{id}/{id}", hStub1) // panic expected, we're duplicating param keys + + tr.InsertRoute(mGET, "/pages/*", hStub) + tr.InsertRoute(mGET, "/pages/*", hStub9) + + tr.InsertRoute(mGET, "/users/{id}", hStub14) + tr.InsertRoute(mGET, "/users/{id}/settings/{key}", hStub15) + tr.InsertRoute(mGET, "/users/{id}/settings/*", hStub16) + + tests := []struct { + h http.Handler + r string + k []string + v []string + m methodTyp + }{ + {m: mGET, r: "/articles/search", h: hStub1, k: []string{}, v: []string{}}, + {m: mGET, r: "/articlefun", h: hStub5, k: []string{}, v: []string{}}, + {m: mGET, r: "/articles/123", h: hStub, k: []string{"id"}, v: []string{"123"}}, + {m: mDELETE, r: "/articles/123mm", h: hStub8, k: []string{"slug"}, v: []string{"123mm"}}, + {m: mGET, r: "/articles/789:delete", h: hStub8, k: []string{"id"}, v: []string{"789"}}, + {m: mGET, r: "/articles/789!sup", h: hStub4, k: []string{"iidd"}, v: []string{"789"}}, + {m: mGET, r: "/articles/123:sync", h: hStub2, k: []string{"id", "op"}, v: []string{"123", "sync"}}, + {m: mGET, r: "/articles/456/posts/1", h: hStub6, k: []string{"id", "pid"}, v: []string{"456", "1"}}, + {m: mGET, r: "/articles/456/posts/09/04/1984/juice", h: hStub7, k: []string{"id", "month", "day", "year", "slug"}, v: []string{"456", "09", "04", "1984", "juice"}}, + {m: mGET, r: "/articles/456.json", h: hStub10, k: []string{"id"}, v: []string{"456"}}, + {m: mGET, r: "/articles/456/data.json", h: hStub11, k: []string{"id"}, v: []string{"456"}}, + + {m: mGET, r: "/articles/files/file.zip", h: hStub12, k: []string{"file", "ext"}, v: []string{"file", "zip"}}, + {m: mGET, r: "/articles/files/photos.tar.gz", h: hStub12, k: []string{"file", "ext"}, v: []string{"photos", "tar.gz"}}, + {m: mGET, r: "/articles/files/photos.tar.gz", h: hStub12, k: []string{"file", "ext"}, v: []string{"photos", "tar.gz"}}, + + {m: mPUT, r: "/articles/me", h: hStub13, k: []string{}, v: []string{}}, + {m: mGET, r: "/articles/me", h: hStub, k: []string{"id"}, v: []string{"me"}}, + {m: mGET, r: "/pages", h: nil, k: []string{}, v: []string{}}, + {m: mGET, r: "/pages/", h: hStub9, k: []string{"*"}, v: []string{""}}, + {m: mGET, r: "/pages/yes", h: hStub9, k: []string{"*"}, v: []string{"yes"}}, + + {m: mGET, r: "/users/1", h: hStub14, k: []string{"id"}, v: []string{"1"}}, + {m: mGET, r: "/users/", h: nil, k: []string{}, v: []string{}}, + {m: mGET, r: "/users/2/settings/password", h: hStub15, k: []string{"id", "key"}, v: []string{"2", "password"}}, + {m: mGET, r: "/users/2/settings/", h: hStub16, k: []string{"id", "*"}, v: []string{"2", ""}}, + } + + // log.Println("~~~~~~~~~") + // log.Println("~~~~~~~~~") + // debugPrintTree(0, 0, tr, 0) + // log.Println("~~~~~~~~~") + // log.Println("~~~~~~~~~") + + for i, tt := range tests { + rctx := &Context{} + + _, handlers, _ := tr.FindRoute(rctx, tt.m, tt.r) + + var handler http.Handler + if methodHandler, ok := handlers[tt.m]; ok { + handler = methodHandler.handler + } + + paramKeys := rctx.routeParams.Keys + paramValues := rctx.routeParams.Values + + if fmt.Sprintf("%v", tt.h) != fmt.Sprintf("%v", handler) { + t.Errorf("input [%d]: find '%s' expecting handler:%v , got:%v", i, tt.r, tt.h, handler) + } + if !stringSliceEqual(tt.k, paramKeys) { + t.Errorf("input [%d]: find '%s' expecting paramKeys:(%d)%v , got:(%d)%v", i, tt.r, len(tt.k), tt.k, len(paramKeys), paramKeys) + } + if !stringSliceEqual(tt.v, paramValues) { + t.Errorf("input [%d]: find '%s' expecting paramValues:(%d)%v , got:(%d)%v", i, tt.r, len(tt.v), tt.v, len(paramValues), paramValues) + } + } +} + +func TestTreeRegexp(t *testing.T) { + hStub1 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + hStub2 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + hStub3 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + hStub4 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + hStub5 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + hStub6 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + hStub7 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + + tr := &node{} + tr.InsertRoute(mGET, "/articles/{rid:^[0-9]{5,6}}", hStub7) + tr.InsertRoute(mGET, "/articles/{zid:^0[0-9]+}", hStub3) + tr.InsertRoute(mGET, "/articles/{name:^@[a-z]+}/posts", hStub4) + tr.InsertRoute(mGET, "/articles/{op:^[0-9]+}/run", hStub5) + tr.InsertRoute(mGET, "/articles/{id:^[0-9]+}", hStub1) + tr.InsertRoute(mGET, "/articles/{id:^[1-9]+}-{aux}", hStub6) + tr.InsertRoute(mGET, "/articles/{slug}", hStub2) + + // log.Println("~~~~~~~~~") + // log.Println("~~~~~~~~~") + // debugPrintTree(0, 0, tr, 0) + // log.Println("~~~~~~~~~") + // log.Println("~~~~~~~~~") + + tests := []struct { + r string // input request path + h http.Handler // output matched handler + k []string // output param keys + v []string // output param values + }{ + {r: "/articles", h: nil, k: []string{}, v: []string{}}, + {r: "/articles/12345", h: hStub7, k: []string{"rid"}, v: []string{"12345"}}, + {r: "/articles/123", h: hStub1, k: []string{"id"}, v: []string{"123"}}, + {r: "/articles/how-to-build-a-router", h: hStub2, k: []string{"slug"}, v: []string{"how-to-build-a-router"}}, + {r: "/articles/0456", h: hStub3, k: []string{"zid"}, v: []string{"0456"}}, + {r: "/articles/@pk/posts", h: hStub4, k: []string{"name"}, v: []string{"@pk"}}, + {r: "/articles/1/run", h: hStub5, k: []string{"op"}, v: []string{"1"}}, + {r: "/articles/1122", h: hStub1, k: []string{"id"}, v: []string{"1122"}}, + {r: "/articles/1122-yes", h: hStub6, k: []string{"id", "aux"}, v: []string{"1122", "yes"}}, + } + + for i, tt := range tests { + rctx := &Context{} + + _, handlers, _ := tr.FindRoute(rctx, mGET, tt.r) + + var handler http.Handler + if methodHandler, ok := handlers[mGET]; ok { + handler = methodHandler.handler + } + + paramKeys := rctx.routeParams.Keys + paramValues := rctx.routeParams.Values + + if fmt.Sprintf("%v", tt.h) != fmt.Sprintf("%v", handler) { + t.Errorf("input [%d]: find '%s' expecting handler:%v , got:%v", i, tt.r, tt.h, handler) + } + if !stringSliceEqual(tt.k, paramKeys) { + t.Errorf("input [%d]: find '%s' expecting paramKeys:(%d)%v , got:(%d)%v", i, tt.r, len(tt.k), tt.k, len(paramKeys), paramKeys) + } + if !stringSliceEqual(tt.v, paramValues) { + t.Errorf("input [%d]: find '%s' expecting paramValues:(%d)%v , got:(%d)%v", i, tt.r, len(tt.v), tt.v, len(paramValues), paramValues) + } + } +} + +func TestTreeRegexpRecursive(t *testing.T) { + hStub1 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + hStub2 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + + tr := &node{} + tr.InsertRoute(mGET, "/one/{firstId:[a-z0-9-]+}/{secondId:[a-z0-9-]+}/first", hStub1) + tr.InsertRoute(mGET, "/one/{firstId:[a-z0-9-_]+}/{secondId:[a-z0-9-_]+}/second", hStub2) + + // log.Println("~~~~~~~~~") + // log.Println("~~~~~~~~~") + // debugPrintTree(0, 0, tr, 0) + // log.Println("~~~~~~~~~") + // log.Println("~~~~~~~~~") + + tests := []struct { + r string // input request path + h http.Handler // output matched handler + k []string // output param keys + v []string // output param values + }{ + {r: "/one/hello/world/first", h: hStub1, k: []string{"firstId", "secondId"}, v: []string{"hello", "world"}}, + {r: "/one/hi_there/ok/second", h: hStub2, k: []string{"firstId", "secondId"}, v: []string{"hi_there", "ok"}}, + {r: "/one///first", h: nil, k: []string{}, v: []string{}}, + {r: "/one/hi/123/second", h: hStub2, k: []string{"firstId", "secondId"}, v: []string{"hi", "123"}}, + } + + for i, tt := range tests { + rctx := &Context{} + + _, handlers, _ := tr.FindRoute(rctx, mGET, tt.r) + + var handler http.Handler + if methodHandler, ok := handlers[mGET]; ok { + handler = methodHandler.handler + } + + paramKeys := rctx.routeParams.Keys + paramValues := rctx.routeParams.Values + + if fmt.Sprintf("%v", tt.h) != fmt.Sprintf("%v", handler) { + t.Errorf("input [%d]: find '%s' expecting handler:%v , got:%v", i, tt.r, tt.h, handler) + } + if !stringSliceEqual(tt.k, paramKeys) { + t.Errorf("input [%d]: find '%s' expecting paramKeys:(%d)%v , got:(%d)%v", i, tt.r, len(tt.k), tt.k, len(paramKeys), paramKeys) + } + if !stringSliceEqual(tt.v, paramValues) { + t.Errorf("input [%d]: find '%s' expecting paramValues:(%d)%v , got:(%d)%v", i, tt.r, len(tt.v), tt.v, len(paramValues), paramValues) + } + } +} + +func TestTreeRegexMatchWholeParam(t *testing.T) { + hStub1 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + + rctx := &Context{} + tr := &node{} + tr.InsertRoute(mGET, "/{id:[0-9]+}", hStub1) + tr.InsertRoute(mGET, "/{x:.+}/foo", hStub1) + tr.InsertRoute(mGET, "/{param:[0-9]*}/test", hStub1) + + tests := []struct { + expectedHandler http.Handler + url string + }{ + {url: "/13", expectedHandler: hStub1}, + {url: "/a13", expectedHandler: nil}, + {url: "/13.jpg", expectedHandler: nil}, + {url: "/a13.jpg", expectedHandler: nil}, + {url: "/a/foo", expectedHandler: hStub1}, + {url: "//foo", expectedHandler: nil}, + {url: "//test", expectedHandler: hStub1}, + } + + for _, tc := range tests { + _, _, handler := tr.FindRoute(rctx, mGET, tc.url) + if fmt.Sprintf("%v", tc.expectedHandler) != fmt.Sprintf("%v", handler) { + t.Errorf("url %v: expecting handler:%v , got:%v", tc.url, tc.expectedHandler, handler) + } + } +} + +func TestTreeFindPattern(t *testing.T) { + hStub1 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + hStub2 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + hStub3 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + + tr := &node{} + tr.InsertRoute(mGET, "/pages/*", hStub1) + tr.InsertRoute(mGET, "/articles/{id}/*", hStub2) + tr.InsertRoute(mGET, "/articles/{slug}/{uid}/*", hStub3) + + if tr.findPattern("/pages") != false { + t.Errorf("find /pages failed") + } + if tr.findPattern("/pages*") != false { + t.Errorf("find /pages* failed - should be nil") + } + if tr.findPattern("/pages/*") == false { + t.Errorf("find /pages/* failed") + } + if tr.findPattern("/articles/{id}/*") == false { + t.Errorf("find /articles/{id}/* failed") + } + if tr.findPattern("/articles/{something}/*") == false { + t.Errorf("find /articles/{something}/* failed") + } + if tr.findPattern("/articles/{slug}/{uid}/*") == false { + t.Errorf("find /articles/{slug}/{uid}/* failed") + } +} + +func debugPrintTree(parent int, i int, n *node, label byte) bool { + numEdges := 0 + for _, nds := range n.children { + numEdges += len(nds) + } + + // if n.handlers != nil { + // log.Printf("[node %d parent:%d] typ:%d prefix:%s label:%s tail:%s numEdges:%d isLeaf:%v handler:%v pat:%s keys:%v\n", i, parent, n.typ, n.prefix, string(label), string(n.tail), numEdges, n.isLeaf(), n.handlers, n.pattern, n.paramKeys) + // } else { + // log.Printf("[node %d parent:%d] typ:%d prefix:%s label:%s tail:%s numEdges:%d isLeaf:%v pat:%s keys:%v\n", i, parent, n.typ, n.prefix, string(label), string(n.tail), numEdges, n.isLeaf(), n.pattern, n.paramKeys) + // } + if n.endpoints != nil { + log.Printf("[node %d parent:%d] typ:%d prefix:%s label:%s tail:%s numEdges:%d isLeaf:%v handler:%v\n", i, parent, n.typ, n.prefix, string(label), string(n.tail), numEdges, n.isLeaf(), n.endpoints) + } else { + log.Printf("[node %d parent:%d] typ:%d prefix:%s label:%s tail:%s numEdges:%d isLeaf:%v\n", i, parent, n.typ, n.prefix, string(label), string(n.tail), numEdges, n.isLeaf()) + } + parent = i + for _, nds := range n.children { + for _, e := range nds { + i++ + if debugPrintTree(parent, i, e, e.label) { + return true + } + } + } + return false +} + +func stringSliceEqual(a, b []string) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if b[i] != a[i] { + return false + } + } + return true +} + +func BenchmarkTreeGet(b *testing.B) { + h1 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + h2 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + + tr := &node{} + tr.InsertRoute(mGET, "/", h1) + tr.InsertRoute(mGET, "/ping", h2) + tr.InsertRoute(mGET, "/pingall", h2) + tr.InsertRoute(mGET, "/ping/{id}", h2) + tr.InsertRoute(mGET, "/ping/{id}/woop", h2) + tr.InsertRoute(mGET, "/ping/{id}/{opt}", h2) + tr.InsertRoute(mGET, "/pinggggg", h2) + tr.InsertRoute(mGET, "/hello", h1) + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + mctx := &Context{} + tr.FindRoute(mctx, mGET, "/ping/123/456") + } +}