Browse Source

[GPT-95] Update go version, add tools for verification and testing (#718)

Fixes # https://gorilla-web-toolkit.atlassian.net/browse/GPT-95

**Summary of Changes**
Added `.github/workflows/test.yml` that runs golangci-lint & go tests on
any PR created or each push to main branch.

---------

Signed-off-by: Corey Daley <cdaley@redhat.com>
Signed-off-by: Apoorva Jagtap <35304110+apoorvajagtap@users.noreply.github.com>
Co-authored-by: Corey Daley <cdaley@redhat.com>
pull/719/head
Apoorva Jagtap 1 year ago committed by GitHub
parent
commit
1fa2ee87bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 70
      .circleci/config.yml
  2. 20
      .editorconfig
  3. 20
      .github/workflows/issues.yml
  4. 55
      .github/workflows/test.yml
  5. 1
      .gitignore
  6. 34
      Makefile
  7. 8
      README.md
  8. 2
      go.mod
  9. 95
      middleware_test.go
  10. 14
      mux_httpserver_test.go
  11. 20
      mux_test.go
  12. 2
      regexp.go
  13. 4
      regexp_test.go
  14. 80
      route.go

70
.circleci/config.yml

@ -1,70 +0,0 @@
version: 2.1
jobs:
"test":
parameters:
version:
type: string
default: "latest"
golint:
type: boolean
default: true
modules:
type: boolean
default: true
goproxy:
type: string
default: ""
docker:
- image: "circleci/golang:<< parameters.version >>"
working_directory: /go/src/github.com/gorilla/mux
environment:
GO111MODULE: "on"
GOPROXY: "<< parameters.goproxy >>"
steps:
- checkout
- run:
name: "Print the Go version"
command: >
go version
- run:
name: "Fetch dependencies"
command: >
if [[ << parameters.modules >> = true ]]; then
go mod download
export GO111MODULE=on
else
go get -v ./...
fi
# Only run gofmt, vet & lint against the latest Go version
- run:
name: "Run golint"
command: >
if [ << parameters.version >> = "latest" ] && [ << parameters.golint >> = true ]; then
go get -u golang.org/x/lint/golint
golint ./...
fi
- run:
name: "Run gofmt"
command: >
if [[ << parameters.version >> = "latest" ]]; then
diff -u <(echo -n) <(gofmt -d -e .)
fi
- run:
name: "Run go vet"
command: >
if [[ << parameters.version >> = "latest" ]]; then
go vet -v ./...
fi
- run:
name: "Run go test (+ race detector)"
command: >
go test -v -race ./...
workflows:
tests:
jobs:
- test:
matrix:
parameters:
version: ["latest", "1.15", "1.14", "1.13", "1.12", "1.11"]

20
.editorconfig

@ -0,0 +1,20 @@
; 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

20
.github/workflows/issues.yml

@ -0,0 +1,20 @@
# Add issues or pull-requests created to the project.
name: Add issue or pull request to Project
on:
issues:
types:
- opened
pull_request:
types:
- opened
jobs:
add-to-project:
runs-on: ubuntu-latest
steps:
- name: Add issue to project
uses: actions/add-to-project@v0.5.0
with:
project-url: https://github.com/orgs/gorilla/projects/4
github-token: ${{ secrets.ADD_TO_PROJECT_TOKEN }}

55
.github/workflows/test.yml

@ -0,0 +1,55 @@
name: CI
on:
push:
branches:
- main
pull_request:
branches:
- main
permissions:
contents: read
jobs:
verify-and-test:
strategy:
matrix:
go: ['1.19','1.20']
os: [ubuntu-latest, macos-latest, windows-latest]
fail-fast: true
runs-on: ${{ matrix.os }}
steps:
- name: Checkout Code
uses: actions/checkout@v3
- name: Setup Go ${{ matrix.go }}
uses: actions/setup-go@v4
with:
go-version: ${{ matrix.go }}
cache: false
- name: Run GolangCI-Lint
uses: golangci/golangci-lint-action@v3
with:
version: v1.53
args: --timeout=5m
- name: Run GoSec
if: matrix.os == 'ubuntu-latest'
uses: securego/gosec@master
with:
args: ./...
- name: Run GoVulnCheck
uses: golang/govulncheck-action@v1
with:
go-version-input: ${{ matrix.go }}
go-package: ./...
- name: Run Tests
run: go test -race -cover -coverprofile=coverage -covermode=atomic -v ./...
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
files: ./coverage

1
.gitignore vendored

@ -0,0 +1 @@
coverage.coverprofile

34
Makefile

@ -0,0 +1,34 @@
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 ./...

8
README.md

@ -1,8 +1,10 @@
# gorilla/mux
[![GoDoc](https://godoc.org/github.com/gorilla/mux?status.svg)](https://godoc.org/github.com/gorilla/mux)
[![CircleCI](https://circleci.com/gh/gorilla/mux.svg?style=svg)](https://circleci.com/gh/gorilla/mux)
[![Sourcegraph](https://sourcegraph.com/github.com/gorilla/mux/-/badge.svg)](https://sourcegraph.com/github.com/gorilla/mux?badge)
![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://cloud-cdn.questionable.services/gorilla-icon-64.png)

2
go.mod

@ -1,3 +1,3 @@
module github.com/gorilla/mux
go 1.12
go 1.19

95
middleware_test.go

@ -158,7 +158,10 @@ func TestMiddlewareExecution(t *testing.T) {
router := NewRouter()
router.HandleFunc("/", func(w http.ResponseWriter, e *http.Request) {
w.Write(handlerStr)
_, 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) {
@ -178,7 +181,10 @@ func TestMiddlewareExecution(t *testing.T) {
router.Use(func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write(mwStr)
_, err := w.Write(mwStr)
if err != nil {
t.Fatalf("Failed writing HTTP response: %v", err)
}
h.ServeHTTP(w, r)
})
})
@ -196,11 +202,17 @@ func TestMiddlewareNotFound(t *testing.T) {
router := NewRouter()
router.HandleFunc("/", func(w http.ResponseWriter, e *http.Request) {
w.Write(handlerStr)
_, 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) {
w.Write(mwStr)
_, err := w.Write(mwStr)
if err != nil {
t.Fatalf("Failed writing HTTP response: %v", err)
}
h.ServeHTTP(w, r)
})
})
@ -221,7 +233,10 @@ func TestMiddlewareNotFound(t *testing.T) {
req := newRequest("GET", "/notfound")
router.NotFoundHandler = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
rw.Write([]byte("Custom 404 handler"))
_, err := rw.Write([]byte("Custom 404 handler"))
if err != nil {
t.Fatalf("Failed writing HTTP response: %v", err)
}
})
router.ServeHTTP(rw, req)
@ -237,12 +252,18 @@ func TestMiddlewareMethodMismatch(t *testing.T) {
router := NewRouter()
router.HandleFunc("/", func(w http.ResponseWriter, e *http.Request) {
w.Write(handlerStr)
_, 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) {
w.Write(mwStr)
_, err := w.Write(mwStr)
if err != nil {
t.Fatalf("Failed writing HTTP response: %v", err)
}
h.ServeHTTP(w, r)
})
})
@ -262,7 +283,10 @@ func TestMiddlewareMethodMismatch(t *testing.T) {
req := newRequest("POST", "/")
router.MethodNotAllowedHandler = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
rw.Write([]byte("Method not allowed"))
_, err := rw.Write([]byte("Method not allowed"))
if err != nil {
t.Fatalf("Failed writing HTTP response: %v", err)
}
})
router.ServeHTTP(rw, req)
@ -278,17 +302,26 @@ func TestMiddlewareNotFoundSubrouter(t *testing.T) {
router := NewRouter()
router.HandleFunc("/", func(w http.ResponseWriter, e *http.Request) {
w.Write(handlerStr)
_, 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) {
w.Write(handlerStr)
_, 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) {
w.Write(mwStr)
_, err := w.Write(mwStr)
if err != nil {
t.Fatalf("Failed writing HTTP response: %v", err)
}
h.ServeHTTP(w, r)
})
})
@ -308,7 +341,10 @@ func TestMiddlewareNotFoundSubrouter(t *testing.T) {
req := newRequest("GET", "/sub/notfound")
subrouter.NotFoundHandler = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
rw.Write([]byte("Custom 404 handler"))
_, err := rw.Write([]byte("Custom 404 handler"))
if err != nil {
t.Fatalf("Failed writing HTTP response: %v", err)
}
})
router.ServeHTTP(rw, req)
@ -324,17 +360,26 @@ func TestMiddlewareMethodMismatchSubrouter(t *testing.T) {
router := NewRouter()
router.HandleFunc("/", func(w http.ResponseWriter, e *http.Request) {
w.Write(handlerStr)
_, 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) {
w.Write(handlerStr)
_, 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) {
w.Write(mwStr)
_, err := w.Write(mwStr)
if err != nil {
t.Fatalf("Failed writing HTTP response: %v", err)
}
h.ServeHTTP(w, r)
})
})
@ -354,7 +399,10 @@ func TestMiddlewareMethodMismatchSubrouter(t *testing.T) {
req := newRequest("POST", "/sub/")
router.MethodNotAllowedHandler = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
rw.Write([]byte("Method not allowed"))
_, err := rw.Write([]byte("Method not allowed"))
if err != nil {
t.Fatalf("Failed writing HTTP response: %v", err)
}
})
router.ServeHTTP(rw, req)
@ -508,7 +556,10 @@ func TestMiddlewareOnMultiSubrouter(t *testing.T) {
secondSubRouter := router.PathPrefix("/").Subrouter()
router.NotFoundHandler = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
rw.Write([]byte(notFound))
_, 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) {
@ -521,14 +572,20 @@ func TestMiddlewareOnMultiSubrouter(t *testing.T) {
firstSubRouter.Use(func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(first))
_, 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) {
w.Write([]byte(second))
_, err := w.Write([]byte(second))
if err != nil {
t.Fatalf("Failed writing HTTP response: %v", err)
}
h.ServeHTTP(w, r)
})
})

14
mux_httpserver_test.go

@ -5,7 +5,7 @@ package mux
import (
"bytes"
"io/ioutil"
"io"
"net/http"
"net/http/httptest"
"testing"
@ -14,10 +14,16 @@ import (
func TestSchemeMatchers(t *testing.T) {
router := NewRouter()
router.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
rw.Write([]byte("hello http world"))
_, 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) {
rw.Write([]byte("hello https world"))
_, 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) {
@ -28,7 +34,7 @@ func TestSchemeMatchers(t *testing.T) {
if resp.StatusCode != 200 {
t.Fatalf("expected a status code of 200, got %v", resp.StatusCode)
}
body, err := ioutil.ReadAll(resp.Body)
body, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("unexpected error reading body: %v", err)
}

20
mux_test.go

@ -10,7 +10,8 @@ import (
"context"
"errors"
"fmt"
"io/ioutil"
"io"
"log"
"net/http"
"net/http/httptest"
"net/url"
@ -2136,7 +2137,10 @@ type methodsSubrouterTest struct {
// methodHandler writes the method string in response.
func methodHandler(method string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(method))
_, err := w.Write([]byte(method))
if err != nil {
log.Printf("Failed writing HTTP response: %v", err)
}
}
}
@ -2778,7 +2782,7 @@ func TestSubrouterCustomMethodNotAllowed(t *testing.T) {
tt.Errorf("Expected status code 405 (got %d)", w.Code)
}
b, err := ioutil.ReadAll(w.Body)
b, err := io.ReadAll(w.Body)
if err != nil {
tt.Errorf("failed to read body: %v", err)
}
@ -2859,7 +2863,10 @@ func stringMapEqual(m1, m2 map[string]string) bool {
// http.ResponseWriter.
func stringHandler(s string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(s))
_, err := w.Write([]byte(s))
if err != nil {
log.Printf("Failed writing HTTP response: %v", err)
}
}
}
@ -2892,7 +2899,10 @@ func newRequest(method, url string) *http.Request {
// Simulate writing to wire
var buff bytes.Buffer
req.Write(&buff)
err = req.Write(&buff)
if err != nil {
log.Printf("Failed writing HTTP request: %v", err)
}
ioreader := bufio.NewReader(&buff)
// Parse request off of 'wire'

2
regexp.go

@ -195,7 +195,7 @@ func (r *routeRegexp) Match(req *http.Request, match *RouteMatch) bool {
// 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), len(r.varsN))
urlValues := make([]interface{}, len(r.varsN))
for k, v := range r.varsN {
value, ok := values[v]
if !ok {

4
regexp_test.go

@ -54,7 +54,7 @@ func Benchmark_findQueryKey(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
for key, _ := range all {
for key := range all {
_, _ = findFirstQueryKey(query, key)
}
}
@ -79,7 +79,7 @@ func Benchmark_findQueryKeyGoLib(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
for key, _ := range all {
for key := range all {
v := u.Query()[key]
if len(v) > 0 {
_ = v[0]

80
route.go

@ -64,7 +64,7 @@ func (r *Route) Match(req *http.Request, match *RouteMatch) bool {
match.MatchErr = nil
}
matchErr = nil
matchErr = nil // nolint:ineffassign
return false
}
}
@ -230,9 +230,9 @@ func (m headerMatcher) Match(r *http.Request, match *RouteMatch) bool {
// Headers adds a matcher for request header values.
// It accepts a sequence of key/value pairs to be matched. For example:
//
// r := mux.NewRouter()
// r.Headers("Content-Type", "application/json",
// "X-Requested-With", "XMLHttpRequest")
// r := mux.NewRouter()
// r.Headers("Content-Type", "application/json",
// "X-Requested-With", "XMLHttpRequest")
//
// The above route will only match if both request header values match.
// If the value is an empty string, it will match any value if the key is set.
@ -255,9 +255,9 @@ func (m headerRegexMatcher) Match(r *http.Request, match *RouteMatch) bool {
// HeadersRegexp accepts a sequence of key/value pairs, where the value has regex
// support. For example:
//
// r := mux.NewRouter()
// r.HeadersRegexp("Content-Type", "application/(text|json)",
// "X-Requested-With", "XMLHttpRequest")
// r := mux.NewRouter()
// r.HeadersRegexp("Content-Type", "application/(text|json)",
// "X-Requested-With", "XMLHttpRequest")
//
// The above route will only match if both the request header matches both regular expressions.
// If the value is an empty string, it will match any value if the key is set.
@ -283,10 +283,10 @@ func (r *Route) HeadersRegexp(pairs ...string) *Route {
//
// For example:
//
// r := mux.NewRouter()
// r.Host("www.example.com")
// r.Host("{subdomain}.domain.com")
// r.Host("{subdomain:[a-z]+}.domain.com")
// r := mux.NewRouter()
// r.Host("www.example.com")
// r.Host("{subdomain}.domain.com")
// r.Host("{subdomain:[a-z]+}.domain.com")
//
// Variable names must be unique in a given route. They can be retrieved
// calling mux.Vars(request).
@ -342,11 +342,11 @@ func (r *Route) Methods(methods ...string) *Route {
//
// For example:
//
// r := mux.NewRouter()
// r.Path("/products/").Handler(ProductsHandler)
// r.Path("/products/{key}").Handler(ProductsHandler)
// r.Path("/articles/{category}/{id:[0-9]+}").
// Handler(ArticleHandler)
// r := mux.NewRouter()
// r.Path("/products/").Handler(ProductsHandler)
// r.Path("/products/{key}").Handler(ProductsHandler)
// r.Path("/articles/{category}/{id:[0-9]+}").
// Handler(ArticleHandler)
//
// Variable names must be unique in a given route. They can be retrieved
// calling mux.Vars(request).
@ -377,8 +377,8 @@ func (r *Route) PathPrefix(tpl string) *Route {
// It accepts a sequence of key/value pairs. Values may define variables.
// For example:
//
// r := mux.NewRouter()
// r.Queries("foo", "bar", "id", "{id:[0-9]+}")
// r := mux.NewRouter()
// r.Queries("foo", "bar", "id", "{id:[0-9]+}")
//
// The above route will only match if the URL contains the defined queries
// values, e.g.: ?foo=bar&id=42.
@ -473,11 +473,11 @@ func (r *Route) BuildVarsFunc(f BuildVarsFunc) *Route {
//
// It will test the inner routes only if the parent route matched. For example:
//
// r := mux.NewRouter()
// s := r.Host("www.example.com").Subrouter()
// s.HandleFunc("/products/", ProductsHandler)
// s.HandleFunc("/products/{key}", ProductHandler)
// s.HandleFunc("/articles/{category}/{id:[0-9]+}"), ArticleHandler)
// r := mux.NewRouter()
// s := r.Host("www.example.com").Subrouter()
// s.HandleFunc("/products/", ProductsHandler)
// s.HandleFunc("/products/{key}", ProductHandler)
// s.HandleFunc("/articles/{category}/{id:[0-9]+}"), ArticleHandler)
//
// Here, the routes registered in the subrouter won't be tested if the host
// doesn't match.
@ -497,36 +497,36 @@ func (r *Route) Subrouter() *Router {
// 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")
// 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")
// url, err := r.Get("article").URL("category", "technology", "id", "42")
//
// ...which will return an url.URL with the following path:
//
// "/articles/technology/42"
// "/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")
// 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")
// // 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()
// url, err := r.Host("example.com")
// .Schemes("https", "http").URL()
// // url.String() will be "https://example.com"
// r := mux.NewRouter()
// 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.

Loading…
Cancel
Save