Merge branch 'master' into master

This commit is contained in:
ffhelicopter 2020-05-15 21:45:10 +08:00 committed by GitHub
commit 8da51b6c82
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
62 changed files with 2754 additions and 1926 deletions

View File

@ -3,11 +3,47 @@
- Please provide source code and commit sha if you found a bug. - Please provide source code and commit sha if you found a bug.
- Review existing issues and provide feedback or react to them. - Review existing issues and provide feedback or react to them.
## Description
<!-- Description of a problem -->
## How to reproduce
<!-- The smallest possible code example to show the problem that can be compiled, like -->
```
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
g := gin.Default()
g.GET("/hello/:name", func(c *gin.Context) {
c.String(200, "Hello %s", c.Param("name"))
})
g.Run(":9000")
}
```
## Expectations
<!-- Your expectation result of 'curl' command, like -->
```
$ curl http://localhost:8201/hello/world
Hello world
```
## Actual result
<!-- Actual result showing the problem -->
```
$ curl -i http://localhost:8201/hello/world
<YOUR RESULT>
```
## Environment
- go version: - go version:
- gin version (or commit ref): - gin version (or commit ref):
- operating system: - operating system:
## Description
## Screenshots

View File

@ -1,7 +1,7 @@
- With pull requests: - With pull requests:
- Open your pull request against `master` - Open your pull request against `master`
- Your pull request should have no more than two commits, if not you should squash them. - Your pull request should have no more than two commits, if not you should squash them.
- It should pass all tests in the available continuous integrations systems such as TravisCI. - It should pass all tests in the available continuous integration systems such as TravisCI.
- You should add/modify tests to cover your proposed code changes. - You should add/modify tests to cover your proposed code changes.
- If your pull request contains a new feature, please document it on the README. - If your pull request contains a new feature, please document it on the README.

View File

@ -3,14 +3,19 @@ language: go
matrix: matrix:
fast_finish: true fast_finish: true
include: include:
- go: 1.10.x
- go: 1.11.x - go: 1.11.x
env: GO111MODULE=on env: GO111MODULE=on
- go: 1.12.x - go: 1.12.x
env: GO111MODULE=on env: GO111MODULE=on
- go: 1.13.x - go: 1.13.x
- go: 1.13.x
env:
- TESTTAGS=nomsgpack
- go: 1.14.x
- go: 1.14.x
env:
- TESTTAGS=nomsgpack
- go: master - go: master
env: GO111MODULE=on
git: git:
depth: 10 depth: 10
@ -19,7 +24,7 @@ before_install:
- if [[ "${GO111MODULE}" = "on" ]]; then mkdir "${HOME}/go"; export GOPATH="${HOME}/go"; fi - if [[ "${GO111MODULE}" = "on" ]]; then mkdir "${HOME}/go"; export GOPATH="${HOME}/go"; fi
install: install:
- if [[ "${GO111MODULE}" = "on" ]]; then go mod download; else make install; fi - if [[ "${GO111MODULE}" = "on" ]]; then go mod download; fi
- if [[ "${GO111MODULE}" = "on" ]]; then export PATH="${GOPATH}/bin:${GOROOT}/bin:${PATH}"; fi - if [[ "${GO111MODULE}" = "on" ]]; then export PATH="${GOPATH}/bin:${GOROOT}/bin:${PATH}"; fi
- if [[ "${GO111MODULE}" = "on" ]]; then make tools; fi - if [[ "${GO111MODULE}" = "on" ]]; then make tools; fi

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,109 @@
# Gin ChangeLog
### Gin 1.4.0 ## Gin v1.6.3
### ENHANCEMENTS
* Improve performance: Change `*sync.RWMutex` to `sync.RWMutex` in context. [#2351](https://github.com/gin-gonic/gin/pull/2351)
## Gin v1.6.2
### BUFIXES
* fix missing initial sync.RWMutex [#2305](https://github.com/gin-gonic/gin/pull/2305)
### ENHANCEMENTS
* Add set samesite in cookie. [#2306](https://github.com/gin-gonic/gin/pull/2306)
## Gin v1.6.1
### BUFIXES
* Revert "fix accept incoming network connections" [#2294](https://github.com/gin-gonic/gin/pull/2294)
## Gin v1.6.0
### BREAKING
* chore(performance): Improve performance for adding RemoveExtraSlash flag [#2159](https://github.com/gin-gonic/gin/pull/2159)
* drop support govendor [#2148](https://github.com/gin-gonic/gin/pull/2148)
* Added support for SameSite cookie flag [#1615](https://github.com/gin-gonic/gin/pull/1615)
### FEATURES
* add yaml negotitation [#2220](https://github.com/gin-gonic/gin/pull/2220)
* FileFromFS [#2112](https://github.com/gin-gonic/gin/pull/2112)
### BUGFIXES
* Unix Socket Handling [#2280](https://github.com/gin-gonic/gin/pull/2280)
* Use json marshall in context json to fix breaking new line issue. Fixes #2209 [#2228](https://github.com/gin-gonic/gin/pull/2228)
* fix accept incoming network connections [#2216](https://github.com/gin-gonic/gin/pull/2216)
* Fixed a bug in the calculation of the maximum number of parameters [#2166](https://github.com/gin-gonic/gin/pull/2166)
* [FIX] allow empty headers on DataFromReader [#2121](https://github.com/gin-gonic/gin/pull/2121)
* Add mutex for protect Context.Keys map [#1391](https://github.com/gin-gonic/gin/pull/1391)
### ENHANCEMENTS
* Add mitigation for log injection [#2277](https://github.com/gin-gonic/gin/pull/2277)
* tree: range over nodes values [#2229](https://github.com/gin-gonic/gin/pull/2229)
* tree: remove duplicate assignment [#2222](https://github.com/gin-gonic/gin/pull/2222)
* chore: upgrade go-isatty and json-iterator/go [#2215](https://github.com/gin-gonic/gin/pull/2215)
* path: sync code with httprouter [#2212](https://github.com/gin-gonic/gin/pull/2212)
* Use zero-copy approach to convert types between string and byte slice [#2206](https://github.com/gin-gonic/gin/pull/2206)
* Reuse bytes when cleaning the URL paths [#2179](https://github.com/gin-gonic/gin/pull/2179)
* tree: remove one else statement [#2177](https://github.com/gin-gonic/gin/pull/2177)
* tree: sync httprouter update (#2173) (#2172) [#2171](https://github.com/gin-gonic/gin/pull/2171)
* tree: sync part httprouter codes and reduce if/else [#2163](https://github.com/gin-gonic/gin/pull/2163)
* use http method constant [#2155](https://github.com/gin-gonic/gin/pull/2155)
* upgrade go-validator to v10 [#2149](https://github.com/gin-gonic/gin/pull/2149)
* Refactor redirect request in gin.go [#1970](https://github.com/gin-gonic/gin/pull/1970)
* Add build tag nomsgpack [#1852](https://github.com/gin-gonic/gin/pull/1852)
### DOCS
* docs(path): improve comments [#2223](https://github.com/gin-gonic/gin/pull/2223)
* Renew README to fit the modification of SetCookie method [#2217](https://github.com/gin-gonic/gin/pull/2217)
* Fix spelling [#2202](https://github.com/gin-gonic/gin/pull/2202)
* Remove broken link from README. [#2198](https://github.com/gin-gonic/gin/pull/2198)
* Update docs on Context.Done(), Context.Deadline() and Context.Err() [#2196](https://github.com/gin-gonic/gin/pull/2196)
* Update validator to v10 [#2190](https://github.com/gin-gonic/gin/pull/2190)
* upgrade go-validator to v10 for README [#2189](https://github.com/gin-gonic/gin/pull/2189)
* Update to currently output [#2188](https://github.com/gin-gonic/gin/pull/2188)
* Fix "Custom Validators" example [#2186](https://github.com/gin-gonic/gin/pull/2186)
* Add project to README [#2165](https://github.com/gin-gonic/gin/pull/2165)
* docs(benchmarks): for gin v1.5 [#2153](https://github.com/gin-gonic/gin/pull/2153)
* Changed wording for clarity in README.md [#2122](https://github.com/gin-gonic/gin/pull/2122)
### MISC
* ci support go1.14 [#2262](https://github.com/gin-gonic/gin/pull/2262)
* chore: upgrade depend version [#2231](https://github.com/gin-gonic/gin/pull/2231)
* Drop support go1.10 [#2147](https://github.com/gin-gonic/gin/pull/2147)
* fix comment in `mode.go` [#2129](https://github.com/gin-gonic/gin/pull/2129)
## Gin v1.5.0
- [FIX] Use DefaultWriter and DefaultErrorWriter for debug messages [#1891](https://github.com/gin-gonic/gin/pull/1891)
- [NEW] Now you can parse the inline lowercase start structure [#1893](https://github.com/gin-gonic/gin/pull/1893)
- [FIX] Some code improvements [#1909](https://github.com/gin-gonic/gin/pull/1909)
- [FIX] Use encode replace json marshal increase json encoder speed [#1546](https://github.com/gin-gonic/gin/pull/1546)
- [NEW] Hold matched route full path in the Context [#1826](https://github.com/gin-gonic/gin/pull/1826)
- [FIX] Fix context.Params race condition on Copy() [#1841](https://github.com/gin-gonic/gin/pull/1841)
- [NEW] Add context param query cache [#1450](https://github.com/gin-gonic/gin/pull/1450)
- [FIX] Improve GetQueryMap performance [#1918](https://github.com/gin-gonic/gin/pull/1918)
- [FIX] Improve get post data [#1920](https://github.com/gin-gonic/gin/pull/1920)
- [FIX] Use context instead of x/net/context [#1922](https://github.com/gin-gonic/gin/pull/1922)
- [FIX] Attempt to fix PostForm cache bug [#1931](https://github.com/gin-gonic/gin/pull/1931)
- [NEW] Add support of multipart multi files [#1949](https://github.com/gin-gonic/gin/pull/1949)
- [NEW] Support bind http header param [#1957](https://github.com/gin-gonic/gin/pull/1957)
- [FIX] Drop support for go1.8 and go1.9 [#1933](https://github.com/gin-gonic/gin/pull/1933)
- [FIX] Bugfix for the FullPath feature [#1919](https://github.com/gin-gonic/gin/pull/1919)
- [FIX] Gin1.5 bytes.Buffer to strings.Builder [#1939](https://github.com/gin-gonic/gin/pull/1939)
- [FIX] Upgrade github.com/ugorji/go/codec [#1969](https://github.com/gin-gonic/gin/pull/1969)
- [NEW] Support bind unix time [#1980](https://github.com/gin-gonic/gin/pull/1980)
- [FIX] Simplify code [#2004](https://github.com/gin-gonic/gin/pull/2004)
- [NEW] Support negative Content-Length in DataFromReader [#1981](https://github.com/gin-gonic/gin/pull/1981)
- [FIX] Identify terminal on a RISC-V architecture for auto-colored logs [#2019](https://github.com/gin-gonic/gin/pull/2019)
- [BREAKING] `Context.JSONP()` now expects a semicolon (`;`) at the end [#2007](https://github.com/gin-gonic/gin/pull/2007)
- [BREAKING] Upgrade default `binding.Validator` to v9 (see [its changelog](https://github.com/go-playground/validator/releases/tag/v9.0.0)) [#1015](https://github.com/gin-gonic/gin/pull/1015)
- [NEW] Add `DisallowUnknownFields()` in `Context.BindJSON()` [#2028](https://github.com/gin-gonic/gin/pull/2028)
- [NEW] Use specific `net.Listener` with `Engine.RunListener()` [#2023](https://github.com/gin-gonic/gin/pull/2023)
- [FIX] Fix some typo [#2079](https://github.com/gin-gonic/gin/pull/2079) [#2080](https://github.com/gin-gonic/gin/pull/2080)
- [FIX] Relocate binding body tests [#2086](https://github.com/gin-gonic/gin/pull/2086)
- [FIX] Use Writer in Context.Status [#1606](https://github.com/gin-gonic/gin/pull/1606)
- [FIX] `Engine.RunUnix()` now returns the error if it can't change the file mode [#2093](https://github.com/gin-gonic/gin/pull/2093)
- [FIX] `RouterGroup.StaticFS()` leaked files. Now it closes them. [#2118](https://github.com/gin-gonic/gin/pull/2118)
- [FIX] `Context.Request.FormFile` leaked file. Now it closes it. [#2114](https://github.com/gin-gonic/gin/pull/2114)
- [FIX] Ignore walking on `form:"-"` mapping [#1943](https://github.com/gin-gonic/gin/pull/1943)
### Gin v1.4.0
- [NEW] Support for [Go Modules](https://github.com/golang/go/wiki/Modules) [#1569](https://github.com/gin-gonic/gin/pull/1569) - [NEW] Support for [Go Modules](https://github.com/golang/go/wiki/Modules) [#1569](https://github.com/gin-gonic/gin/pull/1569)
- [NEW] Refactor of form mapping multipart request [#1829](https://github.com/gin-gonic/gin/pull/1829) - [NEW] Refactor of form mapping multipart request [#1829](https://github.com/gin-gonic/gin/pull/1829)
@ -15,7 +119,7 @@
- [NEW] Refactor form mappings [#1749](https://github.com/gin-gonic/gin/pull/1749) - [NEW] Refactor form mappings [#1749](https://github.com/gin-gonic/gin/pull/1749)
- [NEW] Added flag to context.Stream indicates if client disconnected in middle of stream [#1252](https://github.com/gin-gonic/gin/pull/1252) - [NEW] Added flag to context.Stream indicates if client disconnected in middle of stream [#1252](https://github.com/gin-gonic/gin/pull/1252)
- [FIX] Moved [examples](https://github.com/gin-gonic/examples) to stand alone Repo [#1775](https://github.com/gin-gonic/gin/pull/1775) - [FIX] Moved [examples](https://github.com/gin-gonic/examples) to stand alone Repo [#1775](https://github.com/gin-gonic/gin/pull/1775)
- [NEW] Extend context.File to allow for the content-dispositon attachments via a new method context.Attachment [#1260](https://github.com/gin-gonic/gin/pull/1260) - [NEW] Extend context.File to allow for the content-disposition attachments via a new method context.Attachment [#1260](https://github.com/gin-gonic/gin/pull/1260)
- [FIX] Support HTTP content negotiation wildcards [#1112](https://github.com/gin-gonic/gin/pull/1112) - [FIX] Support HTTP content negotiation wildcards [#1112](https://github.com/gin-gonic/gin/pull/1112)
- [NEW] Add prefix from X-Forwarded-Prefix in redirectTrailingSlash [#1238](https://github.com/gin-gonic/gin/pull/1238) - [NEW] Add prefix from X-Forwarded-Prefix in redirectTrailingSlash [#1238](https://github.com/gin-gonic/gin/pull/1238)
- [FIX] context.Copy() race condition [#1020](https://github.com/gin-gonic/gin/pull/1020) - [FIX] context.Copy() race condition [#1020](https://github.com/gin-gonic/gin/pull/1020)
@ -56,7 +160,7 @@
- [NEW] Upgrade dependency libraries [#1491](https://github.com/gin-gonic/gin/pull/1491) - [NEW] Upgrade dependency libraries [#1491](https://github.com/gin-gonic/gin/pull/1491)
### Gin 1.3.0 ## Gin v1.3.0
- [NEW] Add [`func (*Context) QueryMap`](https://godoc.org/github.com/gin-gonic/gin#Context.QueryMap), [`func (*Context) GetQueryMap`](https://godoc.org/github.com/gin-gonic/gin#Context.GetQueryMap), [`func (*Context) PostFormMap`](https://godoc.org/github.com/gin-gonic/gin#Context.PostFormMap) and [`func (*Context) GetPostFormMap`](https://godoc.org/github.com/gin-gonic/gin#Context.GetPostFormMap) to support `type map[string]string` as query string or form parameters, see [#1383](https://github.com/gin-gonic/gin/pull/1383) - [NEW] Add [`func (*Context) QueryMap`](https://godoc.org/github.com/gin-gonic/gin#Context.QueryMap), [`func (*Context) GetQueryMap`](https://godoc.org/github.com/gin-gonic/gin#Context.GetQueryMap), [`func (*Context) PostFormMap`](https://godoc.org/github.com/gin-gonic/gin#Context.PostFormMap) and [`func (*Context) GetPostFormMap`](https://godoc.org/github.com/gin-gonic/gin#Context.GetPostFormMap) to support `type map[string]string` as query string or form parameters, see [#1383](https://github.com/gin-gonic/gin/pull/1383)
- [NEW] Add [`func (*Context) AsciiJSON`](https://godoc.org/github.com/gin-gonic/gin#Context.AsciiJSON), see [#1358](https://github.com/gin-gonic/gin/pull/1358) - [NEW] Add [`func (*Context) AsciiJSON`](https://godoc.org/github.com/gin-gonic/gin#Context.AsciiJSON), see [#1358](https://github.com/gin-gonic/gin/pull/1358)
@ -78,7 +182,7 @@
- [FIX] Gin Mode `""` when calling [`func Mode`](https://godoc.org/github.com/gin-gonic/gin#Mode) now returns `const DebugMode`, see [#1250](https://github.com/gin-gonic/gin/pull/1250) - [FIX] Gin Mode `""` when calling [`func Mode`](https://godoc.org/github.com/gin-gonic/gin#Mode) now returns `const DebugMode`, see [#1250](https://github.com/gin-gonic/gin/pull/1250)
- [FIX] `Flush()` now doesn't overwrite `responseWriter` status code, see [#1460](https://github.com/gin-gonic/gin/pull/1460) - [FIX] `Flush()` now doesn't overwrite `responseWriter` status code, see [#1460](https://github.com/gin-gonic/gin/pull/1460)
### Gin 1.2.0 ## Gin 1.2.0
- [NEW] Switch from godeps to govendor - [NEW] Switch from godeps to govendor
- [NEW] Add support for Let's Encrypt via gin-gonic/autotls - [NEW] Add support for Let's Encrypt via gin-gonic/autotls
@ -101,15 +205,15 @@
- [FIX] Use X-Forwarded-For before X-Real-Ip - [FIX] Use X-Forwarded-For before X-Real-Ip
- [FIX] time.Time binding (#904) - [FIX] time.Time binding (#904)
### Gin 1.1.4 ## Gin 1.1.4
- [NEW] Support google appengine for IsTerminal func - [NEW] Support google appengine for IsTerminal func
### Gin 1.1.3 ## Gin 1.1.3
- [FIX] Reverted Logger: skip ANSI color commands - [FIX] Reverted Logger: skip ANSI color commands
### Gin 1.1 ## Gin 1.1
- [NEW] Implement QueryArray and PostArray methods - [NEW] Implement QueryArray and PostArray methods
- [NEW] Refactor GetQuery and GetPostForm - [NEW] Refactor GetQuery and GetPostForm
@ -119,7 +223,7 @@
- [FIX] Changed imports to gopkg instead of github in README (#733) - [FIX] Changed imports to gopkg instead of github in README (#733)
- [FIX] Logger: skip ANSI color commands if output is not a tty - [FIX] Logger: skip ANSI color commands if output is not a tty
### Gin 1.0rc2 (...) ## Gin 1.0rc2 (...)
- [PERFORMANCE] Fast path for writing Content-Type. - [PERFORMANCE] Fast path for writing Content-Type.
- [PERFORMANCE] Much faster 404 routing - [PERFORMANCE] Much faster 404 routing
@ -154,7 +258,7 @@
- [FIX] MIT license in every file - [FIX] MIT license in every file
### Gin 1.0rc1 (May 22, 2015) ## Gin 1.0rc1 (May 22, 2015)
- [PERFORMANCE] Zero allocation router - [PERFORMANCE] Zero allocation router
- [PERFORMANCE] Faster JSON, XML and text rendering - [PERFORMANCE] Faster JSON, XML and text rendering
@ -162,7 +266,7 @@
- [PERFORMANCE] Misc code optimizations. Inlining, tail call optimizations - [PERFORMANCE] Misc code optimizations. Inlining, tail call optimizations
- [NEW] Built-in support for golang.org/x/net/context - [NEW] Built-in support for golang.org/x/net/context
- [NEW] Any(path, handler). Create a route that matches any path - [NEW] Any(path, handler). Create a route that matches any path
- [NEW] Refactored rendering pipeline (faster and static typeded) - [NEW] Refactored rendering pipeline (faster and static typed)
- [NEW] Refactored errors API - [NEW] Refactored errors API
- [NEW] IndentedJSON() prints pretty JSON - [NEW] IndentedJSON() prints pretty JSON
- [NEW] Added gin.DefaultWriter - [NEW] Added gin.DefaultWriter
@ -198,7 +302,7 @@
- [FIX] Better support for Google App Engine (using log instead of fmt) - [FIX] Better support for Google App Engine (using log instead of fmt)
### Gin 0.6 (Mar 9, 2015) ## Gin 0.6 (Mar 9, 2015)
- [NEW] Support multipart/form-data - [NEW] Support multipart/form-data
- [NEW] NoMethod handler - [NEW] NoMethod handler
@ -208,14 +312,14 @@
- [FIX] Improve color logger - [FIX] Improve color logger
### Gin 0.5 (Feb 7, 2015) ## Gin 0.5 (Feb 7, 2015)
- [NEW] Content Negotiation - [NEW] Content Negotiation
- [FIX] Solved security bug that allow a client to spoof ip - [FIX] Solved security bug that allow a client to spoof ip
- [FIX] Fix unexported/ignored fields in binding - [FIX] Fix unexported/ignored fields in binding
### Gin 0.4 (Aug 21, 2014) ## Gin 0.4 (Aug 21, 2014)
- [NEW] Development mode - [NEW] Development mode
- [NEW] Unit tests - [NEW] Unit tests
@ -224,14 +328,14 @@
- [FIX] Improved documentation for model binding - [FIX] Improved documentation for model binding
### Gin 0.3 (Jul 18, 2014) ## Gin 0.3 (Jul 18, 2014)
- [PERFORMANCE] Normal log and error log are printed in the same call. - [PERFORMANCE] Normal log and error log are printed in the same call.
- [PERFORMANCE] Improve performance of NoRouter() - [PERFORMANCE] Improve performance of NoRouter()
- [PERFORMANCE] Improve context's memory locality, reduce CPU cache faults. - [PERFORMANCE] Improve context's memory locality, reduce CPU cache faults.
- [NEW] Flexible rendering API - [NEW] Flexible rendering API
- [NEW] Add Context.File() - [NEW] Add Context.File()
- [NEW] Add shorcut RunTLS() for http.ListenAndServeTLS - [NEW] Add shortcut RunTLS() for http.ListenAndServeTLS
- [FIX] Rename NotFound404() to NoRoute() - [FIX] Rename NotFound404() to NoRoute()
- [FIX] Errors in context are purged - [FIX] Errors in context are purged
- [FIX] Adds HEAD method in Static file serving - [FIX] Adds HEAD method in Static file serving
@ -242,7 +346,7 @@
- [FIX] Check application/x-www-form-urlencoded when parsing form - [FIX] Check application/x-www-form-urlencoded when parsing form
### Gin 0.2b (Jul 08, 2014) ## Gin 0.2b (Jul 08, 2014)
- [PERFORMANCE] Using sync.Pool to allocatio/gc overhead - [PERFORMANCE] Using sync.Pool to allocatio/gc overhead
- [NEW] Travis CI integration - [NEW] Travis CI integration
- [NEW] Completely new logger - [NEW] Completely new logger
@ -254,14 +358,14 @@
- [NEW] New Bind() and BindWith() methods for parsing request body. - [NEW] New Bind() and BindWith() methods for parsing request body.
- [NEW] Add Content.Copy() - [NEW] Add Content.Copy()
- [NEW] Add context.LastError() - [NEW] Add context.LastError()
- [NEW] Add shorcut for OPTIONS HTTP method - [NEW] Add shortcut for OPTIONS HTTP method
- [FIX] Tons of README fixes - [FIX] Tons of README fixes
- [FIX] Header is written before body - [FIX] Header is written before body
- [FIX] BasicAuth() and changes API a little bit - [FIX] BasicAuth() and changes API a little bit
- [FIX] Recovery() middleware only prints panics - [FIX] Recovery() middleware only prints panics
- [FIX] Context.Get() does not panic anymore. Use MustGet() instead. - [FIX] Context.Get() does not panic anymore. Use MustGet() instead.
- [FIX] Multiple http.WriteHeader() in NotFound handlers - [FIX] Multiple http.WriteHeader() in NotFound handlers
- [FIX] Engine.Run() panics if http server can't be setted up - [FIX] Engine.Run() panics if http server can't be set up
- [FIX] Crash when route path doesn't start with '/' - [FIX] Crash when route path doesn't start with '/'
- [FIX] Do not update header when status code is negative - [FIX] Do not update header when status code is negative
- [FIX] Setting response headers before calling WriteHeader in context.String() - [FIX] Setting response headers before calling WriteHeader in context.String()

View File

@ -8,6 +8,6 @@
- With pull requests: - With pull requests:
- Open your pull request against `master` - Open your pull request against `master`
- Your pull request should have no more than two commits, if not you should squash them. - Your pull request should have no more than two commits, if not you should squash them.
- It should pass all tests in the available continuous integrations systems such as TravisCI. - It should pass all tests in the available continuous integration systems such as TravisCI.
- You should add/modify tests to cover your proposed code changes. - You should add/modify tests to cover your proposed code changes.
- If your pull request contains a new feature, please document it on the README. - If your pull request contains a new feature, please document it on the README.

View File

@ -1,20 +1,16 @@
GO ?= go GO ?= go
GOFMT ?= gofmt "-s" GOFMT ?= gofmt "-s"
PACKAGES ?= $(shell $(GO) list ./... | grep -v /vendor/) PACKAGES ?= $(shell $(GO) list ./...)
VETPACKAGES ?= $(shell $(GO) list ./... | grep -v /vendor/ | grep -v /examples/) VETPACKAGES ?= $(shell $(GO) list ./... | grep -v /examples/)
GOFILES := $(shell find . -name "*.go" -type f -not -path "./vendor/*") GOFILES := $(shell find . -name "*.go")
TESTFOLDER := $(shell $(GO) list ./... | grep -E 'gin$$|binding$$|render$$' | grep -v examples) TESTFOLDER := $(shell $(GO) list ./... | grep -E 'gin$$|binding$$|render$$' | grep -v examples)
TESTTAGS ?= ""
all: install
install: deps
govendor sync
.PHONY: test .PHONY: test
test: test:
echo "mode: count" > coverage.out echo "mode: count" > coverage.out
for d in $(TESTFOLDER); do \ for d in $(TESTFOLDER); do \
$(GO) test -v -covermode=count -coverprofile=profile.out $$d > tmp.out; \ $(GO) test -tags $(TESTTAGS) -v -covermode=count -coverprofile=profile.out $$d > tmp.out; \
cat tmp.out; \ cat tmp.out; \
if grep -q "^--- FAIL" tmp.out; then \ if grep -q "^--- FAIL" tmp.out; then \
rm tmp.out; \ rm tmp.out; \
@ -48,11 +44,6 @@ fmt-check:
vet: vet:
$(GO) vet $(VETPACKAGES) $(GO) vet $(VETPACKAGES)
deps:
@hash govendor > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
$(GO) get -u github.com/kardianos/govendor; \
fi
.PHONY: lint .PHONY: lint
lint: lint:
@hash golint > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ @hash golint > /dev/null 2>&1; if [ $$? -ne 0 ]; then \

220
README.md
View File

@ -5,19 +5,21 @@
[![Build Status](https://travis-ci.org/gin-gonic/gin.svg)](https://travis-ci.org/gin-gonic/gin) [![Build Status](https://travis-ci.org/gin-gonic/gin.svg)](https://travis-ci.org/gin-gonic/gin)
[![codecov](https://codecov.io/gh/gin-gonic/gin/branch/master/graph/badge.svg)](https://codecov.io/gh/gin-gonic/gin) [![codecov](https://codecov.io/gh/gin-gonic/gin/branch/master/graph/badge.svg)](https://codecov.io/gh/gin-gonic/gin)
[![Go Report Card](https://goreportcard.com/badge/github.com/gin-gonic/gin)](https://goreportcard.com/report/github.com/gin-gonic/gin) [![Go Report Card](https://goreportcard.com/badge/github.com/gin-gonic/gin)](https://goreportcard.com/report/github.com/gin-gonic/gin)
[![GoDoc](https://godoc.org/github.com/gin-gonic/gin?status.svg)](https://godoc.org/github.com/gin-gonic/gin) [![GoDoc](https://godoc.org/github.com/gin-gonic/gin?status.svg)](https://pkg.go.dev/github.com/gin-gonic/gin?tab=doc)
[![Join the chat at https://gitter.im/gin-gonic/gin](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/gin-gonic/gin?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Join the chat at https://gitter.im/gin-gonic/gin](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/gin-gonic/gin?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![Sourcegraph](https://sourcegraph.com/github.com/gin-gonic/gin/-/badge.svg)](https://sourcegraph.com/github.com/gin-gonic/gin?badge) [![Sourcegraph](https://sourcegraph.com/github.com/gin-gonic/gin/-/badge.svg)](https://sourcegraph.com/github.com/gin-gonic/gin?badge)
[![Open Source Helpers](https://www.codetriage.com/gin-gonic/gin/badges/users.svg)](https://www.codetriage.com/gin-gonic/gin) [![Open Source Helpers](https://www.codetriage.com/gin-gonic/gin/badges/users.svg)](https://www.codetriage.com/gin-gonic/gin)
[![Release](https://img.shields.io/github/release/gin-gonic/gin.svg?style=flat-square)](https://github.com/gin-gonic/gin/releases) [![Release](https://img.shields.io/github/release/gin-gonic/gin.svg?style=flat-square)](https://github.com/gin-gonic/gin/releases)
[![TODOs](https://badgen.net/https/api.tickgit.com/badgen/github.com/gin-gonic/gin)](https://www.tickgit.com/browse?repo=github.com/gin-gonic/gin)
Gin is a web framework written in Go (Golang). It features a martini-like API with much better performance, up to 40 times faster thanks to [httprouter](https://github.com/julienschmidt/httprouter). If you need performance and good productivity, you will love Gin. Gin is a web framework written in Go (Golang). It features a martini-like API with performance that is up to 40 times faster thanks to [httprouter](https://github.com/julienschmidt/httprouter). If you need performance and good productivity, you will love Gin.
## Contents ## Contents
- [Gin Web Framework](#gin-web-framework)
- [Contents](#contents)
- [Installation](#installation) - [Installation](#installation)
- [Prerequisite](#prerequisite)
- [Quick start](#quick-start) - [Quick start](#quick-start)
- [Benchmarks](#benchmarks) - [Benchmarks](#benchmarks)
- [Gin v1. stable](#gin-v1-stable) - [Gin v1. stable](#gin-v1-stable)
@ -30,11 +32,14 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi
- [Another example: query + post form](#another-example-query--post-form) - [Another example: query + post form](#another-example-query--post-form)
- [Map as querystring or postform parameters](#map-as-querystring-or-postform-parameters) - [Map as querystring or postform parameters](#map-as-querystring-or-postform-parameters)
- [Upload files](#upload-files) - [Upload files](#upload-files)
- [Single file](#single-file)
- [Multiple files](#multiple-files)
- [Grouping routes](#grouping-routes) - [Grouping routes](#grouping-routes)
- [Blank Gin without middleware by default](#blank-gin-without-middleware-by-default) - [Blank Gin without middleware by default](#blank-gin-without-middleware-by-default)
- [Using middleware](#using-middleware) - [Using middleware](#using-middleware)
- [How to write log file](#how-to-write-log-file) - [How to write log file](#how-to-write-log-file)
- [Custom Log Format](#custom-log-format) - [Custom Log Format](#custom-log-format)
- [Controlling Log output coloring](#controlling-log-output-coloring)
- [Model binding and validation](#model-binding-and-validation) - [Model binding and validation](#model-binding-and-validation)
- [Custom Validators](#custom-validators) - [Custom Validators](#custom-validators)
- [Only Bind Query String](#only-bind-query-string) - [Only Bind Query String](#only-bind-query-string)
@ -44,10 +49,17 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi
- [Bind HTML checkboxes](#bind-html-checkboxes) - [Bind HTML checkboxes](#bind-html-checkboxes)
- [Multipart/Urlencoded binding](#multiparturlencoded-binding) - [Multipart/Urlencoded binding](#multiparturlencoded-binding)
- [XML, JSON, YAML and ProtoBuf rendering](#xml-json-yaml-and-protobuf-rendering) - [XML, JSON, YAML and ProtoBuf rendering](#xml-json-yaml-and-protobuf-rendering)
- [JSONP rendering](#jsonp) - [SecureJSON](#securejson)
- [JSONP](#jsonp)
- [AsciiJSON](#asciijson)
- [PureJSON](#purejson)
- [Serving static files](#serving-static-files) - [Serving static files](#serving-static-files)
- [Serving data from file](#serving-data-from-file)
- [Serving data from reader](#serving-data-from-reader) - [Serving data from reader](#serving-data-from-reader)
- [HTML rendering](#html-rendering) - [HTML rendering](#html-rendering)
- [Custom Template renderer](#custom-template-renderer)
- [Custom Delimiters](#custom-delimiters)
- [Custom Template Funcs](#custom-template-funcs)
- [Multitemplate](#multitemplate) - [Multitemplate](#multitemplate)
- [Redirects](#redirects) - [Redirects](#redirects)
- [Custom Middleware](#custom-middleware) - [Custom Middleware](#custom-middleware)
@ -56,7 +68,9 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi
- [Custom HTTP configuration](#custom-http-configuration) - [Custom HTTP configuration](#custom-http-configuration)
- [Support Let's Encrypt](#support-lets-encrypt) - [Support Let's Encrypt](#support-lets-encrypt)
- [Run multiple service using Gin](#run-multiple-service-using-gin) - [Run multiple service using Gin](#run-multiple-service-using-gin)
- [Graceful restart or stop](#graceful-restart-or-stop) - [Graceful shutdown or restart](#graceful-shutdown-or-restart)
- [Third-party packages](#third-party-packages)
- [Manually](#manually)
- [Build a single binary with templates](#build-a-single-binary-with-templates) - [Build a single binary with templates](#build-a-single-binary-with-templates)
- [Bind form-data request with custom struct](#bind-form-data-request-with-custom-struct) - [Bind form-data request with custom struct](#bind-form-data-request-with-custom-struct)
- [Try to bind body into different structs](#try-to-bind-body-into-different-structs) - [Try to bind body into different structs](#try-to-bind-body-into-different-structs)
@ -70,7 +84,7 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi
To install Gin package, you need to install Go and set your Go workspace first. To install Gin package, you need to install Go and set your Go workspace first.
1. The first need [Go](https://golang.org/) installed (**version 1.10+ is required**), then you can use the below Go command to install Gin. 1. The first need [Go](https://golang.org/) installed (**version 1.11+ is required**), then you can use the below Go command to install Gin.
```sh ```sh
$ go get -u github.com/gin-gonic/gin $ go get -u github.com/gin-gonic/gin
@ -88,44 +102,6 @@ import "github.com/gin-gonic/gin"
import "net/http" import "net/http"
``` ```
### Use a vendor tool like [Govendor](https://github.com/kardianos/govendor)
1. `go get` govendor
```sh
$ go get github.com/kardianos/govendor
```
2. Create your project folder and `cd` inside
```sh
$ mkdir -p $GOPATH/src/github.com/myusername/project && cd "$_"
```
If you are on a Mac and you're installing Go 1.8 (released: Feb 2017) or later, GOPATH is automatically determined by the Go toolchain for you. It defaults to $HOME/go on macOS so you can create your project like this
```sh
$ mkdir -p $HOME/go/src/github.com/myusername/project && cd "$_"
```
3. Vendor init your project and add gin
```sh
$ govendor init
$ govendor fetch github.com/gin-gonic/gin@v1.3
```
4. Copy a starting template inside your project
```sh
$ curl https://raw.githubusercontent.com/gin-gonic/examples/master/basic/main.go > main.go
```
5. Run your project
```sh
$ go run main.go
```
## Quick start ## Quick start
```sh ```sh
@ -145,12 +121,12 @@ func main() {
"message": "pong", "message": "pong",
}) })
}) })
r.Run() // listen and serve on 0.0.0.0:8080 r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
} }
``` ```
``` ```
# run example.go and visit 0.0.0.0:8080/ping on browser # run example.go and visit 0.0.0.0:8080/ping (for windows "localhost:8080/ping") on browser
$ go run example.go $ go run example.go
``` ```
@ -160,35 +136,38 @@ Gin uses a custom version of [HttpRouter](https://github.com/julienschmidt/httpr
[See all benchmarks](/BENCHMARKS.md) [See all benchmarks](/BENCHMARKS.md)
Benchmark name | (1) | (2) | (3) | (4) | Benchmark name | (1) | (2) | (3) | (4) |
--------------------------------------------|-----------:|------------:|-----------:|---------: | ------------------------------ | ---------:| ---------------:| ------------:| ---------------:|
**BenchmarkGin_GithubAll** | **30000** | **48375** | **0** | **0** | BenchmarkGin_GithubAll | **43550** | **27364 ns/op** | **0 B/op** | **0 allocs/op** |
BenchmarkAce_GithubAll | 10000 | 134059 | 13792 | 167 | BenchmarkAce_GithubAll | 40543 | 29670 ns/op | 0 B/op | 0 allocs/op |
BenchmarkBear_GithubAll | 5000 | 534445 | 86448 | 943 | BenchmarkAero_GithubAll | 57632 | 20648 ns/op | 0 B/op | 0 allocs/op |
BenchmarkBeego_GithubAll | 3000 | 592444 | 74705 | 812 | BenchmarkBear_GithubAll | 9234 | 216179 ns/op | 86448 B/op | 943 allocs/op |
BenchmarkBone_GithubAll | 200 | 6957308 | 698784 | 8453 | BenchmarkBeego_GithubAll | 7407 | 243496 ns/op | 71456 B/op | 609 allocs/op |
BenchmarkDenco_GithubAll | 10000 | 158819 | 20224 | 167 | BenchmarkBone_GithubAll | 420 | 2922835 ns/op | 720160 B/op | 8620 allocs/op |
BenchmarkEcho_GithubAll | 10000 | 154700 | 6496 | 203 | BenchmarkChi_GithubAll | 7620 | 238331 ns/op | 87696 B/op | 609 allocs/op |
BenchmarkGocraftWeb_GithubAll | 3000 | 570806 | 131656 | 1686 | BenchmarkDenco_GithubAll | 18355 | 64494 ns/op | 20224 B/op | 167 allocs/op |
BenchmarkGoji_GithubAll | 2000 | 818034 | 56112 | 334 | BenchmarkEcho_GithubAll | 31251 | 38479 ns/op | 0 B/op | 0 allocs/op |
BenchmarkGojiv2_GithubAll | 2000 | 1213973 | 274768 | 3712 | BenchmarkGocraftWeb_GithubAll | 4117 | 300062 ns/op | 131656 B/op | 1686 allocs/op |
BenchmarkGoJsonRest_GithubAll | 2000 | 785796 | 134371 | 2737 | BenchmarkGoji_GithubAll | 3274 | 416158 ns/op | 56112 B/op | 334 allocs/op |
BenchmarkGoRestful_GithubAll | 300 | 5238188 | 689672 | 4519 | BenchmarkGojiv2_GithubAll | 1402 | 870518 ns/op | 352720 B/op | 4321 allocs/op |
BenchmarkGorillaMux_GithubAll | 100 | 10257726 | 211840 | 2272 | BenchmarkGoJsonRest_GithubAll | 2976 | 401507 ns/op | 134371 B/op | 2737 allocs/op |
BenchmarkHttpRouter_GithubAll | 20000 | 105414 | 13792 | 167 | BenchmarkGoRestful_GithubAll | 410 | 2913158 ns/op | 910144 B/op | 2938 allocs/op |
BenchmarkHttpTreeMux_GithubAll | 10000 | 319934 | 65856 | 671 | BenchmarkGorillaMux_GithubAll | 346 | 3384987 ns/op | 251650 B/op | 1994 allocs/op |
BenchmarkKocha_GithubAll | 10000 | 209442 | 23304 | 843 | BenchmarkGowwwRouter_GithubAll | 10000 | 143025 ns/op | 72144 B/op | 501 allocs/op |
BenchmarkLARS_GithubAll | 20000 | 62565 | 0 | 0 | BenchmarkHttpRouter_GithubAll | 55938 | 21360 ns/op | 0 B/op | 0 allocs/op |
BenchmarkMacaron_GithubAll | 2000 | 1161270 | 204194 | 2000 | BenchmarkHttpTreeMux_GithubAll | 10000 | 153944 ns/op | 65856 B/op | 671 allocs/op |
BenchmarkMartini_GithubAll | 200 | 9991713 | 226549 | 2325 | BenchmarkKocha_GithubAll | 10000 | 106315 ns/op | 23304 B/op | 843 allocs/op |
BenchmarkPat_GithubAll | 200 | 5590793 | 1499568 | 27435 | BenchmarkLARS_GithubAll | 47779 | 25084 ns/op | 0 B/op | 0 allocs/op |
BenchmarkPossum_GithubAll | 10000 | 319768 | 84448 | 609 | BenchmarkMacaron_GithubAll | 3266 | 371907 ns/op | 149409 B/op | 1624 allocs/op |
BenchmarkR2router_GithubAll | 10000 | 305134 | 77328 | 979 | BenchmarkMartini_GithubAll | 331 | 3444706 ns/op | 226551 B/op | 2325 allocs/op |
BenchmarkRivet_GithubAll | 10000 | 132134 | 16272 | 167 | BenchmarkPat_GithubAll | 273 | 4381818 ns/op | 1483152 B/op | 26963 allocs/op |
BenchmarkTango_GithubAll | 3000 | 552754 | 63826 | 1618 | BenchmarkPossum_GithubAll | 10000 | 164367 ns/op | 84448 B/op | 609 allocs/op |
BenchmarkTigerTonic_GithubAll | 1000 | 1439483 | 239104 | 5374 | BenchmarkR2router_GithubAll | 10000 | 160220 ns/op | 77328 B/op | 979 allocs/op |
BenchmarkTraffic_GithubAll | 100 | 11383067 | 2659329 | 21848 | BenchmarkRivet_GithubAll | 14625 | 82453 ns/op | 16272 B/op | 167 allocs/op |
BenchmarkVulcan_GithubAll | 5000 | 394253 | 19894 | 609 | BenchmarkTango_GithubAll | 6255 | 279611 ns/op | 63826 B/op | 1618 allocs/op |
| BenchmarkTigerTonic_GithubAll | 2008 | 687874 ns/op | 193856 B/op | 4474 allocs/op |
| BenchmarkTraffic_GithubAll | 355 | 3478508 ns/op | 820744 B/op | 14114 allocs/op |
| BenchmarkVulcan_GithubAll | 6885 | 193333 ns/op | 19894 B/op | 609 allocs/op |
- (1): Total Repetitions achieved in constant time, higher means more confident result - (1): Total Repetitions achieved in constant time, higher means more confident result
- (2): Single Repetition Duration (ns/op), lower is better - (2): Single Repetition Duration (ns/op), lower is better
@ -378,14 +357,14 @@ References issue [#774](https://github.com/gin-gonic/gin/issues/774) and detail
func main() { func main() {
router := gin.Default() router := gin.Default()
// Set a lower memory limit for multipart forms (default is 32 MiB) // Set a lower memory limit for multipart forms (default is 32 MiB)
// router.MaxMultipartMemory = 8 << 20 // 8 MiB router.MaxMultipartMemory = 8 << 20 // 8 MiB
router.POST("/upload", func(c *gin.Context) { router.POST("/upload", func(c *gin.Context) {
// single file // single file
file, _ := c.FormFile("file") file, _ := c.FormFile("file")
log.Println(file.Filename) log.Println(file.Filename)
// Upload the file to specific dst. // Upload the file to specific dst.
// c.SaveUploadedFile(file, dst) c.SaveUploadedFile(file, dst)
c.String(http.StatusOK, fmt.Sprintf("'%s' uploaded!", file.Filename)) c.String(http.StatusOK, fmt.Sprintf("'%s' uploaded!", file.Filename))
}) })
@ -409,7 +388,7 @@ See the detail [example code](https://github.com/gin-gonic/examples/tree/master/
func main() { func main() {
router := gin.Default() router := gin.Default()
// Set a lower memory limit for multipart forms (default is 32 MiB) // Set a lower memory limit for multipart forms (default is 32 MiB)
// router.MaxMultipartMemory = 8 << 20 // 8 MiB router.MaxMultipartMemory = 8 << 20 // 8 MiB
router.POST("/upload", func(c *gin.Context) { router.POST("/upload", func(c *gin.Context) {
// Multipart form // Multipart form
form, _ := c.MultipartForm() form, _ := c.MultipartForm()
@ -419,7 +398,7 @@ func main() {
log.Println(file.Filename) log.Println(file.Filename)
// Upload the file to specific dst. // Upload the file to specific dst.
// c.SaveUploadedFile(file, dst) c.SaveUploadedFile(file, dst)
} }
c.String(http.StatusOK, fmt.Sprintf("%d files uploaded!", len(files))) c.String(http.StatusOK, fmt.Sprintf("%d files uploaded!", len(files)))
}) })
@ -622,7 +601,7 @@ func main() {
To bind a request body into a type, use model binding. We currently support binding of JSON, XML, YAML and standard form values (foo=bar&boo=baz). To bind a request body into a type, use model binding. We currently support binding of JSON, XML, YAML and standard form values (foo=bar&boo=baz).
Gin uses [**go-playground/validator.v8**](https://github.com/go-playground/validator) for validation. Check the full docs on tags usage [here](http://godoc.org/gopkg.in/go-playground/validator.v8#hdr-Baked_In_Validators_and_Tags). Gin uses [**go-playground/validator/v10**](https://github.com/go-playground/validator) for validation. Check the full docs on tags usage [here](https://godoc.org/github.com/go-playground/validator#hdr-Baked_In_Validators_and_Tags).
Note that you need to set the corresponding binding tag on all fields you want to bind. For example, when binding from JSON, set `json:"fieldname"`. Note that you need to set the corresponding binding tag on all fields you want to bind. For example, when binding from JSON, set `json:"fieldname"`.
@ -742,25 +721,22 @@ package main
import ( import (
"net/http" "net/http"
"reflect"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/binding"
"gopkg.in/go-playground/validator.v8" "gopkg.in/go-playground/validator.v10"
) )
// Booking contains binded and validated data. // Booking contains binded and validated data.
type Booking struct { type Booking struct {
CheckIn time.Time `form:"check_in" binding:"required,bookabledate" time_format:"2006-01-02"` CheckIn time.Time `form:"check_in" binding:"required" time_format:"2006-01-02"`
CheckOut time.Time `form:"check_out" binding:"required,gtfield=CheckIn" time_format:"2006-01-02"` CheckOut time.Time `form:"check_out" binding:"required,gtfield=CheckIn" time_format:"2006-01-02"`
} }
func bookableDate( var bookableDate validator.Func = func(fl validator.FieldLevel) bool {
v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value, date, ok := fl.Field().Interface().(time.Time)
field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string, if ok {
) bool {
if date, ok := field.Interface().(time.Time); ok {
today := time.Now() today := time.Now()
if today.After(date) { if today.After(date) {
return false return false
@ -794,8 +770,8 @@ func getBookable(c *gin.Context) {
$ curl "localhost:8085/bookable?check_in=2018-04-16&check_out=2018-04-17" $ curl "localhost:8085/bookable?check_in=2018-04-16&check_out=2018-04-17"
{"message":"Booking dates are valid!"} {"message":"Booking dates are valid!"}
$ curl "localhost:8085/bookable?check_in=2018-03-08&check_out=2018-03-09" $ curl "localhost:8085/bookable?check_in=2018-03-10&check_out=2018-03-09"
{"error":"Key: 'Booking.CheckIn' Error:Field validation for 'CheckIn' failed on the 'bookabledate' tag"} {"error":"Key: 'Booking.CheckOut' Error:Field validation for 'CheckOut' failed on the 'gtfield' tag"}
``` ```
[Struct level validations](https://github.com/go-playground/validator/releases/tag/v8.7) can also be registered this way. [Struct level validations](https://github.com/go-playground/validator/releases/tag/v8.7) can also be registered this way.
@ -1149,7 +1125,7 @@ func main() {
#### AsciiJSON #### AsciiJSON
Using AsciiJSON to Generates ASCII-only JSON with escaped non-ASCII chracters. Using AsciiJSON to Generates ASCII-only JSON with escaped non-ASCII characters.
```go ```go
func main() { func main() {
@ -1212,6 +1188,24 @@ func main() {
} }
``` ```
### Serving data from file
```go
func main() {
router := gin.Default()
router.GET("/local/file", func(c *gin.Context) {
c.File("local/file.go")
})
var fs http.FileSystem = // ...
router.GET("/fs/file", func(c *gin.Context) {
c.FileFromFS("fs/file.go", fs)
})
}
```
### Serving data from reader ### Serving data from reader
```go ```go
@ -1678,11 +1672,19 @@ func main() {
} }
g.Go(func() error { g.Go(func() error {
return server01.ListenAndServe() err := server01.ListenAndServe()
if err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
return err
}) })
g.Go(func() error { g.Go(func() error {
return server02.ListenAndServe() err := server02.ListenAndServe()
if err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
return err
}) })
if err := g.Wait(); err != nil { if err := g.Wait(); err != nil {
@ -1691,12 +1693,13 @@ func main() {
} }
``` ```
### Graceful restart or stop ### Graceful shutdown or restart
Do you want to graceful restart or stop your web server? There are a few approaches you can use to perform a graceful shutdown or restart. You can make use of third-party packages specifically built for that, or you can manually do the same with the functions and methods from the built-in packages.
There are some ways this can be done.
We can use [fvbock/endless](https://github.com/fvbock/endless) to replace the default `ListenAndServe`. Refer issue [#296](https://github.com/gin-gonic/gin/issues/296) for more details. #### Third-party packages
We can use [fvbock/endless](https://github.com/fvbock/endless) to replace the default `ListenAndServe`. Refer to issue [#296](https://github.com/gin-gonic/gin/issues/296) for more details.
```go ```go
router := gin.Default() router := gin.Default()
@ -1705,13 +1708,15 @@ router.GET("/", handler)
endless.ListenAndServe(":4242", router) endless.ListenAndServe(":4242", router)
``` ```
An alternative to endless: Alternatives:
* [manners](https://github.com/braintree/manners): A polite Go HTTP server that shuts down gracefully. * [manners](https://github.com/braintree/manners): A polite Go HTTP server that shuts down gracefully.
* [graceful](https://github.com/tylerb/graceful): Graceful is a Go package enabling graceful shutdown of an http.Handler server. * [graceful](https://github.com/tylerb/graceful): Graceful is a Go package enabling graceful shutdown of an http.Handler server.
* [grace](https://github.com/facebookgo/grace): Graceful restart & zero downtime deploy for Go servers. * [grace](https://github.com/facebookgo/grace): Graceful restart & zero downtime deploy for Go servers.
If you are using Go 1.8, you may not need to use this library! Consider using http.Server's built-in [Shutdown()](https://golang.org/pkg/net/http/#Server.Shutdown) method for graceful shutdowns. See the full [graceful-shutdown](https://github.com/gin-gonic/examples/tree/master/graceful-shutdown) example with gin. #### Manually
In case you are using Go 1.8 or a later version, you may not need to use those libraries. Consider using `http.Server`'s built-in [Shutdown()](https://golang.org/pkg/net/http/#Server.Shutdown) method for graceful shutdowns. The example below describes its usage, and we've got more examples using gin [here](https://github.com/gin-gonic/examples/tree/master/graceful-shutdown).
```go ```go
// +build go1.8 // +build go1.8
@ -1742,8 +1747,9 @@ func main() {
Handler: router, Handler: router,
} }
// Initializing the server in a goroutine so that
// it won't block the graceful shutdown handling below
go func() { go func() {
// service connections
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %s\n", err) log.Fatalf("listen: %s\n", err)
} }
@ -1757,18 +1763,16 @@ func main() {
// kill -9 is syscall.SIGKILL but can't be catch, so don't need add it // kill -9 is syscall.SIGKILL but can't be catch, so don't need add it
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit <-quit
log.Println("Shutdown Server ...") log.Println("Shutting down server...")
// The context is used to inform the server it has 5 seconds to finish
// the request it is currently handling
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() defer cancel()
if err := srv.Shutdown(ctx); err != nil { if err := srv.Shutdown(ctx); err != nil {
log.Fatal("Server Shutdown:", err) log.Fatal("Server forced to shutdown:", err)
}
// catching ctx.Done(). timeout of 5 seconds.
select {
case <-ctx.Done():
log.Println("timeout of 5 seconds.")
} }
log.Println("Server exiting") log.Println("Server exiting")
} }
``` ```
@ -1799,6 +1803,7 @@ func main() {
func loadTemplate() (*template.Template, error) { func loadTemplate() (*template.Template, error) {
t := template.New("") t := template.New("")
for name, file := range Assets.Files { for name, file := range Assets.Files {
defer file.Close()
if file.IsDir() || !strings.HasSuffix(name, ".tmpl") { if file.IsDir() || !strings.HasSuffix(name, ".tmpl") {
continue continue
} }
@ -2125,3 +2130,4 @@ Awesome project lists using [Gin](https://github.com/gin-gonic/gin) web framewor
* [krakend](https://github.com/devopsfaith/krakend): Ultra performant API Gateway with middlewares. * [krakend](https://github.com/devopsfaith/krakend): Ultra performant API Gateway with middlewares.
* [picfit](https://github.com/thoas/picfit): An image resizing server written in Go. * [picfit](https://github.com/thoas/picfit): An image resizing server written in Go.
* [brigade](https://github.com/brigadecore/brigade): Event-based Scripting for Kubernetes. * [brigade](https://github.com/brigadecore/brigade): Event-based Scripting for Kubernetes.
* [dkron](https://github.com/distribworks/dkron): Distributed, fault tolerant job scheduling system.

View File

@ -8,6 +8,8 @@ import (
"encoding/base64" "encoding/base64"
"net/http" "net/http"
"strconv" "strconv"
"github.com/gin-gonic/gin/internal/bytesconv"
) )
// AuthUserKey is the cookie name for user credential in basic auth. // AuthUserKey is the cookie name for user credential in basic auth.
@ -68,8 +70,9 @@ func BasicAuth(accounts Accounts) HandlerFunc {
} }
func processAccounts(accounts Accounts) authPairs { func processAccounts(accounts Accounts) authPairs {
assert1(len(accounts) > 0, "Empty list of authorized credentials") length := len(accounts)
pairs := make(authPairs, 0, len(accounts)) assert1(length > 0, "Empty list of authorized credentials")
pairs := make(authPairs, 0, length)
for user, password := range accounts { for user, password := range accounts {
assert1(user != "", "User can not be empty") assert1(user != "", "User can not be empty")
value := authorizationHeader(user, password) value := authorizationHeader(user, password)
@ -83,5 +86,5 @@ func processAccounts(accounts Accounts) authPairs {
func authorizationHeader(user, password string) string { func authorizationHeader(user, password string) string {
base := user + ":" + password base := user + ":" + password
return "Basic " + base64.StdEncoding.EncodeToString([]byte(base)) return "Basic " + base64.StdEncoding.EncodeToString(bytesconv.StringToBytes(base))
} }

View File

@ -2,6 +2,8 @@
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// +build !nomsgpack
package binding package binding
import "net/http" import "net/http"
@ -84,7 +86,7 @@ var (
// Default returns the appropriate Binding instance based on the HTTP method // Default returns the appropriate Binding instance based on the HTTP method
// and the content type. // and the content type.
func Default(method, contentType string) Binding { func Default(method, contentType string) Binding {
if method == "GET" { if method == http.MethodGet {
return Form return Form
} }

View File

@ -1,72 +0,0 @@
package binding
import (
"bytes"
"io/ioutil"
"testing"
"github.com/gin-gonic/gin/testdata/protoexample"
"github.com/golang/protobuf/proto"
"github.com/stretchr/testify/assert"
"github.com/ugorji/go/codec"
)
func TestBindingBody(t *testing.T) {
for _, tt := range []struct {
name string
binding BindingBody
body string
want string
}{
{
name: "JSON binding",
binding: JSON,
body: `{"foo":"FOO"}`,
},
{
name: "XML binding",
binding: XML,
body: `<?xml version="1.0" encoding="UTF-8"?>
<root>
<foo>FOO</foo>
</root>`,
},
{
name: "MsgPack binding",
binding: MsgPack,
body: msgPackBody(t),
},
{
name: "YAML binding",
binding: YAML,
body: `foo: FOO`,
},
} {
t.Logf("testing: %s", tt.name)
req := requestWithBody("POST", "/", tt.body)
form := FooStruct{}
body, _ := ioutil.ReadAll(req.Body)
assert.NoError(t, tt.binding.BindBody(body, &form))
assert.Equal(t, FooStruct{"FOO"}, form)
}
}
func msgPackBody(t *testing.T) string {
test := FooStruct{"FOO"}
h := new(codec.MsgpackHandle)
buf := bytes.NewBuffer(nil)
assert.NoError(t, codec.NewEncoder(buf, h).Encode(test))
return buf.String()
}
func TestBindingBodyProto(t *testing.T) {
test := protoexample.Test{
Label: proto.String("FOO"),
}
data, _ := proto.Marshal(&test)
req := requestWithBody("POST", "/", string(data))
form := protoexample.Test{}
body, _ := ioutil.ReadAll(req.Body)
assert.NoError(t, ProtoBuf.BindBody(body, &form))
assert.Equal(t, test, form)
}

View File

@ -0,0 +1,57 @@
// Copyright 2020 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
// +build !nomsgpack
package binding
import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
"github.com/ugorji/go/codec"
)
func TestBindingMsgPack(t *testing.T) {
test := FooStruct{
Foo: "bar",
}
h := new(codec.MsgpackHandle)
assert.NotNil(t, h)
buf := bytes.NewBuffer([]byte{})
assert.NotNil(t, buf)
err := codec.NewEncoder(buf, h).Encode(test)
assert.NoError(t, err)
data := buf.Bytes()
testMsgPackBodyBinding(t,
MsgPack, "msgpack",
"/", "/",
string(data), string(data[1:]))
}
func testMsgPackBodyBinding(t *testing.T, b Binding, name, path, badPath, body, badBody string) {
assert.Equal(t, name, b.Name())
obj := FooStruct{}
req := requestWithBody("POST", path, body)
req.Header.Add("Content-Type", MIMEMSGPACK)
err := b.Bind(req, &obj)
assert.NoError(t, err)
assert.Equal(t, "bar", obj.Foo)
obj = FooStruct{}
req = requestWithBody("POST", badPath, badBody)
req.Header.Add("Content-Type", MIMEMSGPACK)
err = MsgPack.Bind(req, &obj)
assert.Error(t, err)
}
func TestBindingDefaultMsgPack(t *testing.T) {
assert.Equal(t, MsgPack, Default("POST", MIMEMSGPACK))
assert.Equal(t, MsgPack, Default("PUT", MIMEMSGPACK2))
}

View File

@ -0,0 +1,111 @@
// Copyright 2020 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
// +build nomsgpack
package binding
import "net/http"
// Content-Type MIME of the most common data formats.
const (
MIMEJSON = "application/json"
MIMEHTML = "text/html"
MIMEXML = "application/xml"
MIMEXML2 = "text/xml"
MIMEPlain = "text/plain"
MIMEPOSTForm = "application/x-www-form-urlencoded"
MIMEMultipartPOSTForm = "multipart/form-data"
MIMEPROTOBUF = "application/x-protobuf"
MIMEYAML = "application/x-yaml"
)
// Binding describes the interface which needs to be implemented for binding the
// data present in the request such as JSON request body, query parameters or
// the form POST.
type Binding interface {
Name() string
Bind(*http.Request, interface{}) error
}
// BindingBody adds BindBody method to Binding. BindBody is similar with Bind,
// but it reads the body from supplied bytes instead of req.Body.
type BindingBody interface {
Binding
BindBody([]byte, interface{}) error
}
// BindingUri adds BindUri method to Binding. BindUri is similar with Bind,
// but it read the Params.
type BindingUri interface {
Name() string
BindUri(map[string][]string, interface{}) error
}
// StructValidator is the minimal interface which needs to be implemented in
// order for it to be used as the validator engine for ensuring the correctness
// of the request. Gin provides a default implementation for this using
// https://github.com/go-playground/validator/tree/v8.18.2.
type StructValidator interface {
// ValidateStruct can receive any kind of type and it should never panic, even if the configuration is not right.
// If the received type is not a struct, any validation should be skipped and nil must be returned.
// If the received type is a struct or pointer to a struct, the validation should be performed.
// If the struct is not valid or the validation itself fails, a descriptive error should be returned.
// Otherwise nil must be returned.
ValidateStruct(interface{}) error
// Engine returns the underlying validator engine which powers the
// StructValidator implementation.
Engine() interface{}
}
// Validator is the default validator which implements the StructValidator
// interface. It uses https://github.com/go-playground/validator/tree/v8.18.2
// under the hood.
var Validator StructValidator = &defaultValidator{}
// These implement the Binding interface and can be used to bind the data
// present in the request to struct instances.
var (
JSON = jsonBinding{}
XML = xmlBinding{}
Form = formBinding{}
Query = queryBinding{}
FormPost = formPostBinding{}
FormMultipart = formMultipartBinding{}
ProtoBuf = protobufBinding{}
YAML = yamlBinding{}
Uri = uriBinding{}
Header = headerBinding{}
)
// Default returns the appropriate Binding instance based on the HTTP method
// and the content type.
func Default(method, contentType string) Binding {
if method == "GET" {
return Form
}
switch contentType {
case MIMEJSON:
return JSON
case MIMEXML, MIMEXML2:
return XML
case MIMEPROTOBUF:
return ProtoBuf
case MIMEYAML:
return YAML
case MIMEMultipartPOSTForm:
return FormMultipart
default: // case MIMEPOSTForm:
return Form
}
}
func validate(obj interface{}) error {
if Validator == nil {
return nil
}
return Validator.ValidateStruct(obj)
}

View File

@ -21,7 +21,6 @@ import (
"github.com/gin-gonic/gin/testdata/protoexample" "github.com/gin-gonic/gin/testdata/protoexample"
"github.com/golang/protobuf/proto" "github.com/golang/protobuf/proto"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/ugorji/go/codec"
) )
type appkey struct { type appkey struct {
@ -163,9 +162,6 @@ func TestBindingDefault(t *testing.T) {
assert.Equal(t, ProtoBuf, Default("POST", MIMEPROTOBUF)) assert.Equal(t, ProtoBuf, Default("POST", MIMEPROTOBUF))
assert.Equal(t, ProtoBuf, Default("PUT", MIMEPROTOBUF)) assert.Equal(t, ProtoBuf, Default("PUT", MIMEPROTOBUF))
assert.Equal(t, MsgPack, Default("POST", MIMEMSGPACK))
assert.Equal(t, MsgPack, Default("PUT", MIMEMSGPACK2))
assert.Equal(t, YAML, Default("POST", MIMEYAML)) assert.Equal(t, YAML, Default("POST", MIMEYAML))
assert.Equal(t, YAML, Default("PUT", MIMEYAML)) assert.Equal(t, YAML, Default("PUT", MIMEYAML))
} }
@ -441,7 +437,8 @@ func createFormFilesMultipartRequest(t *testing.T) *http.Request {
defer f.Close() defer f.Close()
fw, err1 := mw.CreateFormFile("file", "form.go") fw, err1 := mw.CreateFormFile("file", "form.go")
assert.NoError(t, err1) assert.NoError(t, err1)
io.Copy(fw, f) _, err = io.Copy(fw, f)
assert.NoError(t, err)
req, err2 := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", body) req, err2 := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", body)
assert.NoError(t, err2) assert.NoError(t, err2)
@ -465,7 +462,8 @@ func createFormFilesMultipartRequestFail(t *testing.T) *http.Request {
defer f.Close() defer f.Close()
fw, err1 := mw.CreateFormFile("file_foo", "form_foo.go") fw, err1 := mw.CreateFormFile("file_foo", "form_foo.go")
assert.NoError(t, err1) assert.NoError(t, err1)
io.Copy(fw, f) _, err = io.Copy(fw, f)
assert.NoError(t, err)
req, err2 := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", body) req, err2 := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", body)
assert.NoError(t, err2) assert.NoError(t, err2)
@ -554,7 +552,8 @@ func TestBindingFormPostForMapFail(t *testing.T) {
func TestBindingFormFilesMultipart(t *testing.T) { func TestBindingFormFilesMultipart(t *testing.T) {
req := createFormFilesMultipartRequest(t) req := createFormFilesMultipartRequest(t)
var obj FooBarFileStruct var obj FooBarFileStruct
FormMultipart.Bind(req, &obj) err := FormMultipart.Bind(req, &obj)
assert.NoError(t, err)
// file from os // file from os
f, _ := os.Open("form.go") f, _ := os.Open("form.go")
@ -630,26 +629,6 @@ func TestBindingProtoBufFail(t *testing.T) {
string(data), string(data[1:])) string(data), string(data[1:]))
} }
func TestBindingMsgPack(t *testing.T) {
test := FooStruct{
Foo: "bar",
}
h := new(codec.MsgpackHandle)
assert.NotNil(t, h)
buf := bytes.NewBuffer([]byte{})
assert.NotNil(t, buf)
err := codec.NewEncoder(buf, h).Encode(test)
assert.NoError(t, err)
data := buf.Bytes()
testMsgPackBodyBinding(t,
MsgPack, "msgpack",
"/", "/",
string(data), string(data[1:]))
}
func TestValidationFails(t *testing.T) { func TestValidationFails(t *testing.T) {
var obj FooStruct var obj FooStruct
req := requestWithBody("POST", "/", `{"bar": "foo"}`) req := requestWithBody("POST", "/", `{"bar": "foo"}`)
@ -1247,23 +1226,6 @@ func testProtoBodyBindingFail(t *testing.T, b Binding, name, path, badPath, body
assert.Error(t, err) assert.Error(t, err)
} }
func testMsgPackBodyBinding(t *testing.T, b Binding, name, path, badPath, body, badBody string) {
assert.Equal(t, name, b.Name())
obj := FooStruct{}
req := requestWithBody("POST", path, body)
req.Header.Add("Content-Type", MIMEMSGPACK)
err := b.Bind(req, &obj)
assert.NoError(t, err)
assert.Equal(t, "bar", obj.Foo)
obj = FooStruct{}
req = requestWithBody("POST", badPath, badBody)
req.Header.Add("Content-Type", MIMEMSGPACK)
err = MsgPack.Bind(req, &obj)
assert.Error(t, err)
}
func requestWithBody(method, path, body string) (req *http.Request) { func requestWithBody(method, path, body string) (req *http.Request) {
req, _ = http.NewRequest(method, path, bytes.NewBufferString(body)) req, _ = http.NewRequest(method, path, bytes.NewBufferString(body))
return return

View File

@ -8,7 +8,7 @@ import (
"reflect" "reflect"
"sync" "sync"
"gopkg.in/go-playground/validator.v9" "github.com/go-playground/validator/v10"
) )
type defaultValidator struct { type defaultValidator struct {

View File

@ -8,7 +8,7 @@ import (
"net/http" "net/http"
) )
const defaultMemory = 32 * 1024 * 1024 const defaultMemory = 32 << 20
type formBinding struct{} type formBinding struct{}
type formPostBinding struct{} type formPostBinding struct{}

View File

@ -12,10 +12,11 @@ import (
"strings" "strings"
"time" "time"
"github.com/gin-gonic/gin/internal/bytesconv"
"github.com/gin-gonic/gin/internal/json" "github.com/gin-gonic/gin/internal/json"
) )
var errUnknownType = errors.New("Unknown type") var errUnknownType = errors.New("unknown type")
func mapUri(ptr interface{}, m map[string][]string) error { func mapUri(ptr interface{}, m map[string][]string) error {
return mapFormByTag(ptr, m, "uri") return mapFormByTag(ptr, m, "uri")
@ -51,6 +52,10 @@ func mappingByPtr(ptr interface{}, setter setter, tag string) error {
} }
func mapping(value reflect.Value, field reflect.StructField, setter setter, tag string) (bool, error) { func mapping(value reflect.Value, field reflect.StructField, setter setter, tag string) (bool, error) {
if field.Tag.Get(tag) == "-" { // just ignoring this field
return false, nil
}
var vKind = value.Kind() var vKind = value.Kind()
if vKind == reflect.Ptr { if vKind == reflect.Ptr {
@ -112,9 +117,6 @@ func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter
tagValue = field.Tag.Get(tag) tagValue = field.Tag.Get(tag)
tagValue, opts := head(tagValue, ",") tagValue, opts := head(tagValue, ",")
if tagValue == "-" { // just ignoring this field
return false, nil
}
if tagValue == "" { // default value is FieldName if tagValue == "" { // default value is FieldName
tagValue = field.Name tagValue = field.Name
} }
@ -207,9 +209,9 @@ func setWithProperType(val string, value reflect.Value, field reflect.StructFiel
case time.Time: case time.Time:
return setTimeField(val, field, value) return setTimeField(val, field, value)
} }
return json.Unmarshal([]byte(val), value.Addr().Interface()) return json.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
case reflect.Map: case reflect.Map:
return json.Unmarshal([]byte(val), value.Addr().Interface()) return json.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
default: default:
return errUnknownType return errUnknownType
} }

View File

@ -32,7 +32,10 @@ type structFull struct {
func BenchmarkMapFormFull(b *testing.B) { func BenchmarkMapFormFull(b *testing.B) {
var s structFull var s structFull
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
mapForm(&s, form) err := mapForm(&s, form)
if err != nil {
b.Fatalf("Error on a form mapping")
}
} }
b.StopTimer() b.StopTimer()
@ -52,7 +55,10 @@ type structName struct {
func BenchmarkMapFormName(b *testing.B) { func BenchmarkMapFormName(b *testing.B) {
var s structName var s structName
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
mapForm(&s, form) err := mapForm(&s, form)
if err != nil {
b.Fatalf("Error on a form mapping")
}
} }
b.StopTimer() b.StopTimer()

View File

@ -269,3 +269,13 @@ func TestMappingMapField(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, map[string]int{"one": 1}, s.M) assert.Equal(t, map[string]int{"one": 1}, s.M)
} }
func TestMappingIgnoredCircularRef(t *testing.T) {
type S struct {
S *S `form:"-"`
}
var s S
err := mappingByPtr(&s, formSource{}, "form")
assert.NoError(t, err)
}

21
binding/json_test.go Normal file
View File

@ -0,0 +1,21 @@
// Copyright 2019 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package binding
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestJSONBindingBindBody(t *testing.T) {
var s struct {
Foo string `json:"foo"`
}
err := jsonBinding{}.BindBody([]byte(`{"foo": "FOO"}`), &s)
require.NoError(t, err)
assert.Equal(t, "FOO", s.Foo)
}

View File

@ -2,6 +2,8 @@
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// +build !nomsgpack
package binding package binding
import ( import (

34
binding/msgpack_test.go Normal file
View File

@ -0,0 +1,34 @@
// Copyright 2019 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
// +build !nomsgpack
package binding
import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ugorji/go/codec"
)
func TestMsgpackBindingBindBody(t *testing.T) {
type teststruct struct {
Foo string `msgpack:"foo"`
}
var s teststruct
err := msgpackBinding{}.BindBody(msgpackBody(t, teststruct{"FOO"}), &s)
require.NoError(t, err)
assert.Equal(t, "FOO", s.Foo)
}
func msgpackBody(t *testing.T, obj interface{}) []byte {
var bs bytes.Buffer
h := &codec.MsgpackHandle{}
err := codec.NewEncoder(&bs, h).Encode(obj)
require.NoError(t, err)
return bs.Bytes()
}

View File

@ -9,8 +9,8 @@ import (
"testing" "testing"
"time" "time"
"github.com/go-playground/validator/v10"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"gopkg.in/go-playground/validator.v9"
) )
type testInterface interface { type testInterface interface {

25
binding/xml_test.go Normal file
View File

@ -0,0 +1,25 @@
// Copyright 2019 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package binding
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestXMLBindingBindBody(t *testing.T) {
var s struct {
Foo string `xml:"foo"`
}
xmlBody := `<?xml version="1.0" encoding="UTF-8"?>
<root>
<foo>FOO</foo>
</root>`
err := xmlBinding{}.BindBody([]byte(xmlBody), &s)
require.NoError(t, err)
assert.Equal(t, "FOO", s.Foo)
}

21
binding/yaml_test.go Normal file
View File

@ -0,0 +1,21 @@
// Copyright 2019 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package binding
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestYAMLBindingBindBody(t *testing.T) {
var s struct {
Foo string `yaml:"foo"`
}
err := yamlBinding{}.BindBody([]byte("foo: FOO"), &s)
require.NoError(t, err)
assert.Equal(t, "FOO", s.Foo)
}

View File

@ -16,6 +16,7 @@ import (
"net/url" "net/url"
"os" "os"
"strings" "strings"
"sync"
"time" "time"
"github.com/gin-contrib/sse" "github.com/gin-contrib/sse"
@ -33,9 +34,11 @@ const (
MIMEPOSTForm = binding.MIMEPOSTForm MIMEPOSTForm = binding.MIMEPOSTForm
MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm
MIMEYAML = binding.MIMEYAML MIMEYAML = binding.MIMEYAML
BodyBytesKey = "_gin-gonic/gin/bodybyteskey"
) )
// BodyBytesKey indicates a default body bytes key.
const BodyBytesKey = "_gin-gonic/gin/bodybyteskey"
const abortIndex int8 = math.MaxInt8 / 2 const abortIndex int8 = math.MaxInt8 / 2
// Context is the most important part of gin. It allows us to pass variables between middleware, // Context is the most important part of gin. It allows us to pass variables between middleware,
@ -51,6 +54,10 @@ type Context struct {
fullPath string fullPath string
engine *Engine engine *Engine
params *Params
// This mutex protect Keys map
mu sync.RWMutex
// Keys is a key/value pair exclusively for the context of each request. // Keys is a key/value pair exclusively for the context of each request.
Keys map[string]interface{} Keys map[string]interface{}
@ -67,6 +74,10 @@ type Context struct {
// formCache use url.ParseQuery cached PostForm contains the parsed form data from POST, PATCH, // formCache use url.ParseQuery cached PostForm contains the parsed form data from POST, PATCH,
// or PUT body parameters. // or PUT body parameters.
formCache url.Values formCache url.Values
// 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
} }
/************************************/ /************************************/
@ -78,18 +89,25 @@ func (c *Context) reset() {
c.Params = c.Params[0:0] c.Params = c.Params[0:0]
c.handlers = nil c.handlers = nil
c.index = -1 c.index = -1
c.fullPath = "" c.fullPath = ""
c.Keys = nil c.Keys = nil
c.Errors = c.Errors[0:0] c.Errors = c.Errors[0:0]
c.Accepted = nil c.Accepted = nil
c.queryCache = nil c.queryCache = nil
c.formCache = nil c.formCache = nil
*c.params = (*c.params)[0:0]
} }
// Copy returns a copy of the current context that can be safely used outside the request's scope. // Copy returns a copy of the current context that can be safely used outside the request's scope.
// This has to be used when the context has to be passed to a goroutine. // This has to be used when the context has to be passed to a goroutine.
func (c *Context) Copy() *Context { func (c *Context) Copy() *Context {
var cp = *c cp := Context{
writermem: c.writermem,
Request: c.Request,
Params: c.Params,
engine: c.engine,
}
cp.writermem.ResponseWriter = nil cp.writermem.ResponseWriter = nil
cp.Writer = &cp.writermem cp.Writer = &cp.writermem
cp.index = abortIndex cp.index = abortIndex
@ -219,16 +237,21 @@ func (c *Context) Error(err error) *Error {
// Set is used to store a new key/value pair exclusively for this context. // Set is used to store a new key/value pair exclusively for this context.
// It also lazy initializes c.Keys if it was not used previously. // It also lazy initializes c.Keys if it was not used previously.
func (c *Context) Set(key string, value interface{}) { func (c *Context) Set(key string, value interface{}) {
c.mu.Lock()
if c.Keys == nil { if c.Keys == nil {
c.Keys = make(map[string]interface{}) c.Keys = make(map[string]interface{})
} }
c.Keys[key] = value c.Keys[key] = value
c.mu.Unlock()
} }
// Get returns the value for the given key, ie: (value, true). // Get returns the value for the given key, ie: (value, true).
// If the value does not exists it returns (nil, false) // If the value does not exists it returns (nil, false)
func (c *Context) Get(key string) (value interface{}, exists bool) { func (c *Context) Get(key string) (value interface{}, exists bool) {
c.mu.RLock()
value, exists = c.Keys[key] value, exists = c.Keys[key]
c.mu.RUnlock()
return return
} }
@ -391,17 +414,16 @@ func (c *Context) QueryArray(key string) []string {
return values return values
} }
func (c *Context) getQueryCache() { func (c *Context) initQueryCache() {
if c.queryCache == nil { if c.queryCache == nil {
c.queryCache = make(url.Values) c.queryCache = c.Request.URL.Query()
c.queryCache, _ = url.ParseQuery(c.Request.URL.RawQuery)
} }
} }
// GetQueryArray returns a slice of strings for a given query key, plus // GetQueryArray returns a slice of strings for a given query key, plus
// a boolean value whether at least one value exists for the given key. // a boolean value whether at least one value exists for the given key.
func (c *Context) GetQueryArray(key string) ([]string, bool) { func (c *Context) GetQueryArray(key string) ([]string, bool) {
c.getQueryCache() c.initQueryCache()
if values, ok := c.queryCache[key]; ok && len(values) > 0 { if values, ok := c.queryCache[key]; ok && len(values) > 0 {
return values, true return values, true
} }
@ -417,7 +439,7 @@ func (c *Context) QueryMap(key string) map[string]string {
// GetQueryMap returns a map for a given query key, plus a boolean value // GetQueryMap returns a map for a given query key, plus a boolean value
// whether at least one value exists for the given key. // whether at least one value exists for the given key.
func (c *Context) GetQueryMap(key string) (map[string]string, bool) { func (c *Context) GetQueryMap(key string) (map[string]string, bool) {
c.getQueryCache() c.initQueryCache()
return c.get(c.queryCache, key) return c.get(c.queryCache, key)
} }
@ -459,7 +481,7 @@ func (c *Context) PostFormArray(key string) []string {
return values return values
} }
func (c *Context) getFormCache() { func (c *Context) initFormCache() {
if c.formCache == nil { if c.formCache == nil {
c.formCache = make(url.Values) c.formCache = make(url.Values)
req := c.Request req := c.Request
@ -475,7 +497,7 @@ func (c *Context) getFormCache() {
// GetPostFormArray returns a slice of strings for a given form key, plus // GetPostFormArray returns a slice of strings for a given form key, plus
// a boolean value whether at least one value exists for the given key. // a boolean value whether at least one value exists for the given key.
func (c *Context) GetPostFormArray(key string) ([]string, bool) { func (c *Context) GetPostFormArray(key string) ([]string, bool) {
c.getFormCache() c.initFormCache()
if values := c.formCache[key]; len(values) > 0 { if values := c.formCache[key]; len(values) > 0 {
return values, true return values, true
} }
@ -491,7 +513,7 @@ func (c *Context) PostFormMap(key string) map[string]string {
// GetPostFormMap returns a map for a given form key, plus a boolean value // GetPostFormMap returns a map for a given form key, plus a boolean value
// whether at least one value exists for the given key. // whether at least one value exists for the given key.
func (c *Context) GetPostFormMap(key string) (map[string]string, bool) { func (c *Context) GetPostFormMap(key string) (map[string]string, bool) {
c.getFormCache() c.initFormCache()
return c.get(c.formCache, key) return c.get(c.formCache, key)
} }
@ -517,7 +539,11 @@ func (c *Context) FormFile(name string) (*multipart.FileHeader, error) {
return nil, err return nil, err
} }
} }
_, fh, err := c.Request.FormFile(name) f, fh, err := c.Request.FormFile(name)
if err != nil {
return nil, err
}
f.Close()
return fh, err return fh, err
} }
@ -745,7 +771,7 @@ func bodyAllowedForStatus(status int) bool {
// Status sets the HTTP response code. // Status sets the HTTP response code.
func (c *Context) Status(code int) { func (c *Context) Status(code int) {
c.writermem.WriteHeader(code) c.Writer.WriteHeader(code)
} }
// Header is a intelligent shortcut for c.Writer.Header().Set(key, value). // Header is a intelligent shortcut for c.Writer.Header().Set(key, value).
@ -769,6 +795,11 @@ func (c *Context) GetRawData() ([]byte, error) {
return ioutil.ReadAll(c.Request.Body) return ioutil.ReadAll(c.Request.Body)
} }
// SetSameSite with cookie
func (c *Context) SetSameSite(samesite http.SameSite) {
c.sameSite = samesite
}
// SetCookie adds a Set-Cookie header to the ResponseWriter's headers. // SetCookie adds a Set-Cookie header to the ResponseWriter's headers.
// The provided cookie must have a valid Name. Invalid cookies may be // The provided cookie must have a valid Name. Invalid cookies may be
// silently dropped. // silently dropped.
@ -782,6 +813,7 @@ func (c *Context) SetCookie(name, value string, maxAge int, path, domain string,
MaxAge: maxAge, MaxAge: maxAge,
Path: path, Path: path,
Domain: domain, Domain: domain,
SameSite: c.sameSite,
Secure: secure, Secure: secure,
HttpOnly: httpOnly, HttpOnly: httpOnly,
}) })
@ -835,7 +867,7 @@ func (c *Context) IndentedJSON(code int, obj interface{}) {
// Default prepends "while(1)," to response body if the given struct is array values. // Default prepends "while(1)," to response body if the given struct is array values.
// It also sets the Content-Type as "application/json". // It also sets the Content-Type as "application/json".
func (c *Context) SecureJSON(code int, obj interface{}) { func (c *Context) SecureJSON(code int, obj interface{}) {
c.Render(code, render.SecureJSON{Prefix: c.engine.secureJsonPrefix, Data: obj}) c.Render(code, render.SecureJSON{Prefix: c.engine.secureJSONPrefix, Data: obj})
} }
// JSONP serializes the given struct as JSON into the response body. // JSONP serializes the given struct as JSON into the response body.
@ -921,6 +953,17 @@ func (c *Context) File(filepath string) {
http.ServeFile(c.Writer, c.Request, filepath) http.ServeFile(c.Writer, c.Request, filepath)
} }
// FileFromFS writes the specified file from http.FileSytem into the body stream in an efficient way.
func (c *Context) FileFromFS(filepath string, fs http.FileSystem) {
defer func(old string) {
c.Request.URL.Path = old
}(c.Request.URL.Path)
c.Request.URL.Path = filepath
http.FileServer(fs).ServeHTTP(c.Writer, c.Request)
}
// FileAttachment writes the specified file into the body stream in an efficient way // FileAttachment writes the specified file into the body stream in an efficient way
// On the client side, the file will typically be downloaded with the given filename // On the client side, the file will typically be downloaded with the given filename
func (c *Context) FileAttachment(filepath, filename string) { func (c *Context) FileAttachment(filepath, filename string) {
@ -966,6 +1009,7 @@ type Negotiate struct {
HTMLData interface{} HTMLData interface{}
JSONData interface{} JSONData interface{}
XMLData interface{} XMLData interface{}
YAMLData interface{}
Data interface{} Data interface{}
} }
@ -984,6 +1028,10 @@ func (c *Context) Negotiate(code int, config Negotiate) {
data := chooseData(config.XMLData, config.Data) data := chooseData(config.XMLData, config.Data)
c.XML(code, data) c.XML(code, data)
case binding.MIMEYAML:
data := chooseData(config.YAMLData, config.Data)
c.YAML(code, data)
default: default:
c.AbortWithError(http.StatusNotAcceptable, errors.New("the accepted formats are not offered by the server")) // nolint: errcheck c.AbortWithError(http.StatusNotAcceptable, errors.New("the accepted formats are not offered by the server")) // nolint: errcheck
} }
@ -1000,20 +1048,20 @@ func (c *Context) NegotiateFormat(offered ...string) string {
return offered[0] return offered[0]
} }
for _, accepted := range c.Accepted { for _, accepted := range c.Accepted {
for _, offert := range offered { for _, offer := range offered {
// According to RFC 2616 and RFC 2396, non-ASCII characters are not allowed in headers, // According to RFC 2616 and RFC 2396, non-ASCII characters are not allowed in headers,
// therefore we can just iterate over the string without casting it into []rune // therefore we can just iterate over the string without casting it into []rune
i := 0 i := 0
for ; i < len(accepted); i++ { for ; i < len(accepted); i++ {
if accepted[i] == '*' || offert[i] == '*' { if accepted[i] == '*' || offer[i] == '*' {
return offert return offer
} }
if accepted[i] != offert[i] { if accepted[i] != offer[i] {
break break
} }
} }
if i == len(accepted) { if i == len(accepted) {
return offert return offer
} }
} }
} }
@ -1029,26 +1077,20 @@ func (c *Context) SetAccepted(formats ...string) {
/***** GOLANG.ORG/X/NET/CONTEXT *****/ /***** GOLANG.ORG/X/NET/CONTEXT *****/
/************************************/ /************************************/
// Deadline returns the time when work done on behalf of this context // Deadline always returns that there is no deadline (ok==false),
// should be canceled. Deadline returns ok==false when no deadline is // maybe you want to use Request.Context().Deadline() instead.
// set. Successive calls to Deadline return the same results.
func (c *Context) Deadline() (deadline time.Time, ok bool) { func (c *Context) Deadline() (deadline time.Time, ok bool) {
return return
} }
// Done returns a channel that's closed when work done on behalf of this // Done always returns nil (chan which will wait forever),
// context should be canceled. Done may return nil if this context can // if you want to abort your work when the connection was closed
// never be canceled. Successive calls to Done return the same value. // you should use Request.Context().Done() instead.
func (c *Context) Done() <-chan struct{} { func (c *Context) Done() <-chan struct{} {
return nil return nil
} }
// Err returns a non-nil error value after Done is closed, // Err always returns nil, maybe you want to use Request.Context().Err() instead.
// successive calls to Err return the same error.
// If Done is not yet closed, Err returns nil.
// If Done is closed, Err returns a non-nil error explaining why:
// Canceled if the context was canceled
// or DeadlineExceeded if the context's deadline passed.
func (c *Context) Err() error { func (c *Context) Err() error {
return nil return nil
} }

View File

@ -602,14 +602,16 @@ func TestContextPostFormMultipart(t *testing.T) {
func TestContextSetCookie(t *testing.T) { func TestContextSetCookie(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder()) c, _ := CreateTestContext(httptest.NewRecorder())
c.SetSameSite(http.SameSiteLaxMode)
c.SetCookie("user", "gin", 1, "/", "localhost", true, true) c.SetCookie("user", "gin", 1, "/", "localhost", true, true)
assert.Equal(t, "user=gin; Path=/; Domain=localhost; Max-Age=1; HttpOnly; Secure", c.Writer.Header().Get("Set-Cookie")) assert.Equal(t, "user=gin; Path=/; Domain=localhost; Max-Age=1; HttpOnly; Secure; SameSite=Lax", c.Writer.Header().Get("Set-Cookie"))
} }
func TestContextSetCookiePathEmpty(t *testing.T) { func TestContextSetCookiePathEmpty(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder()) c, _ := CreateTestContext(httptest.NewRecorder())
c.SetSameSite(http.SameSiteLaxMode)
c.SetCookie("user", "gin", 1, "", "localhost", true, true) c.SetCookie("user", "gin", 1, "", "localhost", true, true)
assert.Equal(t, "user=gin; Path=/; Domain=localhost; Max-Age=1; HttpOnly; Secure", c.Writer.Header().Get("Set-Cookie")) assert.Equal(t, "user=gin; Path=/; Domain=localhost; Max-Age=1; HttpOnly; Secure; SameSite=Lax", c.Writer.Header().Get("Set-Cookie"))
} }
func TestContextGetCookie(t *testing.T) { func TestContextGetCookie(t *testing.T) {
@ -662,7 +664,7 @@ func TestContextRenderJSON(t *testing.T) {
c.JSON(http.StatusCreated, H{"foo": "bar", "html": "<b>"}) c.JSON(http.StatusCreated, H{"foo": "bar", "html": "<b>"})
assert.Equal(t, http.StatusCreated, w.Code) assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"\\u003cb\\u003e\"}\n", w.Body.String()) assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"\\u003cb\\u003e\"}", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
} }
@ -690,7 +692,7 @@ func TestContextRenderJSONPWithoutCallback(t *testing.T) {
c.JSONP(http.StatusCreated, H{"foo": "bar"}) c.JSONP(http.StatusCreated, H{"foo": "bar"})
assert.Equal(t, http.StatusCreated, w.Code) assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, "{\"foo\":\"bar\"}\n", w.Body.String()) assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
} }
@ -716,7 +718,7 @@ func TestContextRenderAPIJSON(t *testing.T) {
c.JSON(http.StatusCreated, H{"foo": "bar"}) c.JSON(http.StatusCreated, H{"foo": "bar"})
assert.Equal(t, http.StatusCreated, w.Code) assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, "{\"foo\":\"bar\"}\n", w.Body.String()) assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String())
assert.Equal(t, "application/vnd.api+json", w.Header().Get("Content-Type")) assert.Equal(t, "application/vnd.api+json", w.Header().Get("Content-Type"))
} }
@ -992,6 +994,19 @@ func TestContextRenderFile(t *testing.T) {
assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
} }
func TestContextRenderFileFromFS(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "/some/path", nil)
c.FileFromFS("./gin.go", Dir(".", false))
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "func New() *Engine {")
assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
assert.Equal(t, "/some/path", c.Request.URL.Path)
}
func TestContextRenderAttachment(t *testing.T) { func TestContextRenderAttachment(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
c, _ := CreateTestContext(w) c, _ := CreateTestContext(w)
@ -1114,12 +1129,12 @@ func TestContextNegotiationWithJSON(t *testing.T) {
c.Request, _ = http.NewRequest("POST", "", nil) c.Request, _ = http.NewRequest("POST", "", nil)
c.Negotiate(http.StatusOK, Negotiate{ c.Negotiate(http.StatusOK, Negotiate{
Offered: []string{MIMEJSON, MIMEXML}, Offered: []string{MIMEJSON, MIMEXML, MIMEYAML},
Data: H{"foo": "bar"}, Data: H{"foo": "bar"},
}) })
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "{\"foo\":\"bar\"}\n", w.Body.String()) assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
} }
@ -1129,7 +1144,7 @@ func TestContextNegotiationWithXML(t *testing.T) {
c.Request, _ = http.NewRequest("POST", "", nil) c.Request, _ = http.NewRequest("POST", "", nil)
c.Negotiate(http.StatusOK, Negotiate{ c.Negotiate(http.StatusOK, Negotiate{
Offered: []string{MIMEXML, MIMEJSON}, Offered: []string{MIMEXML, MIMEJSON, MIMEYAML},
Data: H{"foo": "bar"}, Data: H{"foo": "bar"},
}) })
@ -1283,7 +1298,7 @@ func TestContextAbortWithStatusJSON(t *testing.T) {
_, err := buf.ReadFrom(w.Body) _, err := buf.ReadFrom(w.Body)
assert.NoError(t, err) assert.NoError(t, err)
jsonStringBody := buf.String() jsonStringBody := buf.String()
assert.Equal(t, fmt.Sprint("{\"foo\":\"fooValue\",\"bar\":\"barValue\"}\n"), jsonStringBody) assert.Equal(t, fmt.Sprint("{\"foo\":\"fooValue\",\"bar\":\"barValue\"}"), jsonStringBody)
} }
func TestContextError(t *testing.T) { func TestContextError(t *testing.T) {
@ -1799,6 +1814,23 @@ func TestContextRenderDataFromReader(t *testing.T) {
assert.Equal(t, extraHeaders["Content-Disposition"], w.Header().Get("Content-Disposition")) assert.Equal(t, extraHeaders["Content-Disposition"], w.Header().Get("Content-Disposition"))
} }
func TestContextRenderDataFromReaderNoHeaders(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
body := "#!PNG some raw data"
reader := strings.NewReader(body)
contentLength := int64(len(body))
contentType := "image/png"
c.DataFromReader(http.StatusOK, contentLength, contentType, reader, nil)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, body, w.Body.String())
assert.Equal(t, contentType, w.Header().Get("Content-Type"))
assert.Equal(t, fmt.Sprintf("%d", contentLength), w.Header().Get("Content-Length"))
}
type TestResponseRecorder struct { type TestResponseRecorder struct {
*httptest.ResponseRecorder *httptest.ResponseRecorder
closeChannel chan bool closeChannel chan bool
@ -1888,3 +1920,16 @@ func TestRaceParamsContextCopy(t *testing.T) {
performRequest(router, "GET", "/name2/api") performRequest(router, "GET", "/name2/api")
wg.Wait() wg.Wait()
} }
func TestContextWithKeysMutex(t *testing.T) {
c := &Context{}
c.Set("foo", "bar")
value, err := c.Get("foo")
assert.Equal(t, "bar", value)
assert.True(t, err)
value, err = c.Get("foo2")
assert.Nil(t, value)
assert.False(t, err)
}

View File

@ -67,7 +67,7 @@ func getMinVer(v string) (uint64, error) {
func debugPrintWARNINGDefault() { func debugPrintWARNINGDefault() {
if v, e := getMinVer(runtime.Version()); e == nil && v <= ginSupportMinGoVer { if v, e := getMinVer(runtime.Version()); e == nil && v <= ginSupportMinGoVer {
debugPrint(`[WARNING] Now Gin requires Go 1.10 or later and Go 1.11 will be required soon. debugPrint(`[WARNING] Now Gin requires Go 1.11 or later and Go 1.12 will be required soon.
`) `)
} }

View File

@ -91,7 +91,7 @@ func TestDebugPrintWARNINGDefault(t *testing.T) {
}) })
m, e := getMinVer(runtime.Version()) m, e := getMinVer(runtime.Version())
if e == nil && m <= ginSupportMinGoVer { if e == nil && m <= ginSupportMinGoVer {
assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.10 or later and Go 1.11 will be required soon.\n\n[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re) assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.11 or later and Go 1.12 will be required soon.\n\n[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
} else { } else {
assert.Equal(t, "[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re) assert.Equal(t, "[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
} }

84
gin.go
View File

@ -13,6 +13,7 @@ import (
"path" "path"
"sync" "sync"
"github.com/gin-gonic/gin/internal/bytesconv"
"github.com/gin-gonic/gin/render" "github.com/gin-gonic/gin/render"
) )
@ -21,16 +22,17 @@ const defaultMultipartMemory = 32 << 20 // 32 MB
var ( var (
default404Body = []byte("404 page not found") default404Body = []byte("404 page not found")
default405Body = []byte("405 method not allowed") default405Body = []byte("405 method not allowed")
defaultAppEngine bool
) )
var defaultAppEngine bool
// HandlerFunc defines the handler used by gin middleware as return value. // HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context) type HandlerFunc func(*Context)
// HandlersChain defines a HandlerFunc array. // HandlersChain defines a HandlerFunc array.
type HandlersChain []HandlerFunc type HandlersChain []HandlerFunc
// Last returns the last handler in the chain. ie. the last handler is the main own. // Last returns the last handler in the chain. ie. the last handler is the main one.
func (c HandlersChain) Last() HandlerFunc { func (c HandlersChain) Last() HandlerFunc {
if length := len(c); length > 0 { if length := len(c); length > 0 {
return c[length-1] return c[length-1]
@ -97,8 +99,12 @@ type Engine struct {
// method call. // method call.
MaxMultipartMemory int64 MaxMultipartMemory int64
// RemoveExtraSlash a parameter can be parsed from the URL even with extra slashes.
// See the PR #1817 and issue #1644
RemoveExtraSlash bool
delims render.Delims delims render.Delims
secureJsonPrefix string secureJSONPrefix string
HTMLRender render.HTMLRender HTMLRender render.HTMLRender
FuncMap template.FuncMap FuncMap template.FuncMap
allNoRoute HandlersChain allNoRoute HandlersChain
@ -107,6 +113,7 @@ type Engine struct {
noMethod HandlersChain noMethod HandlersChain
pool sync.Pool pool sync.Pool
trees methodTrees trees methodTrees
maxParams uint16
} }
var _ IRouter = &Engine{} var _ IRouter = &Engine{}
@ -134,11 +141,12 @@ func New() *Engine {
ForwardedByClientIP: true, ForwardedByClientIP: true,
AppEngine: defaultAppEngine, AppEngine: defaultAppEngine,
UseRawPath: false, UseRawPath: false,
RemoveExtraSlash: false,
UnescapePathValues: true, UnescapePathValues: true,
MaxMultipartMemory: defaultMultipartMemory, MaxMultipartMemory: defaultMultipartMemory,
trees: make(methodTrees, 0, 9), trees: make(methodTrees, 0, 9),
delims: render.Delims{Left: "{{", Right: "}}"}, delims: render.Delims{Left: "{{", Right: "}}"},
secureJsonPrefix: "while(1);", secureJSONPrefix: "while(1);",
} }
engine.RouterGroup.engine = engine engine.RouterGroup.engine = engine
engine.pool.New = func() interface{} { engine.pool.New = func() interface{} {
@ -156,7 +164,8 @@ func Default() *Engine {
} }
func (engine *Engine) allocateContext() *Context { func (engine *Engine) allocateContext() *Context {
return &Context{engine: engine} v := make(Params, 0, engine.maxParams)
return &Context{engine: engine, params: &v}
} }
// Delims sets template left and right delims and returns a Engine instance. // Delims sets template left and right delims and returns a Engine instance.
@ -165,9 +174,9 @@ func (engine *Engine) Delims(left, right string) *Engine {
return engine return engine
} }
// SecureJsonPrefix sets the secureJsonPrefix used in Context.SecureJSON. // SecureJsonPrefix sets the secureJSONPrefix used in Context.SecureJSON.
func (engine *Engine) SecureJsonPrefix(prefix string) *Engine { func (engine *Engine) SecureJsonPrefix(prefix string) *Engine {
engine.secureJsonPrefix = prefix engine.secureJSONPrefix = prefix
return engine return engine
} }
@ -249,6 +258,7 @@ func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
assert1(len(handlers) > 0, "there must be at least one handler") assert1(len(handlers) > 0, "there must be at least one handler")
debugPrintRoute(method, path, handlers) debugPrintRoute(method, path, handlers)
root := engine.trees.get(method) root := engine.trees.get(method)
if root == nil { if root == nil {
root = new(node) root = new(node)
@ -256,6 +266,11 @@ func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
engine.trees = append(engine.trees, methodTree{method: method, root: root}) engine.trees = append(engine.trees, methodTree{method: method, root: root})
} }
root.addRoute(path, handlers) root.addRoute(path, handlers)
// Update maxParams
if paramsCount := countParams(path); paramsCount > engine.maxParams {
engine.maxParams = paramsCount
}
} }
// Routes returns a slice of registered routes, including some useful information, such as: // Routes returns a slice of registered routes, including some useful information, such as:
@ -314,13 +329,13 @@ func (engine *Engine) RunUnix(file string) (err error) {
debugPrint("Listening and serving HTTP on unix:/%s", file) debugPrint("Listening and serving HTTP on unix:/%s", file)
defer func() { debugPrintError(err) }() defer func() { debugPrintError(err) }()
os.Remove(file)
listener, err := net.Listen("unix", file) listener, err := net.Listen("unix", file)
if err != nil { if err != nil {
return return
} }
defer listener.Close() defer listener.Close()
os.Chmod(file, 0777) defer os.Remove(file)
err = http.Serve(listener, engine) err = http.Serve(listener, engine)
return return
} }
@ -338,6 +353,15 @@ func (engine *Engine) RunFd(fd int) (err error) {
return return
} }
defer listener.Close() defer listener.Close()
err = engine.RunListener(listener)
return
}
// RunListener attaches the router to a http.Server and starts listening and serving HTTP requests
// through the specified net.Listener
func (engine *Engine) RunListener(listener net.Listener) (err error) {
debugPrint("Listening and serving HTTP on listener what's bind with address@%s", listener.Addr())
defer func() { debugPrintError(err) }()
err = http.Serve(listener, engine) err = http.Serve(listener, engine)
return return
} }
@ -373,7 +397,10 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
rPath = c.Request.URL.RawPath rPath = c.Request.URL.RawPath
unescape = engine.UnescapePathValues unescape = engine.UnescapePathValues
} }
if engine.RemoveExtraSlash {
rPath = cleanPath(rPath) rPath = cleanPath(rPath)
}
// Find root of the tree for the given HTTP method // Find root of the tree for the given HTTP method
t := engine.trees t := engine.trees
@ -383,10 +410,12 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
} }
root := t[i].root root := t[i].root
// Find route in tree // Find route in tree
value := root.getValue(rPath, c.Params, unescape) value := root.getValue(rPath, c.params, unescape)
if value.params != nil {
c.Params = *value.params
}
if value.handlers != nil { if value.handlers != nil {
c.handlers = value.handlers c.handlers = value.handlers
c.Params = value.params
c.fullPath = value.fullPath c.fullPath = value.fullPath
c.Next() c.Next()
c.writermem.WriteHeaderNow() c.writermem.WriteHeaderNow()
@ -445,18 +474,11 @@ func redirectTrailingSlash(c *Context) {
if prefix := path.Clean(c.Request.Header.Get("X-Forwarded-Prefix")); prefix != "." { if prefix := path.Clean(c.Request.Header.Get("X-Forwarded-Prefix")); prefix != "." {
p = prefix + "/" + req.URL.Path p = prefix + "/" + req.URL.Path
} }
code := http.StatusMovedPermanently // Permanent redirect, request with GET method
if req.Method != "GET" {
code = http.StatusTemporaryRedirect
}
req.URL.Path = p + "/" req.URL.Path = p + "/"
if length := len(p); length > 1 && p[length-1] == '/' { if length := len(p); length > 1 && p[length-1] == '/' {
req.URL.Path = p[:length-1] req.URL.Path = p[:length-1]
} }
debugPrint("redirecting request %d: %s --> %s", code, p, req.URL.String()) redirectRequest(c)
http.Redirect(c.Writer, req, req.URL.String(), code)
c.writermem.WriteHeaderNow()
} }
func redirectFixedPath(c *Context, root *node, trailingSlash bool) bool { func redirectFixedPath(c *Context, root *node, trailingSlash bool) bool {
@ -464,15 +486,23 @@ func redirectFixedPath(c *Context, root *node, trailingSlash bool) bool {
rPath := req.URL.Path rPath := req.URL.Path
if fixedPath, ok := root.findCaseInsensitivePath(cleanPath(rPath), trailingSlash); ok { if fixedPath, ok := root.findCaseInsensitivePath(cleanPath(rPath), trailingSlash); ok {
code := http.StatusMovedPermanently // Permanent redirect, request with GET method req.URL.Path = bytesconv.BytesToString(fixedPath)
if req.Method != "GET" { redirectRequest(c)
code = http.StatusTemporaryRedirect
}
req.URL.Path = string(fixedPath)
debugPrint("redirecting request %d: %s --> %s", code, rPath, req.URL.String())
http.Redirect(c.Writer, req, req.URL.String(), code)
c.writermem.WriteHeaderNow()
return true return true
} }
return false return false
} }
func redirectRequest(c *Context) {
req := c.Request
rPath := req.URL.Path
rURL := req.URL.String()
code := http.StatusMovedPermanently // Permanent redirect, request with GET method
if req.Method != http.MethodGet {
code = http.StatusTemporaryRedirect
}
debugPrint("redirecting request %d: %s --> %s", code, rPath, rURL)
http.Redirect(c.Writer, req, rURL, code)
c.writermem.WriteHeaderNow()
}

View File

@ -90,7 +90,8 @@ func TestPusher(t *testing.T) {
go func() { go func() {
router.GET("/pusher", func(c *Context) { router.GET("/pusher", func(c *Context) {
if pusher := c.Writer.Pusher(); pusher != nil { if pusher := c.Writer.Pusher(); pusher != nil {
pusher.Push("/assets/app.js", nil) err := pusher.Push("/assets/app.js", nil)
assert.NoError(t, err)
} }
c.String(http.StatusOK, "it worked") c.String(http.StatusOK, "it worked")
}) })
@ -145,15 +146,19 @@ func TestRunWithPort(t *testing.T) {
func TestUnixSocket(t *testing.T) { func TestUnixSocket(t *testing.T) {
router := New() router := New()
unixTestSocket := "/tmp/unix_unit_test"
defer os.Remove(unixTestSocket)
go func() { go func() {
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") }) router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
assert.NoError(t, router.RunUnix("/tmp/unix_unit_test")) assert.NoError(t, router.RunUnix(unixTestSocket))
}() }()
// have to wait for the goroutine to start and run the server // have to wait for the goroutine to start and run the server
// otherwise the main thread will complete // otherwise the main thread will complete
time.Sleep(5 * time.Millisecond) time.Sleep(5 * time.Millisecond)
c, err := net.Dial("unix", "/tmp/unix_unit_test") c, err := net.Dial("unix", unixTestSocket)
assert.NoError(t, err) assert.NoError(t, err)
fmt.Fprint(c, "GET /example HTTP/1.0\r\n\r\n") fmt.Fprint(c, "GET /example HTTP/1.0\r\n\r\n")
@ -207,6 +212,43 @@ func TestBadFileDescriptor(t *testing.T) {
assert.Error(t, router.RunFd(0)) assert.Error(t, router.RunFd(0))
} }
func TestListener(t *testing.T) {
router := New()
addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
assert.NoError(t, err)
listener, err := net.ListenTCP("tcp", addr)
assert.NoError(t, err)
go func() {
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
assert.NoError(t, router.RunListener(listener))
}()
// have to wait for the goroutine to start and run the server
// otherwise the main thread will complete
time.Sleep(5 * time.Millisecond)
c, err := net.Dial("tcp", listener.Addr().String())
assert.NoError(t, err)
fmt.Fprintf(c, "GET /example HTTP/1.0\r\n\r\n")
scanner := bufio.NewScanner(c)
var response string
for scanner.Scan() {
response += scanner.Text()
}
assert.Contains(t, response, "HTTP/1.0 200", "should get a 200")
assert.Contains(t, response, "it worked", "resp body should match")
}
func TestBadListener(t *testing.T) {
router := New()
addr, err := net.ResolveTCPAddr("tcp", "localhost:10086")
assert.NoError(t, err)
listener, err := net.ListenTCP("tcp", addr)
assert.NoError(t, err)
listener.Close()
assert.Error(t, router.RunListener(listener))
}
func TestWithHttptestWithAutoSelectedPort(t *testing.T) { func TestWithHttptestWithAutoSelectedPort(t *testing.T) {
router := New() router := New()
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") }) router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
@ -253,7 +295,7 @@ func TestConcurrentHandleContext(t *testing.T) {
// } // }
func testGetRequestHandler(t *testing.T, h http.Handler, url string) { func testGetRequestHandler(t *testing.T, h http.Handler, url string) {
req, err := http.NewRequest("GET", url, nil) req, err := http.NewRequest(http.MethodGet, url, nil)
assert.NoError(t, err) assert.NoError(t, err)
w := httptest.NewRecorder() w := httptest.NewRecorder()

View File

@ -24,265 +24,265 @@ type route struct {
// http://developer.github.com/v3/ // http://developer.github.com/v3/
var githubAPI = []route{ var githubAPI = []route{
// OAuth Authorizations // OAuth Authorizations
{"GET", "/authorizations"}, {http.MethodGet, "/authorizations"},
{"GET", "/authorizations/:id"}, {http.MethodGet, "/authorizations/:id"},
{"POST", "/authorizations"}, {http.MethodPost, "/authorizations"},
//{"PUT", "/authorizations/clients/:client_id"}, //{http.MethodPut, "/authorizations/clients/:client_id"},
//{"PATCH", "/authorizations/:id"}, //{http.MethodPatch, "/authorizations/:id"},
{"DELETE", "/authorizations/:id"}, {http.MethodDelete, "/authorizations/:id"},
{"GET", "/applications/:client_id/tokens/:access_token"}, {http.MethodGet, "/applications/:client_id/tokens/:access_token"},
{"DELETE", "/applications/:client_id/tokens"}, {http.MethodDelete, "/applications/:client_id/tokens"},
{"DELETE", "/applications/:client_id/tokens/:access_token"}, {http.MethodDelete, "/applications/:client_id/tokens/:access_token"},
// Activity // Activity
{"GET", "/events"}, {http.MethodGet, "/events"},
{"GET", "/repos/:owner/:repo/events"}, {http.MethodGet, "/repos/:owner/:repo/events"},
{"GET", "/networks/:owner/:repo/events"}, {http.MethodGet, "/networks/:owner/:repo/events"},
{"GET", "/orgs/:org/events"}, {http.MethodGet, "/orgs/:org/events"},
{"GET", "/users/:user/received_events"}, {http.MethodGet, "/users/:user/received_events"},
{"GET", "/users/:user/received_events/public"}, {http.MethodGet, "/users/:user/received_events/public"},
{"GET", "/users/:user/events"}, {http.MethodGet, "/users/:user/events"},
{"GET", "/users/:user/events/public"}, {http.MethodGet, "/users/:user/events/public"},
{"GET", "/users/:user/events/orgs/:org"}, {http.MethodGet, "/users/:user/events/orgs/:org"},
{"GET", "/feeds"}, {http.MethodGet, "/feeds"},
{"GET", "/notifications"}, {http.MethodGet, "/notifications"},
{"GET", "/repos/:owner/:repo/notifications"}, {http.MethodGet, "/repos/:owner/:repo/notifications"},
{"PUT", "/notifications"}, {http.MethodPut, "/notifications"},
{"PUT", "/repos/:owner/:repo/notifications"}, {http.MethodPut, "/repos/:owner/:repo/notifications"},
{"GET", "/notifications/threads/:id"}, {http.MethodGet, "/notifications/threads/:id"},
//{"PATCH", "/notifications/threads/:id"}, //{http.MethodPatch, "/notifications/threads/:id"},
{"GET", "/notifications/threads/:id/subscription"}, {http.MethodGet, "/notifications/threads/:id/subscription"},
{"PUT", "/notifications/threads/:id/subscription"}, {http.MethodPut, "/notifications/threads/:id/subscription"},
{"DELETE", "/notifications/threads/:id/subscription"}, {http.MethodDelete, "/notifications/threads/:id/subscription"},
{"GET", "/repos/:owner/:repo/stargazers"}, {http.MethodGet, "/repos/:owner/:repo/stargazers"},
{"GET", "/users/:user/starred"}, {http.MethodGet, "/users/:user/starred"},
{"GET", "/user/starred"}, {http.MethodGet, "/user/starred"},
{"GET", "/user/starred/:owner/:repo"}, {http.MethodGet, "/user/starred/:owner/:repo"},
{"PUT", "/user/starred/:owner/:repo"}, {http.MethodPut, "/user/starred/:owner/:repo"},
{"DELETE", "/user/starred/:owner/:repo"}, {http.MethodDelete, "/user/starred/:owner/:repo"},
{"GET", "/repos/:owner/:repo/subscribers"}, {http.MethodGet, "/repos/:owner/:repo/subscribers"},
{"GET", "/users/:user/subscriptions"}, {http.MethodGet, "/users/:user/subscriptions"},
{"GET", "/user/subscriptions"}, {http.MethodGet, "/user/subscriptions"},
{"GET", "/repos/:owner/:repo/subscription"}, {http.MethodGet, "/repos/:owner/:repo/subscription"},
{"PUT", "/repos/:owner/:repo/subscription"}, {http.MethodPut, "/repos/:owner/:repo/subscription"},
{"DELETE", "/repos/:owner/:repo/subscription"}, {http.MethodDelete, "/repos/:owner/:repo/subscription"},
{"GET", "/user/subscriptions/:owner/:repo"}, {http.MethodGet, "/user/subscriptions/:owner/:repo"},
{"PUT", "/user/subscriptions/:owner/:repo"}, {http.MethodPut, "/user/subscriptions/:owner/:repo"},
{"DELETE", "/user/subscriptions/:owner/:repo"}, {http.MethodDelete, "/user/subscriptions/:owner/:repo"},
// Gists // Gists
{"GET", "/users/:user/gists"}, {http.MethodGet, "/users/:user/gists"},
{"GET", "/gists"}, {http.MethodGet, "/gists"},
//{"GET", "/gists/public"}, //{http.MethodGet, "/gists/public"},
//{"GET", "/gists/starred"}, //{http.MethodGet, "/gists/starred"},
{"GET", "/gists/:id"}, {http.MethodGet, "/gists/:id"},
{"POST", "/gists"}, {http.MethodPost, "/gists"},
//{"PATCH", "/gists/:id"}, //{http.MethodPatch, "/gists/:id"},
{"PUT", "/gists/:id/star"}, {http.MethodPut, "/gists/:id/star"},
{"DELETE", "/gists/:id/star"}, {http.MethodDelete, "/gists/:id/star"},
{"GET", "/gists/:id/star"}, {http.MethodGet, "/gists/:id/star"},
{"POST", "/gists/:id/forks"}, {http.MethodPost, "/gists/:id/forks"},
{"DELETE", "/gists/:id"}, {http.MethodDelete, "/gists/:id"},
// Git Data // Git Data
{"GET", "/repos/:owner/:repo/git/blobs/:sha"}, {http.MethodGet, "/repos/:owner/:repo/git/blobs/:sha"},
{"POST", "/repos/:owner/:repo/git/blobs"}, {http.MethodPost, "/repos/:owner/:repo/git/blobs"},
{"GET", "/repos/:owner/:repo/git/commits/:sha"}, {http.MethodGet, "/repos/:owner/:repo/git/commits/:sha"},
{"POST", "/repos/:owner/:repo/git/commits"}, {http.MethodPost, "/repos/:owner/:repo/git/commits"},
//{"GET", "/repos/:owner/:repo/git/refs/*ref"}, //{http.MethodGet, "/repos/:owner/:repo/git/refs/*ref"},
{"GET", "/repos/:owner/:repo/git/refs"}, {http.MethodGet, "/repos/:owner/:repo/git/refs"},
{"POST", "/repos/:owner/:repo/git/refs"}, {http.MethodPost, "/repos/:owner/:repo/git/refs"},
//{"PATCH", "/repos/:owner/:repo/git/refs/*ref"}, //{http.MethodPatch, "/repos/:owner/:repo/git/refs/*ref"},
//{"DELETE", "/repos/:owner/:repo/git/refs/*ref"}, //{http.MethodDelete, "/repos/:owner/:repo/git/refs/*ref"},
{"GET", "/repos/:owner/:repo/git/tags/:sha"}, {http.MethodGet, "/repos/:owner/:repo/git/tags/:sha"},
{"POST", "/repos/:owner/:repo/git/tags"}, {http.MethodPost, "/repos/:owner/:repo/git/tags"},
{"GET", "/repos/:owner/:repo/git/trees/:sha"}, {http.MethodGet, "/repos/:owner/:repo/git/trees/:sha"},
{"POST", "/repos/:owner/:repo/git/trees"}, {http.MethodPost, "/repos/:owner/:repo/git/trees"},
// Issues // Issues
{"GET", "/issues"}, {http.MethodGet, "/issues"},
{"GET", "/user/issues"}, {http.MethodGet, "/user/issues"},
{"GET", "/orgs/:org/issues"}, {http.MethodGet, "/orgs/:org/issues"},
{"GET", "/repos/:owner/:repo/issues"}, {http.MethodGet, "/repos/:owner/:repo/issues"},
{"GET", "/repos/:owner/:repo/issues/:number"}, {http.MethodGet, "/repos/:owner/:repo/issues/:number"},
{"POST", "/repos/:owner/:repo/issues"}, {http.MethodPost, "/repos/:owner/:repo/issues"},
//{"PATCH", "/repos/:owner/:repo/issues/:number"}, //{http.MethodPatch, "/repos/:owner/:repo/issues/:number"},
{"GET", "/repos/:owner/:repo/assignees"}, {http.MethodGet, "/repos/:owner/:repo/assignees"},
{"GET", "/repos/:owner/:repo/assignees/:assignee"}, {http.MethodGet, "/repos/:owner/:repo/assignees/:assignee"},
{"GET", "/repos/:owner/:repo/issues/:number/comments"}, {http.MethodGet, "/repos/:owner/:repo/issues/:number/comments"},
//{"GET", "/repos/:owner/:repo/issues/comments"}, //{http.MethodGet, "/repos/:owner/:repo/issues/comments"},
//{"GET", "/repos/:owner/:repo/issues/comments/:id"}, //{http.MethodGet, "/repos/:owner/:repo/issues/comments/:id"},
{"POST", "/repos/:owner/:repo/issues/:number/comments"}, {http.MethodPost, "/repos/:owner/:repo/issues/:number/comments"},
//{"PATCH", "/repos/:owner/:repo/issues/comments/:id"}, //{http.MethodPatch, "/repos/:owner/:repo/issues/comments/:id"},
//{"DELETE", "/repos/:owner/:repo/issues/comments/:id"}, //{http.MethodDelete, "/repos/:owner/:repo/issues/comments/:id"},
{"GET", "/repos/:owner/:repo/issues/:number/events"}, {http.MethodGet, "/repos/:owner/:repo/issues/:number/events"},
//{"GET", "/repos/:owner/:repo/issues/events"}, //{http.MethodGet, "/repos/:owner/:repo/issues/events"},
//{"GET", "/repos/:owner/:repo/issues/events/:id"}, //{http.MethodGet, "/repos/:owner/:repo/issues/events/:id"},
{"GET", "/repos/:owner/:repo/labels"}, {http.MethodGet, "/repos/:owner/:repo/labels"},
{"GET", "/repos/:owner/:repo/labels/:name"}, {http.MethodGet, "/repos/:owner/:repo/labels/:name"},
{"POST", "/repos/:owner/:repo/labels"}, {http.MethodPost, "/repos/:owner/:repo/labels"},
//{"PATCH", "/repos/:owner/:repo/labels/:name"}, //{http.MethodPatch, "/repos/:owner/:repo/labels/:name"},
{"DELETE", "/repos/:owner/:repo/labels/:name"}, {http.MethodDelete, "/repos/:owner/:repo/labels/:name"},
{"GET", "/repos/:owner/:repo/issues/:number/labels"}, {http.MethodGet, "/repos/:owner/:repo/issues/:number/labels"},
{"POST", "/repos/:owner/:repo/issues/:number/labels"}, {http.MethodPost, "/repos/:owner/:repo/issues/:number/labels"},
{"DELETE", "/repos/:owner/:repo/issues/:number/labels/:name"}, {http.MethodDelete, "/repos/:owner/:repo/issues/:number/labels/:name"},
{"PUT", "/repos/:owner/:repo/issues/:number/labels"}, {http.MethodPut, "/repos/:owner/:repo/issues/:number/labels"},
{"DELETE", "/repos/:owner/:repo/issues/:number/labels"}, {http.MethodDelete, "/repos/:owner/:repo/issues/:number/labels"},
{"GET", "/repos/:owner/:repo/milestones/:number/labels"}, {http.MethodGet, "/repos/:owner/:repo/milestones/:number/labels"},
{"GET", "/repos/:owner/:repo/milestones"}, {http.MethodGet, "/repos/:owner/:repo/milestones"},
{"GET", "/repos/:owner/:repo/milestones/:number"}, {http.MethodGet, "/repos/:owner/:repo/milestones/:number"},
{"POST", "/repos/:owner/:repo/milestones"}, {http.MethodPost, "/repos/:owner/:repo/milestones"},
//{"PATCH", "/repos/:owner/:repo/milestones/:number"}, //{http.MethodPatch, "/repos/:owner/:repo/milestones/:number"},
{"DELETE", "/repos/:owner/:repo/milestones/:number"}, {http.MethodDelete, "/repos/:owner/:repo/milestones/:number"},
// Miscellaneous // Miscellaneous
{"GET", "/emojis"}, {http.MethodGet, "/emojis"},
{"GET", "/gitignore/templates"}, {http.MethodGet, "/gitignore/templates"},
{"GET", "/gitignore/templates/:name"}, {http.MethodGet, "/gitignore/templates/:name"},
{"POST", "/markdown"}, {http.MethodPost, "/markdown"},
{"POST", "/markdown/raw"}, {http.MethodPost, "/markdown/raw"},
{"GET", "/meta"}, {http.MethodGet, "/meta"},
{"GET", "/rate_limit"}, {http.MethodGet, "/rate_limit"},
// Organizations // Organizations
{"GET", "/users/:user/orgs"}, {http.MethodGet, "/users/:user/orgs"},
{"GET", "/user/orgs"}, {http.MethodGet, "/user/orgs"},
{"GET", "/orgs/:org"}, {http.MethodGet, "/orgs/:org"},
//{"PATCH", "/orgs/:org"}, //{http.MethodPatch, "/orgs/:org"},
{"GET", "/orgs/:org/members"}, {http.MethodGet, "/orgs/:org/members"},
{"GET", "/orgs/:org/members/:user"}, {http.MethodGet, "/orgs/:org/members/:user"},
{"DELETE", "/orgs/:org/members/:user"}, {http.MethodDelete, "/orgs/:org/members/:user"},
{"GET", "/orgs/:org/public_members"}, {http.MethodGet, "/orgs/:org/public_members"},
{"GET", "/orgs/:org/public_members/:user"}, {http.MethodGet, "/orgs/:org/public_members/:user"},
{"PUT", "/orgs/:org/public_members/:user"}, {http.MethodPut, "/orgs/:org/public_members/:user"},
{"DELETE", "/orgs/:org/public_members/:user"}, {http.MethodDelete, "/orgs/:org/public_members/:user"},
{"GET", "/orgs/:org/teams"}, {http.MethodGet, "/orgs/:org/teams"},
{"GET", "/teams/:id"}, {http.MethodGet, "/teams/:id"},
{"POST", "/orgs/:org/teams"}, {http.MethodPost, "/orgs/:org/teams"},
//{"PATCH", "/teams/:id"}, //{http.MethodPatch, "/teams/:id"},
{"DELETE", "/teams/:id"}, {http.MethodDelete, "/teams/:id"},
{"GET", "/teams/:id/members"}, {http.MethodGet, "/teams/:id/members"},
{"GET", "/teams/:id/members/:user"}, {http.MethodGet, "/teams/:id/members/:user"},
{"PUT", "/teams/:id/members/:user"}, {http.MethodPut, "/teams/:id/members/:user"},
{"DELETE", "/teams/:id/members/:user"}, {http.MethodDelete, "/teams/:id/members/:user"},
{"GET", "/teams/:id/repos"}, {http.MethodGet, "/teams/:id/repos"},
{"GET", "/teams/:id/repos/:owner/:repo"}, {http.MethodGet, "/teams/:id/repos/:owner/:repo"},
{"PUT", "/teams/:id/repos/:owner/:repo"}, {http.MethodPut, "/teams/:id/repos/:owner/:repo"},
{"DELETE", "/teams/:id/repos/:owner/:repo"}, {http.MethodDelete, "/teams/:id/repos/:owner/:repo"},
{"GET", "/user/teams"}, {http.MethodGet, "/user/teams"},
// Pull Requests // Pull Requests
{"GET", "/repos/:owner/:repo/pulls"}, {http.MethodGet, "/repos/:owner/:repo/pulls"},
{"GET", "/repos/:owner/:repo/pulls/:number"}, {http.MethodGet, "/repos/:owner/:repo/pulls/:number"},
{"POST", "/repos/:owner/:repo/pulls"}, {http.MethodPost, "/repos/:owner/:repo/pulls"},
//{"PATCH", "/repos/:owner/:repo/pulls/:number"}, //{http.MethodPatch, "/repos/:owner/:repo/pulls/:number"},
{"GET", "/repos/:owner/:repo/pulls/:number/commits"}, {http.MethodGet, "/repos/:owner/:repo/pulls/:number/commits"},
{"GET", "/repos/:owner/:repo/pulls/:number/files"}, {http.MethodGet, "/repos/:owner/:repo/pulls/:number/files"},
{"GET", "/repos/:owner/:repo/pulls/:number/merge"}, {http.MethodGet, "/repos/:owner/:repo/pulls/:number/merge"},
{"PUT", "/repos/:owner/:repo/pulls/:number/merge"}, {http.MethodPut, "/repos/:owner/:repo/pulls/:number/merge"},
{"GET", "/repos/:owner/:repo/pulls/:number/comments"}, {http.MethodGet, "/repos/:owner/:repo/pulls/:number/comments"},
//{"GET", "/repos/:owner/:repo/pulls/comments"}, //{http.MethodGet, "/repos/:owner/:repo/pulls/comments"},
//{"GET", "/repos/:owner/:repo/pulls/comments/:number"}, //{http.MethodGet, "/repos/:owner/:repo/pulls/comments/:number"},
{"PUT", "/repos/:owner/:repo/pulls/:number/comments"}, {http.MethodPut, "/repos/:owner/:repo/pulls/:number/comments"},
//{"PATCH", "/repos/:owner/:repo/pulls/comments/:number"}, //{http.MethodPatch, "/repos/:owner/:repo/pulls/comments/:number"},
//{"DELETE", "/repos/:owner/:repo/pulls/comments/:number"}, //{http.MethodDelete, "/repos/:owner/:repo/pulls/comments/:number"},
// Repositories // Repositories
{"GET", "/user/repos"}, {http.MethodGet, "/user/repos"},
{"GET", "/users/:user/repos"}, {http.MethodGet, "/users/:user/repos"},
{"GET", "/orgs/:org/repos"}, {http.MethodGet, "/orgs/:org/repos"},
{"GET", "/repositories"}, {http.MethodGet, "/repositories"},
{"POST", "/user/repos"}, {http.MethodPost, "/user/repos"},
{"POST", "/orgs/:org/repos"}, {http.MethodPost, "/orgs/:org/repos"},
{"GET", "/repos/:owner/:repo"}, {http.MethodGet, "/repos/:owner/:repo"},
//{"PATCH", "/repos/:owner/:repo"}, //{http.MethodPatch, "/repos/:owner/:repo"},
{"GET", "/repos/:owner/:repo/contributors"}, {http.MethodGet, "/repos/:owner/:repo/contributors"},
{"GET", "/repos/:owner/:repo/languages"}, {http.MethodGet, "/repos/:owner/:repo/languages"},
{"GET", "/repos/:owner/:repo/teams"}, {http.MethodGet, "/repos/:owner/:repo/teams"},
{"GET", "/repos/:owner/:repo/tags"}, {http.MethodGet, "/repos/:owner/:repo/tags"},
{"GET", "/repos/:owner/:repo/branches"}, {http.MethodGet, "/repos/:owner/:repo/branches"},
{"GET", "/repos/:owner/:repo/branches/:branch"}, {http.MethodGet, "/repos/:owner/:repo/branches/:branch"},
{"DELETE", "/repos/:owner/:repo"}, {http.MethodDelete, "/repos/:owner/:repo"},
{"GET", "/repos/:owner/:repo/collaborators"}, {http.MethodGet, "/repos/:owner/:repo/collaborators"},
{"GET", "/repos/:owner/:repo/collaborators/:user"}, {http.MethodGet, "/repos/:owner/:repo/collaborators/:user"},
{"PUT", "/repos/:owner/:repo/collaborators/:user"}, {http.MethodPut, "/repos/:owner/:repo/collaborators/:user"},
{"DELETE", "/repos/:owner/:repo/collaborators/:user"}, {http.MethodDelete, "/repos/:owner/:repo/collaborators/:user"},
{"GET", "/repos/:owner/:repo/comments"}, {http.MethodGet, "/repos/:owner/:repo/comments"},
{"GET", "/repos/:owner/:repo/commits/:sha/comments"}, {http.MethodGet, "/repos/:owner/:repo/commits/:sha/comments"},
{"POST", "/repos/:owner/:repo/commits/:sha/comments"}, {http.MethodPost, "/repos/:owner/:repo/commits/:sha/comments"},
{"GET", "/repos/:owner/:repo/comments/:id"}, {http.MethodGet, "/repos/:owner/:repo/comments/:id"},
//{"PATCH", "/repos/:owner/:repo/comments/:id"}, //{http.MethodPatch, "/repos/:owner/:repo/comments/:id"},
{"DELETE", "/repos/:owner/:repo/comments/:id"}, {http.MethodDelete, "/repos/:owner/:repo/comments/:id"},
{"GET", "/repos/:owner/:repo/commits"}, {http.MethodGet, "/repos/:owner/:repo/commits"},
{"GET", "/repos/:owner/:repo/commits/:sha"}, {http.MethodGet, "/repos/:owner/:repo/commits/:sha"},
{"GET", "/repos/:owner/:repo/readme"}, {http.MethodGet, "/repos/:owner/:repo/readme"},
//{"GET", "/repos/:owner/:repo/contents/*path"}, //{http.MethodGet, "/repos/:owner/:repo/contents/*path"},
//{"PUT", "/repos/:owner/:repo/contents/*path"}, //{http.MethodPut, "/repos/:owner/:repo/contents/*path"},
//{"DELETE", "/repos/:owner/:repo/contents/*path"}, //{http.MethodDelete, "/repos/:owner/:repo/contents/*path"},
//{"GET", "/repos/:owner/:repo/:archive_format/:ref"}, //{http.MethodGet, "/repos/:owner/:repo/:archive_format/:ref"},
{"GET", "/repos/:owner/:repo/keys"}, {http.MethodGet, "/repos/:owner/:repo/keys"},
{"GET", "/repos/:owner/:repo/keys/:id"}, {http.MethodGet, "/repos/:owner/:repo/keys/:id"},
{"POST", "/repos/:owner/:repo/keys"}, {http.MethodPost, "/repos/:owner/:repo/keys"},
//{"PATCH", "/repos/:owner/:repo/keys/:id"}, //{http.MethodPatch, "/repos/:owner/:repo/keys/:id"},
{"DELETE", "/repos/:owner/:repo/keys/:id"}, {http.MethodDelete, "/repos/:owner/:repo/keys/:id"},
{"GET", "/repos/:owner/:repo/downloads"}, {http.MethodGet, "/repos/:owner/:repo/downloads"},
{"GET", "/repos/:owner/:repo/downloads/:id"}, {http.MethodGet, "/repos/:owner/:repo/downloads/:id"},
{"DELETE", "/repos/:owner/:repo/downloads/:id"}, {http.MethodDelete, "/repos/:owner/:repo/downloads/:id"},
{"GET", "/repos/:owner/:repo/forks"}, {http.MethodGet, "/repos/:owner/:repo/forks"},
{"POST", "/repos/:owner/:repo/forks"}, {http.MethodPost, "/repos/:owner/:repo/forks"},
{"GET", "/repos/:owner/:repo/hooks"}, {http.MethodGet, "/repos/:owner/:repo/hooks"},
{"GET", "/repos/:owner/:repo/hooks/:id"}, {http.MethodGet, "/repos/:owner/:repo/hooks/:id"},
{"POST", "/repos/:owner/:repo/hooks"}, {http.MethodPost, "/repos/:owner/:repo/hooks"},
//{"PATCH", "/repos/:owner/:repo/hooks/:id"}, //{http.MethodPatch, "/repos/:owner/:repo/hooks/:id"},
{"POST", "/repos/:owner/:repo/hooks/:id/tests"}, {http.MethodPost, "/repos/:owner/:repo/hooks/:id/tests"},
{"DELETE", "/repos/:owner/:repo/hooks/:id"}, {http.MethodDelete, "/repos/:owner/:repo/hooks/:id"},
{"POST", "/repos/:owner/:repo/merges"}, {http.MethodPost, "/repos/:owner/:repo/merges"},
{"GET", "/repos/:owner/:repo/releases"}, {http.MethodGet, "/repos/:owner/:repo/releases"},
{"GET", "/repos/:owner/:repo/releases/:id"}, {http.MethodGet, "/repos/:owner/:repo/releases/:id"},
{"POST", "/repos/:owner/:repo/releases"}, {http.MethodPost, "/repos/:owner/:repo/releases"},
//{"PATCH", "/repos/:owner/:repo/releases/:id"}, //{http.MethodPatch, "/repos/:owner/:repo/releases/:id"},
{"DELETE", "/repos/:owner/:repo/releases/:id"}, {http.MethodDelete, "/repos/:owner/:repo/releases/:id"},
{"GET", "/repos/:owner/:repo/releases/:id/assets"}, {http.MethodGet, "/repos/:owner/:repo/releases/:id/assets"},
{"GET", "/repos/:owner/:repo/stats/contributors"}, {http.MethodGet, "/repos/:owner/:repo/stats/contributors"},
{"GET", "/repos/:owner/:repo/stats/commit_activity"}, {http.MethodGet, "/repos/:owner/:repo/stats/commit_activity"},
{"GET", "/repos/:owner/:repo/stats/code_frequency"}, {http.MethodGet, "/repos/:owner/:repo/stats/code_frequency"},
{"GET", "/repos/:owner/:repo/stats/participation"}, {http.MethodGet, "/repos/:owner/:repo/stats/participation"},
{"GET", "/repos/:owner/:repo/stats/punch_card"}, {http.MethodGet, "/repos/:owner/:repo/stats/punch_card"},
{"GET", "/repos/:owner/:repo/statuses/:ref"}, {http.MethodGet, "/repos/:owner/:repo/statuses/:ref"},
{"POST", "/repos/:owner/:repo/statuses/:ref"}, {http.MethodPost, "/repos/:owner/:repo/statuses/:ref"},
// Search // Search
{"GET", "/search/repositories"}, {http.MethodGet, "/search/repositories"},
{"GET", "/search/code"}, {http.MethodGet, "/search/code"},
{"GET", "/search/issues"}, {http.MethodGet, "/search/issues"},
{"GET", "/search/users"}, {http.MethodGet, "/search/users"},
{"GET", "/legacy/issues/search/:owner/:repository/:state/:keyword"}, {http.MethodGet, "/legacy/issues/search/:owner/:repository/:state/:keyword"},
{"GET", "/legacy/repos/search/:keyword"}, {http.MethodGet, "/legacy/repos/search/:keyword"},
{"GET", "/legacy/user/search/:keyword"}, {http.MethodGet, "/legacy/user/search/:keyword"},
{"GET", "/legacy/user/email/:email"}, {http.MethodGet, "/legacy/user/email/:email"},
// Users // Users
{"GET", "/users/:user"}, {http.MethodGet, "/users/:user"},
{"GET", "/user"}, {http.MethodGet, "/user"},
//{"PATCH", "/user"}, //{http.MethodPatch, "/user"},
{"GET", "/users"}, {http.MethodGet, "/users"},
{"GET", "/user/emails"}, {http.MethodGet, "/user/emails"},
{"POST", "/user/emails"}, {http.MethodPost, "/user/emails"},
{"DELETE", "/user/emails"}, {http.MethodDelete, "/user/emails"},
{"GET", "/users/:user/followers"}, {http.MethodGet, "/users/:user/followers"},
{"GET", "/user/followers"}, {http.MethodGet, "/user/followers"},
{"GET", "/users/:user/following"}, {http.MethodGet, "/users/:user/following"},
{"GET", "/user/following"}, {http.MethodGet, "/user/following"},
{"GET", "/user/following/:user"}, {http.MethodGet, "/user/following/:user"},
{"GET", "/users/:user/following/:target_user"}, {http.MethodGet, "/users/:user/following/:target_user"},
{"PUT", "/user/following/:user"}, {http.MethodPut, "/user/following/:user"},
{"DELETE", "/user/following/:user"}, {http.MethodDelete, "/user/following/:user"},
{"GET", "/users/:user/keys"}, {http.MethodGet, "/users/:user/keys"},
{"GET", "/user/keys"}, {http.MethodGet, "/user/keys"},
{"GET", "/user/keys/:id"}, {http.MethodGet, "/user/keys/:id"},
{"POST", "/user/keys"}, {http.MethodPost, "/user/keys"},
//{"PATCH", "/user/keys/:id"}, //{http.MethodPatch, "/user/keys/:id"},
{"DELETE", "/user/keys/:id"}, {http.MethodDelete, "/user/keys/:id"},
} }
func TestShouldBindUri(t *testing.T) { func TestShouldBindUri(t *testing.T) {
@ -291,18 +291,18 @@ func TestShouldBindUri(t *testing.T) {
type Person struct { type Person struct {
Name string `uri:"name" binding:"required"` Name string `uri:"name" binding:"required"`
Id string `uri:"id" binding:"required"` ID string `uri:"id" binding:"required"`
} }
router.Handle("GET", "/rest/:name/:id", func(c *Context) { router.Handle(http.MethodGet, "/rest/:name/:id", func(c *Context) {
var person Person var person Person
assert.NoError(t, c.ShouldBindUri(&person)) assert.NoError(t, c.ShouldBindUri(&person))
assert.True(t, "" != person.Name) assert.True(t, "" != person.Name)
assert.True(t, "" != person.Id) assert.True(t, "" != person.ID)
c.String(http.StatusOK, "ShouldBindUri test OK") c.String(http.StatusOK, "ShouldBindUri test OK")
}) })
path, _ := exampleFromPath("/rest/:name/:id") path, _ := exampleFromPath("/rest/:name/:id")
w := performRequest(router, "GET", path) w := performRequest(router, http.MethodGet, path)
assert.Equal(t, "ShouldBindUri test OK", w.Body.String()) assert.Equal(t, "ShouldBindUri test OK", w.Body.String())
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, http.StatusOK, w.Code)
} }
@ -313,18 +313,18 @@ func TestBindUri(t *testing.T) {
type Person struct { type Person struct {
Name string `uri:"name" binding:"required"` Name string `uri:"name" binding:"required"`
Id string `uri:"id" binding:"required"` ID string `uri:"id" binding:"required"`
} }
router.Handle("GET", "/rest/:name/:id", func(c *Context) { router.Handle(http.MethodGet, "/rest/:name/:id", func(c *Context) {
var person Person var person Person
assert.NoError(t, c.BindUri(&person)) assert.NoError(t, c.BindUri(&person))
assert.True(t, "" != person.Name) assert.True(t, "" != person.Name)
assert.True(t, "" != person.Id) assert.True(t, "" != person.ID)
c.String(http.StatusOK, "BindUri test OK") c.String(http.StatusOK, "BindUri test OK")
}) })
path, _ := exampleFromPath("/rest/:name/:id") path, _ := exampleFromPath("/rest/:name/:id")
w := performRequest(router, "GET", path) w := performRequest(router, http.MethodGet, path)
assert.Equal(t, "BindUri test OK", w.Body.String()) assert.Equal(t, "BindUri test OK", w.Body.String())
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, http.StatusOK, w.Code)
} }
@ -336,13 +336,13 @@ func TestBindUriError(t *testing.T) {
type Member struct { type Member struct {
Number string `uri:"num" binding:"required,uuid"` Number string `uri:"num" binding:"required,uuid"`
} }
router.Handle("GET", "/new/rest/:num", func(c *Context) { router.Handle(http.MethodGet, "/new/rest/:num", func(c *Context) {
var m Member var m Member
assert.Error(t, c.BindUri(&m)) assert.Error(t, c.BindUri(&m))
}) })
path1, _ := exampleFromPath("/new/rest/:num") path1, _ := exampleFromPath("/new/rest/:num")
w1 := performRequest(router, "GET", path1) w1 := performRequest(router, http.MethodGet, path1)
assert.Equal(t, http.StatusBadRequest, w1.Code) assert.Equal(t, http.StatusBadRequest, w1.Code)
} }
@ -358,7 +358,7 @@ func TestRaceContextCopy(t *testing.T) {
go readWriteKeys(c.Copy()) go readWriteKeys(c.Copy())
c.String(http.StatusOK, "run OK, no panics") c.String(http.StatusOK, "run OK, no panics")
}) })
w := performRequest(router, "GET", "/test/copy/race") w := performRequest(router, http.MethodGet, "/test/copy/race")
assert.Equal(t, "run OK, no panics", w.Body.String()) assert.Equal(t, "run OK, no panics", w.Body.String())
} }
@ -438,7 +438,7 @@ func exampleFromPath(path string) (string, Params) {
func BenchmarkGithub(b *testing.B) { func BenchmarkGithub(b *testing.B) {
router := New() router := New()
githubConfigRouter(router) githubConfigRouter(router)
runRequest(b, router, "GET", "/legacy/issues/search/:owner/:repository/:state/:keyword") runRequest(b, router, http.MethodGet, "/legacy/issues/search/:owner/:repository/:state/:keyword")
} }
func BenchmarkParallelGithub(b *testing.B) { func BenchmarkParallelGithub(b *testing.B) {
@ -446,7 +446,7 @@ func BenchmarkParallelGithub(b *testing.B) {
router := New() router := New()
githubConfigRouter(router) githubConfigRouter(router)
req, _ := http.NewRequest("POST", "/repos/manucorporat/sse/git/blobs", nil) req, _ := http.NewRequest(http.MethodPost, "/repos/manucorporat/sse/git/blobs", nil)
b.RunParallel(func(pb *testing.PB) { b.RunParallel(func(pb *testing.PB) {
// Each goroutine has its own bytes.Buffer. // Each goroutine has its own bytes.Buffer.
@ -462,7 +462,7 @@ func BenchmarkParallelGithubDefault(b *testing.B) {
router := New() router := New()
githubConfigRouter(router) githubConfigRouter(router)
req, _ := http.NewRequest("POST", "/repos/manucorporat/sse/git/blobs", nil) req, _ := http.NewRequest(http.MethodPost, "/repos/manucorporat/sse/git/blobs", nil)
b.RunParallel(func(pb *testing.PB) { b.RunParallel(func(pb *testing.PB) {
// Each goroutine has its own bytes.Buffer. // Each goroutine has its own bytes.Buffer.

16
go.mod
View File

@ -1,18 +1,14 @@
module github.com/gin-gonic/gin module github.com/gin-gonic/gin
go 1.12 go 1.13
require ( require (
github.com/gin-contrib/sse v0.1.0 github.com/gin-contrib/sse v0.1.0
github.com/go-playground/locales v0.12.1 // indirect github.com/go-playground/validator/v10 v10.2.0
github.com/go-playground/universal-translator v0.16.0 // indirect github.com/golang/protobuf v1.3.3
github.com/golang/protobuf v1.3.2 github.com/json-iterator/go v1.1.9
github.com/json-iterator/go v1.1.7 github.com/mattn/go-isatty v0.0.12
github.com/leodido/go-urn v1.1.0 // indirect
github.com/mattn/go-isatty v0.0.9
github.com/stretchr/testify v1.4.0 github.com/stretchr/testify v1.4.0
github.com/ugorji/go/codec v1.1.7 github.com/ugorji/go/codec v1.1.7
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect gopkg.in/yaml.v2 v2.2.8
gopkg.in/go-playground/validator.v9 v9.29.1
gopkg.in/yaml.v2 v2.2.2
) )

42
go.sum
View File

@ -1,20 +1,25 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/go-playground/locales v0.12.1 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotfhkmOqEc= github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM= github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo= github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8= github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
@ -29,13 +34,12 @@ github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
gopkg.in/go-playground/validator.v9 v9.29.1 h1:SvGtYmN60a5CVKTOzMSyfzWDeZRxRuGvRQyEAKbw1xc=
gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@ -0,0 +1,19 @@
package bytesconv
import (
"reflect"
"unsafe"
)
// StringToBytes converts string to byte slice without a memory allocation.
func StringToBytes(s string) (b []byte) {
sh := *(*reflect.StringHeader)(unsafe.Pointer(&s))
bh := (*reflect.SliceHeader)(unsafe.Pointer(&b))
bh.Data, bh.Len, bh.Cap = sh.Data, sh.Len, sh.Len
return b
}
// BytesToString converts byte slice to string without a memory allocation.
func BytesToString(b []byte) string {
return *(*string)(unsafe.Pointer(&b))
}

View File

@ -0,0 +1,95 @@
package bytesconv
import (
"bytes"
"math/rand"
"strings"
"testing"
"time"
)
var testString = "Albert Einstein: Logic will get you from A to B. Imagination will take you everywhere."
var testBytes = []byte(testString)
func rawBytesToStr(b []byte) string {
return string(b)
}
func rawStrToBytes(s string) []byte {
return []byte(s)
}
// go test -v
func TestBytesToString(t *testing.T) {
data := make([]byte, 1024)
for i := 0; i < 100; i++ {
rand.Read(data)
if rawBytesToStr(data) != BytesToString(data) {
t.Fatal("don't match")
}
}
}
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
const (
letterIdxBits = 6 // 6 bits to represent a letter index
letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
)
var src = rand.NewSource(time.Now().UnixNano())
func RandStringBytesMaskImprSrcSB(n int) string {
sb := strings.Builder{}
sb.Grow(n)
// A src.Int63() generates 63 random bits, enough for letterIdxMax characters!
for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; {
if remain == 0 {
cache, remain = src.Int63(), letterIdxMax
}
if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
sb.WriteByte(letterBytes[idx])
i--
}
cache >>= letterIdxBits
remain--
}
return sb.String()
}
func TestStringToBytes(t *testing.T) {
for i := 0; i < 100; i++ {
s := RandStringBytesMaskImprSrcSB(64)
if !bytes.Equal(rawStrToBytes(s), StringToBytes(s)) {
t.Fatal("don't match")
}
}
}
// go test -v -run=none -bench=^BenchmarkBytesConv -benchmem=true
func BenchmarkBytesConvBytesToStrRaw(b *testing.B) {
for i := 0; i < b.N; i++ {
rawBytesToStr(testBytes)
}
}
func BenchmarkBytesConvBytesToStr(b *testing.B) {
for i := 0; i < b.N; i++ {
BytesToString(testBytes)
}
}
func BenchmarkBytesConvStrToBytesRaw(b *testing.B) {
for i := 0; i < b.N; i++ {
rawStrToBytes(testString)
}
}
func BenchmarkBytesConvStrToBytes(b *testing.B) {
for i := 0; i < b.N; i++ {
StringToBytes(testString)
}
}

View File

@ -6,7 +6,7 @@
package json package json
import "github.com/json-iterator/go" import jsoniter "github.com/json-iterator/go"
var ( var (
json = jsoniter.ConfigCompatibleWithStandardLibrary json = jsoniter.ConfigCompatibleWithStandardLibrary

View File

@ -99,19 +99,19 @@ func (p *LogFormatterParams) MethodColor() string {
method := p.Method method := p.Method
switch method { switch method {
case "GET": case http.MethodGet:
return blue return blue
case "POST": case http.MethodPost:
return cyan return cyan
case "PUT": case http.MethodPut:
return yellow return yellow
case "DELETE": case http.MethodDelete:
return red return red
case "PATCH": case http.MethodPatch:
return green return green
case "HEAD": case http.MethodHead:
return magenta return magenta
case "OPTIONS": case http.MethodOptions:
return white return white
default: default:
return reset return reset
@ -141,7 +141,7 @@ var defaultLogFormatter = func(param LogFormatterParams) string {
// Truncate in a golang < 1.8 safe way // Truncate in a golang < 1.8 safe way
param.Latency = param.Latency - param.Latency%time.Second param.Latency = param.Latency - param.Latency%time.Second
} }
return fmt.Sprintf("[GIN] %v |%s %3d %s| %13v | %15s |%s %-7s %s %s\n%s", return fmt.Sprintf("[GIN] %v |%s %3d %s| %13v | %15s |%s %-7s %s %#v\n%s",
param.TimeStamp.Format("2006/01/02 - 15:04:05"), param.TimeStamp.Format("2006/01/02 - 15:04:05"),
statusColor, param.StatusCode, resetColor, statusColor, param.StatusCode, resetColor,
param.Latency, param.Latency,

View File

@ -158,7 +158,7 @@ func TestLoggerWithFormatter(t *testing.T) {
router := New() router := New()
router.Use(LoggerWithFormatter(func(param LogFormatterParams) string { router.Use(LoggerWithFormatter(func(param LogFormatterParams) string {
return fmt.Sprintf("[FORMATTER TEST] %v | %3d | %13v | %15s | %-7s %s\n%s", return fmt.Sprintf("[FORMATTER TEST] %v | %3d | %13v | %15s | %-7s %#v\n%s",
param.TimeStamp.Format("2006/01/02 - 15:04:05"), param.TimeStamp.Format("2006/01/02 - 15:04:05"),
param.StatusCode, param.StatusCode,
param.Latency, param.Latency,
@ -275,11 +275,11 @@ func TestDefaultLogFormatter(t *testing.T) {
isTerm: false, isTerm: false,
} }
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 | 200 | 5s | 20.20.20.20 | GET /\n", defaultLogFormatter(termFalseParam)) assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 | 200 | 5s | 20.20.20.20 | GET \"/\"\n", defaultLogFormatter(termFalseParam))
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 | 200 | 2743h29m3s | 20.20.20.20 | GET /\n", defaultLogFormatter(termFalseLongDurationParam)) assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 | 200 | 2743h29m3s | 20.20.20.20 | GET \"/\"\n", defaultLogFormatter(termFalseLongDurationParam))
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 |\x1b[97;42m 200 \x1b[0m| 5s | 20.20.20.20 |\x1b[97;44m GET \x1b[0m /\n", defaultLogFormatter(termTrueParam)) assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 |\x1b[97;42m 200 \x1b[0m| 5s | 20.20.20.20 |\x1b[97;44m GET \x1b[0m \"/\"\n", defaultLogFormatter(termTrueParam))
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 |\x1b[97;42m 200 \x1b[0m| 2743h29m3s | 20.20.20.20 |\x1b[97;44m GET \x1b[0m /\n", defaultLogFormatter(termTrueLongDurationParam)) assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 |\x1b[97;42m 200 \x1b[0m| 2743h29m3s | 20.20.20.20 |\x1b[97;44m GET \x1b[0m \"/\"\n", defaultLogFormatter(termTrueLongDurationParam))
} }
@ -369,15 +369,15 @@ func TestErrorLogger(t *testing.T) {
w := performRequest(router, "GET", "/error") w := performRequest(router, "GET", "/error")
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "{\"error\":\"this is an error\"}\n", w.Body.String()) assert.Equal(t, "{\"error\":\"this is an error\"}", w.Body.String())
w = performRequest(router, "GET", "/abort") w = performRequest(router, "GET", "/abort")
assert.Equal(t, http.StatusUnauthorized, w.Code) assert.Equal(t, http.StatusUnauthorized, w.Code)
assert.Equal(t, "{\"error\":\"no authorized\"}\n", w.Body.String()) assert.Equal(t, "{\"error\":\"no authorized\"}", w.Body.String())
w = performRequest(router, "GET", "/print") w = performRequest(router, "GET", "/print")
assert.Equal(t, http.StatusInternalServerError, w.Code) assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.Equal(t, "hola!{\"error\":\"this is an error\"}\n", w.Body.String()) assert.Equal(t, "hola!{\"error\":\"this is an error\"}", w.Body.String())
} }
func TestLoggerWithWriterSkippingPaths(t *testing.T) { func TestLoggerWithWriterSkippingPaths(t *testing.T) {

View File

@ -246,5 +246,5 @@ func TestMiddlewareWrite(t *testing.T) {
w := performRequest(router, "GET", "/") w := performRequest(router, "GET", "/")
assert.Equal(t, http.StatusBadRequest, w.Code) assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Equal(t, strings.Replace("hola\n<map><foo>bar</foo></map>{\"foo\":\"bar\"}\n{\"foo\":\"bar\"}\nevent:test\ndata:message\n\n", " ", "", -1), strings.Replace(w.Body.String(), " ", "", -1)) assert.Equal(t, strings.Replace("hola\n<map><foo>bar</foo></map>{\"foo\":\"bar\"}{\"foo\":\"bar\"}event:test\ndata:message\n\n", " ", "", -1), strings.Replace(w.Body.String(), " ", "", -1))
} }

13
mode.go
View File

@ -22,6 +22,7 @@ const (
// TestMode indicates gin mode is test. // TestMode indicates gin mode is test.
TestMode = "test" TestMode = "test"
) )
const ( const (
debugCode = iota debugCode = iota
releaseCode releaseCode
@ -50,8 +51,12 @@ func init() {
// SetMode sets gin mode according to input string. // SetMode sets gin mode according to input string.
func SetMode(value string) { func SetMode(value string) {
if value == "" {
value = DebugMode
}
switch value { switch value {
case DebugMode, "": case DebugMode:
ginMode = debugCode ginMode = debugCode
case ReleaseMode: case ReleaseMode:
ginMode = releaseCode ginMode = releaseCode
@ -60,9 +65,7 @@ func SetMode(value string) {
default: default:
panic("gin mode unknown: " + value) panic("gin mode unknown: " + value)
} }
if value == "" {
value = DebugMode
}
modeName = value modeName = value
} }
@ -77,7 +80,7 @@ func EnableJsonDecoderUseNumber() {
binding.EnableDecoderUseNumber = true binding.EnableDecoderUseNumber = true
} }
// EnableJsonDisallowUnknownFields sets true for binding.EnableDecoderDisallowUnknownFields to // EnableJsonDecoderDisallowUnknownFields sets true for binding.EnableDecoderDisallowUnknownFields to
// call the DisallowUnknownFields method on the JSON Decoder instance. // call the DisallowUnknownFields method on the JSON Decoder instance.
func EnableJsonDecoderDisallowUnknownFields() { func EnableJsonDecoderDisallowUnknownFields() {
binding.EnableDecoderDisallowUnknownFields = true binding.EnableDecoderDisallowUnknownFields = true

View File

@ -40,6 +40,14 @@ func TestSetMode(t *testing.T) {
assert.Panics(t, func() { SetMode("unknown") }) assert.Panics(t, func() { SetMode("unknown") })
} }
func TestDisableBindValidation(t *testing.T) {
v := binding.Validator
assert.NotNil(t, binding.Validator)
DisableBindValidation()
assert.Nil(t, binding.Validator)
binding.Validator = v
}
func TestEnableJsonDecoderUseNumber(t *testing.T) { func TestEnableJsonDecoderUseNumber(t *testing.T) {
assert.False(t, binding.EnableDecoderUseNumber) assert.False(t, binding.EnableDecoderUseNumber)
EnableJsonDecoderUseNumber() EnableJsonDecoderUseNumber()

55
path.go
View File

@ -19,13 +19,17 @@ package gin
// //
// If the result of this process is an empty string, "/" is returned. // If the result of this process is an empty string, "/" is returned.
func cleanPath(p string) string { func cleanPath(p string) string {
const stackBufSize = 128
// Turn empty string into "/" // Turn empty string into "/"
if p == "" { if p == "" {
return "/" return "/"
} }
// Reasonably sized buffer on stack to avoid allocations in the common case.
// If a larger buffer is required, it gets allocated dynamically.
buf := make([]byte, 0, stackBufSize)
n := len(p) n := len(p)
var buf []byte
// Invariants: // Invariants:
// reading from path; r is index of next byte to process. // reading from path; r is index of next byte to process.
@ -37,15 +41,21 @@ func cleanPath(p string) string {
if p[0] != '/' { if p[0] != '/' {
r = 0 r = 0
if n+1 > stackBufSize {
buf = make([]byte, n+1) buf = make([]byte, n+1)
} else {
buf = buf[:n+1]
}
buf[0] = '/' buf[0] = '/'
} }
trailing := n > 1 && p[n-1] == '/' trailing := n > 1 && p[n-1] == '/'
// A bit more clunky without a 'lazybuf' like the path package, but the loop // A bit more clunky without a 'lazybuf' like the path package, but the loop
// gets completely inlined (bufApp). So in contrast to the path package this // gets completely inlined (bufApp calls).
// loop has no expensive function calls (except 1x make) // loop has no expensive function calls (except 1x make) // So in contrast to the path package this loop has no expensive function
// calls (except make, if needed).
for r < n { for r < n {
switch { switch {
@ -69,7 +79,7 @@ func cleanPath(p string) string {
// can backtrack // can backtrack
w-- w--
if buf == nil { if len(buf) == 0 {
for w > 1 && p[w] != '/' { for w > 1 && p[w] != '/' {
w-- w--
} }
@ -81,14 +91,14 @@ func cleanPath(p string) string {
} }
default: default:
// real path element. // Real path element.
// add slash if needed // Add slash if needed
if w > 1 { if w > 1 {
bufApp(&buf, p, w, '/') bufApp(&buf, p, w, '/')
w++ w++
} }
// copy element // Copy element
for r < n && p[r] != '/' { for r < n && p[r] != '/' {
bufApp(&buf, p, w, p[r]) bufApp(&buf, p, w, p[r])
w++ w++
@ -97,27 +107,44 @@ func cleanPath(p string) string {
} }
} }
// re-append trailing slash // Re-append trailing slash
if trailing && w > 1 { if trailing && w > 1 {
bufApp(&buf, p, w, '/') bufApp(&buf, p, w, '/')
w++ w++
} }
if buf == nil { // If the original string was not modified (or only shortened at the end),
// return the respective substring of the original string.
// Otherwise return a new string from the buffer.
if len(buf) == 0 {
return p[:w] return p[:w]
} }
return string(buf[:w]) return string(buf[:w])
} }
// internal helper to lazily create a buffer if necessary. // Internal helper to lazily create a buffer if necessary.
// Calls to this function get inlined.
func bufApp(buf *[]byte, s string, w int, c byte) { func bufApp(buf *[]byte, s string, w int, c byte) {
if *buf == nil { b := *buf
if len(b) == 0 {
// No modification of the original string so far.
// If the next character is the same as in the original string, we do
// not yet have to allocate a buffer.
if s[w] == c { if s[w] == c {
return return
} }
*buf = make([]byte, len(s)) // Otherwise use either the stack buffer, if it is large enough, or
copy(*buf, s[:w]) // allocate a new buffer on the heap, and copy all previous characters.
length := len(s)
if length > cap(b) {
*buf = make([]byte, length)
} else {
*buf = (*buf)[:length]
} }
(*buf)[w] = c b = *buf
copy(b, s[:w])
}
b[w] = c
} }

View File

@ -6,15 +6,17 @@
package gin package gin
import ( import (
"runtime" "strings"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
var cleanTests = []struct { type cleanPathTest struct {
path, result string path, result string
}{ }
var cleanTests = []cleanPathTest{
// Already clean // Already clean
{"/", "/"}, {"/", "/"},
{"/abc", "/abc"}, {"/abc", "/abc"},
@ -77,13 +79,62 @@ func TestPathCleanMallocs(t *testing.T) {
if testing.Short() { if testing.Short() {
t.Skip("skipping malloc count in short mode") t.Skip("skipping malloc count in short mode")
} }
if runtime.GOMAXPROCS(0) > 1 {
t.Log("skipping AllocsPerRun checks; GOMAXPROCS>1")
return
}
for _, test := range cleanTests { for _, test := range cleanTests {
allocs := testing.AllocsPerRun(100, func() { cleanPath(test.result) }) allocs := testing.AllocsPerRun(100, func() { cleanPath(test.result) })
assert.EqualValues(t, allocs, 0) assert.EqualValues(t, allocs, 0)
} }
} }
func BenchmarkPathClean(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for _, test := range cleanTests {
cleanPath(test.path)
}
}
}
func genLongPaths() (testPaths []cleanPathTest) {
for i := 1; i <= 1234; i++ {
ss := strings.Repeat("a", i)
correctPath := "/" + ss
testPaths = append(testPaths, cleanPathTest{
path: correctPath,
result: correctPath,
}, cleanPathTest{
path: ss,
result: correctPath,
}, cleanPathTest{
path: "//" + ss,
result: correctPath,
}, cleanPathTest{
path: "/" + ss + "/b/..",
result: correctPath,
})
}
return
}
func TestPathCleanLong(t *testing.T) {
cleanTests := genLongPaths()
for _, test := range cleanTests {
assert.Equal(t, test.result, cleanPath(test.path))
assert.Equal(t, test.result, cleanPath(test.result))
}
}
func BenchmarkPathCleanLong(b *testing.B) {
cleanTests := genLongPaths()
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for _, test := range cleanTests {
cleanPath(test.path)
}
}
}

View File

@ -146,6 +146,6 @@ func function(pc uintptr) []byte {
} }
func timeFormat(t time.Time) string { func timeFormat(t time.Time) string {
var timeString = t.Format("2006/01/02 - 15:04:05") timeString := t.Format("2006/01/02 - 15:04:05")
return timeString return timeString
} }

View File

@ -10,6 +10,7 @@ import (
"html/template" "html/template"
"net/http" "net/http"
"github.com/gin-gonic/gin/internal/bytesconv"
"github.com/gin-gonic/gin/internal/json" "github.com/gin-gonic/gin/internal/json"
) )
@ -68,8 +69,11 @@ func (r JSON) WriteContentType(w http.ResponseWriter) {
// WriteJSON marshals the given interface object and writes it with custom ContentType. // WriteJSON marshals the given interface object and writes it with custom ContentType.
func WriteJSON(w http.ResponseWriter, obj interface{}) error { func WriteJSON(w http.ResponseWriter, obj interface{}) error {
writeContentType(w, jsonContentType) writeContentType(w, jsonContentType)
encoder := json.NewEncoder(w) jsonBytes, err := json.Marshal(obj)
err := encoder.Encode(&obj) if err != nil {
return err
}
_, err = w.Write(jsonBytes)
return err return err
} }
@ -97,8 +101,9 @@ func (r SecureJSON) Render(w http.ResponseWriter) error {
return err return err
} }
// if the jsonBytes is array values // if the jsonBytes is array values
if bytes.HasPrefix(jsonBytes, []byte("[")) && bytes.HasSuffix(jsonBytes, []byte("]")) { if bytes.HasPrefix(jsonBytes, bytesconv.StringToBytes("[")) && bytes.HasSuffix(jsonBytes,
_, err = w.Write([]byte(r.Prefix)) bytesconv.StringToBytes("]")) {
_, err = w.Write(bytesconv.StringToBytes(r.Prefix))
if err != nil { if err != nil {
return err return err
} }
@ -126,11 +131,11 @@ func (r JsonpJSON) Render(w http.ResponseWriter) (err error) {
} }
callback := template.JSEscapeString(r.Callback) callback := template.JSEscapeString(r.Callback)
_, err = w.Write([]byte(callback)) _, err = w.Write(bytesconv.StringToBytes(callback))
if err != nil { if err != nil {
return err return err
} }
_, err = w.Write([]byte("(")) _, err = w.Write(bytesconv.StringToBytes("("))
if err != nil { if err != nil {
return err return err
} }
@ -138,7 +143,7 @@ func (r JsonpJSON) Render(w http.ResponseWriter) (err error) {
if err != nil { if err != nil {
return err return err
} }
_, err = w.Write([]byte(");")) _, err = w.Write(bytesconv.StringToBytes(");"))
if err != nil { if err != nil {
return err return err
} }
@ -160,7 +165,7 @@ func (r AsciiJSON) Render(w http.ResponseWriter) (err error) {
} }
var buffer bytes.Buffer var buffer bytes.Buffer
for _, r := range string(ret) { for _, r := range bytesconv.BytesToString(ret) {
cvt := string(r) cvt := string(r)
if r >= 128 { if r >= 128 {
cvt = fmt.Sprintf("\\u%04x", int64(r)) cvt = fmt.Sprintf("\\u%04x", int64(r))

View File

@ -2,6 +2,8 @@
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// +build !nomsgpack
package render package render
import ( import (
@ -10,6 +12,10 @@ import (
"github.com/ugorji/go/codec" "github.com/ugorji/go/codec"
) )
var (
_ Render = MsgPack{}
)
// MsgPack contains the given interface object. // MsgPack contains the given interface object.
type MsgPack struct { type MsgPack struct {
Data interface{} Data interface{}

View File

@ -22,6 +22,9 @@ type Reader struct {
func (r Reader) Render(w http.ResponseWriter) (err error) { func (r Reader) Render(w http.ResponseWriter) (err error) {
r.WriteContentType(w) r.WriteContentType(w)
if r.ContentLength >= 0 { if r.ContentLength >= 0 {
if r.Headers == nil {
r.Headers = map[string]string{}
}
r.Headers["Content-Length"] = strconv.FormatInt(r.ContentLength, 10) r.Headers["Content-Length"] = strconv.FormatInt(r.ContentLength, 10)
} }
r.writeHeaders(w, r.Headers) r.writeHeaders(w, r.Headers)

23
render/reader_test.go Normal file
View File

@ -0,0 +1,23 @@
// Copyright 2019 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package render
import (
"net/http/httptest"
"strings"
"testing"
"github.com/stretchr/testify/require"
)
func TestReaderRenderNoHeaders(t *testing.T) {
content := "test"
r := Reader{
ContentLength: int64(len(content)),
Reader: strings.NewReader(content),
}
err := r.Render(httptest.NewRecorder())
require.NoError(t, err)
}

View File

@ -27,7 +27,6 @@ var (
_ HTMLRender = HTMLDebug{} _ HTMLRender = HTMLDebug{}
_ HTMLRender = HTMLProduction{} _ HTMLRender = HTMLProduction{}
_ Render = YAML{} _ Render = YAML{}
_ Render = MsgPack{}
_ Render = Reader{} _ Render = Reader{}
_ Render = AsciiJSON{} _ Render = AsciiJSON{}
_ Render = ProtoBuf{} _ Render = ProtoBuf{}

View File

@ -0,0 +1,43 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
// +build !nomsgpack
package render
import (
"bytes"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/ugorji/go/codec"
)
// TODO unit tests
// test errors
func TestRenderMsgPack(t *testing.T) {
w := httptest.NewRecorder()
data := map[string]interface{}{
"foo": "bar",
}
(MsgPack{data}).WriteContentType(w)
assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type"))
err := (MsgPack{data}).Render(w)
assert.NoError(t, err)
h := new(codec.MsgpackHandle)
assert.NotNil(t, h)
buf := bytes.NewBuffer([]byte{})
assert.NotNil(t, buf)
err = codec.NewEncoder(buf, h).Encode(data)
assert.NoError(t, err)
assert.Equal(t, w.Body.String(), string(buf.Bytes()))
assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type"))
}

View File

@ -5,7 +5,6 @@
package render package render
import ( import (
"bytes"
"encoding/xml" "encoding/xml"
"errors" "errors"
"html/template" "html/template"
@ -17,7 +16,6 @@ import (
"github.com/golang/protobuf/proto" "github.com/golang/protobuf/proto"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/ugorji/go/codec"
testdata "github.com/gin-gonic/gin/testdata/protoexample" testdata "github.com/gin-gonic/gin/testdata/protoexample"
) )
@ -25,30 +23,6 @@ import (
// TODO unit tests // TODO unit tests
// test errors // test errors
func TestRenderMsgPack(t *testing.T) {
w := httptest.NewRecorder()
data := map[string]interface{}{
"foo": "bar",
}
(MsgPack{data}).WriteContentType(w)
assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type"))
err := (MsgPack{data}).Render(w)
assert.NoError(t, err)
h := new(codec.MsgpackHandle)
assert.NotNil(t, h)
buf := bytes.NewBuffer([]byte{})
assert.NotNil(t, buf)
err = codec.NewEncoder(buf, h).Encode(data)
assert.NoError(t, err)
assert.Equal(t, w.Body.String(), buf.String())
assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type"))
}
func TestRenderJSON(t *testing.T) { func TestRenderJSON(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
data := map[string]interface{}{ data := map[string]interface{}{
@ -62,7 +36,7 @@ func TestRenderJSON(t *testing.T) {
err := (JSON{data}).Render(w) err := (JSON{data}).Render(w)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"\\u003cb\\u003e\"}\n", w.Body.String()) assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"\\u003cb\\u003e\"}", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
} }
@ -347,7 +321,10 @@ func TestRenderRedirect(t *testing.T) {
} }
w = httptest.NewRecorder() w = httptest.NewRecorder()
assert.PanicsWithValue(t, "Cannot redirect with status code 200", func() { data2.Render(w) }) assert.PanicsWithValue(t, "Cannot redirect with status code 200", func() {
err := data2.Render(w)
assert.NoError(t, err)
})
data3 := Redirect{ data3 := Redirect{
Code: http.StatusCreated, Code: http.StatusCreated,

View File

@ -6,7 +6,6 @@ package render
import ( import (
"fmt" "fmt"
"io"
"net/http" "net/http"
) )
@ -35,6 +34,6 @@ func WriteString(w http.ResponseWriter, format string, data []interface{}) (err
_, err = fmt.Fprintf(w, format, data...) _, err = fmt.Fprintf(w, format, data...)
return return
} }
_, err = io.WriteString(w, format) _, err = w.Write([]byte(format))
return return
} }

View File

@ -29,38 +29,38 @@ func init() {
} }
func TestResponseWriterReset(t *testing.T) { func TestResponseWriterReset(t *testing.T) {
testWritter := httptest.NewRecorder() testWriter := httptest.NewRecorder()
writer := &responseWriter{} writer := &responseWriter{}
var w ResponseWriter = writer var w ResponseWriter = writer
writer.reset(testWritter) writer.reset(testWriter)
assert.Equal(t, -1, writer.size) assert.Equal(t, -1, writer.size)
assert.Equal(t, http.StatusOK, writer.status) assert.Equal(t, http.StatusOK, writer.status)
assert.Equal(t, testWritter, writer.ResponseWriter) assert.Equal(t, testWriter, writer.ResponseWriter)
assert.Equal(t, -1, w.Size()) assert.Equal(t, -1, w.Size())
assert.Equal(t, http.StatusOK, w.Status()) assert.Equal(t, http.StatusOK, w.Status())
assert.False(t, w.Written()) assert.False(t, w.Written())
} }
func TestResponseWriterWriteHeader(t *testing.T) { func TestResponseWriterWriteHeader(t *testing.T) {
testWritter := httptest.NewRecorder() testWriter := httptest.NewRecorder()
writer := &responseWriter{} writer := &responseWriter{}
writer.reset(testWritter) writer.reset(testWriter)
w := ResponseWriter(writer) w := ResponseWriter(writer)
w.WriteHeader(http.StatusMultipleChoices) w.WriteHeader(http.StatusMultipleChoices)
assert.False(t, w.Written()) assert.False(t, w.Written())
assert.Equal(t, http.StatusMultipleChoices, w.Status()) assert.Equal(t, http.StatusMultipleChoices, w.Status())
assert.NotEqual(t, http.StatusMultipleChoices, testWritter.Code) assert.NotEqual(t, http.StatusMultipleChoices, testWriter.Code)
w.WriteHeader(-1) w.WriteHeader(-1)
assert.Equal(t, http.StatusMultipleChoices, w.Status()) assert.Equal(t, http.StatusMultipleChoices, w.Status())
} }
func TestResponseWriterWriteHeadersNow(t *testing.T) { func TestResponseWriterWriteHeadersNow(t *testing.T) {
testWritter := httptest.NewRecorder() testWriter := httptest.NewRecorder()
writer := &responseWriter{} writer := &responseWriter{}
writer.reset(testWritter) writer.reset(testWriter)
w := ResponseWriter(writer) w := ResponseWriter(writer)
w.WriteHeader(http.StatusMultipleChoices) w.WriteHeader(http.StatusMultipleChoices)
@ -68,7 +68,7 @@ func TestResponseWriterWriteHeadersNow(t *testing.T) {
assert.True(t, w.Written()) assert.True(t, w.Written())
assert.Equal(t, 0, w.Size()) assert.Equal(t, 0, w.Size())
assert.Equal(t, http.StatusMultipleChoices, testWritter.Code) assert.Equal(t, http.StatusMultipleChoices, testWriter.Code)
writer.size = 10 writer.size = 10
w.WriteHeaderNow() w.WriteHeaderNow()
@ -76,30 +76,30 @@ func TestResponseWriterWriteHeadersNow(t *testing.T) {
} }
func TestResponseWriterWrite(t *testing.T) { func TestResponseWriterWrite(t *testing.T) {
testWritter := httptest.NewRecorder() testWriter := httptest.NewRecorder()
writer := &responseWriter{} writer := &responseWriter{}
writer.reset(testWritter) writer.reset(testWriter)
w := ResponseWriter(writer) w := ResponseWriter(writer)
n, err := w.Write([]byte("hola")) n, err := w.Write([]byte("hola"))
assert.Equal(t, 4, n) assert.Equal(t, 4, n)
assert.Equal(t, 4, w.Size()) assert.Equal(t, 4, w.Size())
assert.Equal(t, http.StatusOK, w.Status()) assert.Equal(t, http.StatusOK, w.Status())
assert.Equal(t, http.StatusOK, testWritter.Code) assert.Equal(t, http.StatusOK, testWriter.Code)
assert.Equal(t, "hola", testWritter.Body.String()) assert.Equal(t, "hola", testWriter.Body.String())
assert.NoError(t, err) assert.NoError(t, err)
n, err = w.Write([]byte(" adios")) n, err = w.Write([]byte(" adios"))
assert.Equal(t, 6, n) assert.Equal(t, 6, n)
assert.Equal(t, 10, w.Size()) assert.Equal(t, 10, w.Size())
assert.Equal(t, "hola adios", testWritter.Body.String()) assert.Equal(t, "hola adios", testWriter.Body.String())
assert.NoError(t, err) assert.NoError(t, err)
} }
func TestResponseWriterHijack(t *testing.T) { func TestResponseWriterHijack(t *testing.T) {
testWritter := httptest.NewRecorder() testWriter := httptest.NewRecorder()
writer := &responseWriter{} writer := &responseWriter{}
writer.reset(testWritter) writer.reset(testWriter)
w := ResponseWriter(writer) w := ResponseWriter(writer)
assert.Panics(t, func() { assert.Panics(t, func() {

View File

@ -95,51 +95,51 @@ func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers ...Ha
// POST is a shortcut for router.Handle("POST", path, handle). // POST is a shortcut for router.Handle("POST", path, handle).
func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes { func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle("POST", relativePath, handlers) return group.handle(http.MethodPost, relativePath, handlers)
} }
// GET is a shortcut for router.Handle("GET", path, handle). // GET is a shortcut for router.Handle("GET", path, handle).
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes { func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle("GET", relativePath, handlers) return group.handle(http.MethodGet, relativePath, handlers)
} }
// DELETE is a shortcut for router.Handle("DELETE", path, handle). // DELETE is a shortcut for router.Handle("DELETE", path, handle).
func (group *RouterGroup) DELETE(relativePath string, handlers ...HandlerFunc) IRoutes { func (group *RouterGroup) DELETE(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle("DELETE", relativePath, handlers) return group.handle(http.MethodDelete, relativePath, handlers)
} }
// PATCH is a shortcut for router.Handle("PATCH", path, handle). // PATCH is a shortcut for router.Handle("PATCH", path, handle).
func (group *RouterGroup) PATCH(relativePath string, handlers ...HandlerFunc) IRoutes { func (group *RouterGroup) PATCH(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle("PATCH", relativePath, handlers) return group.handle(http.MethodPatch, relativePath, handlers)
} }
// PUT is a shortcut for router.Handle("PUT", path, handle). // PUT is a shortcut for router.Handle("PUT", path, handle).
func (group *RouterGroup) PUT(relativePath string, handlers ...HandlerFunc) IRoutes { func (group *RouterGroup) PUT(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle("PUT", relativePath, handlers) return group.handle(http.MethodPut, relativePath, handlers)
} }
// OPTIONS is a shortcut for router.Handle("OPTIONS", path, handle). // OPTIONS is a shortcut for router.Handle("OPTIONS", path, handle).
func (group *RouterGroup) OPTIONS(relativePath string, handlers ...HandlerFunc) IRoutes { func (group *RouterGroup) OPTIONS(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle("OPTIONS", relativePath, handlers) return group.handle(http.MethodOptions, relativePath, handlers)
} }
// HEAD is a shortcut for router.Handle("HEAD", path, handle). // HEAD is a shortcut for router.Handle("HEAD", path, handle).
func (group *RouterGroup) HEAD(relativePath string, handlers ...HandlerFunc) IRoutes { func (group *RouterGroup) HEAD(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle("HEAD", relativePath, handlers) return group.handle(http.MethodHead, relativePath, handlers)
} }
// Any registers a route that matches all the HTTP methods. // Any registers a route that matches all the HTTP methods.
// GET, POST, PUT, PATCH, HEAD, OPTIONS, DELETE, CONNECT, TRACE. // GET, POST, PUT, PATCH, HEAD, OPTIONS, DELETE, CONNECT, TRACE.
func (group *RouterGroup) Any(relativePath string, handlers ...HandlerFunc) IRoutes { func (group *RouterGroup) Any(relativePath string, handlers ...HandlerFunc) IRoutes {
group.handle("GET", relativePath, handlers) group.handle(http.MethodGet, relativePath, handlers)
group.handle("POST", relativePath, handlers) group.handle(http.MethodPost, relativePath, handlers)
group.handle("PUT", relativePath, handlers) group.handle(http.MethodPut, relativePath, handlers)
group.handle("PATCH", relativePath, handlers) group.handle(http.MethodPatch, relativePath, handlers)
group.handle("HEAD", relativePath, handlers) group.handle(http.MethodHead, relativePath, handlers)
group.handle("OPTIONS", relativePath, handlers) group.handle(http.MethodOptions, relativePath, handlers)
group.handle("DELETE", relativePath, handlers) group.handle(http.MethodDelete, relativePath, handlers)
group.handle("CONNECT", relativePath, handlers) group.handle(http.MethodConnect, relativePath, handlers)
group.handle("TRACE", relativePath, handlers) group.handle(http.MethodTrace, relativePath, handlers)
return group.returnObj() return group.returnObj()
} }
@ -193,13 +193,15 @@ func (group *RouterGroup) createStaticHandler(relativePath string, fs http.FileS
file := c.Param("filepath") file := c.Param("filepath")
// Check if file exists and/or if we have permission to access it // Check if file exists and/or if we have permission to access it
if _, err := fs.Open(file); err != nil { f, err := fs.Open(file)
if err != nil {
c.Writer.WriteHeader(http.StatusNotFound) c.Writer.WriteHeader(http.StatusNotFound)
c.handlers = group.engine.noRoute c.handlers = group.engine.noRoute
// Reset index // Reset index
c.index = -1 c.index = -1
return return
} }
f.Close()
fileServer.ServeHTTP(c.Writer, c.Request) fileServer.ServeHTTP(c.Writer, c.Request)
} }

View File

@ -33,13 +33,13 @@ func TestRouterGroupBasic(t *testing.T) {
} }
func TestRouterGroupBasicHandle(t *testing.T) { func TestRouterGroupBasicHandle(t *testing.T) {
performRequestInGroup(t, "GET") performRequestInGroup(t, http.MethodGet)
performRequestInGroup(t, "POST") performRequestInGroup(t, http.MethodPost)
performRequestInGroup(t, "PUT") performRequestInGroup(t, http.MethodPut)
performRequestInGroup(t, "PATCH") performRequestInGroup(t, http.MethodPatch)
performRequestInGroup(t, "DELETE") performRequestInGroup(t, http.MethodDelete)
performRequestInGroup(t, "HEAD") performRequestInGroup(t, http.MethodHead)
performRequestInGroup(t, "OPTIONS") performRequestInGroup(t, http.MethodOptions)
} }
func performRequestInGroup(t *testing.T, method string) { func performRequestInGroup(t *testing.T, method string) {
@ -55,25 +55,25 @@ func performRequestInGroup(t *testing.T, method string) {
} }
switch method { switch method {
case "GET": case http.MethodGet:
v1.GET("/test", handler) v1.GET("/test", handler)
login.GET("/test", handler) login.GET("/test", handler)
case "POST": case http.MethodPost:
v1.POST("/test", handler) v1.POST("/test", handler)
login.POST("/test", handler) login.POST("/test", handler)
case "PUT": case http.MethodPut:
v1.PUT("/test", handler) v1.PUT("/test", handler)
login.PUT("/test", handler) login.PUT("/test", handler)
case "PATCH": case http.MethodPatch:
v1.PATCH("/test", handler) v1.PATCH("/test", handler)
login.PATCH("/test", handler) login.PATCH("/test", handler)
case "DELETE": case http.MethodDelete:
v1.DELETE("/test", handler) v1.DELETE("/test", handler)
login.DELETE("/test", handler) login.DELETE("/test", handler)
case "HEAD": case http.MethodHead:
v1.HEAD("/test", handler) v1.HEAD("/test", handler)
login.HEAD("/test", handler) login.HEAD("/test", handler)
case "OPTIONS": case http.MethodOptions:
v1.OPTIONS("/test", handler) v1.OPTIONS("/test", handler)
login.OPTIONS("/test", handler) login.OPTIONS("/test", handler)
default: default:
@ -128,7 +128,7 @@ func TestRouterGroupTooManyHandlers(t *testing.T) {
func TestRouterGroupBadMethod(t *testing.T) { func TestRouterGroupBadMethod(t *testing.T) {
router := New() router := New()
assert.Panics(t, func() { assert.Panics(t, func() {
router.Handle("get", "/") router.Handle(http.MethodGet, "/")
}) })
assert.Panics(t, func() { assert.Panics(t, func() {
router.Handle(" GET", "/") router.Handle(" GET", "/")
@ -162,7 +162,7 @@ func testRoutesInterface(t *testing.T, r IRoutes) {
handler := func(c *Context) {} handler := func(c *Context) {}
assert.Equal(t, r, r.Use(handler)) assert.Equal(t, r, r.Use(handler))
assert.Equal(t, r, r.Handle("GET", "/handler", handler)) assert.Equal(t, r, r.Handle(http.MethodGet, "/handler", handler))
assert.Equal(t, r, r.Any("/any", handler)) assert.Equal(t, r, r.Any("/any", handler))
assert.Equal(t, r, r.GET("/", handler)) assert.Equal(t, r, r.GET("/", handler))
assert.Equal(t, r, r.POST("/", handler)) assert.Equal(t, r, r.POST("/", handler))

View File

@ -70,10 +70,10 @@ func testRouteNotOK2(method string, t *testing.T) {
router := New() router := New()
router.HandleMethodNotAllowed = true router.HandleMethodNotAllowed = true
var methodRoute string var methodRoute string
if method == "POST" { if method == http.MethodPost {
methodRoute = "GET" methodRoute = http.MethodGet
} else { } else {
methodRoute = "POST" methodRoute = http.MethodPost
} }
router.Handle(methodRoute, "/test", func(c *Context) { router.Handle(methodRoute, "/test", func(c *Context) {
passed = true passed = true
@ -99,46 +99,46 @@ func TestRouterMethod(t *testing.T) {
c.String(http.StatusOK, "sup3") c.String(http.StatusOK, "sup3")
}) })
w := performRequest(router, "PUT", "/hey") w := performRequest(router, http.MethodPut, "/hey")
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "called", w.Body.String()) assert.Equal(t, "called", w.Body.String())
} }
func TestRouterGroupRouteOK(t *testing.T) { func TestRouterGroupRouteOK(t *testing.T) {
testRouteOK("GET", t) testRouteOK(http.MethodGet, t)
testRouteOK("POST", t) testRouteOK(http.MethodPost, t)
testRouteOK("PUT", t) testRouteOK(http.MethodPut, t)
testRouteOK("PATCH", t) testRouteOK(http.MethodPatch, t)
testRouteOK("HEAD", t) testRouteOK(http.MethodHead, t)
testRouteOK("OPTIONS", t) testRouteOK(http.MethodOptions, t)
testRouteOK("DELETE", t) testRouteOK(http.MethodDelete, t)
testRouteOK("CONNECT", t) testRouteOK(http.MethodConnect, t)
testRouteOK("TRACE", t) testRouteOK(http.MethodTrace, t)
} }
func TestRouteNotOK(t *testing.T) { func TestRouteNotOK(t *testing.T) {
testRouteNotOK("GET", t) testRouteNotOK(http.MethodGet, t)
testRouteNotOK("POST", t) testRouteNotOK(http.MethodPost, t)
testRouteNotOK("PUT", t) testRouteNotOK(http.MethodPut, t)
testRouteNotOK("PATCH", t) testRouteNotOK(http.MethodPatch, t)
testRouteNotOK("HEAD", t) testRouteNotOK(http.MethodHead, t)
testRouteNotOK("OPTIONS", t) testRouteNotOK(http.MethodOptions, t)
testRouteNotOK("DELETE", t) testRouteNotOK(http.MethodDelete, t)
testRouteNotOK("CONNECT", t) testRouteNotOK(http.MethodConnect, t)
testRouteNotOK("TRACE", t) testRouteNotOK(http.MethodTrace, t)
} }
func TestRouteNotOK2(t *testing.T) { func TestRouteNotOK2(t *testing.T) {
testRouteNotOK2("GET", t) testRouteNotOK2(http.MethodGet, t)
testRouteNotOK2("POST", t) testRouteNotOK2(http.MethodPost, t)
testRouteNotOK2("PUT", t) testRouteNotOK2(http.MethodPut, t)
testRouteNotOK2("PATCH", t) testRouteNotOK2(http.MethodPatch, t)
testRouteNotOK2("HEAD", t) testRouteNotOK2(http.MethodHead, t)
testRouteNotOK2("OPTIONS", t) testRouteNotOK2(http.MethodOptions, t)
testRouteNotOK2("DELETE", t) testRouteNotOK2(http.MethodDelete, t)
testRouteNotOK2("CONNECT", t) testRouteNotOK2(http.MethodConnect, t)
testRouteNotOK2("TRACE", t) testRouteNotOK2(http.MethodTrace, t)
} }
func TestRouteRedirectTrailingSlash(t *testing.T) { func TestRouteRedirectTrailingSlash(t *testing.T) {
@ -150,50 +150,50 @@ func TestRouteRedirectTrailingSlash(t *testing.T) {
router.POST("/path3", func(c *Context) {}) router.POST("/path3", func(c *Context) {})
router.PUT("/path4/", func(c *Context) {}) router.PUT("/path4/", func(c *Context) {})
w := performRequest(router, "GET", "/path/") w := performRequest(router, http.MethodGet, "/path/")
assert.Equal(t, "/path", w.Header().Get("Location")) assert.Equal(t, "/path", w.Header().Get("Location"))
assert.Equal(t, http.StatusMovedPermanently, w.Code) assert.Equal(t, http.StatusMovedPermanently, w.Code)
w = performRequest(router, "GET", "/path2") w = performRequest(router, http.MethodGet, "/path2")
assert.Equal(t, "/path2/", w.Header().Get("Location")) assert.Equal(t, "/path2/", w.Header().Get("Location"))
assert.Equal(t, http.StatusMovedPermanently, w.Code) assert.Equal(t, http.StatusMovedPermanently, w.Code)
w = performRequest(router, "POST", "/path3/") w = performRequest(router, http.MethodPost, "/path3/")
assert.Equal(t, "/path3", w.Header().Get("Location")) assert.Equal(t, "/path3", w.Header().Get("Location"))
assert.Equal(t, http.StatusTemporaryRedirect, w.Code) assert.Equal(t, http.StatusTemporaryRedirect, w.Code)
w = performRequest(router, "PUT", "/path4") w = performRequest(router, http.MethodPut, "/path4")
assert.Equal(t, "/path4/", w.Header().Get("Location")) assert.Equal(t, "/path4/", w.Header().Get("Location"))
assert.Equal(t, http.StatusTemporaryRedirect, w.Code) assert.Equal(t, http.StatusTemporaryRedirect, w.Code)
w = performRequest(router, "GET", "/path") w = performRequest(router, http.MethodGet, "/path")
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, http.StatusOK, w.Code)
w = performRequest(router, "GET", "/path2/") w = performRequest(router, http.MethodGet, "/path2/")
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, http.StatusOK, w.Code)
w = performRequest(router, "POST", "/path3") w = performRequest(router, http.MethodPost, "/path3")
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, http.StatusOK, w.Code)
w = performRequest(router, "PUT", "/path4/") w = performRequest(router, http.MethodPut, "/path4/")
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, http.StatusOK, w.Code)
w = performRequest(router, "GET", "/path2", header{Key: "X-Forwarded-Prefix", Value: "/api"}) w = performRequest(router, http.MethodGet, "/path2", header{Key: "X-Forwarded-Prefix", Value: "/api"})
assert.Equal(t, "/api/path2/", w.Header().Get("Location")) assert.Equal(t, "/api/path2/", w.Header().Get("Location"))
assert.Equal(t, 301, w.Code) assert.Equal(t, 301, w.Code)
w = performRequest(router, "GET", "/path2/", header{Key: "X-Forwarded-Prefix", Value: "/api/"}) w = performRequest(router, http.MethodGet, "/path2/", header{Key: "X-Forwarded-Prefix", Value: "/api/"})
assert.Equal(t, 200, w.Code) assert.Equal(t, 200, w.Code)
router.RedirectTrailingSlash = false router.RedirectTrailingSlash = false
w = performRequest(router, "GET", "/path/") w = performRequest(router, http.MethodGet, "/path/")
assert.Equal(t, http.StatusNotFound, w.Code) assert.Equal(t, http.StatusNotFound, w.Code)
w = performRequest(router, "GET", "/path2") w = performRequest(router, http.MethodGet, "/path2")
assert.Equal(t, http.StatusNotFound, w.Code) assert.Equal(t, http.StatusNotFound, w.Code)
w = performRequest(router, "POST", "/path3/") w = performRequest(router, http.MethodPost, "/path3/")
assert.Equal(t, http.StatusNotFound, w.Code) assert.Equal(t, http.StatusNotFound, w.Code)
w = performRequest(router, "PUT", "/path4") w = performRequest(router, http.MethodPut, "/path4")
assert.Equal(t, http.StatusNotFound, w.Code) assert.Equal(t, http.StatusNotFound, w.Code)
} }
@ -207,19 +207,19 @@ func TestRouteRedirectFixedPath(t *testing.T) {
router.POST("/PATH3", func(c *Context) {}) router.POST("/PATH3", func(c *Context) {})
router.POST("/Path4/", func(c *Context) {}) router.POST("/Path4/", func(c *Context) {})
w := performRequest(router, "GET", "/PATH") w := performRequest(router, http.MethodGet, "/PATH")
assert.Equal(t, "/path", w.Header().Get("Location")) assert.Equal(t, "/path", w.Header().Get("Location"))
assert.Equal(t, http.StatusMovedPermanently, w.Code) assert.Equal(t, http.StatusMovedPermanently, w.Code)
w = performRequest(router, "GET", "/path2") w = performRequest(router, http.MethodGet, "/path2")
assert.Equal(t, "/Path2", w.Header().Get("Location")) assert.Equal(t, "/Path2", w.Header().Get("Location"))
assert.Equal(t, http.StatusMovedPermanently, w.Code) assert.Equal(t, http.StatusMovedPermanently, w.Code)
w = performRequest(router, "POST", "/path3") w = performRequest(router, http.MethodPost, "/path3")
assert.Equal(t, "/PATH3", w.Header().Get("Location")) assert.Equal(t, "/PATH3", w.Header().Get("Location"))
assert.Equal(t, http.StatusTemporaryRedirect, w.Code) assert.Equal(t, http.StatusTemporaryRedirect, w.Code)
w = performRequest(router, "POST", "/path4") w = performRequest(router, http.MethodPost, "/path4")
assert.Equal(t, "/Path4/", w.Header().Get("Location")) assert.Equal(t, "/Path4/", w.Header().Get("Location"))
assert.Equal(t, http.StatusTemporaryRedirect, w.Code) assert.Equal(t, http.StatusTemporaryRedirect, w.Code)
} }
@ -249,7 +249,7 @@ func TestRouteParamsByName(t *testing.T) {
assert.False(t, ok) assert.False(t, ok)
}) })
w := performRequest(router, "GET", "/test/john/smith/is/super/great") w := performRequest(router, http.MethodGet, "/test/john/smith/is/super/great")
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "john", name) assert.Equal(t, "john", name)
@ -263,6 +263,7 @@ func TestRouteParamsByNameWithExtraSlash(t *testing.T) {
lastName := "" lastName := ""
wild := "" wild := ""
router := New() router := New()
router.RemoveExtraSlash = true
router.GET("/test/:name/:last_name/*wild", func(c *Context) { router.GET("/test/:name/:last_name/*wild", func(c *Context) {
name = c.Params.ByName("name") name = c.Params.ByName("name")
lastName = c.Params.ByName("last_name") lastName = c.Params.ByName("last_name")
@ -282,7 +283,7 @@ func TestRouteParamsByNameWithExtraSlash(t *testing.T) {
assert.False(t, ok) assert.False(t, ok)
}) })
w := performRequest(router, "GET", "//test//john//smith//is//super//great") w := performRequest(router, http.MethodGet, "//test//john//smith//is//super//great")
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "john", name) assert.Equal(t, "john", name)
@ -310,16 +311,16 @@ func TestRouteStaticFile(t *testing.T) {
router.Static("/using_static", dir) router.Static("/using_static", dir)
router.StaticFile("/result", f.Name()) router.StaticFile("/result", f.Name())
w := performRequest(router, "GET", "/using_static/"+filename) w := performRequest(router, http.MethodGet, "/using_static/"+filename)
w2 := performRequest(router, "GET", "/result") w2 := performRequest(router, http.MethodGet, "/result")
assert.Equal(t, w, w2) assert.Equal(t, w, w2)
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "Gin Web Framework", w.Body.String()) assert.Equal(t, "Gin Web Framework", w.Body.String())
assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
w3 := performRequest(router, "HEAD", "/using_static/"+filename) w3 := performRequest(router, http.MethodHead, "/using_static/"+filename)
w4 := performRequest(router, "HEAD", "/result") w4 := performRequest(router, http.MethodHead, "/result")
assert.Equal(t, w3, w4) assert.Equal(t, w3, w4)
assert.Equal(t, http.StatusOK, w3.Code) assert.Equal(t, http.StatusOK, w3.Code)
@ -330,7 +331,7 @@ func TestRouteStaticListingDir(t *testing.T) {
router := New() router := New()
router.StaticFS("/", Dir("./", true)) router.StaticFS("/", Dir("./", true))
w := performRequest(router, "GET", "/") w := performRequest(router, http.MethodGet, "/")
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "gin.go") assert.Contains(t, w.Body.String(), "gin.go")
@ -342,7 +343,7 @@ func TestRouteStaticNoListing(t *testing.T) {
router := New() router := New()
router.Static("/", "./") router.Static("/", "./")
w := performRequest(router, "GET", "/") w := performRequest(router, http.MethodGet, "/")
assert.Equal(t, http.StatusNotFound, w.Code) assert.Equal(t, http.StatusNotFound, w.Code)
assert.NotContains(t, w.Body.String(), "gin.go") assert.NotContains(t, w.Body.String(), "gin.go")
@ -357,7 +358,7 @@ func TestRouterMiddlewareAndStatic(t *testing.T) {
}) })
static.Static("/", "./") static.Static("/", "./")
w := performRequest(router, "GET", "/gin.go") w := performRequest(router, http.MethodGet, "/gin.go")
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "package gin") assert.Contains(t, w.Body.String(), "package gin")
@ -371,13 +372,13 @@ func TestRouteNotAllowedEnabled(t *testing.T) {
router := New() router := New()
router.HandleMethodNotAllowed = true router.HandleMethodNotAllowed = true
router.POST("/path", func(c *Context) {}) router.POST("/path", func(c *Context) {})
w := performRequest(router, "GET", "/path") w := performRequest(router, http.MethodGet, "/path")
assert.Equal(t, http.StatusMethodNotAllowed, w.Code) assert.Equal(t, http.StatusMethodNotAllowed, w.Code)
router.NoMethod(func(c *Context) { router.NoMethod(func(c *Context) {
c.String(http.StatusTeapot, "responseText") c.String(http.StatusTeapot, "responseText")
}) })
w = performRequest(router, "GET", "/path") w = performRequest(router, http.MethodGet, "/path")
assert.Equal(t, "responseText", w.Body.String()) assert.Equal(t, "responseText", w.Body.String())
assert.Equal(t, http.StatusTeapot, w.Code) assert.Equal(t, http.StatusTeapot, w.Code)
} }
@ -386,9 +387,9 @@ func TestRouteNotAllowedEnabled2(t *testing.T) {
router := New() router := New()
router.HandleMethodNotAllowed = true router.HandleMethodNotAllowed = true
// add one methodTree to trees // add one methodTree to trees
router.addRoute("POST", "/", HandlersChain{func(_ *Context) {}}) router.addRoute(http.MethodPost, "/", HandlersChain{func(_ *Context) {}})
router.GET("/path2", func(c *Context) {}) router.GET("/path2", func(c *Context) {})
w := performRequest(router, "POST", "/path2") w := performRequest(router, http.MethodPost, "/path2")
assert.Equal(t, http.StatusMethodNotAllowed, w.Code) assert.Equal(t, http.StatusMethodNotAllowed, w.Code)
} }
@ -396,17 +397,40 @@ func TestRouteNotAllowedDisabled(t *testing.T) {
router := New() router := New()
router.HandleMethodNotAllowed = false router.HandleMethodNotAllowed = false
router.POST("/path", func(c *Context) {}) router.POST("/path", func(c *Context) {})
w := performRequest(router, "GET", "/path") w := performRequest(router, http.MethodGet, "/path")
assert.Equal(t, http.StatusNotFound, w.Code) assert.Equal(t, http.StatusNotFound, w.Code)
router.NoMethod(func(c *Context) { router.NoMethod(func(c *Context) {
c.String(http.StatusTeapot, "responseText") c.String(http.StatusTeapot, "responseText")
}) })
w = performRequest(router, "GET", "/path") w = performRequest(router, http.MethodGet, "/path")
assert.Equal(t, "404 page not found", w.Body.String()) assert.Equal(t, "404 page not found", w.Body.String())
assert.Equal(t, http.StatusNotFound, w.Code) assert.Equal(t, http.StatusNotFound, w.Code)
} }
func TestRouterNotFoundWithRemoveExtraSlash(t *testing.T) {
router := New()
router.RemoveExtraSlash = true
router.GET("/path", func(c *Context) {})
router.GET("/", func(c *Context) {})
testRoutes := []struct {
route string
code int
location string
}{
{"/../path", http.StatusOK, ""}, // CleanPath
{"/nope", http.StatusNotFound, ""}, // NotFound
}
for _, tr := range testRoutes {
w := performRequest(router, "GET", tr.route)
assert.Equal(t, tr.code, w.Code)
if w.Code != http.StatusNotFound {
assert.Equal(t, tr.location, fmt.Sprint(w.Header().Get("Location")))
}
}
}
func TestRouterNotFound(t *testing.T) { func TestRouterNotFound(t *testing.T) {
router := New() router := New()
router.RedirectFixedPath = true router.RedirectFixedPath = true
@ -425,11 +449,11 @@ func TestRouterNotFound(t *testing.T) {
{"/DIR/", http.StatusMovedPermanently, "/dir/"}, // Fixed Case {"/DIR/", http.StatusMovedPermanently, "/dir/"}, // Fixed Case
{"/PATH/", http.StatusMovedPermanently, "/path"}, // Fixed Case -/ {"/PATH/", http.StatusMovedPermanently, "/path"}, // Fixed Case -/
{"/DIR", http.StatusMovedPermanently, "/dir/"}, // Fixed Case +/ {"/DIR", http.StatusMovedPermanently, "/dir/"}, // Fixed Case +/
{"/../path", http.StatusOK, ""}, // CleanPath {"/../path", http.StatusMovedPermanently, "/path"}, // Without CleanPath
{"/nope", http.StatusNotFound, ""}, // NotFound {"/nope", http.StatusNotFound, ""}, // NotFound
} }
for _, tr := range testRoutes { for _, tr := range testRoutes {
w := performRequest(router, "GET", tr.route) w := performRequest(router, http.MethodGet, tr.route)
assert.Equal(t, tr.code, w.Code) assert.Equal(t, tr.code, w.Code)
if w.Code != http.StatusNotFound { if w.Code != http.StatusNotFound {
assert.Equal(t, tr.location, fmt.Sprint(w.Header().Get("Location"))) assert.Equal(t, tr.location, fmt.Sprint(w.Header().Get("Location")))
@ -442,20 +466,20 @@ func TestRouterNotFound(t *testing.T) {
c.AbortWithStatus(http.StatusNotFound) c.AbortWithStatus(http.StatusNotFound)
notFound = true notFound = true
}) })
w := performRequest(router, "GET", "/nope") w := performRequest(router, http.MethodGet, "/nope")
assert.Equal(t, http.StatusNotFound, w.Code) assert.Equal(t, http.StatusNotFound, w.Code)
assert.True(t, notFound) assert.True(t, notFound)
// Test other method than GET (want 307 instead of 301) // Test other method than GET (want 307 instead of 301)
router.PATCH("/path", func(c *Context) {}) router.PATCH("/path", func(c *Context) {})
w = performRequest(router, "PATCH", "/path/") w = performRequest(router, http.MethodPatch, "/path/")
assert.Equal(t, http.StatusTemporaryRedirect, w.Code) assert.Equal(t, http.StatusTemporaryRedirect, w.Code)
assert.Equal(t, "map[Location:[/path]]", fmt.Sprint(w.Header())) assert.Equal(t, "map[Location:[/path]]", fmt.Sprint(w.Header()))
// Test special case where no node for the prefix "/" exists // Test special case where no node for the prefix "/" exists
router = New() router = New()
router.GET("/a", func(c *Context) {}) router.GET("/a", func(c *Context) {})
w = performRequest(router, "GET", "/") w = performRequest(router, http.MethodGet, "/")
assert.Equal(t, http.StatusNotFound, w.Code) assert.Equal(t, http.StatusNotFound, w.Code)
} }
@ -466,10 +490,10 @@ func TestRouterStaticFSNotFound(t *testing.T) {
c.String(404, "non existent") c.String(404, "non existent")
}) })
w := performRequest(router, "GET", "/nonexistent") w := performRequest(router, http.MethodGet, "/nonexistent")
assert.Equal(t, "non existent", w.Body.String()) assert.Equal(t, "non existent", w.Body.String())
w = performRequest(router, "HEAD", "/nonexistent") w = performRequest(router, http.MethodHead, "/nonexistent")
assert.Equal(t, "non existent", w.Body.String()) assert.Equal(t, "non existent", w.Body.String())
} }
@ -479,7 +503,7 @@ func TestRouterStaticFSFileNotFound(t *testing.T) {
router.StaticFS("/", http.FileSystem(http.Dir("."))) router.StaticFS("/", http.FileSystem(http.Dir(".")))
assert.NotPanics(t, func() { assert.NotPanics(t, func() {
performRequest(router, "GET", "/nonexistent") performRequest(router, http.MethodGet, "/nonexistent")
}) })
} }
@ -490,17 +514,17 @@ func TestMiddlewareCalledOnceByRouterStaticFSNotFound(t *testing.T) {
// Middleware must be called just only once by per request. // Middleware must be called just only once by per request.
middlewareCalledNum := 0 middlewareCalledNum := 0
router.Use(func(c *Context) { router.Use(func(c *Context) {
middlewareCalledNum += 1 middlewareCalledNum++
}) })
router.StaticFS("/", http.FileSystem(http.Dir("/thisreallydoesntexist/"))) router.StaticFS("/", http.FileSystem(http.Dir("/thisreallydoesntexist/")))
// First access // First access
performRequest(router, "GET", "/nonexistent") performRequest(router, http.MethodGet, "/nonexistent")
assert.Equal(t, 1, middlewareCalledNum) assert.Equal(t, 1, middlewareCalledNum)
// Second access // Second access
performRequest(router, "HEAD", "/nonexistent") performRequest(router, http.MethodHead, "/nonexistent")
assert.Equal(t, 2, middlewareCalledNum) assert.Equal(t, 2, middlewareCalledNum)
} }
@ -519,7 +543,7 @@ func TestRouteRawPath(t *testing.T) {
assert.Equal(t, "222", num) assert.Equal(t, "222", num)
}) })
w := performRequest(route, "POST", "/project/Some%2FOther%2FProject/build/222") w := performRequest(route, http.MethodPost, "/project/Some%2FOther%2FProject/build/222")
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, http.StatusOK, w.Code)
} }
@ -539,7 +563,7 @@ func TestRouteRawPathNoUnescape(t *testing.T) {
assert.Equal(t, "333", num) assert.Equal(t, "333", num)
}) })
w := performRequest(route, "POST", "/project/Some%2FOther%2FProject/build/333") w := performRequest(route, http.MethodPost, "/project/Some%2FOther%2FProject/build/333")
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, http.StatusOK, w.Code)
} }
@ -550,7 +574,7 @@ func TestRouteServeErrorWithWriteHeader(t *testing.T) {
c.Next() c.Next()
}) })
w := performRequest(route, "GET", "/NotFound") w := performRequest(route, http.MethodGet, "/NotFound")
assert.Equal(t, 421, w.Code) assert.Equal(t, 421, w.Code)
assert.Equal(t, 0, w.Body.Len()) assert.Equal(t, 0, w.Body.Len())
} }
@ -569,6 +593,9 @@ func TestRouteContextHoldsFullPath(t *testing.T) {
"/simple-two/one-two", "/simple-two/one-two",
"/project/:name/build/*params", "/project/:name/build/*params",
"/project/:name/bui", "/project/:name/bui",
"/user/:id/status",
"/user/:id",
"/user/:id/profile",
} }
for _, route := range routes { for _, route := range routes {
@ -581,7 +608,7 @@ func TestRouteContextHoldsFullPath(t *testing.T) {
} }
for _, route := range routes { for _, route := range routes {
w := performRequest(router, "GET", route) w := performRequest(router, http.MethodGet, route)
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, http.StatusOK, w.Code)
} }
@ -591,6 +618,6 @@ func TestRouteContextHoldsFullPath(t *testing.T) {
assert.Equal(t, "", c.FullPath()) assert.Equal(t, "", c.FullPath())
}) })
w := performRequest(router, "GET", "/not-found") w := performRequest(router, http.MethodGet, "/not-found")
assert.Equal(t, http.StatusNotFound, w.Code) assert.Equal(t, http.StatusNotFound, w.Code)
} }

511
tree.go
View File

@ -8,6 +8,7 @@ import (
"net/url" "net/url"
"strings" "strings"
"unicode" "unicode"
"unicode/utf8"
) )
// Param is a single URL parameter, consisting of a key and a value. // Param is a single URL parameter, consisting of a key and a value.
@ -62,17 +63,24 @@ func min(a, b int) int {
return b return b
} }
func countParams(path string) uint8 { func longestCommonPrefix(a, b string) int {
i := 0
max := min(len(a), len(b))
for i < max && a[i] == b[i] {
i++
}
return i
}
func countParams(path string) uint16 {
var n uint var n uint
for i := 0; i < len(path); i++ { for i := range []byte(path) {
if path[i] == ':' || path[i] == '*' { switch path[i] {
case ':', '*':
n++ n++
} }
} }
if n >= 255 { return uint16(n)
return 255
}
return uint8(n)
} }
type nodeType uint8 type nodeType uint8
@ -87,34 +95,33 @@ const (
type node struct { type node struct {
path string path string
indices string indices string
wildChild bool
nType nodeType
priority uint32
children []*node children []*node
handlers HandlersChain handlers HandlersChain
priority uint32
nType nodeType
maxParams uint8
wildChild bool
fullPath string fullPath string
} }
// increments priority of the given child and reorders if necessary. // Increments priority of the given child and reorders if necessary
func (n *node) incrementChildPrio(pos int) int { func (n *node) incrementChildPrio(pos int) int {
n.children[pos].priority++ cs := n.children
prio := n.children[pos].priority cs[pos].priority++
prio := cs[pos].priority
// adjust position (move to front) // Adjust position (move to front)
newPos := pos newPos := pos
for newPos > 0 && n.children[newPos-1].priority < prio { for ; newPos > 0 && cs[newPos-1].priority < prio; newPos-- {
// swap node positions // Swap node positions
n.children[newPos-1], n.children[newPos] = n.children[newPos], n.children[newPos-1] cs[newPos-1], cs[newPos] = cs[newPos], cs[newPos-1]
newPos--
} }
// build new index char string // Build new index char string
if newPos != pos { if newPos != pos {
n.indices = n.indices[:newPos] + // unchanged prefix, might be empty n.indices = n.indices[:newPos] + // Unchanged prefix, might be empty
n.indices[pos:pos+1] + // the index char we move n.indices[pos:pos+1] + // The index char we move
n.indices[newPos:pos] + n.indices[pos+1:] // rest without char at 'pos' n.indices[newPos:pos] + n.indices[pos+1:] // Rest without char at 'pos'
} }
return newPos return newPos
@ -125,27 +132,22 @@ func (n *node) incrementChildPrio(pos int) int {
func (n *node) addRoute(path string, handlers HandlersChain) { func (n *node) addRoute(path string, handlers HandlersChain) {
fullPath := path fullPath := path
n.priority++ n.priority++
numParams := countParams(path)
// Empty tree
if len(n.path) == 0 && len(n.children) == 0 {
n.insertChild(path, fullPath, handlers)
n.nType = root
return
}
parentFullPathIndex := 0 parentFullPathIndex := 0
// non-empty tree
if len(n.path) > 0 || len(n.children) > 0 {
walk: walk:
for { for {
// Update maxParams of the current node
if numParams > n.maxParams {
n.maxParams = numParams
}
// Find the longest common prefix. // Find the longest common prefix.
// This also implies that the common prefix contains no ':' or '*' // This also implies that the common prefix contains no ':' or '*'
// since the existing key can't contain those chars. // since the existing key can't contain those chars.
i := 0 i := longestCommonPrefix(path, n.path)
max := min(len(path), len(n.path))
for i < max && path[i] == n.path[i] {
i++
}
// Split edge // Split edge
if i < len(n.path) { if i < len(n.path) {
@ -159,13 +161,6 @@ func (n *node) addRoute(path string, handlers HandlersChain) {
fullPath: n.fullPath, fullPath: n.fullPath,
} }
// Update maxParams (max of all children)
for i := range child.children {
if child.children[i].maxParams > child.maxParams {
child.maxParams = child.children[i].maxParams
}
}
n.children = []*node{&child} n.children = []*node{&child}
// []byte for proper unicode char conversion, see #65 // []byte for proper unicode char conversion, see #65
n.indices = string([]byte{n.path[i]}) n.indices = string([]byte{n.path[i]})
@ -184,19 +179,14 @@ func (n *node) addRoute(path string, handlers HandlersChain) {
n = n.children[0] n = n.children[0]
n.priority++ n.priority++
// Update maxParams of the child node
if numParams > n.maxParams {
n.maxParams = numParams
}
numParams--
// Check if the wildcard matches // Check if the wildcard matches
if len(path) >= len(n.path) && n.path == path[:len(n.path)] { if len(path) >= len(n.path) && n.path == path[:len(n.path)] &&
// check for longer wildcard, e.g. :name and :names // Adding a child to a catchAll is not possible
if len(n.path) >= len(path) || path[len(n.path)] == '/' { n.nType != catchAll &&
// Check for longer wildcard, e.g. :name and :names
(len(n.path) >= len(path) || path[len(n.path)] == '/') {
continue walk continue walk
} }
}
pathSeg := path pathSeg := path
if n.nType != catchAll { if n.nType != catchAll {
@ -221,7 +211,7 @@ func (n *node) addRoute(path string, handlers HandlersChain) {
} }
// Check if a child with the next path byte exists // Check if a child with the next path byte exists
for i := 0; i < len(n.indices); i++ { for i, max := 0, len(n.indices); i < max; i++ {
if c == n.indices[i] { if c == n.indices[i] {
parentFullPathIndex += len(n.path) parentFullPathIndex += len(n.path)
i = n.incrementChildPrio(i) i = n.incrementChildPrio(i)
@ -235,100 +225,115 @@ func (n *node) addRoute(path string, handlers HandlersChain) {
// []byte for proper unicode char conversion, see #65 // []byte for proper unicode char conversion, see #65
n.indices += string([]byte{c}) n.indices += string([]byte{c})
child := &node{ child := &node{
maxParams: numParams,
fullPath: fullPath, fullPath: fullPath,
} }
n.children = append(n.children, child) n.children = append(n.children, child)
n.incrementChildPrio(len(n.indices) - 1) n.incrementChildPrio(len(n.indices) - 1)
n = child n = child
} }
n.insertChild(numParams, path, fullPath, handlers) n.insertChild(path, fullPath, handlers)
return return
}
} else if i == len(path) { // Make node a (in-path) leaf // Otherwise and handle to current node
if n.handlers != nil { if n.handlers != nil {
panic("handlers are already registered for path '" + fullPath + "'") panic("handlers are already registered for path '" + fullPath + "'")
} }
n.handlers = handlers n.handlers = handlers
} n.fullPath = fullPath
return return
} }
} else { // Empty tree
n.insertChild(numParams, path, fullPath, handlers)
n.nType = root
}
} }
func (n *node) insertChild(numParams uint8, path string, fullPath string, handlers HandlersChain) { // Search for a wildcard segment and check the name for invalid characters.
var offset int // already handled bytes of the path // Returns -1 as index, if no wildcard was found.
func findWildcard(path string) (wildcard string, i int, valid bool) {
// find prefix until first wildcard (beginning with ':' or '*') // Find start
for i, max := 0, len(path); numParams > 0; i++ { for start, c := range []byte(path) {
c := path[i] // A wildcard starts with ':' (param) or '*' (catch-all)
if c != ':' && c != '*' { if c != ':' && c != '*' {
continue continue
} }
// find wildcard end (either '/' or path end) // Find end and check for invalid characters
end := i + 1 valid = true
for end < max && path[end] != '/' { for end, c := range []byte(path[start+1:]) {
switch path[end] { switch c {
// the wildcard name must not contain ':' and '*' case '/':
return path[start : start+1+end], start, valid
case ':', '*': case ':', '*':
panic("only one wildcard per path segment is allowed, has: '" + valid = false
path[i:] + "' in path '" + fullPath + "'")
default:
end++
} }
} }
return path[start:], start, valid
}
return "", -1, false
}
// check if this Node existing children which would be func (n *node) insertChild(path string, fullPath string, handlers HandlersChain) {
// unreachable if we insert the wildcard here for {
if len(n.children) > 0 { // Find prefix until first wildcard
panic("wildcard route '" + path[i:end] + wildcard, i, valid := findWildcard(path)
"' conflicts with existing children in path '" + fullPath + "'") if i < 0 { // No wildcard found
break
}
// The wildcard name must not contain ':' and '*'
if !valid {
panic("only one wildcard per path segment is allowed, has: '" +
wildcard + "' in path '" + fullPath + "'")
} }
// check if the wildcard has a name // check if the wildcard has a name
if end-i < 2 { if len(wildcard) < 2 {
panic("wildcards must be named with a non-empty name in path '" + fullPath + "'") panic("wildcards must be named with a non-empty name in path '" + fullPath + "'")
} }
if c == ':' { // param // Check if this node has existing children which would be
// split path at the beginning of the wildcard // unreachable if we insert the wildcard here
if i > 0 { if len(n.children) > 0 {
n.path = path[offset:i] panic("wildcard segment '" + wildcard +
offset = i "' conflicts with existing children in path '" + fullPath + "'")
} }
if wildcard[0] == ':' { // param
if i > 0 {
// Insert prefix before the current wildcard
n.path = path[:i]
path = path[i:]
}
n.wildChild = true
child := &node{ child := &node{
nType: param, nType: param,
maxParams: numParams, path: wildcard,
fullPath: fullPath, fullPath: fullPath,
} }
n.children = []*node{child} n.children = []*node{child}
n.wildChild = true
n = child n = child
n.priority++ n.priority++
numParams--
// if the path doesn't end with the wildcard, then there // if the path doesn't end with the wildcard, then there
// will be another non-wildcard subpath starting with '/' // will be another non-wildcard subpath starting with '/'
if end < max { if len(wildcard) < len(path) {
n.path = path[offset:end] path = path[len(wildcard):]
offset = end
child := &node{ child := &node{
maxParams: numParams,
priority: 1, priority: 1,
fullPath: fullPath, fullPath: fullPath,
} }
n.children = []*node{child} n.children = []*node{child}
n = child n = child
continue
} }
} else { // catchAll // Otherwise we're done. Insert the handle in the new leaf
if end != max || numParams > 1 { n.handlers = handlers
return
}
// catchAll
if i+len(wildcard) != len(path) {
panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'") panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'")
} }
@ -342,17 +347,17 @@ func (n *node) insertChild(numParams uint8, path string, fullPath string, handle
panic("no / before catch-all in path '" + fullPath + "'") panic("no / before catch-all in path '" + fullPath + "'")
} }
n.path = path[offset:i] n.path = path[:i]
// first node: catchAll node with empty path // First node: catchAll node with empty path
child := &node{ child := &node{
wildChild: true, wildChild: true,
nType: catchAll, nType: catchAll,
maxParams: 1,
fullPath: fullPath, fullPath: fullPath,
} }
n.children = []*node{child} n.children = []*node{child}
n.indices = string(path[i]) n.indices = string('/')
n = child n = child
n.priority++ n.priority++
@ -360,7 +365,6 @@ func (n *node) insertChild(numParams uint8, path string, fullPath string, handle
child = &node{ child = &node{
path: path[i:], path: path[i:],
nType: catchAll, nType: catchAll,
maxParams: 1,
handlers: handlers, handlers: handlers,
priority: 1, priority: 1,
fullPath: fullPath, fullPath: fullPath,
@ -369,10 +373,9 @@ func (n *node) insertChild(numParams uint8, path string, fullPath string, handle
return return
} }
}
// insert remaining path part and handle to the leaf // If no wildcard was found, simply insert the path and handle
n.path = path[offset:] n.path = path
n.handlers = handlers n.handlers = handlers
n.fullPath = fullPath n.fullPath = fullPath
} }
@ -380,31 +383,32 @@ func (n *node) insertChild(numParams uint8, path string, fullPath string, handle
// nodeValue holds return values of (*Node).getValue method // nodeValue holds return values of (*Node).getValue method
type nodeValue struct { type nodeValue struct {
handlers HandlersChain handlers HandlersChain
params Params params *Params
tsr bool tsr bool
fullPath string fullPath string
} }
// getValue returns the handle registered with the given path (key). The values of // Returns the handle registered with the given path (key). The values of
// wildcards are saved to a map. // wildcards are saved to a map.
// If no handle can be found, a TSR (trailing slash redirect) recommendation is // If no handle can be found, a TSR (trailing slash redirect) recommendation is
// made if a handle exists with an extra (without the) trailing slash for the // made if a handle exists with an extra (without the) trailing slash for the
// given path. // given path.
func (n *node) getValue(path string, po Params, unescape bool) (value nodeValue) { func (n *node) getValue(path string, params *Params, unescape bool) (value nodeValue) {
value.params = po
walk: // Outer loop for walking the tree walk: // Outer loop for walking the tree
for { for {
if len(path) > len(n.path) {
if strings.ToLower(path[:len(n.path)]) == strings.ToLower(n.path) { prefix := n.path
//if path[:len(n.path)] == n.path { if len(path) > len(prefix) {
path = path[len(n.path):] if strings.ToLower(path[:len(prefix)]) == strings.ToLower(prefix) {
path = path[len(prefix):]
// If this node does not have a wildcard (param or catchAll) // If this node does not have a wildcard (param or catchAll)
// child, we can just look up the next child node and continue // child, we can just look up the next child node and continue
// to walk down the tree // to walk down the tree
if !n.wildChild { if !n.wildChild {
c := path[0] idxc := path[0]
for i := 0; i < len(n.indices); i++ { for i, c := range []byte(n.indices) {
if c == n.indices[i] { if c == idxc {
n = n.children[i] n = n.children[i]
continue walk continue walk
} }
@ -413,35 +417,38 @@ walk: // Outer loop for walking the tree
// Nothing found. // Nothing found.
// We can recommend to redirect to the same URL without a // We can recommend to redirect to the same URL without a
// trailing slash if a leaf exists for that path. // trailing slash if a leaf exists for that path.
value.tsr = path == "/" && n.handlers != nil value.tsr = (path == "/" && n.handlers != nil)
return return
} }
// handle wildcard child // Handle wildcard child
n = n.children[0] n = n.children[0]
switch n.nType { switch n.nType {
case param: case param:
// find param end (either '/' or path end) // Find param end (either '/' or path end)
end := 0 end := 0
for end < len(path) && path[end] != '/' { for end < len(path) && path[end] != '/' {
end++ end++
} }
// save param value // Save param value
if cap(value.params) < int(n.maxParams) { if params != nil {
value.params = make(Params, 0, n.maxParams) if value.params == nil {
value.params = params
} }
i := len(value.params) // Expand slice within preallocated capacity
value.params = value.params[:i+1] // expand slice within preallocated capacity i := len(*value.params)
value.params[i].Key = n.path[1:] *value.params = (*value.params)[:i+1]
val := path[:end] val := path[:end]
if unescape { if unescape {
var err error if v, err := url.QueryUnescape(val); err == nil {
if value.params[i].Value, err = url.QueryUnescape(val); err != nil { val = v
value.params[i].Value = val // fallback, in case of error }
}
(*value.params)[i] = Param{
Key: n.path[1:],
Value: val,
} }
} else {
value.params[i].Value = val
} }
// we need to go deeper! // we need to go deeper!
@ -453,7 +460,7 @@ walk: // Outer loop for walking the tree
} }
// ... but we can't // ... but we can't
value.tsr = len(path) == end+1 value.tsr = (len(path) == end+1)
return return
} }
@ -465,26 +472,29 @@ walk: // Outer loop for walking the tree
// No handle found. Check if a handle for this path + a // No handle found. Check if a handle for this path + a
// trailing slash exists for TSR recommendation // trailing slash exists for TSR recommendation
n = n.children[0] n = n.children[0]
value.tsr = n.path == "/" && n.handlers != nil value.tsr = (n.path == "/" && n.handlers != nil)
} }
return return
case catchAll: case catchAll:
// save param value // Save param value
if cap(value.params) < int(n.maxParams) { if params != nil {
value.params = make(Params, 0, n.maxParams) if value.params == nil {
value.params = params
} }
i := len(value.params) // Expand slice within preallocated capacity
value.params = value.params[:i+1] // expand slice within preallocated capacity i := len(*value.params)
value.params[i].Key = n.path[2:] *value.params = (*value.params)[:i+1]
val := path
if unescape { if unescape {
var err error if v, err := url.QueryUnescape(path); err == nil {
if value.params[i].Value, err = url.QueryUnescape(path); err != nil { val = v
value.params[i].Value = path // fallback, in case of error }
}
(*value.params)[i] = Param{
Key: n.path[2:],
Value: val,
} }
} else {
value.params[i].Value = path
} }
value.handlers = n.handlers value.handlers = n.handlers
@ -495,8 +505,11 @@ walk: // Outer loop for walking the tree
panic("invalid node type") panic("invalid node type")
} }
} }
} else if strings.ToLower(path) == strings.ToLower(n.path) {
//} else if path == n.path { }
if strings.ToLower(path) == strings.ToLower(prefix) {
// We should have reached the node containing the handle. // We should have reached the node containing the handle.
// Check if this node has a handle registered. // Check if this node has a handle registered.
if value.handlers = n.handlers; value.handlers != nil { if value.handlers = n.handlers; value.handlers != nil {
@ -504,6 +517,9 @@ walk: // Outer loop for walking the tree
return return
} }
// If there is no handle for this route, but this route has a
// wildcard child, there must be a handle for this path with an
// additional trailing slash
if path == "/" && n.wildChild && n.nType != root { if path == "/" && n.wildChild && n.nType != root {
value.tsr = true value.tsr = true
return return
@ -511,8 +527,8 @@ walk: // Outer loop for walking the tree
// No handle found. Check if a handle for this path + a // No handle found. Check if a handle for this path + a
// trailing slash exists for trailing slash recommendation // trailing slash exists for trailing slash recommendation
for i := 0; i < len(n.indices); i++ { for i, c := range []byte(n.indices) {
if n.indices[i] == '/' { if c == '/' {
n = n.children[i] n = n.children[i]
value.tsr = (len(n.path) == 1 && n.handlers != nil) || value.tsr = (len(n.path) == 1 && n.handlers != nil) ||
(n.nType == catchAll && n.children[0].handlers != nil) (n.nType == catchAll && n.children[0].handlers != nil)
@ -526,22 +542,61 @@ walk: // Outer loop for walking the tree
// Nothing found. We can recommend to redirect to the same URL with an // Nothing found. We can recommend to redirect to the same URL with an
// extra trailing slash if a leaf exists for that path // extra trailing slash if a leaf exists for that path
value.tsr = (path == "/") || value.tsr = (path == "/") ||
(len(n.path) == len(path)+1 && n.path[len(path)] == '/' && (len(prefix) == len(path)+1 && prefix[len(path)] == '/' &&
path == n.path[:len(n.path)-1] && n.handlers != nil) path == prefix[:len(prefix)-1] && n.handlers != nil)
return return
} }
} }
// findCaseInsensitivePath makes a case-insensitive lookup of the given path and tries to find a handler. // Makes a case-insensitive lookup of the given path and tries to find a handler.
// It can optionally also fix trailing slashes. // It can optionally also fix trailing slashes.
// It returns the case-corrected path and a bool indicating whether the lookup // It returns the case-corrected path and a bool indicating whether the lookup
// was successful. // was successful.
func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (ciPath []byte, found bool) { func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) ([]byte, bool) {
ciPath = make([]byte, 0, len(path)+1) // preallocate enough memory const stackBufSize = 128
// Outer loop for walking the tree // Use a static sized buffer on the stack in the common case.
for len(path) >= len(n.path) && strings.EqualFold(path[:len(n.path)], n.path) { // If the path is too long, allocate a buffer on the heap instead.
path = path[len(n.path):] buf := make([]byte, 0, stackBufSize)
if l := len(path) + 1; l > stackBufSize {
buf = make([]byte, 0, l)
}
ciPath := n.findCaseInsensitivePathRec(
path,
buf, // Preallocate enough memory for new path
[4]byte{}, // Empty rune buffer
fixTrailingSlash,
)
return ciPath, ciPath != nil
}
// Shift bytes in array by n bytes left
func shiftNRuneBytes(rb [4]byte, n int) [4]byte {
switch n {
case 0:
return rb
case 1:
return [4]byte{rb[1], rb[2], rb[3], 0}
case 2:
return [4]byte{rb[2], rb[3]}
case 3:
return [4]byte{rb[3]}
default:
return [4]byte{}
}
}
// Recursive case-insensitive lookup function used by n.findCaseInsensitivePath
func (n *node) findCaseInsensitivePathRec(path string, ciPath []byte, rb [4]byte, fixTrailingSlash bool) []byte {
npLen := len(n.path)
walk: // Outer loop for walking the tree
for len(path) >= npLen && (npLen == 0 || strings.EqualFold(path[1:npLen], n.path[1:])) {
// Add common prefix to result
oldPath := path
path = path[npLen:]
ciPath = append(ciPath, n.path...) ciPath = append(ciPath, n.path...)
if len(path) > 0 { if len(path) > 0 {
@ -549,65 +604,132 @@ func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (ciPa
// we can just look up the next child node and continue to walk down // we can just look up the next child node and continue to walk down
// the tree // the tree
if !n.wildChild { if !n.wildChild {
r := unicode.ToLower(rune(path[0])) // Skip rune bytes already processed
for i, index := range n.indices { rb = shiftNRuneBytes(rb, npLen)
// must use recursive approach since both index and
// ToLower(index) could exist. We must check both. if rb[0] != 0 {
if r == unicode.ToLower(index) { // Old rune not finished
out, found := n.children[i].findCaseInsensitivePath(path, fixTrailingSlash) idxc := rb[0]
if found { for i, c := range []byte(n.indices) {
return append(ciPath, out...), true if c == idxc {
// continue with child node
n = n.children[i]
npLen = len(n.path)
continue walk
}
}
} else {
// Process a new rune
var rv rune
// Find rune start.
// Runes are up to 4 byte long,
// -4 would definitely be another rune.
var off int
for max := min(npLen, 3); off < max; off++ {
if i := npLen - off; utf8.RuneStart(oldPath[i]) {
// read rune from cached path
rv, _ = utf8.DecodeRuneInString(oldPath[i:])
break
}
}
// Calculate lowercase bytes of current rune
lo := unicode.ToLower(rv)
utf8.EncodeRune(rb[:], lo)
// Skip already processed bytes
rb = shiftNRuneBytes(rb, off)
idxc := rb[0]
for i, c := range []byte(n.indices) {
// Lowercase matches
if c == idxc {
// must use a recursive approach since both the
// uppercase byte and the lowercase byte might exist
// as an index
if out := n.children[i].findCaseInsensitivePathRec(
path, ciPath, rb, fixTrailingSlash,
); out != nil {
return out
}
break
}
}
// If we found no match, the same for the uppercase rune,
// if it differs
if up := unicode.ToUpper(rv); up != lo {
utf8.EncodeRune(rb[:], up)
rb = shiftNRuneBytes(rb, off)
idxc := rb[0]
for i, c := range []byte(n.indices) {
// Uppercase matches
if c == idxc {
// Continue with child node
n = n.children[i]
npLen = len(n.path)
continue walk
}
} }
} }
} }
// Nothing found. We can recommend to redirect to the same URL // Nothing found. We can recommend to redirect to the same URL
// without a trailing slash if a leaf exists for that path // without a trailing slash if a leaf exists for that path
found = fixTrailingSlash && path == "/" && n.handlers != nil if fixTrailingSlash && path == "/" && n.handlers != nil {
return return ciPath
}
return nil
} }
n = n.children[0] n = n.children[0]
switch n.nType { switch n.nType {
case param: case param:
// find param end (either '/' or path end) // Find param end (either '/' or path end)
k := 0 end := 0
for k < len(path) && path[k] != '/' { for end < len(path) && path[end] != '/' {
k++ end++
} }
// add param value to case insensitive path // Add param value to case insensitive path
ciPath = append(ciPath, path[:k]...) ciPath = append(ciPath, path[:end]...)
// we need to go deeper! // We need to go deeper!
if k < len(path) { if end < len(path) {
if len(n.children) > 0 { if len(n.children) > 0 {
path = path[k:] // Continue with child node
n = n.children[0] n = n.children[0]
npLen = len(n.path)
path = path[end:]
continue continue
} }
// ... but we can't // ... but we can't
if fixTrailingSlash && len(path) == k+1 { if fixTrailingSlash && len(path) == end+1 {
return ciPath, true return ciPath
} }
return return nil
} }
if n.handlers != nil { if n.handlers != nil {
return ciPath, true return ciPath
} else if fixTrailingSlash && len(n.children) == 1 { }
if fixTrailingSlash && len(n.children) == 1 {
// No handle found. Check if a handle for this path + a // No handle found. Check if a handle for this path + a
// trailing slash exists // trailing slash exists
n = n.children[0] n = n.children[0]
if n.path == "/" && n.handlers != nil { if n.path == "/" && n.handlers != nil {
return append(ciPath, '/'), true return append(ciPath, '/')
} }
} }
return
return nil
case catchAll: case catchAll:
return append(ciPath, path...), true return append(ciPath, path...)
default: default:
panic("invalid node type") panic("invalid node type")
@ -616,24 +738,24 @@ func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (ciPa
// We should have reached the node containing the handle. // We should have reached the node containing the handle.
// Check if this node has a handle registered. // Check if this node has a handle registered.
if n.handlers != nil { if n.handlers != nil {
return ciPath, true return ciPath
} }
// No handle found. // No handle found.
// Try to fix the path by adding a trailing slash // Try to fix the path by adding a trailing slash
if fixTrailingSlash { if fixTrailingSlash {
for i := 0; i < len(n.indices); i++ { for i, c := range []byte(n.indices) {
if n.indices[i] == '/' { if c == '/' {
n = n.children[i] n = n.children[i]
if (len(n.path) == 1 && n.handlers != nil) || if (len(n.path) == 1 && n.handlers != nil) ||
(n.nType == catchAll && n.children[0].handlers != nil) { (n.nType == catchAll && n.children[0].handlers != nil) {
return append(ciPath, '/'), true return append(ciPath, '/')
} }
return return nil
} }
} }
} }
return return nil
} }
} }
@ -641,13 +763,12 @@ func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (ciPa
// Try to fix the path by adding / removing a trailing slash // Try to fix the path by adding / removing a trailing slash
if fixTrailingSlash { if fixTrailingSlash {
if path == "/" { if path == "/" {
return ciPath, true return ciPath
} }
if len(path)+1 == len(n.path) && n.path[len(path)] == '/' && if len(path)+1 == npLen && n.path[len(path)] == '/' &&
strings.EqualFold(path, n.path[:len(path)]) && strings.EqualFold(path[1:], n.path[1:len(path)]) && n.handlers != nil {
n.handlers != nil { return append(ciPath, n.path...)
return append(ciPath, n.path...), true
} }
} }
return return nil
} }

View File

@ -28,6 +28,11 @@ type testRequests []struct {
ps Params ps Params
} }
func getParams() *Params {
ps := make(Params, 0, 20)
return &ps
}
func checkRequests(t *testing.T, tree *node, requests testRequests, unescapes ...bool) { func checkRequests(t *testing.T, tree *node, requests testRequests, unescapes ...bool) {
unescape := false unescape := false
if len(unescapes) >= 1 { if len(unescapes) >= 1 {
@ -35,7 +40,7 @@ func checkRequests(t *testing.T, tree *node, requests testRequests, unescapes ..
} }
for _, request := range requests { for _, request := range requests {
value := tree.getValue(request.path, nil, unescape) value := tree.getValue(request.path, getParams(), unescape)
if value.handlers == nil { if value.handlers == nil {
if !request.nilHandler { if !request.nilHandler {
@ -50,10 +55,13 @@ func checkRequests(t *testing.T, tree *node, requests testRequests, unescapes ..
} }
} }
if !reflect.DeepEqual(value.params, request.ps) { if value.params != nil {
if !reflect.DeepEqual(*value.params, request.ps) {
t.Errorf("Params mismatch for route '%s'", request.path) t.Errorf("Params mismatch for route '%s'", request.path)
} }
} }
}
} }
func checkPriorities(t *testing.T, n *node) uint32 { func checkPriorities(t *testing.T, n *node) uint32 {
@ -76,33 +84,11 @@ func checkPriorities(t *testing.T, n *node) uint32 {
return prio return prio
} }
func checkMaxParams(t *testing.T, n *node) uint8 {
var maxParams uint8
for i := range n.children {
params := checkMaxParams(t, n.children[i])
if params > maxParams {
maxParams = params
}
}
if n.nType > root && !n.wildChild {
maxParams++
}
if n.maxParams != maxParams {
t.Errorf(
"maxParams mismatch for node '%s': is %d, should be %d",
n.path, n.maxParams, maxParams,
)
}
return maxParams
}
func TestCountParams(t *testing.T) { func TestCountParams(t *testing.T) {
if countParams("/path/:param1/static/*catch-all") != 2 { if countParams("/path/:param1/static/*catch-all") != 2 {
t.Fail() t.Fail()
} }
if countParams(strings.Repeat("/:param", 256)) != 255 { if countParams(strings.Repeat("/:param", 256)) != 256 {
t.Fail() t.Fail()
} }
} }
@ -142,7 +128,6 @@ func TestTreeAddAndGet(t *testing.T) {
}) })
checkPriorities(t, tree) checkPriorities(t, tree)
checkMaxParams(t, tree)
} }
func TestTreeWildcard(t *testing.T) { func TestTreeWildcard(t *testing.T) {
@ -186,7 +171,6 @@ func TestTreeWildcard(t *testing.T) {
}) })
checkPriorities(t, tree) checkPriorities(t, tree)
checkMaxParams(t, tree)
} }
func TestUnescapeParameters(t *testing.T) { func TestUnescapeParameters(t *testing.T) {
@ -224,7 +208,6 @@ func TestUnescapeParameters(t *testing.T) {
}, unescape) }, unescape)
checkPriorities(t, tree) checkPriorities(t, tree)
checkMaxParams(t, tree)
} }
func catchPanic(testFunc func()) (recv interface{}) { func catchPanic(testFunc func()) (recv interface{}) {
@ -323,12 +306,14 @@ func TestTreeDupliatePath(t *testing.T) {
} }
} }
//printChildren(tree, "")
checkRequests(t, tree, testRequests{ checkRequests(t, tree, testRequests{
{"/", false, "/", nil}, {"/", false, "/", nil},
{"/doc/", false, "/doc/", nil}, {"/doc/", false, "/doc/", nil},
{"/src/some/file.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file.png"}}}, {"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file.png"}}},
{"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{Key: "query", Value: "someth!ng+in+ünìcodé"}}}, {"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{"query", "someth!ng+in+ünìcodé"}}},
{"/user_gopher", false, "/user_:name", Params{Param{Key: "name", Value: "gopher"}}}, {"/user_gopher", false, "/user_:name", Params{Param{"name", "gopher"}}},
}) })
} }
@ -356,6 +341,8 @@ func TestTreeCatchAllConflict(t *testing.T) {
{"/src/*filepath/x", true}, {"/src/*filepath/x", true},
{"/src2/", false}, {"/src2/", false},
{"/src2/*filepath/x", true}, {"/src2/*filepath/x", true},
{"/src3/*filepath", false},
{"/src3/*filepath/x", true},
} }
testRoutes(t, routes) testRoutes(t, routes)
} }
@ -368,6 +355,12 @@ func TestTreeCatchAllConflictRoot(t *testing.T) {
testRoutes(t, routes) testRoutes(t, routes)
} }
func TestTreeCatchMaxParams(t *testing.T) {
tree := &node{}
var route = "/cmd/*filepath"
tree.addRoute(route, fakeHandler(route))
}
func TestTreeDoubleWildcard(t *testing.T) { func TestTreeDoubleWildcard(t *testing.T) {
const panicMsg = "only one wildcard per path segment is allowed" const panicMsg = "only one wildcard per path segment is allowed"
@ -501,6 +494,9 @@ func TestTreeRootTrailingSlashRedirect(t *testing.T) {
func TestTreeFindCaseInsensitivePath(t *testing.T) { func TestTreeFindCaseInsensitivePath(t *testing.T) {
tree := &node{} tree := &node{}
longPath := "/l" + strings.Repeat("o", 128) + "ng"
lOngPath := "/l" + strings.Repeat("O", 128) + "ng/"
routes := [...]string{ routes := [...]string{
"/hi", "/hi",
"/b/", "/b/",
@ -524,6 +520,17 @@ func TestTreeFindCaseInsensitivePath(t *testing.T) {
"/doc/go/away", "/doc/go/away",
"/no/a", "/no/a",
"/no/b", "/no/b",
"/Π",
"/u/apfêl/",
"/u/äpfêl/",
"/u/öpfêl",
"/v/Äpfêl/",
"/v/Öpfêl",
"/w/♬", // 3 byte
"/w/♭/", // 3 byte, last byte differs
"/w/𠜎", // 4 byte
"/w/𠜏/", // 4 byte
longPath,
} }
for _, route := range routes { for _, route := range routes {
@ -602,6 +609,21 @@ func TestTreeFindCaseInsensitivePath(t *testing.T) {
{"/DOC/", "/doc", true, true}, {"/DOC/", "/doc", true, true},
{"/NO", "", false, true}, {"/NO", "", false, true},
{"/DOC/GO", "", false, true}, {"/DOC/GO", "", false, true},
{"/π", "/Π", true, false},
{"/π/", "/Π", true, true},
{"/u/ÄPFÊL/", "/u/äpfêl/", true, false},
{"/u/ÄPFÊL", "/u/äpfêl/", true, true},
{"/u/ÖPFÊL/", "/u/öpfêl", true, true},
{"/u/ÖPFÊL", "/u/öpfêl", true, false},
{"/v/äpfêL/", "/v/Äpfêl/", true, false},
{"/v/äpfêL", "/v/Äpfêl/", true, true},
{"/v/öpfêL/", "/v/Öpfêl", true, true},
{"/v/öpfêL", "/v/Öpfêl", true, false},
{"/w/♬/", "/w/♬", true, true},
{"/w/♭", "/w/♭/", true, true},
{"/w/𠜎/", "/w/𠜎", true, true},
{"/w/𠜏", "/w/𠜏/", true, true},
{lOngPath, longPath, true, true},
} }
// With fixTrailingSlash = true // With fixTrailingSlash = true
for _, test := range tests { for _, test := range tests {
@ -689,8 +711,7 @@ func TestTreeWildcardConflictEx(t *testing.T) {
tree.addRoute(conflict.route, fakeHandler(conflict.route)) tree.addRoute(conflict.route, fakeHandler(conflict.route))
}) })
if !regexp.MustCompile(fmt.Sprintf("'%s' in new path .* conflicts with existing wildcard '%s' in existing prefix '%s'", if !regexp.MustCompile(fmt.Sprintf("'%s' in new path .* conflicts with existing wildcard '%s' in existing prefix '%s'", conflict.segPath, conflict.existSegPath, conflict.existPath)).MatchString(fmt.Sprint(recv)) {
conflict.segPath, conflict.existSegPath, conflict.existPath)).MatchString(fmt.Sprint(recv)) {
t.Fatalf("invalid wildcard conflict error (%v)", recv) t.Fatalf("invalid wildcard conflict error (%v)", recv)
} }
} }

View File

@ -90,13 +90,13 @@ func filterFlags(content string) string {
} }
func chooseData(custom, wildcard interface{}) interface{} { func chooseData(custom, wildcard interface{}) interface{} {
if custom == nil { if custom != nil {
if wildcard == nil { return custom
panic("negotiation config is invalid")
} }
if wildcard != nil {
return wildcard return wildcard
} }
return custom panic("negotiation config is invalid")
} }
func parseAccept(acceptHeader string) []string { func parseAccept(acceptHeader string) []string {
@ -127,8 +127,7 @@ func joinPaths(absolutePath, relativePath string) string {
} }
finalPath := path.Join(absolutePath, relativePath) finalPath := path.Join(absolutePath, relativePath)
appendSlash := lastChar(relativePath) == '/' && lastChar(finalPath) != '/' if lastChar(relativePath) == '/' && lastChar(finalPath) != '/' {
if appendSlash {
return finalPath + "/" return finalPath + "/"
} }
return finalPath return finalPath

147
vendor/vendor.json vendored
View File

@ -1,147 +0,0 @@
{
"comment": "v1.4.0",
"ignore": "test",
"package": [
{
"checksumSHA1": "CSPbwbyzqA6sfORicn4HFtIhF/c=",
"path": "github.com/davecgh/go-spew/spew",
"revision": "8991bc29aa16c548c550c7ff78260e27b9ab7c73",
"revisionTime": "2018-02-21T22:46:20Z",
"version": "v1.1",
"versionExact": "v1.1.1"
},
{
"checksumSHA1": "qlEzrgKgIkh7y0ePm9BNo1cNdXo=",
"path": "github.com/gin-contrib/sse",
"revision": "54d8467d122d380a14768b6b4e5cd7ca4755938f",
"revisionTime": "2019-06-02T15:02:53Z",
"version": "v0.1",
"versionExact": "v0.1.0"
},
{
"checksumSHA1": "b4DmyMT9bicTRVJw1hJXHLhIH+0=",
"path": "github.com/go-playground/locales",
"revision": "f63010822830b6fe52288ee52d5a1151088ce039",
"revisionTime": "2018-03-23T16:04:04Z",
"version": "v0.12",
"versionExact": "v0.12.1"
},
{
"checksumSHA1": "JgF260rC9YpWyY5WEljjimWLUXs=",
"path": "github.com/go-playground/locales/currency",
"revision": "630ebbb602847eba93e75ae38bbc7bb7abcf1ff3",
"revisionTime": "2019-04-30T15:33:29Z"
},
{
"checksumSHA1": "9pKcUHBaVS+360X6h4IowhmOPjk=",
"path": "github.com/go-playground/universal-translator",
"revision": "b32fa301c9fe55953584134cb6853a13c87ec0a1",
"revisionTime": "2017-02-09T16:11:52Z",
"version": "v0.16",
"versionExact": "v0.16.0"
},
{
"checksumSHA1": "Y2MOwzNZfl4NRNDbLCZa6sgx7O0=",
"path": "github.com/golang/protobuf/proto",
"revision": "c823c79ea1570fb5ff454033735a8e68575d1d0f",
"revisionTime": "2019-02-05T22:20:52Z",
"version": "v1.3",
"versionExact": "v1.3.0"
},
{
"checksumSHA1": "zNo6yGy/bCJuzkEcP70oEBtOB2M=",
"path": "github.com/leodido/go-urn",
"revision": "70078a794e8ea4b497ba7c19a78cd60f90ccf0f4",
"revisionTime": "2018-05-24T03:26:21Z",
"version": "v1.1",
"versionExact": "v1.1.0"
},
{
"checksumSHA1": "TB2vxux9xQbvsTHOVt4aRTuvSn4=",
"path": "github.com/json-iterator/go",
"revision": "0ff49de124c6f76f8494e194af75bde0f1a49a29",
"revisionTime": "2019-03-06T14:29:09Z",
"version": "v1.1",
"versionExact": "v1.1.6"
},
{
"checksumSHA1": "Ya+baVBU/RkXXUWD3LGFmGJiiIg=",
"path": "github.com/mattn/go-isatty",
"revision": "c2a7a6ca930a4cd0bc33a3f298eb71960732a3a7",
"revisionTime": "2019-03-12T13:58:54Z",
"version": "v0.0",
"versionExact": "v0.0.7"
},
{
"checksumSHA1": "ZTcgWKWHsrX0RXYVXn5Xeb8Q0go=",
"path": "github.com/modern-go/concurrent",
"revision": "bacd9c7ef1dd9b15be4a9909b8ac7a4e313eec94",
"revisionTime": "2018-03-06T01:26:44Z"
},
{
"checksumSHA1": "qvH48wzTIV3QKSDqI0dLFtVjaDI=",
"path": "github.com/modern-go/reflect2",
"revision": "94122c33edd36123c84d5368cfb2b69df93a0ec8",
"revisionTime": "2018-07-18T01:23:57Z"
},
{
"checksumSHA1": "LuFv4/jlrmFNnDb/5SCSEPAM9vU=",
"path": "github.com/pmezard/go-difflib/difflib",
"revision": "5d4384ee4fb2527b0a1256a821ebfc92f91efefc",
"revisionTime": "2018-12-26T10:54:42Z"
},
{
"checksumSHA1": "cpNsoLqBprpKh+VZTBOZNVXzBEk=",
"path": "github.com/stretchr/objx",
"revision": "c61a9dfcced1815e7d40e214d00d1a8669a9f58c",
"revisionTime": "2019-02-11T16:23:28Z"
},
{
"checksumSHA1": "DBdcVxnvaINHhWyyGgih/Mel6gE=",
"path": "github.com/stretchr/testify",
"revision": "ffdc059bfe9ce6a4e144ba849dbedead332c6053",
"revisionTime": "2018-12-05T02:12:43Z",
"version": "v1.3",
"versionExact": "v1.3.0"
},
{
"checksumSHA1": "c6pbpF7eowwO59phRTpF8cQ80Z0=",
"path": "github.com/stretchr/testify/assert",
"revision": "f35b8ab0b5a2cef36673838d662e249dd9c94686",
"revisionTime": "2018-05-06T18:05:49Z",
"version": "v1.2",
"versionExact": "v1.2.2"
},
{
"checksumSHA1": "S4ei9eSqVThDio0Jn2sav6yUbvg=",
"path": "github.com/ugorji/go/codec",
"revision": "82dbfaf494e3b01d2d481376f11f6a5c8cf9599f",
"revisionTime": "2019-07-02T14:15:27Z",
"version": "v1.1",
"versionExact": "v1.1.6"
},
{
"checksumSHA1": "2gaep1KNRDNyDA3O+KgPTQsGWvs=",
"path": "golang.org/x/sys/unix",
"revision": "a43fa875dd822b81eb6d2ad538bc1f4caba169bd",
"revisionTime": "2019-05-02T15:41:39Z"
},
{
"checksumSHA1": "ACzc7AkwLtNgKhqtj8V7SGUJgnw=",
"path": "gopkg.in/go-playground/validator.v9",
"revision": "46b4b1e301c24cac870ffcb4ba5c8a703d1ef475",
"revisionTime": "2019-03-31T13:31:25Z",
"version": "v9.28",
"versionExact": "v9.28.0"
},
{
"checksumSHA1": "QqDq2x8XOU7IoOR98Cx1eiV5QY8=",
"path": "gopkg.in/yaml.v2",
"revision": "51d6538a90f86fe93ac480b35f37b2be17fef232",
"revisionTime": "2018-11-15T11:05:04Z",
"version": "v2.2",
"versionExact": "v2.2.2"
}
],
"rootPath": "github.com/gin-gonic/gin"
}

View File

@ -5,4 +5,4 @@
package gin package gin
// Version is the current gin framework's version. // Version is the current gin framework's version.
const Version = "v1.4.0-dev" const Version = "v1.6.3"