mirror of
https://github.com/gin-gonic/gin.git
synced 2025-10-18 23:12:17 +08:00
Merge branch 'master' into master
This commit is contained in:
commit
5edecccf44
46
.github/ISSUE_TEMPLATE.md
vendored
46
.github/ISSUE_TEMPLATE.md
vendored
@ -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
|
|
||||||
|
|
||||||
|
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -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.
|
||||||
|
|
||||||
|
22
.travis.yml
22
.travis.yml
@ -3,17 +3,21 @@ language: go
|
|||||||
matrix:
|
matrix:
|
||||||
fast_finish: true
|
fast_finish: true
|
||||||
include:
|
include:
|
||||||
- go: 1.6.x
|
|
||||||
- go: 1.7.x
|
|
||||||
- go: 1.8.x
|
|
||||||
- go: 1.9.x
|
|
||||||
- go: 1.10.x
|
|
||||||
- go: 1.11.x
|
|
||||||
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
|
||||||
|
env:
|
||||||
|
- TESTTAGS=nomsgpack
|
||||||
|
- go: 1.14.x
|
||||||
|
- go: 1.14.x
|
||||||
|
env:
|
||||||
|
- TESTTAGS=nomsgpack
|
||||||
|
- go: 1.15.x
|
||||||
|
- go: 1.15.x
|
||||||
|
env:
|
||||||
|
- TESTTAGS=nomsgpack
|
||||||
- go: master
|
- go: master
|
||||||
env: GO111MODULE=on
|
|
||||||
|
|
||||||
git:
|
git:
|
||||||
depth: 10
|
depth: 10
|
||||||
@ -22,7 +26,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
|
||||||
|
|
||||||
|
1176
BENCHMARKS.md
1176
BENCHMARKS.md
File diff suppressed because it is too large
Load Diff
194
CHANGELOG.md
194
CHANGELOG.md
@ -1,6 +1,166 @@
|
|||||||
# CHANGELOG
|
# Gin ChangeLog
|
||||||
|
|
||||||
### Gin 1.3.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
|
||||||
|
|
||||||
|
### BUGFIXES
|
||||||
|
* 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
|
||||||
|
|
||||||
|
### BUGFIXES
|
||||||
|
* 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 negotiation [#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] Refactor of form mapping multipart request [#1829](https://github.com/gin-gonic/gin/pull/1829)
|
||||||
|
- [FIX] Truncate Latency precision in long running request [#1830](https://github.com/gin-gonic/gin/pull/1830)
|
||||||
|
- [FIX] IsTerm flag should not be affected by DisableConsoleColor method. [#1802](https://github.com/gin-gonic/gin/pull/1802)
|
||||||
|
- [NEW] Supporting file binding [#1264](https://github.com/gin-gonic/gin/pull/1264)
|
||||||
|
- [NEW] Add support for mapping arrays [#1797](https://github.com/gin-gonic/gin/pull/1797)
|
||||||
|
- [FIX] Readme updates [#1793](https://github.com/gin-gonic/gin/pull/1793) [#1788](https://github.com/gin-gonic/gin/pull/1788) [1789](https://github.com/gin-gonic/gin/pull/1789)
|
||||||
|
- [FIX] StaticFS: Fixed Logging two log lines on 404. [#1805](https://github.com/gin-gonic/gin/pull/1805), [#1804](https://github.com/gin-gonic/gin/pull/1804)
|
||||||
|
- [NEW] Make context.Keys available as LogFormatterParams [#1779](https://github.com/gin-gonic/gin/pull/1779)
|
||||||
|
- [NEW] Use internal/json for Marshal/Unmarshal [#1791](https://github.com/gin-gonic/gin/pull/1791)
|
||||||
|
- [NEW] Support mapping time.Duration [#1794](https://github.com/gin-gonic/gin/pull/1794)
|
||||||
|
- [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)
|
||||||
|
- [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-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)
|
||||||
|
- [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)
|
||||||
|
- [NEW] Add context.HandlerNames() [#1729](https://github.com/gin-gonic/gin/pull/1729)
|
||||||
|
- [FIX] Change color methods to public in the defaultLogger. [#1771](https://github.com/gin-gonic/gin/pull/1771)
|
||||||
|
- [FIX] Update writeHeaders method to use http.Header.Set [#1722](https://github.com/gin-gonic/gin/pull/1722)
|
||||||
|
- [NEW] Add response size to LogFormatterParams [#1752](https://github.com/gin-gonic/gin/pull/1752)
|
||||||
|
- [NEW] Allow ignoring field on form mapping [#1733](https://github.com/gin-gonic/gin/pull/1733)
|
||||||
|
- [NEW] Add a function to force color in console output. [#1724](https://github.com/gin-gonic/gin/pull/1724)
|
||||||
|
- [FIX] Context.Next() - recheck len of handlers on every iteration. [#1745](https://github.com/gin-gonic/gin/pull/1745)
|
||||||
|
- [FIX] Fix all errcheck warnings [#1739](https://github.com/gin-gonic/gin/pull/1739) [#1653](https://github.com/gin-gonic/gin/pull/1653)
|
||||||
|
- [NEW] context: inherits context cancellation and deadline from http.Request context for Go>=1.7 [#1690](https://github.com/gin-gonic/gin/pull/1690)
|
||||||
|
- [NEW] Binding for URL Params [#1694](https://github.com/gin-gonic/gin/pull/1694)
|
||||||
|
- [NEW] Add LoggerWithFormatter method [#1677](https://github.com/gin-gonic/gin/pull/1677)
|
||||||
|
- [FIX] CI testing updates [#1671](https://github.com/gin-gonic/gin/pull/1671) [#1670](https://github.com/gin-gonic/gin/pull/1670) [#1682](https://github.com/gin-gonic/gin/pull/1682) [#1669](https://github.com/gin-gonic/gin/pull/1669)
|
||||||
|
- [FIX] StaticFS(): Send 404 when path does not exist [#1663](https://github.com/gin-gonic/gin/pull/1663)
|
||||||
|
- [FIX] Handle nil body for JSON binding [#1638](https://github.com/gin-gonic/gin/pull/1638)
|
||||||
|
- [FIX] Support bind uri param [#1612](https://github.com/gin-gonic/gin/pull/1612)
|
||||||
|
- [FIX] recovery: fix issue with syscall import on google app engine [#1640](https://github.com/gin-gonic/gin/pull/1640)
|
||||||
|
- [FIX] Make sure the debug log contains line breaks [#1650](https://github.com/gin-gonic/gin/pull/1650)
|
||||||
|
- [FIX] Panic stack trace being printed during recovery of broken pipe [#1089](https://github.com/gin-gonic/gin/pull/1089) [#1259](https://github.com/gin-gonic/gin/pull/1259)
|
||||||
|
- [NEW] RunFd method to run http.Server through a file descriptor [#1609](https://github.com/gin-gonic/gin/pull/1609)
|
||||||
|
- [NEW] Yaml binding support [#1618](https://github.com/gin-gonic/gin/pull/1618)
|
||||||
|
- [FIX] Pass MaxMultipartMemory when FormFile is called [#1600](https://github.com/gin-gonic/gin/pull/1600)
|
||||||
|
- [FIX] LoadHTML* tests [#1559](https://github.com/gin-gonic/gin/pull/1559)
|
||||||
|
- [FIX] Removed use of sync.pool from HandleContext [#1565](https://github.com/gin-gonic/gin/pull/1565)
|
||||||
|
- [FIX] Format output log to os.Stderr [#1571](https://github.com/gin-gonic/gin/pull/1571)
|
||||||
|
- [FIX] Make logger use a yellow background and a darkgray text for legibility [#1570](https://github.com/gin-gonic/gin/pull/1570)
|
||||||
|
- [FIX] Remove sensitive request information from panic log. [#1370](https://github.com/gin-gonic/gin/pull/1370)
|
||||||
|
- [FIX] log.Println() does not print timestamp [#829](https://github.com/gin-gonic/gin/pull/829) [#1560](https://github.com/gin-gonic/gin/pull/1560)
|
||||||
|
- [NEW] Add PureJSON renderer [#694](https://github.com/gin-gonic/gin/pull/694)
|
||||||
|
- [FIX] Add missing copyright and update if/else [#1497](https://github.com/gin-gonic/gin/pull/1497)
|
||||||
|
- [FIX] Update msgpack usage [#1498](https://github.com/gin-gonic/gin/pull/1498)
|
||||||
|
- [FIX] Use protobuf on render [#1496](https://github.com/gin-gonic/gin/pull/1496)
|
||||||
|
- [FIX] Add support for Protobuf format response [#1479](https://github.com/gin-gonic/gin/pull/1479)
|
||||||
|
- [NEW] Set default time format in form binding [#1487](https://github.com/gin-gonic/gin/pull/1487)
|
||||||
|
- [FIX] Add BindXML and ShouldBindXML [#1485](https://github.com/gin-gonic/gin/pull/1485)
|
||||||
|
- [NEW] Upgrade dependency libraries [#1491](https://github.com/gin-gonic/gin/pull/1491)
|
||||||
|
|
||||||
|
|
||||||
|
## 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)
|
||||||
@ -22,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
|
||||||
@ -45,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
|
||||||
@ -63,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
|
||||||
@ -98,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
|
||||||
@ -106,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
|
||||||
@ -142,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
|
||||||
@ -152,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
|
||||||
@ -168,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
|
||||||
@ -186,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
|
||||||
@ -198,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()
|
||||||
|
@ -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.
|
||||||
|
19
Makefile
19
Makefile
@ -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 \
|
||||||
|
415
README.md
415
README.md
@ -5,48 +5,61 @@
|
|||||||
[](https://travis-ci.org/gin-gonic/gin)
|
[](https://travis-ci.org/gin-gonic/gin)
|
||||||
[](https://codecov.io/gh/gin-gonic/gin)
|
[](https://codecov.io/gh/gin-gonic/gin)
|
||||||
[](https://goreportcard.com/report/github.com/gin-gonic/gin)
|
[](https://goreportcard.com/report/github.com/gin-gonic/gin)
|
||||||
[](https://godoc.org/github.com/gin-gonic/gin)
|
[](https://pkg.go.dev/github.com/gin-gonic/gin?tab=doc)
|
||||||
[](https://gitter.im/gin-gonic/gin?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
[](https://gitter.im/gin-gonic/gin?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||||
[](https://sourcegraph.com/github.com/gin-gonic/gin?badge)
|
[](https://sourcegraph.com/github.com/gin-gonic/gin?badge)
|
||||||
[](https://www.codetriage.com/gin-gonic/gin)
|
[](https://www.codetriage.com/gin-gonic/gin)
|
||||||
[](https://github.com/gin-gonic/gin/releases)
|
[](https://github.com/gin-gonic/gin/releases)
|
||||||
|
[](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
|
||||||
|
|
||||||
- [Installation](#installation)
|
- [Gin Web Framework](#gin-web-framework)
|
||||||
- [Prerequisite](#prerequisite)
|
- [Contents](#contents)
|
||||||
- [Quick start](#quick-start)
|
- [Installation](#installation)
|
||||||
- [Benchmarks](#benchmarks)
|
- [Quick start](#quick-start)
|
||||||
- [Gin v1.stable](#gin-v1-stable)
|
- [Benchmarks](#benchmarks)
|
||||||
- [Build with jsoniter](#build-with-jsoniter)
|
- [Gin v1. stable](#gin-v1-stable)
|
||||||
- [API Examples](#api-examples)
|
- [Build with jsoniter](#build-with-jsoniter)
|
||||||
- [Using GET,POST,PUT,PATCH,DELETE and OPTIONS](#using-get-post-put-patch-delete-and-options)
|
- [API Examples](#api-examples)
|
||||||
|
- [Using GET, POST, PUT, PATCH, DELETE and OPTIONS](#using-get-post-put-patch-delete-and-options)
|
||||||
- [Parameters in path](#parameters-in-path)
|
- [Parameters in path](#parameters-in-path)
|
||||||
- [Querystring parameters](#querystring-parameters)
|
- [Querystring parameters](#querystring-parameters)
|
||||||
- [Multipart/Urlencoded Form](#multiparturlencoded-form)
|
- [Multipart/Urlencoded Form](#multiparturlencoded-form)
|
||||||
- [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)
|
||||||
- [Bind Query String or Post Data](#bind-query-string-or-post-data)
|
- [Bind Query String or Post Data](#bind-query-string-or-post-data)
|
||||||
- [Bind Uri](#bind-uri)
|
- [Bind Uri](#bind-uri)
|
||||||
|
- [Bind Header](#bind-header)
|
||||||
- [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)
|
||||||
@ -55,21 +68,23 @@ 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)
|
||||||
- [http2 server push](#http2-server-push)
|
- [http2 server push](#http2-server-push)
|
||||||
- [Define format for the log of routes](#define-format-for-the-log-of-routes)
|
- [Define format for the log of routes](#define-format-for-the-log-of-routes)
|
||||||
- [Set and get a cookie](#set-and-get-a-cookie)
|
- [Set and get a cookie](#set-and-get-a-cookie)
|
||||||
- [Testing](#testing)
|
- [Testing](#testing)
|
||||||
- [Users](#users)
|
- [Users](#users)
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
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. Download and install it:
|
1. The first need [Go](https://golang.org/) installed (**version 1.12+ 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
|
||||||
@ -87,42 +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 "$_"
|
|
||||||
```
|
|
||||||
|
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|
||||||
## Prerequisite
|
|
||||||
|
|
||||||
Now Gin requires Go 1.6 or later and Go 1.7 will be required soon.
|
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@ -142,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
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -157,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
|
||||||
@ -196,8 +178,8 @@ BenchmarkVulcan_GithubAll | 5000 | 394253 | 19894
|
|||||||
|
|
||||||
- [x] Zero allocation router.
|
- [x] Zero allocation router.
|
||||||
- [x] Still the fastest http router and framework. From routing to writing.
|
- [x] Still the fastest http router and framework. From routing to writing.
|
||||||
- [x] Complete suite of unit tests
|
- [x] Complete suite of unit tests.
|
||||||
- [x] Battle tested
|
- [x] Battle tested.
|
||||||
- [x] API frozen, new releases will not break your code.
|
- [x] API frozen, new releases will not break your code.
|
||||||
|
|
||||||
## Build with [jsoniter](https://github.com/json-iterator/go)
|
## Build with [jsoniter](https://github.com/json-iterator/go)
|
||||||
@ -256,6 +238,11 @@ func main() {
|
|||||||
c.String(http.StatusOK, message)
|
c.String(http.StatusOK, message)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// For each matched request Context will hold the route definition
|
||||||
|
router.POST("/user/:name/*action", func(c *gin.Context) {
|
||||||
|
c.FullPath() == "/user/:name/*action" // true
|
||||||
|
})
|
||||||
|
|
||||||
router.Run(":8080")
|
router.Run(":8080")
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -353,7 +340,7 @@ func main() {
|
|||||||
```
|
```
|
||||||
|
|
||||||
```
|
```
|
||||||
ids: map[b:hello a:1234], names: map[second:tianou first:thinkerou]
|
ids: map[b:hello a:1234]; names: map[second:tianou first:thinkerou]
|
||||||
```
|
```
|
||||||
|
|
||||||
### Upload files
|
### Upload files
|
||||||
@ -370,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))
|
||||||
})
|
})
|
||||||
@ -401,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()
|
||||||
@ -411,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)))
|
||||||
})
|
})
|
||||||
@ -509,6 +496,39 @@ func main() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Custom Recovery behavior
|
||||||
|
```go
|
||||||
|
func main() {
|
||||||
|
// Creates a router without any middleware by default
|
||||||
|
r := gin.New()
|
||||||
|
|
||||||
|
// Global middleware
|
||||||
|
// Logger middleware will write the logs to gin.DefaultWriter even if you set with GIN_MODE=release.
|
||||||
|
// By default gin.DefaultWriter = os.Stdout
|
||||||
|
r.Use(gin.Logger())
|
||||||
|
|
||||||
|
// Recovery middleware recovers from any panics and writes a 500 if there was one.
|
||||||
|
r.Use(gin.CustomRecovery(func(c *gin.Context, recovered interface{}) {
|
||||||
|
if err, ok := recovered.(string); ok {
|
||||||
|
c.String(http.StatusInternalServerError, fmt.Sprintf("error: %s", err))
|
||||||
|
}
|
||||||
|
c.AbortWithStatus(http.StatusInternalServerError)
|
||||||
|
}))
|
||||||
|
|
||||||
|
r.GET("/panic", func(c *gin.Context) {
|
||||||
|
// panic with a string -- the custom middleware could save this to a database or report it to the user
|
||||||
|
panic("foo")
|
||||||
|
})
|
||||||
|
|
||||||
|
r.GET("/", func(c *gin.Context) {
|
||||||
|
c.String(http.StatusOK, "ohai")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Listen and serve on 0.0.0.0:8080
|
||||||
|
r.Run(":8080")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### How to write log file
|
### How to write log file
|
||||||
```go
|
```go
|
||||||
func main() {
|
func main() {
|
||||||
@ -614,16 +634,16 @@ 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"`.
|
||||||
|
|
||||||
Also, Gin provides two sets of methods for binding:
|
Also, Gin provides two sets of methods for binding:
|
||||||
- **Type** - Must bind
|
- **Type** - Must bind
|
||||||
- **Methods** - `Bind`, `BindJSON`, `BindXML`, `BindQuery`, `BindYAML`
|
- **Methods** - `Bind`, `BindJSON`, `BindXML`, `BindQuery`, `BindYAML`, `BindHeader`
|
||||||
- **Behavior** - These methods use `MustBindWith` under the hood. If there is a binding error, the request is aborted with `c.AbortWithError(400, err).SetType(ErrorTypeBind)`. This sets the response status code to 400 and the `Content-Type` header is set to `text/plain; charset=utf-8`. Note that if you try to set the response code after this, it will result in a warning `[GIN-debug] [WARNING] Headers were already written. Wanted to override status code 400 with 422`. If you wish to have greater control over the behavior, consider using the `ShouldBind` equivalent method.
|
- **Behavior** - These methods use `MustBindWith` under the hood. If there is a binding error, the request is aborted with `c.AbortWithError(400, err).SetType(ErrorTypeBind)`. This sets the response status code to 400 and the `Content-Type` header is set to `text/plain; charset=utf-8`. Note that if you try to set the response code after this, it will result in a warning `[GIN-debug] [WARNING] Headers were already written. Wanted to override status code 400 with 422`. If you wish to have greater control over the behavior, consider using the `ShouldBind` equivalent method.
|
||||||
- **Type** - Should bind
|
- **Type** - Should bind
|
||||||
- **Methods** - `ShouldBind`, `ShouldBindJSON`, `ShouldBindXML`, `ShouldBindQuery`, `ShouldBindYAML`
|
- **Methods** - `ShouldBind`, `ShouldBindJSON`, `ShouldBindXML`, `ShouldBindQuery`, `ShouldBindYAML`, `ShouldBindHeader`
|
||||||
- **Behavior** - These methods use `ShouldBindWith` under the hood. If there is a binding error, the error is returned and it is the developer's responsibility to handle the request and error appropriately.
|
- **Behavior** - These methods use `ShouldBindWith` under the hood. If there is a binding error, the error is returned and it is the developer's responsibility to handle the request and error appropriately.
|
||||||
|
|
||||||
When using the Bind-method, Gin tries to infer the binder depending on the Content-Type header. If you are sure what you are binding, you can use `MustBindWith` or `ShouldBindWith`.
|
When using the Bind-method, Gin tries to infer the binder depending on the Content-Type header. If you are sure what you are binding, you can use `MustBindWith` or `ShouldBindWith`.
|
||||||
@ -734,12 +754,11 @@ 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"
|
"github.com/go-playground/validator/v10"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Booking contains binded and validated data.
|
// Booking contains binded and validated data.
|
||||||
@ -748,13 +767,11 @@ type Booking struct {
|
|||||||
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.Year() > date.Year() || today.YearDay() > date.YearDay() {
|
if today.After(date) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -783,11 +800,14 @@ func getBookable(c *gin.Context) {
|
|||||||
```
|
```
|
||||||
|
|
||||||
```console
|
```console
|
||||||
$ curl "localhost:8085/bookable?check_in=2018-04-16&check_out=2018-04-17"
|
$ curl "localhost:8085/bookable?check_in=2030-04-16&check_out=2030-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=2030-03-10&check_out=2030-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"}
|
||||||
|
|
||||||
|
$ curl "localhost:8085/bookable?check_in=2000-03-09&check_out=2000-03-10"
|
||||||
|
{"error":"Key: 'Booking.CheckIn' Error:Field validation for 'CheckIn' failed on the 'bookabledate' 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.
|
||||||
@ -844,9 +864,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Person struct {
|
type Person struct {
|
||||||
Name string `form:"name"`
|
Name string `form:"name"`
|
||||||
Address string `form:"address"`
|
Address string `form:"address"`
|
||||||
Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"`
|
Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"`
|
||||||
|
CreateTime time.Time `form:"createTime" time_format:"unixNano"`
|
||||||
|
UnixTime time.Time `form:"unixTime" time_format:"unix"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@ -860,11 +882,13 @@ func startPage(c *gin.Context) {
|
|||||||
// If `GET`, only `Form` binding engine (`query`) used.
|
// If `GET`, only `Form` binding engine (`query`) used.
|
||||||
// If `POST`, first checks the `content-type` for `JSON` or `XML`, then uses `Form` (`form-data`).
|
// If `POST`, first checks the `content-type` for `JSON` or `XML`, then uses `Form` (`form-data`).
|
||||||
// See more at https://github.com/gin-gonic/gin/blob/master/binding/binding.go#L48
|
// See more at https://github.com/gin-gonic/gin/blob/master/binding/binding.go#L48
|
||||||
if c.ShouldBind(&person) == nil {
|
if c.ShouldBind(&person) == nil {
|
||||||
log.Println(person.Name)
|
log.Println(person.Name)
|
||||||
log.Println(person.Address)
|
log.Println(person.Address)
|
||||||
log.Println(person.Birthday)
|
log.Println(person.Birthday)
|
||||||
}
|
log.Println(person.CreateTime)
|
||||||
|
log.Println(person.UnixTime)
|
||||||
|
}
|
||||||
|
|
||||||
c.String(200, "Success")
|
c.String(200, "Success")
|
||||||
}
|
}
|
||||||
@ -872,7 +896,7 @@ func startPage(c *gin.Context) {
|
|||||||
|
|
||||||
Test it with:
|
Test it with:
|
||||||
```sh
|
```sh
|
||||||
$ curl -X GET "localhost:8085/testing?name=appleboy&address=xyz&birthday=1992-03-15"
|
$ curl -X GET "localhost:8085/testing?name=appleboy&address=xyz&birthday=1992-03-15&createTime=1562400033000000123&unixTime=1562400033"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Bind Uri
|
### Bind Uri
|
||||||
@ -909,6 +933,43 @@ $ curl -v localhost:8088/thinkerou/987fbc97-4bed-5078-9f07-9141ba07c9f3
|
|||||||
$ curl -v localhost:8088/thinkerou/not-uuid
|
$ curl -v localhost:8088/thinkerou/not-uuid
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Bind Header
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testHeader struct {
|
||||||
|
Rate int `header:"Rate"`
|
||||||
|
Domain string `header:"Domain"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
r := gin.Default()
|
||||||
|
r.GET("/", func(c *gin.Context) {
|
||||||
|
h := testHeader{}
|
||||||
|
|
||||||
|
if err := c.ShouldBindHeader(&h); err != nil {
|
||||||
|
c.JSON(200, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("%#v\n", h)
|
||||||
|
c.JSON(200, gin.H{"Rate": h.Rate, "Domain": h.Domain})
|
||||||
|
})
|
||||||
|
|
||||||
|
r.Run()
|
||||||
|
|
||||||
|
// client
|
||||||
|
// curl -H "rate:300" -H "domain:music" 127.0.0.1:8080/
|
||||||
|
// output
|
||||||
|
// {"Domain":"music","Rate":300}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Bind HTML checkboxes
|
### Bind HTML checkboxes
|
||||||
|
|
||||||
See the [detail information](https://github.com/gin-gonic/gin/issues/129#issuecomment-124260092)
|
See the [detail information](https://github.com/gin-gonic/gin/issues/129#issuecomment-124260092)
|
||||||
@ -958,32 +1019,36 @@ result:
|
|||||||
### Multipart/Urlencoded binding
|
### Multipart/Urlencoded binding
|
||||||
|
|
||||||
```go
|
```go
|
||||||
package main
|
type ProfileForm struct {
|
||||||
|
Name string `form:"name" binding:"required"`
|
||||||
|
Avatar *multipart.FileHeader `form:"avatar" binding:"required"`
|
||||||
|
|
||||||
import (
|
// or for multiple files
|
||||||
"github.com/gin-gonic/gin"
|
// Avatars []*multipart.FileHeader `form:"avatar" binding:"required"`
|
||||||
)
|
|
||||||
|
|
||||||
type LoginForm struct {
|
|
||||||
User string `form:"user" binding:"required"`
|
|
||||||
Password string `form:"password" binding:"required"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
router := gin.Default()
|
router := gin.Default()
|
||||||
router.POST("/login", func(c *gin.Context) {
|
router.POST("/profile", func(c *gin.Context) {
|
||||||
// you can bind multipart form with explicit binding declaration:
|
// you can bind multipart form with explicit binding declaration:
|
||||||
// c.ShouldBindWith(&form, binding.Form)
|
// c.ShouldBindWith(&form, binding.Form)
|
||||||
// or you can simply use autobinding with ShouldBind method:
|
// or you can simply use autobinding with ShouldBind method:
|
||||||
var form LoginForm
|
var form ProfileForm
|
||||||
// in this case proper binding will be automatically selected
|
// in this case proper binding will be automatically selected
|
||||||
if c.ShouldBind(&form) == nil {
|
if err := c.ShouldBind(&form); err != nil {
|
||||||
if form.User == "user" && form.Password == "password" {
|
c.String(http.StatusBadRequest, "bad request")
|
||||||
c.JSON(200, gin.H{"status": "you are logged in"})
|
return
|
||||||
} else {
|
|
||||||
c.JSON(401, gin.H{"status": "unauthorized"})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err := c.SaveUploadedFile(form.Avatar, form.Avatar.Filename)
|
||||||
|
if err != nil {
|
||||||
|
c.String(http.StatusInternalServerError, "unknown error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// db.Save(&form)
|
||||||
|
|
||||||
|
c.String(http.StatusOK, "ok")
|
||||||
})
|
})
|
||||||
router.Run(":8080")
|
router.Run(":8080")
|
||||||
}
|
}
|
||||||
@ -991,7 +1056,7 @@ func main() {
|
|||||||
|
|
||||||
Test it with:
|
Test it with:
|
||||||
```sh
|
```sh
|
||||||
$ curl -v --form user=user --form password=password http://localhost:8080/login
|
$ curl -X POST -v --form name=user --form "avatar=@./avatar.png" http://localhost:8080/profile
|
||||||
```
|
```
|
||||||
|
|
||||||
### XML, JSON, YAML and ProtoBuf rendering
|
### XML, JSON, YAML and ProtoBuf rendering
|
||||||
@ -1076,8 +1141,8 @@ Using JSONP to request data from a server in a different domain. Add callback t
|
|||||||
func main() {
|
func main() {
|
||||||
r := gin.Default()
|
r := gin.Default()
|
||||||
|
|
||||||
r.GET("/JSONP?callback=x", func(c *gin.Context) {
|
r.GET("/JSONP", func(c *gin.Context) {
|
||||||
data := map[string]interface{}{
|
data := gin.H{
|
||||||
"foo": "bar",
|
"foo": "bar",
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1088,19 +1153,22 @@ func main() {
|
|||||||
|
|
||||||
// Listen and serve on 0.0.0.0:8080
|
// Listen and serve on 0.0.0.0:8080
|
||||||
r.Run(":8080")
|
r.Run(":8080")
|
||||||
|
|
||||||
|
// client
|
||||||
|
// curl http://127.0.0.1:8080/JSONP?callback=x
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 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() {
|
||||||
r := gin.Default()
|
r := gin.Default()
|
||||||
|
|
||||||
r.GET("/someJSON", func(c *gin.Context) {
|
r.GET("/someJSON", func(c *gin.Context) {
|
||||||
data := map[string]interface{}{
|
data := gin.H{
|
||||||
"lang": "GO语言",
|
"lang": "GO语言",
|
||||||
"tag": "<br>",
|
"tag": "<br>",
|
||||||
}
|
}
|
||||||
@ -1156,6 +1224,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
|
||||||
@ -1169,6 +1255,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
reader := response.Body
|
reader := response.Body
|
||||||
|
defer reader.Close()
|
||||||
contentLength := response.ContentLength
|
contentLength := response.ContentLength
|
||||||
contentType := response.Header.Get("Content-Type")
|
contentType := response.Header.Get("Content-Type")
|
||||||
|
|
||||||
@ -1373,7 +1460,7 @@ func main() {
|
|||||||
router.LoadHTMLFiles("./testdata/template/raw.tmpl")
|
router.LoadHTMLFiles("./testdata/template/raw.tmpl")
|
||||||
|
|
||||||
router.GET("/raw", func(c *gin.Context) {
|
router.GET("/raw", func(c *gin.Context) {
|
||||||
c.HTML(http.StatusOK, "raw.tmpl", map[string]interface{}{
|
c.HTML(http.StatusOK, "raw.tmpl", gin.H{
|
||||||
"now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC),
|
"now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -1408,6 +1495,12 @@ r.GET("/test", func(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Issuing a HTTP redirect from POST. Refer to issue: [#444](https://github.com/gin-gonic/gin/issues/444)
|
||||||
|
```go
|
||||||
|
r.POST("/test", func(c *gin.Context) {
|
||||||
|
c.Redirect(http.StatusFound, "/foo")
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
Issuing a Router redirect, use `HandleContext` like below.
|
Issuing a Router redirect, use `HandleContext` like below.
|
||||||
|
|
||||||
@ -1686,11 +1779,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 {
|
||||||
@ -1699,12 +1800,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()
|
||||||
@ -1713,13 +1815,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
|
||||||
@ -1750,33 +1854,33 @@ 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 && errors.Is(err, http.ErrServerClosed) {
|
||||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
log.Printf("listen: %s\n", err)
|
||||||
log.Fatalf("listen: %s\n", err)
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Wait for interrupt signal to gracefully shutdown the server with
|
// Wait for interrupt signal to gracefully shutdown the server with
|
||||||
// a timeout of 5 seconds.
|
// a timeout of 5 seconds.
|
||||||
quit := make(chan os.Signal)
|
quit := make(chan os.Signal)
|
||||||
// kill (no param) default send syscanll.SIGTERM
|
// kill (no param) default send syscall.SIGTERM
|
||||||
// kill -2 is syscall.SIGINT
|
// kill -2 is syscall.SIGINT
|
||||||
// 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")
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -1807,6 +1911,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
|
||||||
}
|
}
|
||||||
@ -2132,3 +2237,5 @@ Awesome project lists using [Gin](https://github.com/gin-gonic/gin) web framewor
|
|||||||
* [photoprism](https://github.com/photoprism/photoprism): Personal photo management powered by Go and Google TensorFlow.
|
* [photoprism](https://github.com/photoprism/photoprism): Personal photo management powered by Go and Google TensorFlow.
|
||||||
* [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.
|
||||||
|
* [dkron](https://github.com/distribworks/dkron): Distributed, fault tolerant job scheduling system.
|
||||||
|
19
auth.go
19
auth.go
@ -9,6 +9,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.
|
||||||
@ -29,7 +31,7 @@ func (a authPairs) searchCredential(authValue string) (string, bool) {
|
|||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
for _, pair := range a {
|
for _, pair := range a {
|
||||||
if pair.value == authValue {
|
if subtle.ConstantTimeCompare([]byte(pair.value), []byte(authValue)) == 1 {
|
||||||
return pair.user, true
|
return pair.user, true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -69,8 +71,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)
|
||||||
@ -84,13 +87,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))
|
||||||
}
|
|
||||||
|
|
||||||
func secureCompare(given, actual string) bool {
|
|
||||||
if subtle.ConstantTimeEq(int32(len(given)), int32(len(actual))) == 1 {
|
|
||||||
return subtle.ConstantTimeCompare([]byte(given), []byte(actual)) == 1
|
|
||||||
}
|
|
||||||
// Securely compare actual to itself to keep constant time, but always return false.
|
|
||||||
return subtle.ConstantTimeCompare([]byte(actual), []byte(actual)) == 1 && false
|
|
||||||
}
|
}
|
||||||
|
@ -81,13 +81,6 @@ func TestBasicAuthAuthorizationHeader(t *testing.T) {
|
|||||||
assert.Equal(t, "Basic YWRtaW46cGFzc3dvcmQ=", authorizationHeader("admin", "password"))
|
assert.Equal(t, "Basic YWRtaW46cGFzc3dvcmQ=", authorizationHeader("admin", "password"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBasicAuthSecureCompare(t *testing.T) {
|
|
||||||
assert.True(t, secureCompare("1234567890", "1234567890"))
|
|
||||||
assert.False(t, secureCompare("123456789", "1234567890"))
|
|
||||||
assert.False(t, secureCompare("12345678900", "1234567890"))
|
|
||||||
assert.False(t, secureCompare("1234567891", "1234567890"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBasicAuthSucceed(t *testing.T) {
|
func TestBasicAuthSucceed(t *testing.T) {
|
||||||
accounts := Accounts{"admin": "password"}
|
accounts := Accounts{"admin": "password"}
|
||||||
router := New()
|
router := New()
|
||||||
|
@ -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"
|
||||||
@ -49,7 +51,8 @@ type BindingUri interface {
|
|||||||
// https://github.com/go-playground/validator/tree/v8.18.2.
|
// https://github.com/go-playground/validator/tree/v8.18.2.
|
||||||
type StructValidator interface {
|
type StructValidator interface {
|
||||||
// ValidateStruct can receive any kind of type and it should never panic, even if the configuration is not right.
|
// 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 slice|array, the validation should be performed travel on every element.
|
||||||
|
// If the received type is not a struct or slice|array, 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 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.
|
// If the struct is not valid or the validation itself fails, a descriptive error should be returned.
|
||||||
// Otherwise nil must be returned.
|
// Otherwise nil must be returned.
|
||||||
@ -78,12 +81,13 @@ var (
|
|||||||
MsgPack = msgpackBinding{}
|
MsgPack = msgpackBinding{}
|
||||||
YAML = yamlBinding{}
|
YAML = yamlBinding{}
|
||||||
Uri = uriBinding{}
|
Uri = uriBinding{}
|
||||||
|
Header = headerBinding{}
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
|
||||||
}
|
|
57
binding/binding_msgpack_test.go
Normal file
57
binding/binding_msgpack_test.go
Normal 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))
|
||||||
|
}
|
111
binding/binding_nomsgpack.go
Normal file
111
binding/binding_nomsgpack.go
Normal 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)
|
||||||
|
}
|
@ -13,6 +13,7 @@ import (
|
|||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@ -21,11 +22,20 @@ 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 {
|
||||||
|
Appkey string `json:"appkey" form:"appkey"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type QueryTest struct {
|
||||||
|
Page int `json:"page" form:"page"`
|
||||||
|
Size int `json:"size" form:"size"`
|
||||||
|
appkey
|
||||||
|
}
|
||||||
|
|
||||||
type FooStruct struct {
|
type FooStruct struct {
|
||||||
Foo string `msgpack:"foo" json:"foo" form:"foo" xml:"foo" binding:"required"`
|
Foo string `msgpack:"foo" json:"foo" form:"foo" xml:"foo" binding:"required,max=32"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FooBarStruct struct {
|
type FooBarStruct struct {
|
||||||
@ -54,9 +64,20 @@ type FooStructUseNumber struct {
|
|||||||
Foo interface{} `json:"foo" binding:"required"`
|
Foo interface{} `json:"foo" binding:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FooStructDisallowUnknownFields struct {
|
||||||
|
Foo interface{} `json:"foo" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
type FooBarStructForTimeType struct {
|
type FooBarStructForTimeType struct {
|
||||||
TimeFoo time.Time `form:"time_foo" time_format:"2006-01-02" time_utc:"1" time_location:"Asia/Chongqing"`
|
TimeFoo time.Time `form:"time_foo" time_format:"2006-01-02" time_utc:"1" time_location:"Asia/Chongqing"`
|
||||||
TimeBar time.Time `form:"time_bar" time_format:"2006-01-02" time_utc:"1"`
|
TimeBar time.Time `form:"time_bar" time_format:"2006-01-02" time_utc:"1"`
|
||||||
|
CreateTime time.Time `form:"createTime" time_format:"unixNano"`
|
||||||
|
UnixTime time.Time `form:"unixTime" time_format:"unix"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FooStructForTimeTypeNotUnixFormat struct {
|
||||||
|
CreateTime time.Time `form:"createTime" time_format:"unixNano"`
|
||||||
|
UnixTime time.Time `form:"unixTime" time_format:"unix"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FooStructForTimeTypeNotFormat struct {
|
type FooStructForTimeTypeNotFormat struct {
|
||||||
@ -114,71 +135,6 @@ type FooStructForBoolType struct {
|
|||||||
BoolFoo bool `form:"bool_foo"`
|
BoolFoo bool `form:"bool_foo"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FooBarStructForIntType struct {
|
|
||||||
IntFoo int `form:"int_foo"`
|
|
||||||
IntBar int `form:"int_bar" binding:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type FooBarStructForInt8Type struct {
|
|
||||||
Int8Foo int8 `form:"int8_foo"`
|
|
||||||
Int8Bar int8 `form:"int8_bar" binding:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type FooBarStructForInt16Type struct {
|
|
||||||
Int16Foo int16 `form:"int16_foo"`
|
|
||||||
Int16Bar int16 `form:"int16_bar" binding:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type FooBarStructForInt32Type struct {
|
|
||||||
Int32Foo int32 `form:"int32_foo"`
|
|
||||||
Int32Bar int32 `form:"int32_bar" binding:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type FooBarStructForInt64Type struct {
|
|
||||||
Int64Foo int64 `form:"int64_foo"`
|
|
||||||
Int64Bar int64 `form:"int64_bar" binding:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type FooBarStructForUintType struct {
|
|
||||||
UintFoo uint `form:"uint_foo"`
|
|
||||||
UintBar uint `form:"uint_bar" binding:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type FooBarStructForUint8Type struct {
|
|
||||||
Uint8Foo uint8 `form:"uint8_foo"`
|
|
||||||
Uint8Bar uint8 `form:"uint8_bar" binding:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type FooBarStructForUint16Type struct {
|
|
||||||
Uint16Foo uint16 `form:"uint16_foo"`
|
|
||||||
Uint16Bar uint16 `form:"uint16_bar" binding:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type FooBarStructForUint32Type struct {
|
|
||||||
Uint32Foo uint32 `form:"uint32_foo"`
|
|
||||||
Uint32Bar uint32 `form:"uint32_bar" binding:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type FooBarStructForUint64Type struct {
|
|
||||||
Uint64Foo uint64 `form:"uint64_foo"`
|
|
||||||
Uint64Bar uint64 `form:"uint64_bar" binding:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type FooBarStructForBoolType struct {
|
|
||||||
BoolFoo bool `form:"bool_foo"`
|
|
||||||
BoolBar bool `form:"bool_bar" binding:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type FooBarStructForFloat32Type struct {
|
|
||||||
Float32Foo float32 `form:"float32_foo"`
|
|
||||||
Float32Bar float32 `form:"float32_bar" binding:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type FooBarStructForFloat64Type struct {
|
|
||||||
Float64Foo float64 `form:"float64_foo"`
|
|
||||||
Float64Bar float64 `form:"float64_bar" binding:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type FooStructForStringPtrType struct {
|
type FooStructForStringPtrType struct {
|
||||||
PtrFoo *string `form:"ptr_foo"`
|
PtrFoo *string `form:"ptr_foo"`
|
||||||
PtrBar *string `form:"ptr_bar" binding:"required"`
|
PtrBar *string `form:"ptr_bar" binding:"required"`
|
||||||
@ -207,9 +163,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))
|
||||||
}
|
}
|
||||||
@ -228,6 +181,20 @@ func TestBindingJSON(t *testing.T) {
|
|||||||
`{"foo": "bar"}`, `{"bar": "foo"}`)
|
`{"foo": "bar"}`, `{"bar": "foo"}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBindingJSONSlice(t *testing.T) {
|
||||||
|
EnableDecoderDisallowUnknownFields = true
|
||||||
|
defer func() {
|
||||||
|
EnableDecoderDisallowUnknownFields = false
|
||||||
|
}()
|
||||||
|
|
||||||
|
testBodyBindingSlice(t, JSON, "json", "/", "/", `[]`, ``)
|
||||||
|
testBodyBindingSlice(t, JSON, "json", "/", "/", `[{"foo": "123"}]`, `[{}]`)
|
||||||
|
testBodyBindingSlice(t, JSON, "json", "/", "/", `[{"foo": "123"}]`, `[{"foo": ""}]`)
|
||||||
|
testBodyBindingSlice(t, JSON, "json", "/", "/", `[{"foo": "123"}]`, `[{"foo": 123}]`)
|
||||||
|
testBodyBindingSlice(t, JSON, "json", "/", "/", `[{"foo": "123"}]`, `[{"bar": 123}]`)
|
||||||
|
testBodyBindingSlice(t, JSON, "json", "/", "/", `[{"foo": "123"}]`, `[{"foo": "123456789012345678901234567890123"}]`)
|
||||||
|
}
|
||||||
|
|
||||||
func TestBindingJSONUseNumber(t *testing.T) {
|
func TestBindingJSONUseNumber(t *testing.T) {
|
||||||
testBodyBindingUseNumber(t,
|
testBodyBindingUseNumber(t,
|
||||||
JSON, "json",
|
JSON, "json",
|
||||||
@ -242,6 +209,18 @@ func TestBindingJSONUseNumber2(t *testing.T) {
|
|||||||
`{"foo": 123}`, `{"bar": "foo"}`)
|
`{"foo": 123}`, `{"bar": "foo"}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBindingJSONDisallowUnknownFields(t *testing.T) {
|
||||||
|
testBodyBindingDisallowUnknownFields(t, JSON,
|
||||||
|
"/", "/",
|
||||||
|
`{"foo": "bar"}`, `{"foo": "bar", "what": "this"}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBindingJSONStringMap(t *testing.T) {
|
||||||
|
testBodyBindingStringMap(t, JSON,
|
||||||
|
"/", "/",
|
||||||
|
`{"foo": "bar", "hello": "world"}`, `{"num": 2}`)
|
||||||
|
}
|
||||||
|
|
||||||
func TestBindingForm(t *testing.T) {
|
func TestBindingForm(t *testing.T) {
|
||||||
testFormBinding(t, "POST",
|
testFormBinding(t, "POST",
|
||||||
"/", "/",
|
"/", "/",
|
||||||
@ -254,6 +233,18 @@ func TestBindingForm2(t *testing.T) {
|
|||||||
"", "")
|
"", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBindingFormEmbeddedStruct(t *testing.T) {
|
||||||
|
testFormBindingEmbeddedStruct(t, "POST",
|
||||||
|
"/", "/",
|
||||||
|
"page=1&size=2&appkey=test-appkey", "bar2=foo")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBindingFormEmbeddedStruct2(t *testing.T) {
|
||||||
|
testFormBindingEmbeddedStruct(t, "GET",
|
||||||
|
"/?page=1&size=2&appkey=test-appkey", "/?bar2=foo",
|
||||||
|
"", "")
|
||||||
|
}
|
||||||
|
|
||||||
func TestBindingFormDefaultValue(t *testing.T) {
|
func TestBindingFormDefaultValue(t *testing.T) {
|
||||||
testFormBindingDefaultValue(t, "POST",
|
testFormBindingDefaultValue(t, "POST",
|
||||||
"/", "/",
|
"/", "/",
|
||||||
@ -269,7 +260,10 @@ func TestBindingFormDefaultValue2(t *testing.T) {
|
|||||||
func TestBindingFormForTime(t *testing.T) {
|
func TestBindingFormForTime(t *testing.T) {
|
||||||
testFormBindingForTime(t, "POST",
|
testFormBindingForTime(t, "POST",
|
||||||
"/", "/",
|
"/", "/",
|
||||||
"time_foo=2017-11-15&time_bar=", "bar2=foo")
|
"time_foo=2017-11-15&time_bar=&createTime=1562400033000000123&unixTime=1562400033", "bar2=foo")
|
||||||
|
testFormBindingForTimeNotUnixFormat(t, "POST",
|
||||||
|
"/", "/",
|
||||||
|
"time_foo=2017-11-15&createTime=bad&unixTime=bad", "bar2=foo")
|
||||||
testFormBindingForTimeNotFormat(t, "POST",
|
testFormBindingForTimeNotFormat(t, "POST",
|
||||||
"/", "/",
|
"/", "/",
|
||||||
"time_foo=2017-11-15", "bar2=foo")
|
"time_foo=2017-11-15", "bar2=foo")
|
||||||
@ -283,8 +277,11 @@ func TestBindingFormForTime(t *testing.T) {
|
|||||||
|
|
||||||
func TestBindingFormForTime2(t *testing.T) {
|
func TestBindingFormForTime2(t *testing.T) {
|
||||||
testFormBindingForTime(t, "GET",
|
testFormBindingForTime(t, "GET",
|
||||||
"/?time_foo=2017-11-15&time_bar=", "/?bar2=foo",
|
"/?time_foo=2017-11-15&time_bar=&createTime=1562400033000000123&unixTime=1562400033", "/?bar2=foo",
|
||||||
"", "")
|
"", "")
|
||||||
|
testFormBindingForTimeNotUnixFormat(t, "POST",
|
||||||
|
"/", "/",
|
||||||
|
"time_foo=2017-11-15&createTime=bad&unixTime=bad", "bar2=foo")
|
||||||
testFormBindingForTimeNotFormat(t, "GET",
|
testFormBindingForTimeNotFormat(t, "GET",
|
||||||
"/?time_foo=2017-11-15", "/?bar2=foo",
|
"/?time_foo=2017-11-15", "/?bar2=foo",
|
||||||
"", "")
|
"", "")
|
||||||
@ -335,110 +332,6 @@ func TestBindingFormForType(t *testing.T) {
|
|||||||
"/?slice_map_foo=1&slice_map_foo=2", "/?bar2=1&bar2=2",
|
"/?slice_map_foo=1&slice_map_foo=2", "/?bar2=1&bar2=2",
|
||||||
"", "", "SliceMap")
|
"", "", "SliceMap")
|
||||||
|
|
||||||
testFormBindingForType(t, "POST",
|
|
||||||
"/", "/",
|
|
||||||
"int_foo=&int_bar=-12", "bar2=-123", "Int")
|
|
||||||
|
|
||||||
testFormBindingForType(t, "GET",
|
|
||||||
"/?int_foo=&int_bar=-12", "/?bar2=-123",
|
|
||||||
"", "", "Int")
|
|
||||||
|
|
||||||
testFormBindingForType(t, "POST",
|
|
||||||
"/", "/",
|
|
||||||
"int8_foo=&int8_bar=-12", "bar2=-123", "Int8")
|
|
||||||
|
|
||||||
testFormBindingForType(t, "GET",
|
|
||||||
"/?int8_foo=&int8_bar=-12", "/?bar2=-123",
|
|
||||||
"", "", "Int8")
|
|
||||||
|
|
||||||
testFormBindingForType(t, "POST",
|
|
||||||
"/", "/",
|
|
||||||
"int16_foo=&int16_bar=-12", "bar2=-123", "Int16")
|
|
||||||
|
|
||||||
testFormBindingForType(t, "GET",
|
|
||||||
"/?int16_foo=&int16_bar=-12", "/?bar2=-123",
|
|
||||||
"", "", "Int16")
|
|
||||||
|
|
||||||
testFormBindingForType(t, "POST",
|
|
||||||
"/", "/",
|
|
||||||
"int32_foo=&int32_bar=-12", "bar2=-123", "Int32")
|
|
||||||
|
|
||||||
testFormBindingForType(t, "GET",
|
|
||||||
"/?int32_foo=&int32_bar=-12", "/?bar2=-123",
|
|
||||||
"", "", "Int32")
|
|
||||||
|
|
||||||
testFormBindingForType(t, "POST",
|
|
||||||
"/", "/",
|
|
||||||
"int64_foo=&int64_bar=-12", "bar2=-123", "Int64")
|
|
||||||
|
|
||||||
testFormBindingForType(t, "GET",
|
|
||||||
"/?int64_foo=&int64_bar=-12", "/?bar2=-123",
|
|
||||||
"", "", "Int64")
|
|
||||||
|
|
||||||
testFormBindingForType(t, "POST",
|
|
||||||
"/", "/",
|
|
||||||
"uint_foo=&uint_bar=12", "bar2=123", "Uint")
|
|
||||||
|
|
||||||
testFormBindingForType(t, "GET",
|
|
||||||
"/?uint_foo=&uint_bar=12", "/?bar2=123",
|
|
||||||
"", "", "Uint")
|
|
||||||
|
|
||||||
testFormBindingForType(t, "POST",
|
|
||||||
"/", "/",
|
|
||||||
"uint8_foo=&uint8_bar=12", "bar2=123", "Uint8")
|
|
||||||
|
|
||||||
testFormBindingForType(t, "GET",
|
|
||||||
"/?uint8_foo=&uint8_bar=12", "/?bar2=123",
|
|
||||||
"", "", "Uint8")
|
|
||||||
|
|
||||||
testFormBindingForType(t, "POST",
|
|
||||||
"/", "/",
|
|
||||||
"uint16_foo=&uint16_bar=12", "bar2=123", "Uint16")
|
|
||||||
|
|
||||||
testFormBindingForType(t, "GET",
|
|
||||||
"/?uint16_foo=&uint16_bar=12", "/?bar2=123",
|
|
||||||
"", "", "Uint16")
|
|
||||||
|
|
||||||
testFormBindingForType(t, "POST",
|
|
||||||
"/", "/",
|
|
||||||
"uint32_foo=&uint32_bar=12", "bar2=123", "Uint32")
|
|
||||||
|
|
||||||
testFormBindingForType(t, "GET",
|
|
||||||
"/?uint32_foo=&uint32_bar=12", "/?bar2=123",
|
|
||||||
"", "", "Uint32")
|
|
||||||
|
|
||||||
testFormBindingForType(t, "POST",
|
|
||||||
"/", "/",
|
|
||||||
"uint64_foo=&uint64_bar=12", "bar2=123", "Uint64")
|
|
||||||
|
|
||||||
testFormBindingForType(t, "GET",
|
|
||||||
"/?uint64_foo=&uint64_bar=12", "/?bar2=123",
|
|
||||||
"", "", "Uint64")
|
|
||||||
|
|
||||||
testFormBindingForType(t, "POST",
|
|
||||||
"/", "/",
|
|
||||||
"bool_foo=&bool_bar=true", "bar2=true", "Bool")
|
|
||||||
|
|
||||||
testFormBindingForType(t, "GET",
|
|
||||||
"/?bool_foo=&bool_bar=true", "/?bar2=true",
|
|
||||||
"", "", "Bool")
|
|
||||||
|
|
||||||
testFormBindingForType(t, "POST",
|
|
||||||
"/", "/",
|
|
||||||
"float32_foo=&float32_bar=-12.34", "bar2=12.3", "Float32")
|
|
||||||
|
|
||||||
testFormBindingForType(t, "GET",
|
|
||||||
"/?float32_foo=&float32_bar=-12.34", "/?bar2=12.3",
|
|
||||||
"", "", "Float32")
|
|
||||||
|
|
||||||
testFormBindingForType(t, "POST",
|
|
||||||
"/", "/",
|
|
||||||
"float64_foo=&float64_bar=-12.34", "bar2=12.3", "Float64")
|
|
||||||
|
|
||||||
testFormBindingForType(t, "GET",
|
|
||||||
"/?float64_foo=&float64_bar=-12.34", "/?bar2=12.3",
|
|
||||||
"", "", "Float64")
|
|
||||||
|
|
||||||
testFormBindingForType(t, "POST",
|
testFormBindingForType(t, "POST",
|
||||||
"/", "/",
|
"/", "/",
|
||||||
"ptr_bar=test", "bar2=test", "Ptr")
|
"ptr_bar=test", "bar2=test", "Ptr")
|
||||||
@ -464,6 +357,37 @@ func TestBindingFormForType(t *testing.T) {
|
|||||||
"", "", "StructPointer")
|
"", "", "StructPointer")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBindingFormStringMap(t *testing.T) {
|
||||||
|
testBodyBindingStringMap(t, Form,
|
||||||
|
"/", "",
|
||||||
|
`foo=bar&hello=world`, "")
|
||||||
|
// Should pick the last value
|
||||||
|
testBodyBindingStringMap(t, Form,
|
||||||
|
"/", "",
|
||||||
|
`foo=something&foo=bar&hello=world`, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBindingFormStringSliceMap(t *testing.T) {
|
||||||
|
obj := make(map[string][]string)
|
||||||
|
req := requestWithBody("POST", "/", "foo=something&foo=bar&hello=world")
|
||||||
|
req.Header.Add("Content-Type", MIMEPOSTForm)
|
||||||
|
err := Form.Bind(req, &obj)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, obj)
|
||||||
|
assert.Len(t, obj, 2)
|
||||||
|
target := map[string][]string{
|
||||||
|
"foo": {"something", "bar"},
|
||||||
|
"hello": {"world"},
|
||||||
|
}
|
||||||
|
assert.True(t, reflect.DeepEqual(obj, target))
|
||||||
|
|
||||||
|
objInvalid := make(map[string][]int)
|
||||||
|
req = requestWithBody("POST", "/", "foo=something&foo=bar&hello=world")
|
||||||
|
req.Header.Add("Content-Type", MIMEPOSTForm)
|
||||||
|
err = Form.Bind(req, &objInvalid)
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
func TestBindingQuery(t *testing.T) {
|
func TestBindingQuery(t *testing.T) {
|
||||||
testQueryBinding(t, "POST",
|
testQueryBinding(t, "POST",
|
||||||
"/?foo=bar&bar=foo", "/",
|
"/?foo=bar&bar=foo", "/",
|
||||||
@ -494,6 +418,28 @@ func TestBindingQueryBoolFail(t *testing.T) {
|
|||||||
"bool_foo=unused", "")
|
"bool_foo=unused", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBindingQueryStringMap(t *testing.T) {
|
||||||
|
b := Query
|
||||||
|
|
||||||
|
obj := make(map[string]string)
|
||||||
|
req := requestWithBody("GET", "/?foo=bar&hello=world", "")
|
||||||
|
err := b.Bind(req, &obj)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, obj)
|
||||||
|
assert.Len(t, obj, 2)
|
||||||
|
assert.Equal(t, "bar", obj["foo"])
|
||||||
|
assert.Equal(t, "world", obj["hello"])
|
||||||
|
|
||||||
|
obj = make(map[string]string)
|
||||||
|
req = requestWithBody("GET", "/?foo=bar&foo=2&hello=world", "") // should pick last
|
||||||
|
err = b.Bind(req, &obj)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, obj)
|
||||||
|
assert.Len(t, obj, 2)
|
||||||
|
assert.Equal(t, "2", obj["foo"])
|
||||||
|
assert.Equal(t, "world", obj["hello"])
|
||||||
|
}
|
||||||
|
|
||||||
func TestBindingXML(t *testing.T) {
|
func TestBindingXML(t *testing.T) {
|
||||||
testBodyBinding(t,
|
testBodyBinding(t,
|
||||||
XML, "xml",
|
XML, "xml",
|
||||||
@ -515,6 +461,13 @@ func TestBindingYAML(t *testing.T) {
|
|||||||
`foo: bar`, `bar: foo`)
|
`foo: bar`, `bar: foo`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBindingYAMLStringMap(t *testing.T) {
|
||||||
|
// YAML is a superset of JSON, so the test below is JSON (to avoid newlines)
|
||||||
|
testBodyBindingStringMap(t, YAML,
|
||||||
|
"/", "/",
|
||||||
|
`{"foo": "bar", "hello": "world"}`, `{"nested": {"foo": "bar"}}`)
|
||||||
|
}
|
||||||
|
|
||||||
func TestBindingYAMLFail(t *testing.T) {
|
func TestBindingYAMLFail(t *testing.T) {
|
||||||
testBodyBindingFail(t,
|
testBodyBindingFail(t,
|
||||||
YAML, "yaml",
|
YAML, "yaml",
|
||||||
@ -565,7 +518,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)
|
||||||
@ -589,7 +543,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)
|
||||||
@ -678,7 +633,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")
|
||||||
@ -754,26 +710,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"}`)
|
||||||
@ -792,9 +728,9 @@ func TestValidationDisabled(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestExistsSucceeds(t *testing.T) {
|
func TestRequiredSucceeds(t *testing.T) {
|
||||||
type HogeStruct struct {
|
type HogeStruct struct {
|
||||||
Hoge *int `json:"hoge" binding:"exists"`
|
Hoge *int `json:"hoge" binding:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var obj HogeStruct
|
var obj HogeStruct
|
||||||
@ -803,9 +739,9 @@ func TestExistsSucceeds(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestExistsFails(t *testing.T) {
|
func TestRequiredFails(t *testing.T) {
|
||||||
type HogeStruct struct {
|
type HogeStruct struct {
|
||||||
Hoge *int `json:"foo" binding:"exists"`
|
Hoge *int `json:"foo" binding:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var obj HogeStruct
|
var obj HogeStruct
|
||||||
@ -814,6 +750,31 @@ func TestExistsFails(t *testing.T) {
|
|||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHeaderBinding(t *testing.T) {
|
||||||
|
h := Header
|
||||||
|
assert.Equal(t, "header", h.Name())
|
||||||
|
|
||||||
|
type tHeader struct {
|
||||||
|
Limit int `header:"limit"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var theader tHeader
|
||||||
|
req := requestWithBody("GET", "/", "")
|
||||||
|
req.Header.Add("limit", "1000")
|
||||||
|
assert.NoError(t, h.Bind(req, &theader))
|
||||||
|
assert.Equal(t, 1000, theader.Limit)
|
||||||
|
|
||||||
|
req = requestWithBody("GET", "/", "")
|
||||||
|
req.Header.Add("fail", `{fail:fail}`)
|
||||||
|
|
||||||
|
type failStruct struct {
|
||||||
|
Fail map[string]interface{} `header:"fail"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err := h.Bind(req, &failStruct{})
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
func TestUriBinding(t *testing.T) {
|
func TestUriBinding(t *testing.T) {
|
||||||
b := Uri
|
b := Uri
|
||||||
assert.Equal(t, "uri", b.Name())
|
assert.Equal(t, "uri", b.Name())
|
||||||
@ -857,6 +818,23 @@ func TestUriInnerBinding(t *testing.T) {
|
|||||||
assert.Equal(t, tag.S.Age, expectedAge)
|
assert.Equal(t, tag.S.Age, expectedAge)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testFormBindingEmbeddedStruct(t *testing.T, method, path, badPath, body, badBody string) {
|
||||||
|
b := Form
|
||||||
|
assert.Equal(t, "form", b.Name())
|
||||||
|
|
||||||
|
obj := QueryTest{}
|
||||||
|
req := requestWithBody(method, path, body)
|
||||||
|
if method == "POST" {
|
||||||
|
req.Header.Add("Content-Type", MIMEPOSTForm)
|
||||||
|
}
|
||||||
|
err := b.Bind(req, &obj)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 1, obj.Page)
|
||||||
|
assert.Equal(t, 2, obj.Size)
|
||||||
|
assert.Equal(t, "test-appkey", obj.Appkey)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func testFormBinding(t *testing.T, method, path, badPath, body, badBody string) {
|
func testFormBinding(t *testing.T, method, path, badPath, body, badBody string) {
|
||||||
b := Form
|
b := Form
|
||||||
assert.Equal(t, "form", b.Name())
|
assert.Equal(t, "form", b.Name())
|
||||||
@ -954,6 +932,8 @@ func testFormBindingForTime(t *testing.T, method, path, badPath, body, badBody s
|
|||||||
assert.Equal(t, "Asia/Chongqing", obj.TimeFoo.Location().String())
|
assert.Equal(t, "Asia/Chongqing", obj.TimeFoo.Location().String())
|
||||||
assert.Equal(t, int64(-62135596800), obj.TimeBar.Unix())
|
assert.Equal(t, int64(-62135596800), obj.TimeBar.Unix())
|
||||||
assert.Equal(t, "UTC", obj.TimeBar.Location().String())
|
assert.Equal(t, "UTC", obj.TimeBar.Location().String())
|
||||||
|
assert.Equal(t, int64(1562400033000000123), obj.CreateTime.UnixNano())
|
||||||
|
assert.Equal(t, int64(1562400033), obj.UnixTime.Unix())
|
||||||
|
|
||||||
obj = FooBarStructForTimeType{}
|
obj = FooBarStructForTimeType{}
|
||||||
req = requestWithBody(method, badPath, badBody)
|
req = requestWithBody(method, badPath, badBody)
|
||||||
@ -961,6 +941,24 @@ func testFormBindingForTime(t *testing.T, method, path, badPath, body, badBody s
|
|||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testFormBindingForTimeNotUnixFormat(t *testing.T, method, path, badPath, body, badBody string) {
|
||||||
|
b := Form
|
||||||
|
assert.Equal(t, "form", b.Name())
|
||||||
|
|
||||||
|
obj := FooStructForTimeTypeNotUnixFormat{}
|
||||||
|
req := requestWithBody(method, path, body)
|
||||||
|
if method == "POST" {
|
||||||
|
req.Header.Add("Content-Type", MIMEPOSTForm)
|
||||||
|
}
|
||||||
|
err := b.Bind(req, &obj)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
obj = FooStructForTimeTypeNotUnixFormat{}
|
||||||
|
req = requestWithBody(method, badPath, badBody)
|
||||||
|
err = JSON.Bind(req, &obj)
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
func testFormBindingForTimeNotFormat(t *testing.T, method, path, badPath, body, badBody string) {
|
func testFormBindingForTimeNotFormat(t *testing.T, method, path, badPath, body, badBody string) {
|
||||||
b := Form
|
b := Form
|
||||||
assert.Equal(t, "form", b.Name())
|
assert.Equal(t, "form", b.Name())
|
||||||
@ -1076,149 +1074,6 @@ func testFormBindingForType(t *testing.T, method, path, badPath, body, badBody s
|
|||||||
req.Header.Add("Content-Type", MIMEPOSTForm)
|
req.Header.Add("Content-Type", MIMEPOSTForm)
|
||||||
}
|
}
|
||||||
switch typ {
|
switch typ {
|
||||||
case "Int":
|
|
||||||
obj := FooBarStructForIntType{}
|
|
||||||
err := b.Bind(req, &obj)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, int(0), obj.IntFoo)
|
|
||||||
assert.Equal(t, int(-12), obj.IntBar)
|
|
||||||
|
|
||||||
obj = FooBarStructForIntType{}
|
|
||||||
req = requestWithBody(method, badPath, badBody)
|
|
||||||
err = JSON.Bind(req, &obj)
|
|
||||||
assert.Error(t, err)
|
|
||||||
case "Int8":
|
|
||||||
obj := FooBarStructForInt8Type{}
|
|
||||||
err := b.Bind(req, &obj)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, int8(0), obj.Int8Foo)
|
|
||||||
assert.Equal(t, int8(-12), obj.Int8Bar)
|
|
||||||
|
|
||||||
obj = FooBarStructForInt8Type{}
|
|
||||||
req = requestWithBody(method, badPath, badBody)
|
|
||||||
err = JSON.Bind(req, &obj)
|
|
||||||
assert.Error(t, err)
|
|
||||||
case "Int16":
|
|
||||||
obj := FooBarStructForInt16Type{}
|
|
||||||
err := b.Bind(req, &obj)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, int16(0), obj.Int16Foo)
|
|
||||||
assert.Equal(t, int16(-12), obj.Int16Bar)
|
|
||||||
|
|
||||||
obj = FooBarStructForInt16Type{}
|
|
||||||
req = requestWithBody(method, badPath, badBody)
|
|
||||||
err = JSON.Bind(req, &obj)
|
|
||||||
assert.Error(t, err)
|
|
||||||
case "Int32":
|
|
||||||
obj := FooBarStructForInt32Type{}
|
|
||||||
err := b.Bind(req, &obj)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, int32(0), obj.Int32Foo)
|
|
||||||
assert.Equal(t, int32(-12), obj.Int32Bar)
|
|
||||||
|
|
||||||
obj = FooBarStructForInt32Type{}
|
|
||||||
req = requestWithBody(method, badPath, badBody)
|
|
||||||
err = JSON.Bind(req, &obj)
|
|
||||||
assert.Error(t, err)
|
|
||||||
case "Int64":
|
|
||||||
obj := FooBarStructForInt64Type{}
|
|
||||||
err := b.Bind(req, &obj)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, int64(0), obj.Int64Foo)
|
|
||||||
assert.Equal(t, int64(-12), obj.Int64Bar)
|
|
||||||
|
|
||||||
obj = FooBarStructForInt64Type{}
|
|
||||||
req = requestWithBody(method, badPath, badBody)
|
|
||||||
err = JSON.Bind(req, &obj)
|
|
||||||
assert.Error(t, err)
|
|
||||||
case "Uint":
|
|
||||||
obj := FooBarStructForUintType{}
|
|
||||||
err := b.Bind(req, &obj)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, uint(0x0), obj.UintFoo)
|
|
||||||
assert.Equal(t, uint(0xc), obj.UintBar)
|
|
||||||
|
|
||||||
obj = FooBarStructForUintType{}
|
|
||||||
req = requestWithBody(method, badPath, badBody)
|
|
||||||
err = JSON.Bind(req, &obj)
|
|
||||||
assert.Error(t, err)
|
|
||||||
case "Uint8":
|
|
||||||
obj := FooBarStructForUint8Type{}
|
|
||||||
err := b.Bind(req, &obj)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, uint8(0x0), obj.Uint8Foo)
|
|
||||||
assert.Equal(t, uint8(0xc), obj.Uint8Bar)
|
|
||||||
|
|
||||||
obj = FooBarStructForUint8Type{}
|
|
||||||
req = requestWithBody(method, badPath, badBody)
|
|
||||||
err = JSON.Bind(req, &obj)
|
|
||||||
assert.Error(t, err)
|
|
||||||
case "Uint16":
|
|
||||||
obj := FooBarStructForUint16Type{}
|
|
||||||
err := b.Bind(req, &obj)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, uint16(0x0), obj.Uint16Foo)
|
|
||||||
assert.Equal(t, uint16(0xc), obj.Uint16Bar)
|
|
||||||
|
|
||||||
obj = FooBarStructForUint16Type{}
|
|
||||||
req = requestWithBody(method, badPath, badBody)
|
|
||||||
err = JSON.Bind(req, &obj)
|
|
||||||
assert.Error(t, err)
|
|
||||||
case "Uint32":
|
|
||||||
obj := FooBarStructForUint32Type{}
|
|
||||||
err := b.Bind(req, &obj)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, uint32(0x0), obj.Uint32Foo)
|
|
||||||
assert.Equal(t, uint32(0xc), obj.Uint32Bar)
|
|
||||||
|
|
||||||
obj = FooBarStructForUint32Type{}
|
|
||||||
req = requestWithBody(method, badPath, badBody)
|
|
||||||
err = JSON.Bind(req, &obj)
|
|
||||||
assert.Error(t, err)
|
|
||||||
case "Uint64":
|
|
||||||
obj := FooBarStructForUint64Type{}
|
|
||||||
err := b.Bind(req, &obj)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, uint64(0x0), obj.Uint64Foo)
|
|
||||||
assert.Equal(t, uint64(0xc), obj.Uint64Bar)
|
|
||||||
|
|
||||||
obj = FooBarStructForUint64Type{}
|
|
||||||
req = requestWithBody(method, badPath, badBody)
|
|
||||||
err = JSON.Bind(req, &obj)
|
|
||||||
assert.Error(t, err)
|
|
||||||
case "Float32":
|
|
||||||
obj := FooBarStructForFloat32Type{}
|
|
||||||
err := b.Bind(req, &obj)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, float32(0.0), obj.Float32Foo)
|
|
||||||
assert.Equal(t, float32(-12.34), obj.Float32Bar)
|
|
||||||
|
|
||||||
obj = FooBarStructForFloat32Type{}
|
|
||||||
req = requestWithBody(method, badPath, badBody)
|
|
||||||
err = JSON.Bind(req, &obj)
|
|
||||||
assert.Error(t, err)
|
|
||||||
case "Float64":
|
|
||||||
obj := FooBarStructForFloat64Type{}
|
|
||||||
err := b.Bind(req, &obj)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, float64(0.0), obj.Float64Foo)
|
|
||||||
assert.Equal(t, float64(-12.34), obj.Float64Bar)
|
|
||||||
|
|
||||||
obj = FooBarStructForFloat64Type{}
|
|
||||||
req = requestWithBody(method, badPath, badBody)
|
|
||||||
err = JSON.Bind(req, &obj)
|
|
||||||
assert.Error(t, err)
|
|
||||||
case "Bool":
|
|
||||||
obj := FooBarStructForBoolType{}
|
|
||||||
err := b.Bind(req, &obj)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.False(t, obj.BoolFoo)
|
|
||||||
assert.True(t, obj.BoolBar)
|
|
||||||
|
|
||||||
obj = FooBarStructForBoolType{}
|
|
||||||
req = requestWithBody(method, badPath, badBody)
|
|
||||||
err = JSON.Bind(req, &obj)
|
|
||||||
assert.Error(t, err)
|
|
||||||
case "Slice":
|
case "Slice":
|
||||||
obj := FooStructForSliceType{}
|
obj := FooStructForSliceType{}
|
||||||
err := b.Bind(req, &obj)
|
err := b.Bind(req, &obj)
|
||||||
@ -1340,6 +1195,46 @@ func testBodyBinding(t *testing.T, b Binding, name, path, badPath, body, badBody
|
|||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testBodyBindingSlice(t *testing.T, b Binding, name, path, badPath, body, badBody string) {
|
||||||
|
assert.Equal(t, name, b.Name())
|
||||||
|
|
||||||
|
var obj1 []FooStruct
|
||||||
|
req := requestWithBody("POST", path, body)
|
||||||
|
err := b.Bind(req, &obj1)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
var obj2 []FooStruct
|
||||||
|
req = requestWithBody("POST", badPath, badBody)
|
||||||
|
err = JSON.Bind(req, &obj2)
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testBodyBindingStringMap(t *testing.T, b Binding, path, badPath, body, badBody string) {
|
||||||
|
obj := make(map[string]string)
|
||||||
|
req := requestWithBody("POST", path, body)
|
||||||
|
if b.Name() == "form" {
|
||||||
|
req.Header.Add("Content-Type", MIMEPOSTForm)
|
||||||
|
}
|
||||||
|
err := b.Bind(req, &obj)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, obj)
|
||||||
|
assert.Len(t, obj, 2)
|
||||||
|
assert.Equal(t, "bar", obj["foo"])
|
||||||
|
assert.Equal(t, "world", obj["hello"])
|
||||||
|
|
||||||
|
if badPath != "" && badBody != "" {
|
||||||
|
obj = make(map[string]string)
|
||||||
|
req = requestWithBody("POST", badPath, badBody)
|
||||||
|
err = b.Bind(req, &obj)
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
objInt := make(map[string]int)
|
||||||
|
req = requestWithBody("POST", path, body)
|
||||||
|
err = b.Bind(req, &objInt)
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
func testBodyBindingUseNumber(t *testing.T, b Binding, name, path, badPath, body, badBody string) {
|
func testBodyBindingUseNumber(t *testing.T, b Binding, name, path, badPath, body, badBody string) {
|
||||||
assert.Equal(t, name, b.Name())
|
assert.Equal(t, name, b.Name())
|
||||||
|
|
||||||
@ -1377,6 +1272,25 @@ func testBodyBindingUseNumber2(t *testing.T, b Binding, name, path, badPath, bod
|
|||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testBodyBindingDisallowUnknownFields(t *testing.T, b Binding, path, badPath, body, badBody string) {
|
||||||
|
EnableDecoderDisallowUnknownFields = true
|
||||||
|
defer func() {
|
||||||
|
EnableDecoderDisallowUnknownFields = false
|
||||||
|
}()
|
||||||
|
|
||||||
|
obj := FooStructDisallowUnknownFields{}
|
||||||
|
req := requestWithBody("POST", path, body)
|
||||||
|
err := b.Bind(req, &obj)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "bar", obj.Foo)
|
||||||
|
|
||||||
|
obj = FooStructDisallowUnknownFields{}
|
||||||
|
req = requestWithBody("POST", badPath, badBody)
|
||||||
|
err = JSON.Bind(req, &obj)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "what")
|
||||||
|
}
|
||||||
|
|
||||||
func testBodyBindingFail(t *testing.T, b Binding, name, path, badPath, body, badBody string) {
|
func testBodyBindingFail(t *testing.T, b Binding, name, path, badPath, body, badBody string) {
|
||||||
assert.Equal(t, name, b.Name())
|
assert.Equal(t, name, b.Name())
|
||||||
|
|
||||||
@ -1433,118 +1347,7 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCanSet(t *testing.T) {
|
|
||||||
type CanSetStruct struct {
|
|
||||||
lowerStart string `form:"lower"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var c CanSetStruct
|
|
||||||
assert.Nil(t, mapForm(&c, nil))
|
|
||||||
}
|
|
||||||
|
|
||||||
func formPostRequest(path, body string) *http.Request {
|
|
||||||
req := requestWithBody("POST", path, body)
|
|
||||||
req.Header.Add("Content-Type", MIMEPOSTForm)
|
|
||||||
return req
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBindingSliceDefault(t *testing.T) {
|
|
||||||
var s struct {
|
|
||||||
Friends []string `form:"friends,default=mike"`
|
|
||||||
}
|
|
||||||
req := formPostRequest("", "")
|
|
||||||
err := Form.Bind(req, &s)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Len(t, s.Friends, 1)
|
|
||||||
assert.Equal(t, "mike", s.Friends[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBindingStructField(t *testing.T) {
|
|
||||||
var s struct {
|
|
||||||
Opts struct {
|
|
||||||
Port int
|
|
||||||
} `form:"opts"`
|
|
||||||
}
|
|
||||||
req := formPostRequest("", `opts={"Port": 8000}`)
|
|
||||||
err := Form.Bind(req, &s)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, 8000, s.Opts.Port)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBindingUnknownTypeChan(t *testing.T) {
|
|
||||||
var s struct {
|
|
||||||
Stop chan bool `form:"stop"`
|
|
||||||
}
|
|
||||||
req := formPostRequest("", "stop=true")
|
|
||||||
err := Form.Bind(req, &s)
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.Equal(t, errUnknownType, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBindingTimeDuration(t *testing.T) {
|
|
||||||
var s struct {
|
|
||||||
Timeout time.Duration `form:"timeout"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ok
|
|
||||||
req := formPostRequest("", "timeout=5s")
|
|
||||||
err := Form.Bind(req, &s)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, 5*time.Second, s.Timeout)
|
|
||||||
|
|
||||||
// error
|
|
||||||
req = formPostRequest("", "timeout=wrong")
|
|
||||||
err = Form.Bind(req, &s)
|
|
||||||
assert.Error(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBindingArray(t *testing.T) {
|
|
||||||
var s struct {
|
|
||||||
Nums [2]int `form:"nums,default=4"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// default
|
|
||||||
req := formPostRequest("", "")
|
|
||||||
err := Form.Bind(req, &s)
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.Equal(t, [2]int{0, 0}, s.Nums)
|
|
||||||
|
|
||||||
// ok
|
|
||||||
req = formPostRequest("", "nums=3&nums=8")
|
|
||||||
err = Form.Bind(req, &s)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, [2]int{3, 8}, s.Nums)
|
|
||||||
|
|
||||||
// not enough vals
|
|
||||||
req = formPostRequest("", "nums=3")
|
|
||||||
err = Form.Bind(req, &s)
|
|
||||||
assert.Error(t, err)
|
|
||||||
|
|
||||||
// error
|
|
||||||
req = formPostRequest("", "nums=3&nums=wrong")
|
|
||||||
err = Form.Bind(req, &s)
|
|
||||||
assert.Error(t, err)
|
|
||||||
}
|
|
||||||
|
@ -5,10 +5,12 @@
|
|||||||
package binding
|
package binding
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"gopkg.in/go-playground/validator.v8"
|
"github.com/go-playground/validator/v10"
|
||||||
)
|
)
|
||||||
|
|
||||||
type defaultValidator struct {
|
type defaultValidator struct {
|
||||||
@ -16,22 +18,54 @@ type defaultValidator struct {
|
|||||||
validate *validator.Validate
|
validate *validator.Validate
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type sliceValidateError []error
|
||||||
|
|
||||||
|
func (err sliceValidateError) Error() string {
|
||||||
|
var errMsgs []string
|
||||||
|
for i, e := range err {
|
||||||
|
if e == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
errMsgs = append(errMsgs, fmt.Sprintf("[%d]: %s", i, e.Error()))
|
||||||
|
}
|
||||||
|
return strings.Join(errMsgs, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
var _ StructValidator = &defaultValidator{}
|
var _ StructValidator = &defaultValidator{}
|
||||||
|
|
||||||
// ValidateStruct receives any kind of type, but only performed struct or pointer to struct type.
|
// ValidateStruct receives any kind of type, but only performed struct or pointer to struct type.
|
||||||
func (v *defaultValidator) ValidateStruct(obj interface{}) error {
|
func (v *defaultValidator) ValidateStruct(obj interface{}) error {
|
||||||
|
if obj == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
value := reflect.ValueOf(obj)
|
value := reflect.ValueOf(obj)
|
||||||
valueType := value.Kind()
|
switch value.Kind() {
|
||||||
if valueType == reflect.Ptr {
|
case reflect.Ptr:
|
||||||
valueType = value.Elem().Kind()
|
return v.ValidateStruct(value.Elem().Interface())
|
||||||
}
|
case reflect.Struct:
|
||||||
if valueType == reflect.Struct {
|
return v.validateStruct(obj)
|
||||||
v.lazyinit()
|
case reflect.Slice, reflect.Array:
|
||||||
if err := v.validate.Struct(obj); err != nil {
|
count := value.Len()
|
||||||
return err
|
validateRet := make(sliceValidateError, 0)
|
||||||
|
for i := 0; i < count; i++ {
|
||||||
|
if err := v.ValidateStruct(value.Index(i).Interface()); err != nil {
|
||||||
|
validateRet = append(validateRet, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if len(validateRet) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return validateRet
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
return nil
|
}
|
||||||
|
|
||||||
|
// validateStruct receives struct type
|
||||||
|
func (v *defaultValidator) validateStruct(obj interface{}) error {
|
||||||
|
v.lazyinit()
|
||||||
|
return v.validate.Struct(obj)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Engine returns the underlying validator engine which powers the default
|
// Engine returns the underlying validator engine which powers the default
|
||||||
@ -45,7 +79,7 @@ func (v *defaultValidator) Engine() interface{} {
|
|||||||
|
|
||||||
func (v *defaultValidator) lazyinit() {
|
func (v *defaultValidator) lazyinit() {
|
||||||
v.once.Do(func() {
|
v.once.Do(func() {
|
||||||
config := &validator.Config{TagName: "binding"}
|
v.validate = validator.New()
|
||||||
v.validate = validator.New(config)
|
v.validate.SetTagName("binding")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
68
binding/default_validator_test.go
Normal file
68
binding/default_validator_test.go
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
package binding
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSliceValidateError(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
err sliceValidateError
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"has nil elements", sliceValidateError{errors.New("test error"), nil}, "[0]: test error"},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := tt.err.Error(); got != tt.want {
|
||||||
|
t.Errorf("sliceValidateError.Error() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDefaultValidator(t *testing.T) {
|
||||||
|
type exampleStruct struct {
|
||||||
|
A string `binding:"max=8"`
|
||||||
|
B int `binding:"gt=0"`
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
v *defaultValidator
|
||||||
|
obj interface{}
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{"validate nil obj", &defaultValidator{}, nil, false},
|
||||||
|
{"validate int obj", &defaultValidator{}, 3, false},
|
||||||
|
{"validate struct failed-1", &defaultValidator{}, exampleStruct{A: "123456789", B: 1}, true},
|
||||||
|
{"validate struct failed-2", &defaultValidator{}, exampleStruct{A: "12345678", B: 0}, true},
|
||||||
|
{"validate struct passed", &defaultValidator{}, exampleStruct{A: "12345678", B: 1}, false},
|
||||||
|
{"validate *struct failed-1", &defaultValidator{}, &exampleStruct{A: "123456789", B: 1}, true},
|
||||||
|
{"validate *struct failed-2", &defaultValidator{}, &exampleStruct{A: "12345678", B: 0}, true},
|
||||||
|
{"validate *struct passed", &defaultValidator{}, &exampleStruct{A: "12345678", B: 1}, false},
|
||||||
|
{"validate []struct failed-1", &defaultValidator{}, []exampleStruct{{A: "123456789", B: 1}}, true},
|
||||||
|
{"validate []struct failed-2", &defaultValidator{}, []exampleStruct{{A: "12345678", B: 0}}, true},
|
||||||
|
{"validate []struct passed", &defaultValidator{}, []exampleStruct{{A: "12345678", B: 1}}, false},
|
||||||
|
{"validate []*struct failed-1", &defaultValidator{}, []*exampleStruct{{A: "123456789", B: 1}}, true},
|
||||||
|
{"validate []*struct failed-2", &defaultValidator{}, []*exampleStruct{{A: "12345678", B: 0}}, true},
|
||||||
|
{"validate []*struct passed", &defaultValidator{}, []*exampleStruct{{A: "12345678", B: 1}}, false},
|
||||||
|
{"validate *[]struct failed-1", &defaultValidator{}, &[]exampleStruct{{A: "123456789", B: 1}}, true},
|
||||||
|
{"validate *[]struct failed-2", &defaultValidator{}, &[]exampleStruct{{A: "12345678", B: 0}}, true},
|
||||||
|
{"validate *[]struct passed", &defaultValidator{}, &[]exampleStruct{{A: "12345678", B: 1}}, false},
|
||||||
|
{"validate *[]*struct failed-1", &defaultValidator{}, &[]*exampleStruct{{A: "123456789", B: 1}}, true},
|
||||||
|
{"validate *[]*struct failed-2", &defaultValidator{}, &[]*exampleStruct{{A: "12345678", B: 0}}, true},
|
||||||
|
{"validate *[]*struct passed", &defaultValidator{}, &[]*exampleStruct{{A: "12345678", B: 1}}, false},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if err := tt.v.ValidateStruct(tt.obj); (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("defaultValidator.Validate() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -5,12 +5,10 @@
|
|||||||
package binding
|
package binding
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"mime/multipart"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"reflect"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const defaultMemory = 32 * 1024 * 1024
|
const defaultMemory = 32 << 20
|
||||||
|
|
||||||
type formBinding struct{}
|
type formBinding struct{}
|
||||||
type formPostBinding struct{}
|
type formPostBinding struct{}
|
||||||
@ -63,27 +61,3 @@ func (formMultipartBinding) Bind(req *http.Request, obj interface{}) error {
|
|||||||
|
|
||||||
return validate(obj)
|
return validate(obj)
|
||||||
}
|
}
|
||||||
|
|
||||||
type multipartRequest http.Request
|
|
||||||
|
|
||||||
var _ setter = (*multipartRequest)(nil)
|
|
||||||
|
|
||||||
var (
|
|
||||||
multipartFileHeaderStructType = reflect.TypeOf(multipart.FileHeader{})
|
|
||||||
)
|
|
||||||
|
|
||||||
// TrySet tries to set a value by the multipart request with the binding a form file
|
|
||||||
func (r *multipartRequest) TrySet(value reflect.Value, field reflect.StructField, key string, opt setOptions) (isSetted bool, err error) {
|
|
||||||
if value.Type() == multipartFileHeaderStructType {
|
|
||||||
_, file, err := (*http.Request)(r).FormFile(key)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
if file != nil {
|
|
||||||
value.Set(reflect.ValueOf(*file))
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return setByForm(value, field, r.MultipartForm.Value, key, opt)
|
|
||||||
}
|
|
||||||
|
@ -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")
|
||||||
@ -28,6 +29,21 @@ func mapForm(ptr interface{}, form map[string][]string) error {
|
|||||||
var emptyField = reflect.StructField{}
|
var emptyField = reflect.StructField{}
|
||||||
|
|
||||||
func mapFormByTag(ptr interface{}, form map[string][]string, tag string) error {
|
func mapFormByTag(ptr interface{}, form map[string][]string, tag string) error {
|
||||||
|
// Check if ptr is a map
|
||||||
|
ptrVal := reflect.ValueOf(ptr)
|
||||||
|
var pointed interface{}
|
||||||
|
if ptrVal.Kind() == reflect.Ptr {
|
||||||
|
ptrVal = ptrVal.Elem()
|
||||||
|
pointed = ptrVal.Interface()
|
||||||
|
}
|
||||||
|
if ptrVal.Kind() == reflect.Map &&
|
||||||
|
ptrVal.Type().Key().Kind() == reflect.String {
|
||||||
|
if pointed != nil {
|
||||||
|
ptr = pointed
|
||||||
|
}
|
||||||
|
return setFormMap(ptr, form)
|
||||||
|
}
|
||||||
|
|
||||||
return mappingByPtr(ptr, formSource(form), tag)
|
return mappingByPtr(ptr, formSource(form), tag)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,6 +67,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 {
|
||||||
@ -70,12 +90,14 @@ func mapping(value reflect.Value, field reflect.StructField, setter setter, tag
|
|||||||
return isSetted, nil
|
return isSetted, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
ok, err := tryToSetValue(value, field, setter, tag)
|
if vKind != reflect.Struct || !field.Anonymous {
|
||||||
if err != nil {
|
ok, err := tryToSetValue(value, field, setter, tag)
|
||||||
return false, err
|
if err != nil {
|
||||||
}
|
return false, err
|
||||||
if ok {
|
}
|
||||||
return true, nil
|
if ok {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if vKind == reflect.Struct {
|
if vKind == reflect.Struct {
|
||||||
@ -83,7 +105,8 @@ func mapping(value reflect.Value, field reflect.StructField, setter setter, tag
|
|||||||
|
|
||||||
var isSetted bool
|
var isSetted bool
|
||||||
for i := 0; i < value.NumField(); i++ {
|
for i := 0; i < value.NumField(); i++ {
|
||||||
if !value.Field(i).CanSet() {
|
sf := tValue.Field(i)
|
||||||
|
if sf.PkgPath != "" && !sf.Anonymous { // unexported
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
ok, err := mapping(value.Field(i), tValue.Field(i), setter, tag)
|
ok, err := mapping(value.Field(i), tValue.Field(i), setter, tag)
|
||||||
@ -109,9 +132,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
|
||||||
}
|
}
|
||||||
@ -123,9 +143,7 @@ func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter
|
|||||||
for len(opts) > 0 {
|
for len(opts) > 0 {
|
||||||
opt, opts = head(opts, ",")
|
opt, opts = head(opts, ",")
|
||||||
|
|
||||||
k, v := head(opt, "=")
|
if k, v := head(opt, "="); k == "default" {
|
||||||
switch k {
|
|
||||||
case "default":
|
|
||||||
setOpt.isDefaultExists = true
|
setOpt.isDefaultExists = true
|
||||||
setOpt.defaultValue = v
|
setOpt.defaultValue = v
|
||||||
}
|
}
|
||||||
@ -206,9 +224,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
|
||||||
}
|
}
|
||||||
@ -265,6 +283,24 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val
|
|||||||
timeFormat = time.RFC3339
|
timeFormat = time.RFC3339
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch tf := strings.ToLower(timeFormat); tf {
|
||||||
|
case "unix", "unixnano":
|
||||||
|
tv, err := strconv.ParseInt(val, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
d := time.Duration(1)
|
||||||
|
if tf == "unixnano" {
|
||||||
|
d = time.Second
|
||||||
|
}
|
||||||
|
|
||||||
|
t := time.Unix(tv/int64(d), tv%int64(d))
|
||||||
|
value.Set(reflect.ValueOf(t))
|
||||||
|
return nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
if val == "" {
|
if val == "" {
|
||||||
value.Set(reflect.ValueOf(time.Time{}))
|
value.Set(reflect.ValueOf(time.Time{}))
|
||||||
return nil
|
return nil
|
||||||
@ -328,3 +364,29 @@ func head(str, sep string) (head string, tail string) {
|
|||||||
}
|
}
|
||||||
return str[:idx], str[idx+len(sep):]
|
return str[:idx], str[idx+len(sep):]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setFormMap(ptr interface{}, form map[string][]string) error {
|
||||||
|
el := reflect.TypeOf(ptr).Elem()
|
||||||
|
|
||||||
|
if el.Kind() == reflect.Slice {
|
||||||
|
ptrMap, ok := ptr.(map[string][]string)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("cannot convert to map slices of strings")
|
||||||
|
}
|
||||||
|
for k, v := range form {
|
||||||
|
ptrMap[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ptrMap, ok := ptr.(map[string]string)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("cannot convert to map of strings")
|
||||||
|
}
|
||||||
|
for k, v := range form {
|
||||||
|
ptrMap[k] = v[len(v)-1] // pick last
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
281
binding/form_mapping_test.go
Normal file
281
binding/form_mapping_test.go
Normal file
@ -0,0 +1,281 @@
|
|||||||
|
// 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 (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMappingBaseTypes(t *testing.T) {
|
||||||
|
intPtr := func(i int) *int {
|
||||||
|
return &i
|
||||||
|
}
|
||||||
|
for _, tt := range []struct {
|
||||||
|
name string
|
||||||
|
value interface{}
|
||||||
|
form string
|
||||||
|
expect interface{}
|
||||||
|
}{
|
||||||
|
{"base type", struct{ F int }{}, "9", int(9)},
|
||||||
|
{"base type", struct{ F int8 }{}, "9", int8(9)},
|
||||||
|
{"base type", struct{ F int16 }{}, "9", int16(9)},
|
||||||
|
{"base type", struct{ F int32 }{}, "9", int32(9)},
|
||||||
|
{"base type", struct{ F int64 }{}, "9", int64(9)},
|
||||||
|
{"base type", struct{ F uint }{}, "9", uint(9)},
|
||||||
|
{"base type", struct{ F uint8 }{}, "9", uint8(9)},
|
||||||
|
{"base type", struct{ F uint16 }{}, "9", uint16(9)},
|
||||||
|
{"base type", struct{ F uint32 }{}, "9", uint32(9)},
|
||||||
|
{"base type", struct{ F uint64 }{}, "9", uint64(9)},
|
||||||
|
{"base type", struct{ F bool }{}, "True", true},
|
||||||
|
{"base type", struct{ F float32 }{}, "9.1", float32(9.1)},
|
||||||
|
{"base type", struct{ F float64 }{}, "9.1", float64(9.1)},
|
||||||
|
{"base type", struct{ F string }{}, "test", string("test")},
|
||||||
|
{"base type", struct{ F *int }{}, "9", intPtr(9)},
|
||||||
|
|
||||||
|
// zero values
|
||||||
|
{"zero value", struct{ F int }{}, "", int(0)},
|
||||||
|
{"zero value", struct{ F uint }{}, "", uint(0)},
|
||||||
|
{"zero value", struct{ F bool }{}, "", false},
|
||||||
|
{"zero value", struct{ F float32 }{}, "", float32(0)},
|
||||||
|
} {
|
||||||
|
tp := reflect.TypeOf(tt.value)
|
||||||
|
testName := tt.name + ":" + tp.Field(0).Type.String()
|
||||||
|
|
||||||
|
val := reflect.New(reflect.TypeOf(tt.value))
|
||||||
|
val.Elem().Set(reflect.ValueOf(tt.value))
|
||||||
|
|
||||||
|
field := val.Elem().Type().Field(0)
|
||||||
|
|
||||||
|
_, err := mapping(val, emptyField, formSource{field.Name: {tt.form}}, "form")
|
||||||
|
assert.NoError(t, err, testName)
|
||||||
|
|
||||||
|
actual := val.Elem().Field(0).Interface()
|
||||||
|
assert.Equal(t, tt.expect, actual, testName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMappingDefault(t *testing.T) {
|
||||||
|
var s struct {
|
||||||
|
Int int `form:",default=9"`
|
||||||
|
Slice []int `form:",default=9"`
|
||||||
|
Array [1]int `form:",default=9"`
|
||||||
|
}
|
||||||
|
err := mappingByPtr(&s, formSource{}, "form")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, 9, s.Int)
|
||||||
|
assert.Equal(t, []int{9}, s.Slice)
|
||||||
|
assert.Equal(t, [1]int{9}, s.Array)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMappingSkipField(t *testing.T) {
|
||||||
|
var s struct {
|
||||||
|
A int
|
||||||
|
}
|
||||||
|
err := mappingByPtr(&s, formSource{}, "form")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, 0, s.A)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMappingIgnoreField(t *testing.T) {
|
||||||
|
var s struct {
|
||||||
|
A int `form:"A"`
|
||||||
|
B int `form:"-"`
|
||||||
|
}
|
||||||
|
err := mappingByPtr(&s, formSource{"A": {"9"}, "B": {"9"}}, "form")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, 9, s.A)
|
||||||
|
assert.Equal(t, 0, s.B)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMappingUnexportedField(t *testing.T) {
|
||||||
|
var s struct {
|
||||||
|
A int `form:"a"`
|
||||||
|
b int `form:"b"`
|
||||||
|
}
|
||||||
|
err := mappingByPtr(&s, formSource{"a": {"9"}, "b": {"9"}}, "form")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, 9, s.A)
|
||||||
|
assert.Equal(t, 0, s.b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMappingPrivateField(t *testing.T) {
|
||||||
|
var s struct {
|
||||||
|
f int `form:"field"`
|
||||||
|
}
|
||||||
|
err := mappingByPtr(&s, formSource{"field": {"6"}}, "form")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, int(0), s.f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMappingUnknownFieldType(t *testing.T) {
|
||||||
|
var s struct {
|
||||||
|
U uintptr
|
||||||
|
}
|
||||||
|
|
||||||
|
err := mappingByPtr(&s, formSource{"U": {"unknown"}}, "form")
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Equal(t, errUnknownType, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMappingURI(t *testing.T) {
|
||||||
|
var s struct {
|
||||||
|
F int `uri:"field"`
|
||||||
|
}
|
||||||
|
err := mapUri(&s, map[string][]string{"field": {"6"}})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, int(6), s.F)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMappingForm(t *testing.T) {
|
||||||
|
var s struct {
|
||||||
|
F int `form:"field"`
|
||||||
|
}
|
||||||
|
err := mapForm(&s, map[string][]string{"field": {"6"}})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, int(6), s.F)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMappingTime(t *testing.T) {
|
||||||
|
var s struct {
|
||||||
|
Time time.Time
|
||||||
|
LocalTime time.Time `time_format:"2006-01-02"`
|
||||||
|
ZeroValue time.Time
|
||||||
|
CSTTime time.Time `time_format:"2006-01-02" time_location:"Asia/Shanghai"`
|
||||||
|
UTCTime time.Time `time_format:"2006-01-02" time_utc:"1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
time.Local, err = time.LoadLocation("Europe/Berlin")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = mapForm(&s, map[string][]string{
|
||||||
|
"Time": {"2019-01-20T16:02:58Z"},
|
||||||
|
"LocalTime": {"2019-01-20"},
|
||||||
|
"ZeroValue": {},
|
||||||
|
"CSTTime": {"2019-01-20"},
|
||||||
|
"UTCTime": {"2019-01-20"},
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, "2019-01-20 16:02:58 +0000 UTC", s.Time.String())
|
||||||
|
assert.Equal(t, "2019-01-20 00:00:00 +0100 CET", s.LocalTime.String())
|
||||||
|
assert.Equal(t, "2019-01-19 23:00:00 +0000 UTC", s.LocalTime.UTC().String())
|
||||||
|
assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", s.ZeroValue.String())
|
||||||
|
assert.Equal(t, "2019-01-20 00:00:00 +0800 CST", s.CSTTime.String())
|
||||||
|
assert.Equal(t, "2019-01-19 16:00:00 +0000 UTC", s.CSTTime.UTC().String())
|
||||||
|
assert.Equal(t, "2019-01-20 00:00:00 +0000 UTC", s.UTCTime.String())
|
||||||
|
|
||||||
|
// wrong location
|
||||||
|
var wrongLoc struct {
|
||||||
|
Time time.Time `time_location:"wrong"`
|
||||||
|
}
|
||||||
|
err = mapForm(&wrongLoc, map[string][]string{"Time": {"2019-01-20T16:02:58Z"}})
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
// wrong time value
|
||||||
|
var wrongTime struct {
|
||||||
|
Time time.Time
|
||||||
|
}
|
||||||
|
err = mapForm(&wrongTime, map[string][]string{"Time": {"wrong"}})
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMappingTimeDuration(t *testing.T) {
|
||||||
|
var s struct {
|
||||||
|
D time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// ok
|
||||||
|
err := mappingByPtr(&s, formSource{"D": {"5s"}}, "form")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 5*time.Second, s.D)
|
||||||
|
|
||||||
|
// error
|
||||||
|
err = mappingByPtr(&s, formSource{"D": {"wrong"}}, "form")
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMappingSlice(t *testing.T) {
|
||||||
|
var s struct {
|
||||||
|
Slice []int `form:"slice,default=9"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// default value
|
||||||
|
err := mappingByPtr(&s, formSource{}, "form")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, []int{9}, s.Slice)
|
||||||
|
|
||||||
|
// ok
|
||||||
|
err = mappingByPtr(&s, formSource{"slice": {"3", "4"}}, "form")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, []int{3, 4}, s.Slice)
|
||||||
|
|
||||||
|
// error
|
||||||
|
err = mappingByPtr(&s, formSource{"slice": {"wrong"}}, "form")
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMappingArray(t *testing.T) {
|
||||||
|
var s struct {
|
||||||
|
Array [2]int `form:"array,default=9"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// wrong default
|
||||||
|
err := mappingByPtr(&s, formSource{}, "form")
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
// ok
|
||||||
|
err = mappingByPtr(&s, formSource{"array": {"3", "4"}}, "form")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, [2]int{3, 4}, s.Array)
|
||||||
|
|
||||||
|
// error - not enough vals
|
||||||
|
err = mappingByPtr(&s, formSource{"array": {"3"}}, "form")
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
// error - wrong value
|
||||||
|
err = mappingByPtr(&s, formSource{"array": {"wrong"}}, "form")
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMappingStructField(t *testing.T) {
|
||||||
|
var s struct {
|
||||||
|
J struct {
|
||||||
|
I int
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := mappingByPtr(&s, formSource{"J": {`{"I": 9}`}}, "form")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 9, s.J.I)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMappingMapField(t *testing.T) {
|
||||||
|
var s struct {
|
||||||
|
M map[string]int
|
||||||
|
}
|
||||||
|
|
||||||
|
err := mappingByPtr(&s, formSource{"M": {`{"one": 1}`}}, "form")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
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)
|
||||||
|
}
|
34
binding/header.go
Normal file
34
binding/header.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package binding
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/textproto"
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
type headerBinding struct{}
|
||||||
|
|
||||||
|
func (headerBinding) Name() string {
|
||||||
|
return "header"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (headerBinding) Bind(req *http.Request, obj interface{}) error {
|
||||||
|
|
||||||
|
if err := mapHeader(obj, req.Header); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return validate(obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapHeader(ptr interface{}, h map[string][]string) error {
|
||||||
|
return mappingByPtr(ptr, headerSource(h), "header")
|
||||||
|
}
|
||||||
|
|
||||||
|
type headerSource map[string][]string
|
||||||
|
|
||||||
|
var _ setter = headerSource(nil)
|
||||||
|
|
||||||
|
func (hs headerSource) TrySet(value reflect.Value, field reflect.StructField, tagValue string, opt setOptions) (isSetted bool, err error) {
|
||||||
|
return setByForm(value, field, hs, textproto.CanonicalMIMEHeaderKey(tagValue), opt)
|
||||||
|
}
|
@ -18,6 +18,12 @@ import (
|
|||||||
// interface{} as a Number instead of as a float64.
|
// interface{} as a Number instead of as a float64.
|
||||||
var EnableDecoderUseNumber = false
|
var EnableDecoderUseNumber = false
|
||||||
|
|
||||||
|
// EnableDecoderDisallowUnknownFields is used to call the DisallowUnknownFields method
|
||||||
|
// on the JSON Decoder instance. DisallowUnknownFields causes the Decoder to
|
||||||
|
// return an error when the destination is a struct and the input contains object
|
||||||
|
// keys which do not match any non-ignored, exported fields in the destination.
|
||||||
|
var EnableDecoderDisallowUnknownFields = false
|
||||||
|
|
||||||
type jsonBinding struct{}
|
type jsonBinding struct{}
|
||||||
|
|
||||||
func (jsonBinding) Name() string {
|
func (jsonBinding) Name() string {
|
||||||
@ -40,6 +46,9 @@ func decodeJSON(r io.Reader, obj interface{}) error {
|
|||||||
if EnableDecoderUseNumber {
|
if EnableDecoderUseNumber {
|
||||||
decoder.UseNumber()
|
decoder.UseNumber()
|
||||||
}
|
}
|
||||||
|
if EnableDecoderDisallowUnknownFields {
|
||||||
|
decoder.DisallowUnknownFields()
|
||||||
|
}
|
||||||
if err := decoder.Decode(obj); err != nil {
|
if err := decoder.Decode(obj); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
30
binding/json_test.go
Normal file
30
binding/json_test.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJSONBindingBindBodyMap(t *testing.T) {
|
||||||
|
s := make(map[string]string)
|
||||||
|
err := jsonBinding{}.BindBody([]byte(`{"foo": "FOO","hello":"world"}`), &s)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, s, 2)
|
||||||
|
assert.Equal(t, "FOO", s["foo"])
|
||||||
|
assert.Equal(t, "world", s["hello"])
|
||||||
|
}
|
@ -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
34
binding/msgpack_test.go
Normal 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()
|
||||||
|
}
|
66
binding/multipart_form_mapping.go
Normal file
66
binding/multipart_form_mapping.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
// 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 (
|
||||||
|
"errors"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/http"
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
type multipartRequest http.Request
|
||||||
|
|
||||||
|
var _ setter = (*multipartRequest)(nil)
|
||||||
|
|
||||||
|
// TrySet tries to set a value by the multipart request with the binding a form file
|
||||||
|
func (r *multipartRequest) TrySet(value reflect.Value, field reflect.StructField, key string, opt setOptions) (isSetted bool, err error) {
|
||||||
|
if files := r.MultipartForm.File[key]; len(files) != 0 {
|
||||||
|
return setByMultipartFormFile(value, field, files)
|
||||||
|
}
|
||||||
|
|
||||||
|
return setByForm(value, field, r.MultipartForm.Value, key, opt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setByMultipartFormFile(value reflect.Value, field reflect.StructField, files []*multipart.FileHeader) (isSetted bool, err error) {
|
||||||
|
switch value.Kind() {
|
||||||
|
case reflect.Ptr:
|
||||||
|
switch value.Interface().(type) {
|
||||||
|
case *multipart.FileHeader:
|
||||||
|
value.Set(reflect.ValueOf(files[0]))
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
case reflect.Struct:
|
||||||
|
switch value.Interface().(type) {
|
||||||
|
case multipart.FileHeader:
|
||||||
|
value.Set(reflect.ValueOf(*files[0]))
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
case reflect.Slice:
|
||||||
|
slice := reflect.MakeSlice(value.Type(), len(files), len(files))
|
||||||
|
isSetted, err = setArrayOfMultipartFormFiles(slice, field, files)
|
||||||
|
if err != nil || !isSetted {
|
||||||
|
return isSetted, err
|
||||||
|
}
|
||||||
|
value.Set(slice)
|
||||||
|
return true, nil
|
||||||
|
case reflect.Array:
|
||||||
|
return setArrayOfMultipartFormFiles(value, field, files)
|
||||||
|
}
|
||||||
|
return false, errors.New("unsupported field type for multipart.FileHeader")
|
||||||
|
}
|
||||||
|
|
||||||
|
func setArrayOfMultipartFormFiles(value reflect.Value, field reflect.StructField, files []*multipart.FileHeader) (isSetted bool, err error) {
|
||||||
|
if value.Len() != len(files) {
|
||||||
|
return false, errors.New("unsupported len of array for []*multipart.FileHeader")
|
||||||
|
}
|
||||||
|
for i := range files {
|
||||||
|
setted, err := setByMultipartFormFile(value.Index(i), field, files[i:i+1])
|
||||||
|
if err != nil || !setted {
|
||||||
|
return setted, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
138
binding/multipart_form_mapping_test.go
Normal file
138
binding/multipart_form_mapping_test.go
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
// 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 (
|
||||||
|
"bytes"
|
||||||
|
"io/ioutil"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFormMultipartBindingBindOneFile(t *testing.T) {
|
||||||
|
var s struct {
|
||||||
|
FileValue multipart.FileHeader `form:"file"`
|
||||||
|
FilePtr *multipart.FileHeader `form:"file"`
|
||||||
|
SliceValues []multipart.FileHeader `form:"file"`
|
||||||
|
SlicePtrs []*multipart.FileHeader `form:"file"`
|
||||||
|
ArrayValues [1]multipart.FileHeader `form:"file"`
|
||||||
|
ArrayPtrs [1]*multipart.FileHeader `form:"file"`
|
||||||
|
}
|
||||||
|
file := testFile{"file", "file1", []byte("hello")}
|
||||||
|
|
||||||
|
req := createRequestMultipartFiles(t, file)
|
||||||
|
err := FormMultipart.Bind(req, &s)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assertMultipartFileHeader(t, &s.FileValue, file)
|
||||||
|
assertMultipartFileHeader(t, s.FilePtr, file)
|
||||||
|
assert.Len(t, s.SliceValues, 1)
|
||||||
|
assertMultipartFileHeader(t, &s.SliceValues[0], file)
|
||||||
|
assert.Len(t, s.SlicePtrs, 1)
|
||||||
|
assertMultipartFileHeader(t, s.SlicePtrs[0], file)
|
||||||
|
assertMultipartFileHeader(t, &s.ArrayValues[0], file)
|
||||||
|
assertMultipartFileHeader(t, s.ArrayPtrs[0], file)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFormMultipartBindingBindTwoFiles(t *testing.T) {
|
||||||
|
var s struct {
|
||||||
|
SliceValues []multipart.FileHeader `form:"file"`
|
||||||
|
SlicePtrs []*multipart.FileHeader `form:"file"`
|
||||||
|
ArrayValues [2]multipart.FileHeader `form:"file"`
|
||||||
|
ArrayPtrs [2]*multipart.FileHeader `form:"file"`
|
||||||
|
}
|
||||||
|
files := []testFile{
|
||||||
|
{"file", "file1", []byte("hello")},
|
||||||
|
{"file", "file2", []byte("world")},
|
||||||
|
}
|
||||||
|
|
||||||
|
req := createRequestMultipartFiles(t, files...)
|
||||||
|
err := FormMultipart.Bind(req, &s)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Len(t, s.SliceValues, len(files))
|
||||||
|
assert.Len(t, s.SlicePtrs, len(files))
|
||||||
|
assert.Len(t, s.ArrayValues, len(files))
|
||||||
|
assert.Len(t, s.ArrayPtrs, len(files))
|
||||||
|
|
||||||
|
for i, file := range files {
|
||||||
|
assertMultipartFileHeader(t, &s.SliceValues[i], file)
|
||||||
|
assertMultipartFileHeader(t, s.SlicePtrs[i], file)
|
||||||
|
assertMultipartFileHeader(t, &s.ArrayValues[i], file)
|
||||||
|
assertMultipartFileHeader(t, s.ArrayPtrs[i], file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFormMultipartBindingBindError(t *testing.T) {
|
||||||
|
files := []testFile{
|
||||||
|
{"file", "file1", []byte("hello")},
|
||||||
|
{"file", "file2", []byte("world")},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range []struct {
|
||||||
|
name string
|
||||||
|
s interface{}
|
||||||
|
}{
|
||||||
|
{"wrong type", &struct {
|
||||||
|
Files int `form:"file"`
|
||||||
|
}{}},
|
||||||
|
{"wrong array size", &struct {
|
||||||
|
Files [1]*multipart.FileHeader `form:"file"`
|
||||||
|
}{}},
|
||||||
|
{"wrong slice type", &struct {
|
||||||
|
Files []int `form:"file"`
|
||||||
|
}{}},
|
||||||
|
} {
|
||||||
|
req := createRequestMultipartFiles(t, files...)
|
||||||
|
err := FormMultipart.Bind(req, tt.s)
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type testFile struct {
|
||||||
|
Fieldname string
|
||||||
|
Filename string
|
||||||
|
Content []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func createRequestMultipartFiles(t *testing.T, files ...testFile) *http.Request {
|
||||||
|
var body bytes.Buffer
|
||||||
|
|
||||||
|
mw := multipart.NewWriter(&body)
|
||||||
|
for _, file := range files {
|
||||||
|
fw, err := mw.CreateFormFile(file.Fieldname, file.Filename)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
n, err := fw.Write(file.Content)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, len(file.Content), n)
|
||||||
|
}
|
||||||
|
err := mw.Close()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", "/", &body)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+mw.Boundary())
|
||||||
|
return req
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertMultipartFileHeader(t *testing.T, fh *multipart.FileHeader, file testFile) {
|
||||||
|
assert.Equal(t, file.Filename, fh.Filename)
|
||||||
|
// assert.Equal(t, int64(len(file.Content)), fh.Size) // fh.Size does not exist on go1.8
|
||||||
|
|
||||||
|
fl, err := fh.Open()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
body, err := ioutil.ReadAll(fl)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, string(file.Content), string(body))
|
||||||
|
|
||||||
|
err = fl.Close()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
@ -6,12 +6,11 @@ package binding
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"reflect"
|
|
||||||
"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.v8"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type testInterface interface {
|
type testInterface interface {
|
||||||
@ -200,15 +199,8 @@ type structCustomValidation struct {
|
|||||||
Integer int `binding:"notone"`
|
Integer int `binding:"notone"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// notOne is a custom validator meant to be used with `validator.v8` library.
|
func notOne(f1 validator.FieldLevel) bool {
|
||||||
// The method signature for `v9` is significantly different and this function
|
if val, ok := f1.Field().Interface().(int); ok {
|
||||||
// would need to be changed for tests to pass after upgrade.
|
|
||||||
// See https://github.com/gin-gonic/gin/pull/1015.
|
|
||||||
func notOne(
|
|
||||||
v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value,
|
|
||||||
field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string,
|
|
||||||
) bool {
|
|
||||||
if val, ok := field.Interface().(int); ok {
|
|
||||||
return val != 1
|
return val != 1
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
25
binding/xml_test.go
Normal file
25
binding/xml_test.go
Normal 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
21
binding/yaml_test.go
Normal 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)
|
||||||
|
}
|
208
context.go
208
context.go
@ -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,
|
||||||
@ -48,8 +51,13 @@ type Context struct {
|
|||||||
Params Params
|
Params Params
|
||||||
handlers HandlersChain
|
handlers HandlersChain
|
||||||
index int8
|
index int8
|
||||||
|
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{}
|
||||||
@ -59,6 +67,17 @@ type Context struct {
|
|||||||
|
|
||||||
// Accepted defines a list of manually accepted formats for content negotiation.
|
// Accepted defines a list of manually accepted formats for content negotiation.
|
||||||
Accepted []string
|
Accepted []string
|
||||||
|
|
||||||
|
// queryCache use url.ParseQuery cached the param query result from c.Request.URL.Query()
|
||||||
|
queryCache url.Values
|
||||||
|
|
||||||
|
// formCache use url.ParseQuery cached PostForm contains the parsed form data from POST, PATCH,
|
||||||
|
// or PUT body parameters.
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
/************************************/
|
/************************************/
|
||||||
@ -70,15 +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.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.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
|
||||||
@ -87,6 +116,9 @@ func (c *Context) Copy() *Context {
|
|||||||
for k, v := range c.Keys {
|
for k, v := range c.Keys {
|
||||||
cp.Keys[k] = v
|
cp.Keys[k] = v
|
||||||
}
|
}
|
||||||
|
paramCopy := make([]Param, len(cp.Params))
|
||||||
|
copy(paramCopy, cp.Params)
|
||||||
|
cp.Params = paramCopy
|
||||||
return &cp
|
return &cp
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,6 +143,15 @@ func (c *Context) Handler() HandlerFunc {
|
|||||||
return c.handlers.Last()
|
return c.handlers.Last()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FullPath returns a matched route full path. For not found routes
|
||||||
|
// returns an empty string.
|
||||||
|
// router.GET("/user/:id", func(c *gin.Context) {
|
||||||
|
// c.FullPath() == "/user/:id" // true
|
||||||
|
// })
|
||||||
|
func (c *Context) FullPath() string {
|
||||||
|
return c.fullPath
|
||||||
|
}
|
||||||
|
|
||||||
/************************************/
|
/************************************/
|
||||||
/*********** FLOW CONTROL ***********/
|
/*********** FLOW CONTROL ***********/
|
||||||
/************************************/
|
/************************************/
|
||||||
@ -196,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -249,6 +295,22 @@ func (c *Context) GetInt64(key string) (i64 int64) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUint returns the value associated with the key as an unsigned integer.
|
||||||
|
func (c *Context) GetUint(key string) (ui uint) {
|
||||||
|
if val, ok := c.Get(key); ok && val != nil {
|
||||||
|
ui, _ = val.(uint)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUint64 returns the value associated with the key as an unsigned integer.
|
||||||
|
func (c *Context) GetUint64(key string) (ui64 uint64) {
|
||||||
|
if val, ok := c.Get(key); ok && val != nil {
|
||||||
|
ui64, _ = val.(uint64)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// GetFloat64 returns the value associated with the key as a float64.
|
// GetFloat64 returns the value associated with the key as a float64.
|
||||||
func (c *Context) GetFloat64(key string) (f64 float64) {
|
func (c *Context) GetFloat64(key string) (f64 float64) {
|
||||||
if val, ok := c.Get(key); ok && val != nil {
|
if val, ok := c.Get(key); ok && val != nil {
|
||||||
@ -368,10 +430,21 @@ func (c *Context) QueryArray(key string) []string {
|
|||||||
return values
|
return values
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Context) initQueryCache() {
|
||||||
|
if c.queryCache == nil {
|
||||||
|
if c.Request != nil {
|
||||||
|
c.queryCache = c.Request.URL.Query()
|
||||||
|
} else {
|
||||||
|
c.queryCache = url.Values{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 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) {
|
||||||
if values, ok := c.Request.URL.Query()[key]; ok && len(values) > 0 {
|
c.initQueryCache()
|
||||||
|
if values, ok := c.queryCache[key]; ok && len(values) > 0 {
|
||||||
return values, true
|
return values, true
|
||||||
}
|
}
|
||||||
return []string{}, false
|
return []string{}, false
|
||||||
@ -386,7 +459,8 @@ 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) {
|
||||||
return c.get(c.Request.URL.Query(), key)
|
c.initQueryCache()
|
||||||
|
return c.get(c.queryCache, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PostForm returns the specified key from a POST urlencoded form or multipart form
|
// PostForm returns the specified key from a POST urlencoded form or multipart form
|
||||||
@ -427,23 +501,26 @@ func (c *Context) PostFormArray(key string) []string {
|
|||||||
return values
|
return values
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Context) initFormCache() {
|
||||||
|
if c.formCache == nil {
|
||||||
|
c.formCache = make(url.Values)
|
||||||
|
req := c.Request
|
||||||
|
if err := req.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil {
|
||||||
|
if err != http.ErrNotMultipart {
|
||||||
|
debugPrint("error on parse multipart form array: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.formCache = req.PostForm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 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) {
|
||||||
req := c.Request
|
c.initFormCache()
|
||||||
if err := req.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil {
|
if values := c.formCache[key]; len(values) > 0 {
|
||||||
if err != http.ErrNotMultipart {
|
|
||||||
debugPrint("error on parse multipart form array: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if values := req.PostForm[key]; len(values) > 0 {
|
|
||||||
return values, true
|
return values, true
|
||||||
}
|
}
|
||||||
if req.MultipartForm != nil && req.MultipartForm.File != nil {
|
|
||||||
if values := req.MultipartForm.Value[key]; len(values) > 0 {
|
|
||||||
return values, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return []string{}, false
|
return []string{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -456,19 +533,8 @@ 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) {
|
||||||
req := c.Request
|
c.initFormCache()
|
||||||
if err := req.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil {
|
return c.get(c.formCache, key)
|
||||||
if err != http.ErrNotMultipart {
|
|
||||||
debugPrint("error on parse multipart form map: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dicts, exist := c.get(req.PostForm, key)
|
|
||||||
|
|
||||||
if !exist && req.MultipartForm != nil && req.MultipartForm.File != nil {
|
|
||||||
dicts, exist = c.get(req.MultipartForm.Value, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
return dicts, exist
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// get is an internal method and returns a map which satisfy conditions.
|
// get is an internal method and returns a map which satisfy conditions.
|
||||||
@ -493,7 +559,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -554,6 +624,11 @@ func (c *Context) BindYAML(obj interface{}) error {
|
|||||||
return c.MustBindWith(obj, binding.YAML)
|
return c.MustBindWith(obj, binding.YAML)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BindHeader is a shortcut for c.MustBindWith(obj, binding.Header).
|
||||||
|
func (c *Context) BindHeader(obj interface{}) error {
|
||||||
|
return c.MustBindWith(obj, binding.Header)
|
||||||
|
}
|
||||||
|
|
||||||
// BindUri binds the passed struct pointer using binding.Uri.
|
// BindUri binds the passed struct pointer using binding.Uri.
|
||||||
// It will abort the request with HTTP 400 if any error occurs.
|
// It will abort the request with HTTP 400 if any error occurs.
|
||||||
func (c *Context) BindUri(obj interface{}) error {
|
func (c *Context) BindUri(obj interface{}) error {
|
||||||
@ -608,6 +683,11 @@ func (c *Context) ShouldBindYAML(obj interface{}) error {
|
|||||||
return c.ShouldBindWith(obj, binding.YAML)
|
return c.ShouldBindWith(obj, binding.YAML)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ShouldBindHeader is a shortcut for c.ShouldBindWith(obj, binding.Header).
|
||||||
|
func (c *Context) ShouldBindHeader(obj interface{}) error {
|
||||||
|
return c.ShouldBindWith(obj, binding.Header)
|
||||||
|
}
|
||||||
|
|
||||||
// ShouldBindUri binds the passed struct pointer using the specified binding engine.
|
// ShouldBindUri binds the passed struct pointer using the specified binding engine.
|
||||||
func (c *Context) ShouldBindUri(obj interface{}) error {
|
func (c *Context) ShouldBindUri(obj interface{}) error {
|
||||||
m := make(map[string][]string)
|
m := make(map[string][]string)
|
||||||
@ -682,7 +762,7 @@ func (c *Context) ContentType() string {
|
|||||||
// handshake is being initiated by the client.
|
// handshake is being initiated by the client.
|
||||||
func (c *Context) IsWebsocket() bool {
|
func (c *Context) IsWebsocket() bool {
|
||||||
if strings.Contains(strings.ToLower(c.requestHeader("Connection")), "upgrade") &&
|
if strings.Contains(strings.ToLower(c.requestHeader("Connection")), "upgrade") &&
|
||||||
strings.ToLower(c.requestHeader("Upgrade")) == "websocket" {
|
strings.EqualFold(c.requestHeader("Upgrade"), "websocket") {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@ -711,7 +791,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).
|
||||||
@ -735,6 +815,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.
|
||||||
@ -748,6 +833,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,
|
||||||
})
|
})
|
||||||
@ -801,11 +887,11 @@ 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.
|
||||||
// It add padding to response body to request data from a server residing in a different domain than the client.
|
// It adds padding to response body to request data from a server residing in a different domain than the client.
|
||||||
// It also sets the Content-Type as "application/javascript".
|
// It also sets the Content-Type as "application/javascript".
|
||||||
func (c *Context) JSONP(code int, obj interface{}) {
|
func (c *Context) JSONP(code int, obj interface{}) {
|
||||||
callback := c.DefaultQuery("callback", "")
|
callback := c.DefaultQuery("callback", "")
|
||||||
@ -828,6 +914,12 @@ func (c *Context) AsciiJSON(code int, obj interface{}) {
|
|||||||
c.Render(code, render.AsciiJSON{Data: obj})
|
c.Render(code, render.AsciiJSON{Data: obj})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PureJSON serializes the given struct as JSON into the response body.
|
||||||
|
// PureJSON, unlike JSON, does not replace special html characters with their unicode entities.
|
||||||
|
func (c *Context) PureJSON(code int, obj interface{}) {
|
||||||
|
c.Render(code, render.PureJSON{Data: obj})
|
||||||
|
}
|
||||||
|
|
||||||
// XML serializes the given struct as XML into the response body.
|
// XML serializes the given struct as XML into the response body.
|
||||||
// It also sets the Content-Type as "application/xml".
|
// It also sets the Content-Type as "application/xml".
|
||||||
func (c *Context) XML(code int, obj interface{}) {
|
func (c *Context) XML(code int, obj interface{}) {
|
||||||
@ -876,15 +968,26 @@ func (c *Context) DataFromReader(code int, contentLength int64, contentType stri
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// File writes the specified file into the body stream in a efficient way.
|
// File writes the specified file into the body stream in an efficient way.
|
||||||
func (c *Context) File(filepath string) {
|
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.FileSystem 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) {
|
||||||
c.Writer.Header().Set("content-disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
|
c.Writer.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
|
||||||
http.ServeFile(c.Writer, c.Request, filepath)
|
http.ServeFile(c.Writer, c.Request, filepath)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -926,6 +1029,7 @@ type Negotiate struct {
|
|||||||
HTMLData interface{}
|
HTMLData interface{}
|
||||||
JSONData interface{}
|
JSONData interface{}
|
||||||
XMLData interface{}
|
XMLData interface{}
|
||||||
|
YAMLData interface{}
|
||||||
Data interface{}
|
Data interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -944,6 +1048,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
|
||||||
}
|
}
|
||||||
@ -960,20 +1068,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -989,26 +1097,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
|
||||||
}
|
}
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
// Copyright 2018 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 go1.7
|
|
||||||
|
|
||||||
package gin
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/gin-gonic/gin/render"
|
|
||||||
)
|
|
||||||
|
|
||||||
// PureJSON serializes the given struct as JSON into the response body.
|
|
||||||
// PureJSON, unlike JSON, does not replace special html characters with their unicode entities.
|
|
||||||
func (c *Context) PureJSON(code int, obj interface{}) {
|
|
||||||
c.Render(code, render.PureJSON{Data: obj})
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
// Copyright 2018 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 go1.7
|
|
||||||
|
|
||||||
package gin
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Tests that the response is serialized as JSON
|
|
||||||
// and Content-Type is set to application/json
|
|
||||||
// and special HTML characters are preserved
|
|
||||||
func TestContextRenderPureJSON(t *testing.T) {
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
c, _ := CreateTestContext(w)
|
|
||||||
c.PureJSON(http.StatusCreated, H{"foo": "bar", "html": "<b>"})
|
|
||||||
assert.Equal(t, http.StatusCreated, w.Code)
|
|
||||||
assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"<b>\"}\n", w.Body.String())
|
|
||||||
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
|
|
||||||
}
|
|
176
context_test.go
176
context_test.go
@ -6,6 +6,7 @@ package gin
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
@ -13,8 +14,10 @@ import (
|
|||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -22,7 +25,6 @@ import (
|
|||||||
"github.com/gin-gonic/gin/binding"
|
"github.com/gin-gonic/gin/binding"
|
||||||
"github.com/golang/protobuf/proto"
|
"github.com/golang/protobuf/proto"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"golang.org/x/net/context"
|
|
||||||
|
|
||||||
testdata "github.com/gin-gonic/gin/testdata/protoexample"
|
testdata "github.com/gin-gonic/gin/testdata/protoexample"
|
||||||
)
|
)
|
||||||
@ -259,6 +261,18 @@ func TestContextGetInt64(t *testing.T) {
|
|||||||
assert.Equal(t, int64(42424242424242), c.GetInt64("int64"))
|
assert.Equal(t, int64(42424242424242), c.GetInt64("int64"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestContextGetUint(t *testing.T) {
|
||||||
|
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||||
|
c.Set("uint", uint(1))
|
||||||
|
assert.Equal(t, uint(1), c.GetUint("uint"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContextGetUint64(t *testing.T) {
|
||||||
|
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||||
|
c.Set("uint64", uint64(18446744073709551615))
|
||||||
|
assert.Equal(t, uint64(18446744073709551615), c.GetUint64("uint64"))
|
||||||
|
}
|
||||||
|
|
||||||
func TestContextGetFloat64(t *testing.T) {
|
func TestContextGetFloat64(t *testing.T) {
|
||||||
c, _ := CreateTestContext(httptest.NewRecorder())
|
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||||
c.Set("float64", 4.2)
|
c.Set("float64", 4.2)
|
||||||
@ -408,6 +422,21 @@ func TestContextQuery(t *testing.T) {
|
|||||||
assert.Empty(t, c.PostForm("foo"))
|
assert.Empty(t, c.PostForm("foo"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestContextDefaultQueryOnEmptyRequest(t *testing.T) {
|
||||||
|
c, _ := CreateTestContext(httptest.NewRecorder()) // here c.Request == nil
|
||||||
|
assert.NotPanics(t, func() {
|
||||||
|
value, ok := c.GetQuery("NoKey")
|
||||||
|
assert.False(t, ok)
|
||||||
|
assert.Empty(t, value)
|
||||||
|
})
|
||||||
|
assert.NotPanics(t, func() {
|
||||||
|
assert.Equal(t, "nada", c.DefaultQuery("NoKey", "nada"))
|
||||||
|
})
|
||||||
|
assert.NotPanics(t, func() {
|
||||||
|
assert.Empty(t, c.Query("NoKey"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestContextQueryAndPostForm(t *testing.T) {
|
func TestContextQueryAndPostForm(t *testing.T) {
|
||||||
c, _ := CreateTestContext(httptest.NewRecorder())
|
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||||
body := bytes.NewBufferString("foo=bar&page=11&both=&foo=second")
|
body := bytes.NewBufferString("foo=bar&page=11&both=&foo=second")
|
||||||
@ -600,14 +629,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) {
|
||||||
@ -622,8 +653,7 @@ func TestContextGetCookie(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestContextBodyAllowedForStatus(t *testing.T) {
|
func TestContextBodyAllowedForStatus(t *testing.T) {
|
||||||
// todo(thinkerou): go1.6 not support StatusProcessing
|
assert.False(t, false, bodyAllowedForStatus(http.StatusProcessing))
|
||||||
assert.False(t, false, bodyAllowedForStatus(102))
|
|
||||||
assert.False(t, false, bodyAllowedForStatus(http.StatusNoContent))
|
assert.False(t, false, bodyAllowedForStatus(http.StatusNoContent))
|
||||||
assert.False(t, false, bodyAllowedForStatus(http.StatusNotModified))
|
assert.False(t, false, bodyAllowedForStatus(http.StatusNotModified))
|
||||||
assert.True(t, true, bodyAllowedForStatus(http.StatusInternalServerError))
|
assert.True(t, true, bodyAllowedForStatus(http.StatusInternalServerError))
|
||||||
@ -675,7 +705,7 @@ func TestContextRenderJSONP(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, "x({\"foo\":\"bar\"})", w.Body.String())
|
assert.Equal(t, "x({\"foo\":\"bar\"});", w.Body.String())
|
||||||
assert.Equal(t, "application/javascript; charset=utf-8", w.Header().Get("Content-Type"))
|
assert.Equal(t, "application/javascript; charset=utf-8", w.Header().Get("Content-Type"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -794,6 +824,18 @@ func TestContextRenderNoContentAsciiJSON(t *testing.T) {
|
|||||||
assert.Equal(t, "application/json", w.Header().Get("Content-Type"))
|
assert.Equal(t, "application/json", w.Header().Get("Content-Type"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tests that the response is serialized as JSON
|
||||||
|
// and Content-Type is set to application/json
|
||||||
|
// and special HTML characters are preserved
|
||||||
|
func TestContextRenderPureJSON(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
c, _ := CreateTestContext(w)
|
||||||
|
c.PureJSON(http.StatusCreated, H{"foo": "bar", "html": "<b>"})
|
||||||
|
assert.Equal(t, http.StatusCreated, w.Code)
|
||||||
|
assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"<b>\"}\n", w.Body.String())
|
||||||
|
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
|
||||||
|
}
|
||||||
|
|
||||||
// Tests that the response executes the templates
|
// Tests that the response executes the templates
|
||||||
// and responds with Content-Type set to text/html
|
// and responds with Content-Type set to text/html
|
||||||
func TestContextRenderHTML(t *testing.T) {
|
func TestContextRenderHTML(t *testing.T) {
|
||||||
@ -925,7 +967,7 @@ func TestContextRenderNoContentHTMLString(t *testing.T) {
|
|||||||
assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type"))
|
assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestContextData tests that the response can be written from `bytesting`
|
// TestContextData tests that the response can be written from `bytestring`
|
||||||
// with specified MIME type
|
// with specified MIME type
|
||||||
func TestContextRenderData(t *testing.T) {
|
func TestContextRenderData(t *testing.T) {
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
@ -979,6 +1021,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)
|
||||||
@ -1092,9 +1147,7 @@ func TestContextRenderRedirectAll(t *testing.T) {
|
|||||||
assert.Panics(t, func() { c.Redirect(299, "/resource") })
|
assert.Panics(t, func() { c.Redirect(299, "/resource") })
|
||||||
assert.Panics(t, func() { c.Redirect(309, "/resource") })
|
assert.Panics(t, func() { c.Redirect(309, "/resource") })
|
||||||
assert.NotPanics(t, func() { c.Redirect(http.StatusMultipleChoices, "/resource") })
|
assert.NotPanics(t, func() { c.Redirect(http.StatusMultipleChoices, "/resource") })
|
||||||
// todo(thinkerou): go1.6 not support StatusPermanentRedirect(308)
|
assert.NotPanics(t, func() { c.Redirect(http.StatusPermanentRedirect, "/resource") })
|
||||||
// when we upgrade go version we can use http.StatusPermanentRedirect
|
|
||||||
assert.NotPanics(t, func() { c.Redirect(308, "/resource") })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestContextNegotiationWithJSON(t *testing.T) {
|
func TestContextNegotiationWithJSON(t *testing.T) {
|
||||||
@ -1103,7 +1156,7 @@ 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"},
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -1118,7 +1171,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"},
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -1229,7 +1282,7 @@ func TestContextIsAborted(t *testing.T) {
|
|||||||
assert.True(t, c.IsAborted())
|
assert.True(t, c.IsAborted())
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestContextData tests that the response can be written from `bytesting`
|
// TestContextData tests that the response can be written from `bytestring`
|
||||||
// with specified MIME type
|
// with specified MIME type
|
||||||
func TestContextAbortWithStatus(t *testing.T) {
|
func TestContextAbortWithStatus(t *testing.T) {
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
@ -1272,7 +1325,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"}`), jsonStringBody)
|
assert.Equal(t, fmt.Sprint("{\"foo\":\"fooValue\",\"bar\":\"barValue\"}"), jsonStringBody)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestContextError(t *testing.T) {
|
func TestContextError(t *testing.T) {
|
||||||
@ -1425,6 +1478,28 @@ func TestContextBindWithXML(t *testing.T) {
|
|||||||
assert.Equal(t, 0, w.Body.Len())
|
assert.Equal(t, 0, w.Body.Len())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestContextBindHeader(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
c, _ := CreateTestContext(w)
|
||||||
|
|
||||||
|
c.Request, _ = http.NewRequest("POST", "/", nil)
|
||||||
|
c.Request.Header.Add("rate", "8000")
|
||||||
|
c.Request.Header.Add("domain", "music")
|
||||||
|
c.Request.Header.Add("limit", "1000")
|
||||||
|
|
||||||
|
var testHeader struct {
|
||||||
|
Rate int `header:"Rate"`
|
||||||
|
Domain string `header:"Domain"`
|
||||||
|
Limit int `header:"limit"`
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.NoError(t, c.BindHeader(&testHeader))
|
||||||
|
assert.Equal(t, 8000, testHeader.Rate)
|
||||||
|
assert.Equal(t, "music", testHeader.Domain)
|
||||||
|
assert.Equal(t, 1000, testHeader.Limit)
|
||||||
|
assert.Equal(t, 0, w.Body.Len())
|
||||||
|
}
|
||||||
|
|
||||||
func TestContextBindWithQuery(t *testing.T) {
|
func TestContextBindWithQuery(t *testing.T) {
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
c, _ := CreateTestContext(w)
|
c, _ := CreateTestContext(w)
|
||||||
@ -1532,6 +1607,28 @@ func TestContextShouldBindWithXML(t *testing.T) {
|
|||||||
assert.Equal(t, 0, w.Body.Len())
|
assert.Equal(t, 0, w.Body.Len())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestContextShouldBindHeader(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
c, _ := CreateTestContext(w)
|
||||||
|
|
||||||
|
c.Request, _ = http.NewRequest("POST", "/", nil)
|
||||||
|
c.Request.Header.Add("rate", "8000")
|
||||||
|
c.Request.Header.Add("domain", "music")
|
||||||
|
c.Request.Header.Add("limit", "1000")
|
||||||
|
|
||||||
|
var testHeader struct {
|
||||||
|
Rate int `header:"Rate"`
|
||||||
|
Domain string `header:"Domain"`
|
||||||
|
Limit int `header:"limit"`
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.NoError(t, c.ShouldBindHeader(&testHeader))
|
||||||
|
assert.Equal(t, 8000, testHeader.Rate)
|
||||||
|
assert.Equal(t, "music", testHeader.Domain)
|
||||||
|
assert.Equal(t, 1000, testHeader.Limit)
|
||||||
|
assert.Equal(t, 0, w.Body.Len())
|
||||||
|
}
|
||||||
|
|
||||||
func TestContextShouldBindWithQuery(t *testing.T) {
|
func TestContextShouldBindWithQuery(t *testing.T) {
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
c, _ := CreateTestContext(w)
|
c, _ := CreateTestContext(w)
|
||||||
@ -1744,6 +1841,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
|
||||||
@ -1812,3 +1926,37 @@ func TestContextResetInHandler(t *testing.T) {
|
|||||||
c.Next()
|
c.Next()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRaceParamsContextCopy(t *testing.T) {
|
||||||
|
DefaultWriter = os.Stdout
|
||||||
|
router := Default()
|
||||||
|
nameGroup := router.Group("/:name")
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(2)
|
||||||
|
{
|
||||||
|
nameGroup.GET("/api", func(c *Context) {
|
||||||
|
go func(c *Context, param string) {
|
||||||
|
defer wg.Done()
|
||||||
|
// First assert must be executed after the second request
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
assert.Equal(t, c.Param("name"), param)
|
||||||
|
}(c.Copy(), c.Param("name"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
performRequest(router, "GET", "/name1/api")
|
||||||
|
performRequest(router, "GET", "/name2/api")
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
14
debug.go
14
debug.go
@ -5,16 +5,14 @@
|
|||||||
package gin
|
package gin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"os"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const ginSupportMinGoVer = 6
|
const ginSupportMinGoVer = 12
|
||||||
|
|
||||||
// IsDebugging returns true if the framework is running in debug mode.
|
// IsDebugging returns true if the framework is running in debug mode.
|
||||||
// Use SetMode(gin.ReleaseMode) to disable debug mode.
|
// Use SetMode(gin.ReleaseMode) to disable debug mode.
|
||||||
@ -39,7 +37,7 @@ func debugPrintRoute(httpMethod, absolutePath string, handlers HandlersChain) {
|
|||||||
|
|
||||||
func debugPrintLoadTemplate(tmpl *template.Template) {
|
func debugPrintLoadTemplate(tmpl *template.Template) {
|
||||||
if IsDebugging() {
|
if IsDebugging() {
|
||||||
var buf bytes.Buffer
|
var buf strings.Builder
|
||||||
for _, tmpl := range tmpl.Templates() {
|
for _, tmpl := range tmpl.Templates() {
|
||||||
buf.WriteString("\t- ")
|
buf.WriteString("\t- ")
|
||||||
buf.WriteString(tmpl.Name())
|
buf.WriteString(tmpl.Name())
|
||||||
@ -54,7 +52,7 @@ func debugPrint(format string, values ...interface{}) {
|
|||||||
if !strings.HasSuffix(format, "\n") {
|
if !strings.HasSuffix(format, "\n") {
|
||||||
format += "\n"
|
format += "\n"
|
||||||
}
|
}
|
||||||
fmt.Fprintf(os.Stderr, "[GIN-debug] "+format, values...)
|
fmt.Fprintf(DefaultWriter, "[GIN-debug] "+format, values...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,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.6 or later and Go 1.7 will be required soon.
|
debugPrint(`[WARNING] Now Gin requires Go 1.12+.
|
||||||
|
|
||||||
`)
|
`)
|
||||||
}
|
}
|
||||||
@ -98,6 +96,8 @@ at initialization. ie. before any route is registered or the router is listening
|
|||||||
|
|
||||||
func debugPrintError(err error) {
|
func debugPrintError(err error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
debugPrint("[ERROR] %v\n", err)
|
if IsDebugging() {
|
||||||
|
fmt.Fprintf(DefaultErrorWriter, "[GIN-debug] [ERROR] %v\n", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ package gin
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
@ -64,6 +65,18 @@ func TestDebugPrintRoutes(t *testing.T) {
|
|||||||
assert.Regexp(t, `^\[GIN-debug\] GET /path/to/route/:param --> (.*/vendor/)?github.com/gin-gonic/gin.handlerNameTest \(2 handlers\)\n$`, re)
|
assert.Regexp(t, `^\[GIN-debug\] GET /path/to/route/:param --> (.*/vendor/)?github.com/gin-gonic/gin.handlerNameTest \(2 handlers\)\n$`, re)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDebugPrintRouteFunc(t *testing.T) {
|
||||||
|
DebugPrintRouteFunc = func(httpMethod, absolutePath, handlerName string, nuHandlers int) {
|
||||||
|
fmt.Fprintf(DefaultWriter, "[GIN-debug] %-6s %-40s --> %s (%d handlers)\n", httpMethod, absolutePath, handlerName, nuHandlers)
|
||||||
|
}
|
||||||
|
re := captureOutput(t, func() {
|
||||||
|
SetMode(DebugMode)
|
||||||
|
debugPrintRoute("GET", "/path/to/route/:param1/:param2", HandlersChain{func(c *Context) {}, handlerNameTest})
|
||||||
|
SetMode(TestMode)
|
||||||
|
})
|
||||||
|
assert.Regexp(t, `^\[GIN-debug\] GET /path/to/route/:param1/:param2 --> (.*/vendor/)?github.com/gin-gonic/gin.handlerNameTest \(2 handlers\)\n$`, re)
|
||||||
|
}
|
||||||
|
|
||||||
func TestDebugPrintLoadTemplate(t *testing.T) {
|
func TestDebugPrintLoadTemplate(t *testing.T) {
|
||||||
re := captureOutput(t, func() {
|
re := captureOutput(t, func() {
|
||||||
SetMode(DebugMode)
|
SetMode(DebugMode)
|
||||||
@ -91,7 +104,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.6 or later and Go 1.7 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.12+.\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)
|
||||||
}
|
}
|
||||||
@ -111,15 +124,15 @@ func captureOutput(t *testing.T, f func()) string {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
stdout := os.Stdout
|
defaultWriter := DefaultWriter
|
||||||
stderr := os.Stderr
|
defaultErrorWriter := DefaultErrorWriter
|
||||||
defer func() {
|
defer func() {
|
||||||
os.Stdout = stdout
|
DefaultWriter = defaultWriter
|
||||||
os.Stderr = stderr
|
DefaultErrorWriter = defaultErrorWriter
|
||||||
log.SetOutput(os.Stderr)
|
log.SetOutput(os.Stderr)
|
||||||
}()
|
}()
|
||||||
os.Stdout = writer
|
DefaultWriter = writer
|
||||||
os.Stderr = writer
|
DefaultErrorWriter = writer
|
||||||
log.SetOutput(writer)
|
log.SetOutput(writer)
|
||||||
out := make(chan string)
|
out := make(chan string)
|
||||||
wg := new(sync.WaitGroup)
|
wg := new(sync.WaitGroup)
|
||||||
|
29
errors.go
29
errors.go
@ -5,9 +5,9 @@
|
|||||||
package gin
|
package gin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin/internal/json"
|
"github.com/gin-gonic/gin/internal/json"
|
||||||
)
|
)
|
||||||
@ -55,7 +55,7 @@ func (msg *Error) SetMeta(data interface{}) *Error {
|
|||||||
|
|
||||||
// JSON creates a properly formatted JSON
|
// JSON creates a properly formatted JSON
|
||||||
func (msg *Error) JSON() interface{} {
|
func (msg *Error) JSON() interface{} {
|
||||||
json := H{}
|
jsonData := H{}
|
||||||
if msg.Meta != nil {
|
if msg.Meta != nil {
|
||||||
value := reflect.ValueOf(msg.Meta)
|
value := reflect.ValueOf(msg.Meta)
|
||||||
switch value.Kind() {
|
switch value.Kind() {
|
||||||
@ -63,16 +63,16 @@ func (msg *Error) JSON() interface{} {
|
|||||||
return msg.Meta
|
return msg.Meta
|
||||||
case reflect.Map:
|
case reflect.Map:
|
||||||
for _, key := range value.MapKeys() {
|
for _, key := range value.MapKeys() {
|
||||||
json[key.String()] = value.MapIndex(key).Interface()
|
jsonData[key.String()] = value.MapIndex(key).Interface()
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
json["meta"] = msg.Meta
|
jsonData["meta"] = msg.Meta
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if _, ok := json["error"]; !ok {
|
if _, ok := jsonData["error"]; !ok {
|
||||||
json["error"] = msg.Error()
|
jsonData["error"] = msg.Error()
|
||||||
}
|
}
|
||||||
return json
|
return jsonData
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarshalJSON implements the json.Marshaller interface.
|
// MarshalJSON implements the json.Marshaller interface.
|
||||||
@ -90,6 +90,11 @@ func (msg *Error) IsType(flags ErrorType) bool {
|
|||||||
return (msg.Type & flags) > 0
|
return (msg.Type & flags) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unwrap returns the wrapped error, to allow interoperability with errors.Is(), errors.As() and errors.Unwrap()
|
||||||
|
func (msg *Error) Unwrap() error {
|
||||||
|
return msg.Err
|
||||||
|
}
|
||||||
|
|
||||||
// ByType returns a readonly copy filtered the byte.
|
// ByType returns a readonly copy filtered the byte.
|
||||||
// ie ByType(gin.ErrorTypePublic) returns a slice of errors with type=ErrorTypePublic.
|
// ie ByType(gin.ErrorTypePublic) returns a slice of errors with type=ErrorTypePublic.
|
||||||
func (a errorMsgs) ByType(typ ErrorType) errorMsgs {
|
func (a errorMsgs) ByType(typ ErrorType) errorMsgs {
|
||||||
@ -135,17 +140,17 @@ func (a errorMsgs) Errors() []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a errorMsgs) JSON() interface{} {
|
func (a errorMsgs) JSON() interface{} {
|
||||||
switch len(a) {
|
switch length := len(a); length {
|
||||||
case 0:
|
case 0:
|
||||||
return nil
|
return nil
|
||||||
case 1:
|
case 1:
|
||||||
return a.Last().JSON()
|
return a.Last().JSON()
|
||||||
default:
|
default:
|
||||||
json := make([]interface{}, len(a))
|
jsonData := make([]interface{}, length)
|
||||||
for i, err := range a {
|
for i, err := range a {
|
||||||
json[i] = err.JSON()
|
jsonData[i] = err.JSON()
|
||||||
}
|
}
|
||||||
return json
|
return jsonData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,7 +163,7 @@ func (a errorMsgs) String() string {
|
|||||||
if len(a) == 0 {
|
if len(a) == 0 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
var buffer bytes.Buffer
|
var buffer strings.Builder
|
||||||
for i, msg := range a {
|
for i, msg := range a {
|
||||||
fmt.Fprintf(&buffer, "Error #%02d: %s\n", i+1, msg.Err)
|
fmt.Fprintf(&buffer, "Error #%02d: %s\n", i+1, msg.Err)
|
||||||
if msg.Meta != nil {
|
if msg.Meta != nil {
|
||||||
|
33
errors_1.13_test.go
Normal file
33
errors_1.13_test.go
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
// +build go1.13
|
||||||
|
|
||||||
|
package gin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TestErr string
|
||||||
|
|
||||||
|
func (e TestErr) Error() string { return string(e) }
|
||||||
|
|
||||||
|
// TestErrorUnwrap tests the behavior of gin.Error with "errors.Is()" and "errors.As()".
|
||||||
|
// "errors.Is()" and "errors.As()" have been added to the standard library in go 1.13,
|
||||||
|
// hence the "// +build go1.13" directive at the beginning of this file.
|
||||||
|
func TestErrorUnwrap(t *testing.T) {
|
||||||
|
innerErr := TestErr("somme error")
|
||||||
|
|
||||||
|
// 2 layers of wrapping : use 'fmt.Errorf("%w")' to wrap a gin.Error{}, which itself wraps innerErr
|
||||||
|
err := fmt.Errorf("wrapped: %w", &Error{
|
||||||
|
Err: innerErr,
|
||||||
|
Type: ErrorTypeAny,
|
||||||
|
})
|
||||||
|
|
||||||
|
// check that 'errors.Is()' and 'errors.As()' behave as expected :
|
||||||
|
assert.True(t, errors.Is(err, innerErr))
|
||||||
|
var testErr TestErr
|
||||||
|
assert.True(t, errors.As(err, &testErr))
|
||||||
|
}
|
6
fs.go
6
fs.go
@ -9,7 +9,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
type onlyfilesFS struct {
|
type onlyFilesFS struct {
|
||||||
fs http.FileSystem
|
fs http.FileSystem
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -26,11 +26,11 @@ func Dir(root string, listDirectory bool) http.FileSystem {
|
|||||||
if listDirectory {
|
if listDirectory {
|
||||||
return fs
|
return fs
|
||||||
}
|
}
|
||||||
return &onlyfilesFS{fs}
|
return &onlyFilesFS{fs}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open conforms to http.Filesystem.
|
// Open conforms to http.Filesystem.
|
||||||
func (fs onlyfilesFS) Open(name string) (http.File, error) {
|
func (fs onlyFilesFS) Open(name string) (http.File, error) {
|
||||||
f, err := fs.fs.Open(name)
|
f, err := fs.fs.Open(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
100
gin.go
100
gin.go
@ -13,24 +13,26 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin/internal/bytesconv"
|
||||||
"github.com/gin-gonic/gin/render"
|
"github.com/gin-gonic/gin/render"
|
||||||
)
|
)
|
||||||
|
|
||||||
const defaultMultipartMemory = 32 << 20 // 32 MB
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -295,12 +304,19 @@ 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)
|
||||||
|
root.fullPath = "/"
|
||||||
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:
|
||||||
@ -359,13 +375,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
|
||||||
}
|
}
|
||||||
@ -383,6 +399,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
|
||||||
}
|
}
|
||||||
@ -419,6 +444,10 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
|
|||||||
unescape = engine.UnescapePathValues
|
unescape = engine.UnescapePathValues
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if engine.RemoveExtraSlash {
|
||||||
|
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
|
||||||
for i, tl := 0, len(t); i < tl; i++ {
|
for i, tl := 0, len(t); i < tl; i++ {
|
||||||
@ -427,16 +456,19 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
|
|||||||
}
|
}
|
||||||
root := t[i].root
|
root := t[i].root
|
||||||
// Find route in tree
|
// Find route in tree
|
||||||
handlers, params, tsr := root.getValue(rPath, c.Params, unescape)
|
value := root.getValue(rPath, c.params, unescape)
|
||||||
if handlers != nil {
|
if value.params != nil {
|
||||||
c.handlers = handlers
|
c.Params = *value.params
|
||||||
c.Params = params
|
}
|
||||||
|
if value.handlers != nil {
|
||||||
|
c.handlers = value.handlers
|
||||||
|
c.fullPath = value.fullPath
|
||||||
c.Next()
|
c.Next()
|
||||||
c.writermem.WriteHeaderNow()
|
c.writermem.WriteHeaderNow()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if httpMethod != "CONNECT" && rPath != "/" {
|
if httpMethod != "CONNECT" && rPath != "/" {
|
||||||
if tsr && engine.RedirectTrailingSlash {
|
if value.tsr && engine.RedirectTrailingSlash {
|
||||||
redirectTrailingSlash(c)
|
redirectTrailingSlash(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -452,7 +484,7 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
|
|||||||
if tree.method == httpMethod {
|
if tree.method == httpMethod {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if handlers, _, _ := tree.root.getValue(rPath, nil, unescape); handlers != nil {
|
if value := tree.root.getValue(rPath, nil, unescape); value.handlers != nil {
|
||||||
c.handlers = engine.allNoMethod
|
c.handlers = engine.allNoMethod
|
||||||
serveError(c, http.StatusMethodNotAllowed, default405Body)
|
serveError(c, http.StatusMethodNotAllowed, default405Body)
|
||||||
return
|
return
|
||||||
@ -480,7 +512,6 @@ func serveError(c *Context, code int, defaultMessage []byte) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.writermem.WriteHeaderNow()
|
c.writermem.WriteHeaderNow()
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func redirectTrailingSlash(c *Context) {
|
func redirectTrailingSlash(c *Context) {
|
||||||
@ -489,18 +520,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 {
|
||||||
@ -508,15 +532,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()
|
||||||
|
}
|
||||||
|
@ -8,11 +8,13 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"html/template"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@ -69,6 +71,43 @@ func TestRunTLS(t *testing.T) {
|
|||||||
testRequest(t, "https://localhost:8443/example")
|
testRequest(t, "https://localhost:8443/example")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPusher(t *testing.T) {
|
||||||
|
var html = template.Must(template.New("https").Parse(`
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Https Test</title>
|
||||||
|
<script src="/assets/app.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1 style="color:red;">Welcome, Ginner!</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`))
|
||||||
|
|
||||||
|
router := New()
|
||||||
|
router.Static("./assets", "./assets")
|
||||||
|
router.SetHTMLTemplate(html)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
router.GET("/pusher", func(c *Context) {
|
||||||
|
if pusher := c.Writer.Pusher(); pusher != nil {
|
||||||
|
err := pusher.Push("/assets/app.js", nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
c.String(http.StatusOK, "it worked")
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.NoError(t, router.RunTLS(":8449", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem"))
|
||||||
|
}()
|
||||||
|
|
||||||
|
// have to wait for the goroutine to start and run the server
|
||||||
|
// otherwise the main thread will complete
|
||||||
|
time.Sleep(5 * time.Millisecond)
|
||||||
|
|
||||||
|
assert.Error(t, router.RunTLS(":8449", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem"))
|
||||||
|
testRequest(t, "https://localhost:8449/pusher")
|
||||||
|
}
|
||||||
|
|
||||||
func TestRunEmptyWithEnv(t *testing.T) {
|
func TestRunEmptyWithEnv(t *testing.T) {
|
||||||
os.Setenv("PORT", "3123")
|
os.Setenv("PORT", "3123")
|
||||||
router := New()
|
router := New()
|
||||||
@ -108,15 +147,19 @@ func TestRunWithPort(t *testing.T) {
|
|||||||
func TestUnixSocket(t *testing.T) {
|
func TestUnixSocket(t *testing.T) {
|
||||||
router := New()
|
router := New()
|
||||||
|
|
||||||
|
unixTestSocket := filepath.Join(os.TempDir(), "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")
|
||||||
@ -170,6 +213,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") })
|
||||||
@ -216,7 +296,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()
|
||||||
|
@ -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.
|
||||||
|
22
go.mod
22
go.mod
@ -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.0.0-20190301062529-5545eab6dad3
|
github.com/gin-contrib/sse v0.1.0
|
||||||
github.com/golang/protobuf v1.3.0
|
github.com/go-playground/validator/v10 v10.4.1
|
||||||
github.com/json-iterator/go v1.1.5
|
github.com/golang/protobuf v1.3.3
|
||||||
github.com/mattn/go-isatty v0.0.6
|
github.com/json-iterator/go v1.1.9
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/mattn/go-isatty v0.0.12
|
||||||
github.com/modern-go/reflect2 v1.0.1 // indirect
|
github.com/stretchr/testify v1.4.0
|
||||||
github.com/stretchr/testify v1.3.0
|
github.com/ugorji/go/codec v1.1.7
|
||||||
github.com/ugorji/go/codec v0.0.0-20190204201341-e444a5086c43
|
gopkg.in/yaml.v2 v2.2.8
|
||||||
golang.org/x/net v0.0.0-20190301231341-16b79f2e4e95
|
|
||||||
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
|
|
||||||
gopkg.in/go-playground/validator.v8 v8.18.2
|
|
||||||
gopkg.in/yaml.v2 v2.2.2
|
|
||||||
)
|
)
|
||||||
|
75
go.sum
75
go.sum
@ -1,39 +1,52 @@
|
|||||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
|
||||||
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/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3 h1:t8FVkw33L+wilf2QiWkw0UV77qRpcH/JHPKGpKa2E8g=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/golang/protobuf v1.3.0 h1:kbxbvI4Un1LUWKxufD+BiE6AEExYYgkQLQmLFqA1LFk=
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0=
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
github.com/json-iterator/go v1.1.5 h1:gL2yXlmiIo4+t+y32d4WGwOjKGYcGOuyrg46vadswDE=
|
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
||||||
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
github.com/mattn/go-isatty v0.0.6 h1:SrwhHcpV4nWrMGdNcC2kXpMfcBVYGDuTArqyhocJgvA=
|
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
||||||
github.com/mattn/go-isatty v0.0.6/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||||
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
|
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
|
||||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
|
||||||
|
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/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
|
||||||
|
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
|
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||||
|
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||||
|
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||||
|
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/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/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/ugorji/go v1.1.2 h1:JON3E2/GPW2iDNGoSAusl1KDf5TRQ8k8q7Tp097pZGs=
|
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||||
github.com/ugorji/go v1.1.2/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/ugorji/go/codec v0.0.0-20190204201341-e444a5086c43 h1:BasDe+IErOQKrMVXab7UayvSlIpiyGwRvuX3EKYY7UA=
|
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
||||||
github.com/ugorji/go/codec v0.0.0-20190204201341-e444a5086c43/go.mod h1:iT03XoTwV7xq/+UGwKO3UbC1nNNlopQiY61beSdrtOA=
|
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
||||||
golang.org/x/net v0.0.0-20190301231341-16b79f2e4e95 h1:fY7Dsw114eJN4boqzVSbpVHO6rTdhq6/GnXeu+PKnzU=
|
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||||
golang.org/x/net v0.0.0-20190301231341-16b79f2e4e95/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
|
||||||
|
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
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.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ=
|
|
||||||
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
|
|
||||||
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=
|
||||||
|
24
internal/bytesconv/bytesconv.go
Normal file
24
internal/bytesconv/bytesconv.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
package bytesconv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StringToBytes converts string to byte slice without a memory allocation.
|
||||||
|
func StringToBytes(s string) (b []byte) {
|
||||||
|
return *(*[]byte)(unsafe.Pointer(
|
||||||
|
&struct {
|
||||||
|
string
|
||||||
|
Cap int
|
||||||
|
}{s, len(s)},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
// BytesToString converts byte slice to string without a memory allocation.
|
||||||
|
func BytesToString(b []byte) string {
|
||||||
|
return *(*string)(unsafe.Pointer(&b))
|
||||||
|
}
|
99
internal/bytesconv/bytesconv_test.go
Normal file
99
internal/bytesconv/bytesconv_test.go
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
37
logger.go
37
logger.go
@ -22,18 +22,19 @@ const (
|
|||||||
forceColor
|
forceColor
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
const (
|
||||||
green = string([]byte{27, 91, 57, 55, 59, 52, 50, 109})
|
green = "\033[97;42m"
|
||||||
white = string([]byte{27, 91, 57, 48, 59, 52, 55, 109})
|
white = "\033[90;47m"
|
||||||
yellow = string([]byte{27, 91, 57, 48, 59, 52, 51, 109})
|
yellow = "\033[90;43m"
|
||||||
red = string([]byte{27, 91, 57, 55, 59, 52, 49, 109})
|
red = "\033[97;41m"
|
||||||
blue = string([]byte{27, 91, 57, 55, 59, 52, 52, 109})
|
blue = "\033[97;44m"
|
||||||
magenta = string([]byte{27, 91, 57, 55, 59, 52, 53, 109})
|
magenta = "\033[97;45m"
|
||||||
cyan = string([]byte{27, 91, 57, 55, 59, 52, 54, 109})
|
cyan = "\033[97;46m"
|
||||||
reset = string([]byte{27, 91, 48, 109})
|
reset = "\033[0m"
|
||||||
consoleColorMode = autoColor
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var consoleColorMode = autoColor
|
||||||
|
|
||||||
// LoggerConfig defines the config for Logger middleware.
|
// LoggerConfig defines the config for Logger middleware.
|
||||||
type LoggerConfig struct {
|
type LoggerConfig struct {
|
||||||
// Optional. Default value is gin.defaultLogFormatter
|
// Optional. Default value is gin.defaultLogFormatter
|
||||||
@ -98,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
|
||||||
@ -140,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,
|
||||||
|
@ -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))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -291,14 +291,14 @@ func TestColorForMethod(t *testing.T) {
|
|||||||
return p.MethodColor()
|
return p.MethodColor()
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 52, 109}), colorForMethod("GET"), "get should be blue")
|
assert.Equal(t, blue, colorForMethod("GET"), "get should be blue")
|
||||||
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 54, 109}), colorForMethod("POST"), "post should be cyan")
|
assert.Equal(t, cyan, colorForMethod("POST"), "post should be cyan")
|
||||||
assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 51, 109}), colorForMethod("PUT"), "put should be yellow")
|
assert.Equal(t, yellow, colorForMethod("PUT"), "put should be yellow")
|
||||||
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 49, 109}), colorForMethod("DELETE"), "delete should be red")
|
assert.Equal(t, red, colorForMethod("DELETE"), "delete should be red")
|
||||||
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 50, 109}), colorForMethod("PATCH"), "patch should be green")
|
assert.Equal(t, green, colorForMethod("PATCH"), "patch should be green")
|
||||||
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 53, 109}), colorForMethod("HEAD"), "head should be magenta")
|
assert.Equal(t, magenta, colorForMethod("HEAD"), "head should be magenta")
|
||||||
assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 55, 109}), colorForMethod("OPTIONS"), "options should be white")
|
assert.Equal(t, white, colorForMethod("OPTIONS"), "options should be white")
|
||||||
assert.Equal(t, string([]byte{27, 91, 48, 109}), colorForMethod("TRACE"), "trace is not defined and should be the reset color")
|
assert.Equal(t, reset, colorForMethod("TRACE"), "trace is not defined and should be the reset color")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestColorForStatus(t *testing.T) {
|
func TestColorForStatus(t *testing.T) {
|
||||||
@ -309,10 +309,10 @@ func TestColorForStatus(t *testing.T) {
|
|||||||
return p.StatusCodeColor()
|
return p.StatusCodeColor()
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 50, 109}), colorForStatus(http.StatusOK), "2xx should be green")
|
assert.Equal(t, green, colorForStatus(http.StatusOK), "2xx should be green")
|
||||||
assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 55, 109}), colorForStatus(http.StatusMovedPermanently), "3xx should be white")
|
assert.Equal(t, white, colorForStatus(http.StatusMovedPermanently), "3xx should be white")
|
||||||
assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 51, 109}), colorForStatus(http.StatusNotFound), "4xx should be yellow")
|
assert.Equal(t, yellow, colorForStatus(http.StatusNotFound), "4xx should be yellow")
|
||||||
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 49, 109}), colorForStatus(2), "other things should be red")
|
assert.Equal(t, red, colorForStatus(2), "other things should be red")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestResetColor(t *testing.T) {
|
func TestResetColor(t *testing.T) {
|
||||||
|
21
mode.go
21
mode.go
@ -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,19 +51,21 @@ 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
|
||||||
case TestMode:
|
case TestMode:
|
||||||
ginMode = testCode
|
ginMode = testCode
|
||||||
default:
|
default:
|
||||||
panic("gin mode unknown: " + value)
|
panic("gin mode unknown: " + value + " (available mode: debug release test)")
|
||||||
}
|
|
||||||
if value == "" {
|
|
||||||
value = DebugMode
|
|
||||||
}
|
}
|
||||||
|
|
||||||
modeName = value
|
modeName = value
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,12 +74,18 @@ func DisableBindValidation() {
|
|||||||
binding.Validator = nil
|
binding.Validator = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// EnableJsonDecoderUseNumber sets true for binding.EnableDecoderUseNumberto to
|
// EnableJsonDecoderUseNumber sets true for binding.EnableDecoderUseNumber to
|
||||||
// call the UseNumber method on the JSON Decoder instance.
|
// call the UseNumber method on the JSON Decoder instance.
|
||||||
func EnableJsonDecoderUseNumber() {
|
func EnableJsonDecoderUseNumber() {
|
||||||
binding.EnableDecoderUseNumber = true
|
binding.EnableDecoderUseNumber = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EnableJsonDecoderDisallowUnknownFields sets true for binding.EnableDecoderDisallowUnknownFields to
|
||||||
|
// call the DisallowUnknownFields method on the JSON Decoder instance.
|
||||||
|
func EnableJsonDecoderDisallowUnknownFields() {
|
||||||
|
binding.EnableDecoderDisallowUnknownFields = true
|
||||||
|
}
|
||||||
|
|
||||||
// Mode returns currently gin mode.
|
// Mode returns currently gin mode.
|
||||||
func Mode() string {
|
func Mode() string {
|
||||||
return modeName
|
return modeName
|
||||||
|
14
mode_test.go
14
mode_test.go
@ -40,8 +40,22 @@ 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()
|
||||||
assert.True(t, binding.EnableDecoderUseNumber)
|
assert.True(t, binding.EnableDecoderUseNumber)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEnableJsonDecoderDisallowUnknownFields(t *testing.T) {
|
||||||
|
assert.False(t, binding.EnableDecoderDisallowUnknownFields)
|
||||||
|
EnableJsonDecoderDisallowUnknownFields()
|
||||||
|
assert.True(t, binding.EnableDecoderDisallowUnknownFields)
|
||||||
|
}
|
||||||
|
57
path.go
57
path.go
@ -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
|
||||||
buf = make([]byte, n+1)
|
|
||||||
|
if n+1 > stackBufSize {
|
||||||
|
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]
|
||||||
|
}
|
||||||
|
b = *buf
|
||||||
|
|
||||||
|
copy(b, s[:w])
|
||||||
}
|
}
|
||||||
(*buf)[w] = c
|
b[w] = c
|
||||||
}
|
}
|
||||||
|
65
path_test.go
65
path_test.go
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
34
recovery.go
34
recovery.go
@ -26,13 +26,29 @@ var (
|
|||||||
slash = []byte("/")
|
slash = []byte("/")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// RecoveryFunc defines the function passable to CustomRecovery.
|
||||||
|
type RecoveryFunc func(c *Context, err interface{})
|
||||||
|
|
||||||
// Recovery returns a middleware that recovers from any panics and writes a 500 if there was one.
|
// Recovery returns a middleware that recovers from any panics and writes a 500 if there was one.
|
||||||
func Recovery() HandlerFunc {
|
func Recovery() HandlerFunc {
|
||||||
return RecoveryWithWriter(DefaultErrorWriter)
|
return RecoveryWithWriter(DefaultErrorWriter)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//CustomRecovery returns a middleware that recovers from any panics and calls the provided handle func to handle it.
|
||||||
|
func CustomRecovery(handle RecoveryFunc) HandlerFunc {
|
||||||
|
return RecoveryWithWriter(DefaultErrorWriter, handle)
|
||||||
|
}
|
||||||
|
|
||||||
// RecoveryWithWriter returns a middleware for a given writer that recovers from any panics and writes a 500 if there was one.
|
// RecoveryWithWriter returns a middleware for a given writer that recovers from any panics and writes a 500 if there was one.
|
||||||
func RecoveryWithWriter(out io.Writer) HandlerFunc {
|
func RecoveryWithWriter(out io.Writer, recovery ...RecoveryFunc) HandlerFunc {
|
||||||
|
if len(recovery) > 0 {
|
||||||
|
return CustomRecoveryWithWriter(out, recovery[0])
|
||||||
|
}
|
||||||
|
return CustomRecoveryWithWriter(out, defaultHandleRecovery)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CustomRecoveryWithWriter returns a middleware for a given writer that recovers from any panics and calls the provided handle func to handle it.
|
||||||
|
func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
|
||||||
var logger *log.Logger
|
var logger *log.Logger
|
||||||
if out != nil {
|
if out != nil {
|
||||||
logger = log.New(out, "\n\n\x1b[31m", log.LstdFlags)
|
logger = log.New(out, "\n\n\x1b[31m", log.LstdFlags)
|
||||||
@ -60,23 +76,23 @@ func RecoveryWithWriter(out io.Writer) HandlerFunc {
|
|||||||
headers[idx] = current[0] + ": *"
|
headers[idx] = current[0] + ": *"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
headersToStr := strings.Join(headers, "\r\n")
|
||||||
if brokenPipe {
|
if brokenPipe {
|
||||||
logger.Printf("%s\n%s%s", err, string(httpRequest), reset)
|
logger.Printf("%s\n%s%s", err, headersToStr, reset)
|
||||||
} else if IsDebugging() {
|
} else if IsDebugging() {
|
||||||
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s",
|
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s",
|
||||||
timeFormat(time.Now()), strings.Join(headers, "\r\n"), err, stack, reset)
|
timeFormat(time.Now()), headersToStr, err, stack, reset)
|
||||||
} else {
|
} else {
|
||||||
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s",
|
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s",
|
||||||
timeFormat(time.Now()), err, stack, reset)
|
timeFormat(time.Now()), err, stack, reset)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the connection is dead, we can't write a status to it.
|
|
||||||
if brokenPipe {
|
if brokenPipe {
|
||||||
|
// If the connection is dead, we can't write a status to it.
|
||||||
c.Error(err.(error)) // nolint: errcheck
|
c.Error(err.(error)) // nolint: errcheck
|
||||||
c.Abort()
|
c.Abort()
|
||||||
} else {
|
} else {
|
||||||
c.AbortWithStatus(http.StatusInternalServerError)
|
handle(c, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@ -84,6 +100,10 @@ func RecoveryWithWriter(out io.Writer) HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func defaultHandleRecovery(c *Context, err interface{}) {
|
||||||
|
c.AbortWithStatus(http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
// stack returns a nicely formatted stack frame, skipping skip frames.
|
// stack returns a nicely formatted stack frame, skipping skip frames.
|
||||||
func stack(skip int) []byte {
|
func stack(skip int) []byte {
|
||||||
buf := new(bytes.Buffer) // the returned data
|
buf := new(bytes.Buffer) // the returned data
|
||||||
@ -146,6 +166,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
|
||||||
}
|
}
|
||||||
|
108
recovery_test.go
108
recovery_test.go
@ -2,8 +2,6 @@
|
|||||||
// 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 go1.7
|
|
||||||
|
|
||||||
package gin
|
package gin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -64,7 +62,7 @@ func TestPanicInHandler(t *testing.T) {
|
|||||||
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||||
assert.Contains(t, buffer.String(), "panic recovered")
|
assert.Contains(t, buffer.String(), "panic recovered")
|
||||||
assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem")
|
assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem")
|
||||||
assert.Contains(t, buffer.String(), "TestPanicInHandler")
|
assert.Contains(t, buffer.String(), t.Name())
|
||||||
assert.NotContains(t, buffer.String(), "GET /recovery")
|
assert.NotContains(t, buffer.String(), "GET /recovery")
|
||||||
|
|
||||||
// Debug mode prints the request
|
// Debug mode prints the request
|
||||||
@ -146,3 +144,107 @@ func TestPanicWithBrokenPipe(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCustomRecoveryWithWriter(t *testing.T) {
|
||||||
|
errBuffer := new(bytes.Buffer)
|
||||||
|
buffer := new(bytes.Buffer)
|
||||||
|
router := New()
|
||||||
|
handleRecovery := func(c *Context, err interface{}) {
|
||||||
|
errBuffer.WriteString(err.(string))
|
||||||
|
c.AbortWithStatus(http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
router.Use(CustomRecoveryWithWriter(buffer, handleRecovery))
|
||||||
|
router.GET("/recovery", func(_ *Context) {
|
||||||
|
panic("Oupps, Houston, we have a problem")
|
||||||
|
})
|
||||||
|
// RUN
|
||||||
|
w := performRequest(router, "GET", "/recovery")
|
||||||
|
// TEST
|
||||||
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||||
|
assert.Contains(t, buffer.String(), "panic recovered")
|
||||||
|
assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem")
|
||||||
|
assert.Contains(t, buffer.String(), t.Name())
|
||||||
|
assert.NotContains(t, buffer.String(), "GET /recovery")
|
||||||
|
|
||||||
|
// Debug mode prints the request
|
||||||
|
SetMode(DebugMode)
|
||||||
|
// RUN
|
||||||
|
w = performRequest(router, "GET", "/recovery")
|
||||||
|
// TEST
|
||||||
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||||
|
assert.Contains(t, buffer.String(), "GET /recovery")
|
||||||
|
|
||||||
|
assert.Equal(t, strings.Repeat("Oupps, Houston, we have a problem", 2), errBuffer.String())
|
||||||
|
|
||||||
|
SetMode(TestMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCustomRecovery(t *testing.T) {
|
||||||
|
errBuffer := new(bytes.Buffer)
|
||||||
|
buffer := new(bytes.Buffer)
|
||||||
|
router := New()
|
||||||
|
DefaultErrorWriter = buffer
|
||||||
|
handleRecovery := func(c *Context, err interface{}) {
|
||||||
|
errBuffer.WriteString(err.(string))
|
||||||
|
c.AbortWithStatus(http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
router.Use(CustomRecovery(handleRecovery))
|
||||||
|
router.GET("/recovery", func(_ *Context) {
|
||||||
|
panic("Oupps, Houston, we have a problem")
|
||||||
|
})
|
||||||
|
// RUN
|
||||||
|
w := performRequest(router, "GET", "/recovery")
|
||||||
|
// TEST
|
||||||
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||||
|
assert.Contains(t, buffer.String(), "panic recovered")
|
||||||
|
assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem")
|
||||||
|
assert.Contains(t, buffer.String(), t.Name())
|
||||||
|
assert.NotContains(t, buffer.String(), "GET /recovery")
|
||||||
|
|
||||||
|
// Debug mode prints the request
|
||||||
|
SetMode(DebugMode)
|
||||||
|
// RUN
|
||||||
|
w = performRequest(router, "GET", "/recovery")
|
||||||
|
// TEST
|
||||||
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||||
|
assert.Contains(t, buffer.String(), "GET /recovery")
|
||||||
|
|
||||||
|
assert.Equal(t, strings.Repeat("Oupps, Houston, we have a problem", 2), errBuffer.String())
|
||||||
|
|
||||||
|
SetMode(TestMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRecoveryWithWriterWithCustomRecovery(t *testing.T) {
|
||||||
|
errBuffer := new(bytes.Buffer)
|
||||||
|
buffer := new(bytes.Buffer)
|
||||||
|
router := New()
|
||||||
|
DefaultErrorWriter = buffer
|
||||||
|
handleRecovery := func(c *Context, err interface{}) {
|
||||||
|
errBuffer.WriteString(err.(string))
|
||||||
|
c.AbortWithStatus(http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
router.Use(RecoveryWithWriter(DefaultErrorWriter, handleRecovery))
|
||||||
|
router.GET("/recovery", func(_ *Context) {
|
||||||
|
panic("Oupps, Houston, we have a problem")
|
||||||
|
})
|
||||||
|
// RUN
|
||||||
|
w := performRequest(router, "GET", "/recovery")
|
||||||
|
// TEST
|
||||||
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||||
|
assert.Contains(t, buffer.String(), "panic recovered")
|
||||||
|
assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem")
|
||||||
|
assert.Contains(t, buffer.String(), t.Name())
|
||||||
|
assert.NotContains(t, buffer.String(), "GET /recovery")
|
||||||
|
|
||||||
|
// Debug mode prints the request
|
||||||
|
SetMode(DebugMode)
|
||||||
|
// RUN
|
||||||
|
w = performRequest(router, "GET", "/recovery")
|
||||||
|
// TEST
|
||||||
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||||
|
assert.Contains(t, buffer.String(), "GET /recovery")
|
||||||
|
|
||||||
|
assert.Equal(t, strings.Repeat("Oupps, Houston, we have a problem", 2), errBuffer.String())
|
||||||
|
|
||||||
|
SetMode(TestMode)
|
||||||
|
}
|
||||||
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -40,8 +41,10 @@ type AsciiJSON struct {
|
|||||||
Data interface{}
|
Data interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SecureJSONPrefix is a string which represents SecureJSON prefix.
|
// PureJSON contains the given interface object.
|
||||||
type SecureJSONPrefix string
|
type PureJSON struct {
|
||||||
|
Data interface{}
|
||||||
|
}
|
||||||
|
|
||||||
var jsonContentType = []string{"application/json; charset=utf-8"}
|
var jsonContentType = []string{"application/json; charset=utf-8"}
|
||||||
var jsonpContentType = []string{"application/javascript; charset=utf-8"}
|
var jsonpContentType = []string{"application/javascript; charset=utf-8"}
|
||||||
@ -95,8 +98,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
|
||||||
}
|
}
|
||||||
@ -124,11 +128,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
|
||||||
}
|
}
|
||||||
@ -136,7 +140,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
|
||||||
}
|
}
|
||||||
@ -158,7 +162,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))
|
||||||
@ -174,3 +178,16 @@ func (r AsciiJSON) Render(w http.ResponseWriter) (err error) {
|
|||||||
func (r AsciiJSON) WriteContentType(w http.ResponseWriter) {
|
func (r AsciiJSON) WriteContentType(w http.ResponseWriter) {
|
||||||
writeContentType(w, jsonAsciiContentType)
|
writeContentType(w, jsonAsciiContentType)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Render (PureJSON) writes custom ContentType and encodes the given interface object.
|
||||||
|
func (r PureJSON) Render(w http.ResponseWriter) error {
|
||||||
|
r.WriteContentType(w)
|
||||||
|
encoder := json.NewEncoder(w)
|
||||||
|
encoder.SetEscapeHTML(false)
|
||||||
|
return encoder.Encode(r.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteContentType (PureJSON) writes custom ContentType.
|
||||||
|
func (r PureJSON) WriteContentType(w http.ResponseWriter) {
|
||||||
|
writeContentType(w, jsonContentType)
|
||||||
|
}
|
||||||
|
@ -1,31 +0,0 @@
|
|||||||
// Copyright 2018 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 go1.7
|
|
||||||
|
|
||||||
package render
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin/internal/json"
|
|
||||||
)
|
|
||||||
|
|
||||||
// PureJSON contains the given interface object.
|
|
||||||
type PureJSON struct {
|
|
||||||
Data interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render (PureJSON) writes custom ContentType and encodes the given interface object.
|
|
||||||
func (r PureJSON) Render(w http.ResponseWriter) error {
|
|
||||||
r.WriteContentType(w)
|
|
||||||
encoder := json.NewEncoder(w)
|
|
||||||
encoder.SetEscapeHTML(false)
|
|
||||||
return encoder.Encode(r.Data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteContentType (PureJSON) writes custom ContentType.
|
|
||||||
func (r PureJSON) WriteContentType(w http.ResponseWriter) {
|
|
||||||
writeContentType(w, jsonContentType)
|
|
||||||
}
|
|
@ -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{}
|
||||||
|
@ -21,7 +21,12 @@ type Reader struct {
|
|||||||
// Render (Reader) writes data with custom ContentType and headers.
|
// Render (Reader) writes data with custom ContentType and headers.
|
||||||
func (r Reader) Render(w http.ResponseWriter) (err error) {
|
func (r Reader) Render(w http.ResponseWriter) (err error) {
|
||||||
r.WriteContentType(w)
|
r.WriteContentType(w)
|
||||||
r.Headers["Content-Length"] = strconv.FormatInt(r.ContentLength, 10)
|
if r.ContentLength >= 0 {
|
||||||
|
if r.Headers == nil {
|
||||||
|
r.Headers = map[string]string{}
|
||||||
|
}
|
||||||
|
r.Headers["Content-Length"] = strconv.FormatInt(r.ContentLength, 10)
|
||||||
|
}
|
||||||
r.writeHeaders(w, r.Headers)
|
r.writeHeaders(w, r.Headers)
|
||||||
_, err = io.Copy(w, r.Reader)
|
_, err = io.Copy(w, r.Reader)
|
||||||
return
|
return
|
||||||
|
23
render/reader_test.go
Normal file
23
render/reader_test.go
Normal 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)
|
||||||
|
}
|
@ -18,9 +18,7 @@ type Redirect struct {
|
|||||||
|
|
||||||
// Render (Redirect) redirects the http request to new location and writes redirect response.
|
// Render (Redirect) redirects the http request to new location and writes redirect response.
|
||||||
func (r Redirect) Render(w http.ResponseWriter) error {
|
func (r Redirect) Render(w http.ResponseWriter) error {
|
||||||
// todo(thinkerou): go1.6 not support StatusPermanentRedirect(308)
|
if (r.Code < http.StatusMultipleChoices || r.Code > http.StatusPermanentRedirect) && r.Code != http.StatusCreated {
|
||||||
// when we upgrade go version we can use http.StatusPermanentRedirect
|
|
||||||
if (r.Code < 300 || r.Code > 308) && r.Code != 201 {
|
|
||||||
panic(fmt.Sprintf("Cannot redirect with status code %d", r.Code))
|
panic(fmt.Sprintf("Cannot redirect with status code %d", r.Code))
|
||||||
}
|
}
|
||||||
http.Redirect(w, r.Request, r.Location, r.Code)
|
http.Redirect(w, r.Request, r.Location, r.Code)
|
||||||
|
@ -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{}
|
||||||
|
@ -1,26 +0,0 @@
|
|||||||
// Copyright 2018 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 go1.7
|
|
||||||
|
|
||||||
package render
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestRenderPureJSON(t *testing.T) {
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
data := map[string]interface{}{
|
|
||||||
"foo": "bar",
|
|
||||||
"html": "<b>",
|
|
||||||
}
|
|
||||||
err := (PureJSON{data}).Render(w)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"<b>\"}\n", w.Body.String())
|
|
||||||
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
|
|
||||||
}
|
|
43
render/render_msgpack_test.go
Normal file
43
render/render_msgpack_test.go
Normal 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"))
|
||||||
|
}
|
@ -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(), string(buf.Bytes()))
|
|
||||||
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{}{
|
||||||
@ -146,7 +120,7 @@ func TestRenderJsonpJSON(t *testing.T) {
|
|||||||
err1 := (JsonpJSON{"x", data}).Render(w1)
|
err1 := (JsonpJSON{"x", data}).Render(w1)
|
||||||
|
|
||||||
assert.NoError(t, err1)
|
assert.NoError(t, err1)
|
||||||
assert.Equal(t, "x({\"foo\":\"bar\"})", w1.Body.String())
|
assert.Equal(t, "x({\"foo\":\"bar\"});", w1.Body.String())
|
||||||
assert.Equal(t, "application/javascript; charset=utf-8", w1.Header().Get("Content-Type"))
|
assert.Equal(t, "application/javascript; charset=utf-8", w1.Header().Get("Content-Type"))
|
||||||
|
|
||||||
w2 := httptest.NewRecorder()
|
w2 := httptest.NewRecorder()
|
||||||
@ -158,7 +132,7 @@ func TestRenderJsonpJSON(t *testing.T) {
|
|||||||
|
|
||||||
err2 := (JsonpJSON{"x", datas}).Render(w2)
|
err2 := (JsonpJSON{"x", datas}).Render(w2)
|
||||||
assert.NoError(t, err2)
|
assert.NoError(t, err2)
|
||||||
assert.Equal(t, "x([{\"foo\":\"bar\"},{\"bar\":\"foo\"}])", w2.Body.String())
|
assert.Equal(t, "x([{\"foo\":\"bar\"},{\"bar\":\"foo\"}]);", w2.Body.String())
|
||||||
assert.Equal(t, "application/javascript; charset=utf-8", w2.Header().Get("Content-Type"))
|
assert.Equal(t, "application/javascript; charset=utf-8", w2.Header().Get("Content-Type"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -215,6 +189,18 @@ func TestRenderAsciiJSONFail(t *testing.T) {
|
|||||||
assert.Error(t, (AsciiJSON{data}).Render(w))
|
assert.Error(t, (AsciiJSON{data}).Render(w))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRenderPureJSON(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"foo": "bar",
|
||||||
|
"html": "<b>",
|
||||||
|
}
|
||||||
|
err := (PureJSON{data}).Render(w)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"<b>\"}\n", w.Body.String())
|
||||||
|
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
|
||||||
|
}
|
||||||
|
|
||||||
type xmlmap map[string]interface{}
|
type xmlmap map[string]interface{}
|
||||||
|
|
||||||
// Allows type H to be used with xml.Marshal
|
// Allows type H to be used with xml.Marshal
|
||||||
@ -335,7 +321,20 @@ func TestRenderRedirect(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
w = httptest.NewRecorder()
|
w = httptest.NewRecorder()
|
||||||
assert.Panics(t, func() { assert.NoError(t, data2.Render(w)) })
|
assert.PanicsWithValue(t, "Cannot redirect with status code 200", func() {
|
||||||
|
err := data2.Render(w)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
data3 := Redirect{
|
||||||
|
Code: http.StatusCreated,
|
||||||
|
Request: req,
|
||||||
|
Location: "/new/location",
|
||||||
|
}
|
||||||
|
|
||||||
|
w = httptest.NewRecorder()
|
||||||
|
err = data3.Render(w)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// only improve coverage
|
// only improve coverage
|
||||||
data2.WriteContentType(w)
|
data2.WriteContentType(w)
|
||||||
@ -566,3 +565,26 @@ func TestRenderReader(t *testing.T) {
|
|||||||
assert.Equal(t, headers["Content-Disposition"], w.Header().Get("Content-Disposition"))
|
assert.Equal(t, headers["Content-Disposition"], w.Header().Get("Content-Disposition"))
|
||||||
assert.Equal(t, headers["x-request-id"], w.Header().Get("x-request-id"))
|
assert.Equal(t, headers["x-request-id"], w.Header().Get("x-request-id"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRenderReaderNoContentLength(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
body := "#!PNG some raw data"
|
||||||
|
headers := make(map[string]string)
|
||||||
|
headers["Content-Disposition"] = `attachment; filename="filename.png"`
|
||||||
|
headers["x-request-id"] = "requestId"
|
||||||
|
|
||||||
|
err := (Reader{
|
||||||
|
ContentLength: -1,
|
||||||
|
ContentType: "image/png",
|
||||||
|
Reader: strings.NewReader(body),
|
||||||
|
Headers: headers,
|
||||||
|
}).Render(w)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, body, w.Body.String())
|
||||||
|
assert.Equal(t, "image/png", w.Header().Get("Content-Type"))
|
||||||
|
assert.NotContains(t, "Content-Length", w.Header())
|
||||||
|
assert.Equal(t, headers["Content-Disposition"], w.Header().Get("Content-Disposition"))
|
||||||
|
assert.Equal(t, headers["x-request-id"], w.Header().Get("x-request-id"))
|
||||||
|
}
|
||||||
|
@ -6,8 +6,9 @@ package render
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin/internal/bytesconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
// String contains the given interface object slice and its format.
|
// String contains the given interface object slice and its format.
|
||||||
@ -35,6 +36,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(bytesconv.StringToBytes(format))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,8 @@ const (
|
|||||||
defaultStatus = http.StatusOK
|
defaultStatus = http.StatusOK
|
||||||
)
|
)
|
||||||
|
|
||||||
type responseWriterBase interface {
|
// ResponseWriter ...
|
||||||
|
type ResponseWriter interface {
|
||||||
http.ResponseWriter
|
http.ResponseWriter
|
||||||
http.Hijacker
|
http.Hijacker
|
||||||
http.Flusher
|
http.Flusher
|
||||||
@ -37,6 +38,9 @@ type responseWriterBase interface {
|
|||||||
|
|
||||||
// Forces to write the http header (status code + headers).
|
// Forces to write the http header (status code + headers).
|
||||||
WriteHeaderNow()
|
WriteHeaderNow()
|
||||||
|
|
||||||
|
// get the http.Pusher for server push
|
||||||
|
Pusher() http.Pusher
|
||||||
}
|
}
|
||||||
|
|
||||||
type responseWriter struct {
|
type responseWriter struct {
|
||||||
@ -113,3 +117,10 @@ func (w *responseWriter) Flush() {
|
|||||||
w.WriteHeaderNow()
|
w.WriteHeaderNow()
|
||||||
w.ResponseWriter.(http.Flusher).Flush()
|
w.ResponseWriter.(http.Flusher).Flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *responseWriter) Pusher() (pusher http.Pusher) {
|
||||||
|
if pusher, ok := w.ResponseWriter.(http.Pusher); ok {
|
||||||
|
return pusher
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
// +build !go1.8
|
|
||||||
|
|
||||||
// Copyright 2018 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 gin
|
|
||||||
|
|
||||||
// ResponseWriter ...
|
|
||||||
type ResponseWriter interface {
|
|
||||||
responseWriterBase
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
// +build go1.8
|
|
||||||
|
|
||||||
// Copyright 2018 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 gin
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ResponseWriter ...
|
|
||||||
type ResponseWriter interface {
|
|
||||||
responseWriterBase
|
|
||||||
// get the http.Pusher for server push
|
|
||||||
Pusher() http.Pusher
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *responseWriter) Pusher() (pusher http.Pusher) {
|
|
||||||
if pusher, ok := w.ResponseWriter.(http.Pusher); ok {
|
|
||||||
return pusher
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -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() {
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -187,19 +187,21 @@ func (group *RouterGroup) createStaticHandler(relativePath string, fs http.FileS
|
|||||||
fileServer := http.StripPrefix(absolutePath, http.FileServer(fs))
|
fileServer := http.StripPrefix(absolutePath, http.FileServer(fs))
|
||||||
|
|
||||||
return func(c *Context) {
|
return func(c *Context) {
|
||||||
if _, nolisting := fs.(*onlyfilesFS); nolisting {
|
if _, noListing := fs.(*onlyFilesFS); noListing {
|
||||||
c.Writer.WriteHeader(http.StatusNotFound)
|
c.Writer.WriteHeader(http.StatusNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
@ -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))
|
||||||
|
257
routes_test.go
257
routes_test.go
@ -22,7 +22,7 @@ type header struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func performRequest(r http.Handler, method, path string, headers ...header) *httptest.ResponseRecorder {
|
func performRequest(r http.Handler, method, path string, headers ...header) *httptest.ResponseRecorder {
|
||||||
req, _ := http.NewRequest(method, path, nil)
|
req := httptest.NewRequest(method, path, nil)
|
||||||
for _, h := range headers {
|
for _, h := range headers {
|
||||||
req.Header.Add(h.Key, h.Value)
|
req.Header.Add(h.Key, h.Value)
|
||||||
}
|
}
|
||||||
@ -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,41 @@ 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, "john", name)
|
||||||
|
assert.Equal(t, "smith", lastName)
|
||||||
|
assert.Equal(t, "/is/super/great", wild)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestContextParamsGet tests that a parameter can be parsed from the URL even with extra slashes.
|
||||||
|
func TestRouteParamsByNameWithExtraSlash(t *testing.T) {
|
||||||
|
name := ""
|
||||||
|
lastName := ""
|
||||||
|
wild := ""
|
||||||
|
router := New()
|
||||||
|
router.RemoveExtraSlash = true
|
||||||
|
router.GET("/test/:name/:last_name/*wild", func(c *Context) {
|
||||||
|
name = c.Params.ByName("name")
|
||||||
|
lastName = c.Params.ByName("last_name")
|
||||||
|
var ok bool
|
||||||
|
wild, ok = c.Params.Get("wild")
|
||||||
|
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, name, c.Param("name"))
|
||||||
|
assert.Equal(t, name, c.Param("name"))
|
||||||
|
assert.Equal(t, lastName, c.Param("last_name"))
|
||||||
|
|
||||||
|
assert.Empty(t, c.Param("wtf"))
|
||||||
|
assert.Empty(t, c.Params.ByName("wtf"))
|
||||||
|
|
||||||
|
wtf, ok := c.Params.Get("wtf")
|
||||||
|
assert.Empty(t, wtf)
|
||||||
|
assert.False(t, ok)
|
||||||
|
})
|
||||||
|
|
||||||
|
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)
|
||||||
@ -277,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)
|
||||||
@ -297,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")
|
||||||
@ -309,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")
|
||||||
@ -324,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")
|
||||||
@ -338,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)
|
||||||
}
|
}
|
||||||
@ -353,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -363,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
|
||||||
@ -388,16 +445,15 @@ func TestRouterNotFound(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{"/path/", http.StatusMovedPermanently, "/path"}, // TSR -/
|
{"/path/", http.StatusMovedPermanently, "/path"}, // TSR -/
|
||||||
{"/dir", http.StatusMovedPermanently, "/dir/"}, // TSR +/
|
{"/dir", http.StatusMovedPermanently, "/dir/"}, // TSR +/
|
||||||
{"", http.StatusMovedPermanently, "/"}, // TSR +/
|
|
||||||
{"/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.StatusMovedPermanently, "/path"}, // Fixed Case -/
|
{"/PATH/", http.StatusMovedPermanently, "/path"}, // Fixed Case -/
|
||||||
{"/DIR", http.StatusMovedPermanently, "/dir/"}, // Fixed Case +/
|
{"/DIR", http.StatusMovedPermanently, "/dir/"}, // Fixed Case +/
|
||||||
{"/../path", http.StatusMovedPermanently, "/path"}, // 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")))
|
||||||
@ -410,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -434,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())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -447,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")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -458,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -487,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -507,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -518,7 +574,50 @@ 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())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRouteContextHoldsFullPath(t *testing.T) {
|
||||||
|
router := New()
|
||||||
|
|
||||||
|
// Test routes
|
||||||
|
routes := []string{
|
||||||
|
"/simple",
|
||||||
|
"/project/:name",
|
||||||
|
"/",
|
||||||
|
"/news/home",
|
||||||
|
"/news",
|
||||||
|
"/simple-two/one",
|
||||||
|
"/simple-two/one-two",
|
||||||
|
"/project/:name/build/*params",
|
||||||
|
"/project/:name/bui",
|
||||||
|
"/user/:id/status",
|
||||||
|
"/user/:id",
|
||||||
|
"/user/:id/profile",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, route := range routes {
|
||||||
|
actualRoute := route
|
||||||
|
router.GET(route, func(c *Context) {
|
||||||
|
// For each defined route context should contain its full path
|
||||||
|
assert.Equal(t, actualRoute, c.FullPath())
|
||||||
|
c.AbortWithStatus(http.StatusOK)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, route := range routes {
|
||||||
|
w := performRequest(router, http.MethodGet, route)
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test not found
|
||||||
|
router.Use(func(c *Context) {
|
||||||
|
// For not found routes full path is empty
|
||||||
|
assert.Equal(t, "", c.FullPath())
|
||||||
|
})
|
||||||
|
|
||||||
|
w := performRequest(router, http.MethodGet, "/not-found")
|
||||||
|
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||||
|
}
|
||||||
|
111
tree_test.go
111
tree_test.go
@ -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,24 +40,27 @@ func checkRequests(t *testing.T, tree *node, requests testRequests, unescapes ..
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, request := range requests {
|
for _, request := range requests {
|
||||||
handler, ps, _ := tree.getValue(request.path, nil, unescape)
|
value := tree.getValue(request.path, getParams(), unescape)
|
||||||
|
|
||||||
if handler == nil {
|
if value.handlers == nil {
|
||||||
if !request.nilHandler {
|
if !request.nilHandler {
|
||||||
t.Errorf("handle mismatch for route '%s': Expected non-nil handle", request.path)
|
t.Errorf("handle mismatch for route '%s': Expected non-nil handle", request.path)
|
||||||
}
|
}
|
||||||
} else if request.nilHandler {
|
} else if request.nilHandler {
|
||||||
t.Errorf("handle mismatch for route '%s': Expected nil handle", request.path)
|
t.Errorf("handle mismatch for route '%s': Expected nil handle", request.path)
|
||||||
} else {
|
} else {
|
||||||
handler[0](nil)
|
value.handlers[0](nil)
|
||||||
if fakeHandlerValue != request.route {
|
if fakeHandlerValue != request.route {
|
||||||
t.Errorf("handle mismatch for route '%s': Wrong handle (%s != %s)", request.path, fakeHandlerValue, request.route)
|
t.Errorf("handle mismatch for route '%s': Wrong handle (%s != %s)", request.path, fakeHandlerValue, request.route)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !reflect.DeepEqual(ps, request.ps) {
|
if value.params != nil {
|
||||||
t.Errorf("Params mismatch for route '%s'", request.path)
|
if !reflect.DeepEqual(*value.params, request.ps) {
|
||||||
|
t.Errorf("Params mismatch for route '%s'", request.path)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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"
|
||||||
|
|
||||||
@ -454,10 +447,10 @@ func TestTreeTrailingSlashRedirect(t *testing.T) {
|
|||||||
"/doc/",
|
"/doc/",
|
||||||
}
|
}
|
||||||
for _, route := range tsrRoutes {
|
for _, route := range tsrRoutes {
|
||||||
handler, _, tsr := tree.getValue(route, nil, false)
|
value := tree.getValue(route, nil, false)
|
||||||
if handler != nil {
|
if value.handlers != nil {
|
||||||
t.Fatalf("non-nil handler for TSR route '%s", route)
|
t.Fatalf("non-nil handler for TSR route '%s", route)
|
||||||
} else if !tsr {
|
} else if !value.tsr {
|
||||||
t.Errorf("expected TSR recommendation for route '%s'", route)
|
t.Errorf("expected TSR recommendation for route '%s'", route)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -471,10 +464,10 @@ func TestTreeTrailingSlashRedirect(t *testing.T) {
|
|||||||
"/api/world/abc",
|
"/api/world/abc",
|
||||||
}
|
}
|
||||||
for _, route := range noTsrRoutes {
|
for _, route := range noTsrRoutes {
|
||||||
handler, _, tsr := tree.getValue(route, nil, false)
|
value := tree.getValue(route, nil, false)
|
||||||
if handler != nil {
|
if value.handlers != nil {
|
||||||
t.Fatalf("non-nil handler for No-TSR route '%s", route)
|
t.Fatalf("non-nil handler for No-TSR route '%s", route)
|
||||||
} else if tsr {
|
} else if value.tsr {
|
||||||
t.Errorf("expected no TSR recommendation for route '%s'", route)
|
t.Errorf("expected no TSR recommendation for route '%s'", route)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -490,10 +483,10 @@ func TestTreeRootTrailingSlashRedirect(t *testing.T) {
|
|||||||
t.Fatalf("panic inserting test route: %v", recv)
|
t.Fatalf("panic inserting test route: %v", recv)
|
||||||
}
|
}
|
||||||
|
|
||||||
handler, _, tsr := tree.getValue("/", nil, false)
|
value := tree.getValue("/", nil, false)
|
||||||
if handler != nil {
|
if value.handlers != nil {
|
||||||
t.Fatalf("non-nil handler")
|
t.Fatalf("non-nil handler")
|
||||||
} else if tsr {
|
} else if value.tsr {
|
||||||
t.Errorf("expected no TSR recommendation")
|
t.Errorf("expected no TSR recommendation")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
20
utils.go
20
utils.go
@ -90,20 +90,23 @@ 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 {
|
||||||
parts := strings.Split(acceptHeader, ",")
|
parts := strings.Split(acceptHeader, ",")
|
||||||
out := make([]string, 0, len(parts))
|
out := make([]string, 0, len(parts))
|
||||||
for _, part := range parts {
|
for _, part := range parts {
|
||||||
if part = strings.TrimSpace(strings.Split(part, ";")[0]); part != "" {
|
if i := strings.IndexByte(part, ';'); i > 0 {
|
||||||
|
part = part[:i]
|
||||||
|
}
|
||||||
|
if part = strings.TrimSpace(part); part != "" {
|
||||||
out = append(out, part)
|
out = append(out, part)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -127,8 +130,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
|
||||||
@ -146,6 +148,6 @@ func resolveAddress(addr []string) string {
|
|||||||
case 1:
|
case 1:
|
||||||
return addr[0]
|
return addr[0]
|
||||||
default:
|
default:
|
||||||
panic("too much parameters")
|
panic("too many parameters")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,12 @@ func init() {
|
|||||||
SetMode(TestMode)
|
SetMode(TestMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func BenchmarkParseAccept(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
parseAccept("text/html , application/xhtml+xml,application/xml;q=0.9, */* ;q=0.8")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type testStruct struct {
|
type testStruct struct {
|
||||||
T *testing.T
|
T *testing.T
|
||||||
}
|
}
|
||||||
|
95
vendor/vendor.json
vendored
95
vendor/vendor.json
vendored
@ -1,95 +0,0 @@
|
|||||||
{
|
|
||||||
"comment": "v1.3.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": "QeKwBtN2df+j+4stw3bQJ6yO4EY=",
|
|
||||||
"path": "github.com/gin-contrib/sse",
|
|
||||||
"revision": "22d885f9ecc78bf4ee5d72b937e4bbcdc58e8cae",
|
|
||||||
"revisionTime": "2017-01-09T09:34:21Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"checksumSHA1": "mE9XW26JSpe4meBObM6J/Oeq0eg=",
|
|
||||||
"path": "github.com/golang/protobuf/proto",
|
|
||||||
"revision": "aa810b61a9c79d51363740d207bb46cf8e620ed5",
|
|
||||||
"revisionTime": "2018-08-14T21:14:27Z",
|
|
||||||
"version": "v1.2",
|
|
||||||
"versionExact": "v1.2.0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"checksumSHA1": "WqeEgS7pqqkwK8mlrAZmDgtWJMY=",
|
|
||||||
"path": "github.com/json-iterator/go",
|
|
||||||
"revision": "1624edc4454b8682399def8740d46db5e4362ba4",
|
|
||||||
"revisionTime": "2018-08-06T06:07:27Z",
|
|
||||||
"version": "v1.1",
|
|
||||||
"versionExact": "v1.1.5"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"checksumSHA1": "w5RcOnfv5YDr3j2bd1YydkPiZx4=",
|
|
||||||
"path": "github.com/mattn/go-isatty",
|
|
||||||
"revision": "6ca4dbf54d38eea1a992b3c722a76a5d1c4cb25c",
|
|
||||||
"revisionTime": "2017-11-07T05:05:31Z",
|
|
||||||
"version": "v0.0",
|
|
||||||
"versionExact": "v0.0.4"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"checksumSHA1": "LuFv4/jlrmFNnDb/5SCSEPAM9vU=",
|
|
||||||
"path": "github.com/pmezard/go-difflib/difflib",
|
|
||||||
"revision": "5d4384ee4fb2527b0a1256a821ebfc92f91efefc",
|
|
||||||
"revisionTime": "2018-12-26T10:54:42Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"checksumSHA1": "c6pbpF7eowwO59phRTpF8cQ80Z0=",
|
|
||||||
"path": "github.com/stretchr/testify/assert",
|
|
||||||
"revision": "f35b8ab0b5a2cef36673838d662e249dd9c94686",
|
|
||||||
"revisionTime": "2018-05-06T18:05:49Z",
|
|
||||||
"version": "v1.2",
|
|
||||||
"versionExact": "v1.2.2"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"checksumSHA1": "5Bd8RPhhaKcEXkagzPqymP4Gx5E=",
|
|
||||||
"path": "github.com/ugorji/go/codec",
|
|
||||||
"revision": "b4c50a2b199d93b13dc15e78929cfb23bfdf21ab",
|
|
||||||
"revisionTime": "2018-04-07T10:07:33Z",
|
|
||||||
"version": "v1.1",
|
|
||||||
"versionExact": "v1.1.1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"checksumSHA1": "GtamqiJoL7PGHsN454AoffBFMa8=",
|
|
||||||
"path": "golang.org/x/net/context",
|
|
||||||
"revision": "49bb7cea24b1df9410e1712aa6433dae904ff66a",
|
|
||||||
"revisionTime": "2018-10-11T05:27:23Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"checksumSHA1": "SiJNkx+YGtq3Gtr6Ldu6OW83O+U=",
|
|
||||||
"path": "golang.org/x/sys/unix",
|
|
||||||
"revision": "fa43e7bc11baaae89f3f902b2b4d832b68234844",
|
|
||||||
"revisionTime": "2018-10-11T14:35:51Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"checksumSHA1": "P/k5ZGf0lEBgpKgkwy++F7K1PSg=",
|
|
||||||
"path": "gopkg.in/go-playground/validator.v8",
|
|
||||||
"revision": "5f1438d3fca68893a817e4a66806cea46a9e4ebf",
|
|
||||||
"revisionTime": "2017-07-30T05:02:35Z",
|
|
||||||
"version": "v8.18.2",
|
|
||||||
"versionExact": "v8.18.2"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"checksumSHA1": "ZSWoOPUNRr5+3dhkLK3C4cZAQPk=",
|
|
||||||
"path": "gopkg.in/yaml.v2",
|
|
||||||
"revision": "5420a8b6744d3b0345ab293f6fcba19c978f1183",
|
|
||||||
"revisionTime": "2018-03-28T19:50:20Z",
|
|
||||||
"version": "v2.2",
|
|
||||||
"versionExact": "v2.2.1"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"rootPath": "github.com/gin-gonic/gin"
|
|
||||||
}
|
|
@ -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"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user