mirror of
https://github.com/gin-gonic/gin.git
synced 2025-12-16 23:27:10 +08:00
Merge branch 'master' into master
This commit is contained in:
commit
2478256eda
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:9000/hello/world
|
||||||
|
Hello world
|
||||||
|
```
|
||||||
|
|
||||||
|
## Actual result
|
||||||
|
|
||||||
|
<!-- Actual result showing the problem -->
|
||||||
|
```
|
||||||
|
$ curl -i http://localhost:9000/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 GitHub Actions.
|
||||||
- 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.
|
||||||
|
|
||||||
|
|||||||
10
.github/dependabot.yml
vendored
Normal file
10
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: github-actions
|
||||||
|
directory: /
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
- package-ecosystem: gomod
|
||||||
|
directory: /
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
49
.github/workflows/codeql.yml
vendored
Normal file
49
.github/workflows/codeql.yml
vendored
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
# For most projects, this workflow file will not need changing; you simply need
|
||||||
|
# to commit it to your repository.
|
||||||
|
#
|
||||||
|
# You may wish to alter this file to override the set of languages analyzed,
|
||||||
|
# or to provide custom queries or build logic.
|
||||||
|
name: "CodeQL"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
pull_request:
|
||||||
|
# The branches below must be a subset of the branches above
|
||||||
|
branches: [master]
|
||||||
|
schedule:
|
||||||
|
- cron: "0 17 * * 5"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
name: Analyze
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
# required for all workflows
|
||||||
|
security-events: write
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
# Override automatic language detection by changing the below list
|
||||||
|
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
|
||||||
|
# TODO: Enable for javascript later
|
||||||
|
language: ["go"]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
# Initializes the CodeQL tools for scanning.
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v3
|
||||||
|
with:
|
||||||
|
languages: ${{ matrix.language }}
|
||||||
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
|
# By default, queries listed here will override any specified in a config file.
|
||||||
|
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||||
|
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v3
|
||||||
82
.github/workflows/gin.yml
vendored
Normal file
82
.github/workflows/gin.yml
vendored
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
name: Run Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version-file: "go.mod"
|
||||||
|
check-latest: true
|
||||||
|
- name: Setup golangci-lint
|
||||||
|
uses: golangci/golangci-lint-action@v5
|
||||||
|
with:
|
||||||
|
version: v1.56.2
|
||||||
|
args: --verbose
|
||||||
|
test:
|
||||||
|
needs: lint
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, macos-latest]
|
||||||
|
go: ["1.18", "1.19", "1.20", "1.21", "1.22"]
|
||||||
|
test-tags:
|
||||||
|
["", "-tags nomsgpack", '-tags "sonic avx"', "-tags go_json", "-race"]
|
||||||
|
include:
|
||||||
|
- os: ubuntu-latest
|
||||||
|
go-build: ~/.cache/go-build
|
||||||
|
- os: macos-latest
|
||||||
|
go-build: ~/Library/Caches/go-build
|
||||||
|
name: ${{ matrix.os }} @ Go ${{ matrix.go }} ${{ matrix.test-tags }}
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
env:
|
||||||
|
GO111MODULE: on
|
||||||
|
TESTTAGS: ${{ matrix.test-tags }}
|
||||||
|
GOPROXY: https://proxy.golang.org
|
||||||
|
steps:
|
||||||
|
- name: Set up Go ${{ matrix.go }}
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.go }}
|
||||||
|
cache: false
|
||||||
|
|
||||||
|
- name: Checkout Code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ github.ref }}
|
||||||
|
|
||||||
|
- uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
${{ matrix.go-build }}
|
||||||
|
~/go/pkg/mod
|
||||||
|
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-go-
|
||||||
|
|
||||||
|
- name: Run Tests
|
||||||
|
run: make test
|
||||||
|
|
||||||
|
- name: Upload coverage to Codecov
|
||||||
|
uses: codecov/codecov-action@v4
|
||||||
|
with:
|
||||||
|
flags: ${{ matrix.os }},go-${{ matrix.go }},${{ matrix.test-tags }}
|
||||||
|
|
||||||
|
- name: Format
|
||||||
|
if: matrix.go-version == '1.22.x'
|
||||||
|
run: diff -u <(echo -n) <(gofmt -d .)
|
||||||
31
.github/workflows/goreleaser.yml
vendored
Normal file
31
.github/workflows/goreleaser.yml
vendored
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
name: Goreleaser
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "*"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
goreleaser:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: "^1"
|
||||||
|
- name: Run GoReleaser
|
||||||
|
uses: goreleaser/goreleaser-action@v5
|
||||||
|
with:
|
||||||
|
# either 'goreleaser' (default) or 'goreleaser-pro'
|
||||||
|
distribution: goreleaser
|
||||||
|
version: latest
|
||||||
|
args: release --clean
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@ -5,3 +5,7 @@ count.out
|
|||||||
test
|
test
|
||||||
profile.out
|
profile.out
|
||||||
tmp.out
|
tmp.out
|
||||||
|
|
||||||
|
# Develop tools
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
|||||||
57
.golangci.yml
Normal file
57
.golangci.yml
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
run:
|
||||||
|
timeout: 5m
|
||||||
|
linters:
|
||||||
|
enable:
|
||||||
|
- asciicheck
|
||||||
|
- dogsled
|
||||||
|
- durationcheck
|
||||||
|
- errcheck
|
||||||
|
- errorlint
|
||||||
|
- exportloopref
|
||||||
|
- gci
|
||||||
|
- gofmt
|
||||||
|
- goimports
|
||||||
|
- gosec
|
||||||
|
- misspell
|
||||||
|
- nakedret
|
||||||
|
- nilerr
|
||||||
|
- nolintlint
|
||||||
|
- revive
|
||||||
|
- wastedassign
|
||||||
|
|
||||||
|
linters-settings:
|
||||||
|
gosec:
|
||||||
|
# To select a subset of rules to run.
|
||||||
|
# Available rules: https://github.com/securego/gosec#available-rules
|
||||||
|
# Default: [] - means include all rules
|
||||||
|
includes:
|
||||||
|
- G102
|
||||||
|
- G106
|
||||||
|
- G108
|
||||||
|
- G109
|
||||||
|
- G111
|
||||||
|
- G112
|
||||||
|
- G201
|
||||||
|
- G203
|
||||||
|
|
||||||
|
issues:
|
||||||
|
exclude-rules:
|
||||||
|
- linters:
|
||||||
|
- structcheck
|
||||||
|
- unused
|
||||||
|
text: "`data` is unused"
|
||||||
|
- linters:
|
||||||
|
- staticcheck
|
||||||
|
text: "SA1019:"
|
||||||
|
- linters:
|
||||||
|
- revive
|
||||||
|
text: "var-naming:"
|
||||||
|
- linters:
|
||||||
|
- revive
|
||||||
|
text: "exported:"
|
||||||
|
- path: _test\.go
|
||||||
|
linters:
|
||||||
|
- gosec # security is not make sense in tests
|
||||||
|
- linters:
|
||||||
|
- revive
|
||||||
|
path: _test\.go
|
||||||
56
.goreleaser.yaml
Normal file
56
.goreleaser.yaml
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
project_name: gin
|
||||||
|
|
||||||
|
builds:
|
||||||
|
- # If true, skip the build.
|
||||||
|
# Useful for library projects.
|
||||||
|
# Default is false
|
||||||
|
skip: true
|
||||||
|
|
||||||
|
changelog:
|
||||||
|
# Set it to true if you wish to skip the changelog generation.
|
||||||
|
# This may result in an empty release notes on GitHub/GitLab/Gitea.
|
||||||
|
disable: false
|
||||||
|
|
||||||
|
# Changelog generation implementation to use.
|
||||||
|
#
|
||||||
|
# Valid options are:
|
||||||
|
# - `git`: uses `git log`;
|
||||||
|
# - `github`: uses the compare GitHub API, appending the author login to the changelog.
|
||||||
|
# - `gitlab`: uses the compare GitLab API, appending the author name and email to the changelog.
|
||||||
|
# - `github-native`: uses the GitHub release notes generation API, disables the groups feature.
|
||||||
|
#
|
||||||
|
# Defaults to `git`.
|
||||||
|
use: github
|
||||||
|
|
||||||
|
# Sorts the changelog by the commit's messages.
|
||||||
|
# Could either be asc, desc or empty
|
||||||
|
# Default is empty
|
||||||
|
sort: asc
|
||||||
|
|
||||||
|
# Group commits messages by given regex and title.
|
||||||
|
# Order value defines the order of the groups.
|
||||||
|
# Proving no regex means all commits will be grouped under the default group.
|
||||||
|
# Groups are disabled when using github-native, as it already groups things by itself.
|
||||||
|
#
|
||||||
|
# Default is no groups.
|
||||||
|
groups:
|
||||||
|
- title: Features
|
||||||
|
regexp: "^.*feat[(\\w)]*:+.*$"
|
||||||
|
order: 0
|
||||||
|
- title: "Bug fixes"
|
||||||
|
regexp: "^.*fix[(\\w)]*:+.*$"
|
||||||
|
order: 1
|
||||||
|
- title: "Enhancements"
|
||||||
|
regexp: "^.*chore[(\\w)]*:+.*$"
|
||||||
|
order: 2
|
||||||
|
- title: "Refactor"
|
||||||
|
regexp: "^.*refactor[(\\w)]*:+.*$"
|
||||||
|
order: 3
|
||||||
|
- title: "Build process updates"
|
||||||
|
regexp: ^.*?(build|ci)(\(.+\))??!?:.+$
|
||||||
|
order: 4
|
||||||
|
- title: "Documentation updates"
|
||||||
|
regexp: ^.*?docs?(\(.+\))??!?:.+$
|
||||||
|
order: 4
|
||||||
|
- title: Others
|
||||||
|
order: 999
|
||||||
44
.travis.yml
44
.travis.yml
@ -1,44 +0,0 @@
|
|||||||
language: go
|
|
||||||
|
|
||||||
matrix:
|
|
||||||
fast_finish: true
|
|
||||||
include:
|
|
||||||
- go: 1.8.x
|
|
||||||
- go: 1.9.x
|
|
||||||
- go: 1.10.x
|
|
||||||
- go: 1.11.x
|
|
||||||
env: GO111MODULE=on
|
|
||||||
- go: 1.12.x
|
|
||||||
env: GO111MODULE=on
|
|
||||||
- go: master
|
|
||||||
env: GO111MODULE=on
|
|
||||||
|
|
||||||
git:
|
|
||||||
depth: 10
|
|
||||||
|
|
||||||
before_install:
|
|
||||||
- if [[ "${GO111MODULE}" = "on" ]]; then mkdir "${HOME}/go"; export GOPATH="${HOME}/go"; fi
|
|
||||||
|
|
||||||
install:
|
|
||||||
- if [[ "${GO111MODULE}" = "on" ]]; then go mod download; else make install; fi
|
|
||||||
- if [[ "${GO111MODULE}" = "on" ]]; then export PATH="${GOPATH}/bin:${GOROOT}/bin:${PATH}"; fi
|
|
||||||
- if [[ "${GO111MODULE}" = "on" ]]; then make tools; fi
|
|
||||||
|
|
||||||
go_import_path: github.com/gin-gonic/gin
|
|
||||||
|
|
||||||
script:
|
|
||||||
- make vet
|
|
||||||
- make fmt-check
|
|
||||||
- make misspell-check
|
|
||||||
- make test
|
|
||||||
|
|
||||||
after_success:
|
|
||||||
- bash <(curl -s https://codecov.io/bash)
|
|
||||||
|
|
||||||
notifications:
|
|
||||||
webhooks:
|
|
||||||
urls:
|
|
||||||
- https://webhooks.gitter.im/e/7f95bf605c4d356372f4
|
|
||||||
on_success: change # options: [always|never|change] default: always
|
|
||||||
on_failure: always # options: [always|never|change] default: always
|
|
||||||
on_start: false # default: false
|
|
||||||
615
AUTHORS.md
615
AUTHORS.md
@ -2,230 +2,405 @@ List of all the awesome people working to make Gin the best Web Framework in Go.
|
|||||||
|
|
||||||
## gin 1.x series authors
|
## gin 1.x series authors
|
||||||
|
|
||||||
**Gin Core Team:** Bo-Yi Wu (@appleboy), 田欧 (@thinkerou), Javier Provecho (@javierprovecho)
|
**Gin Core Team:** Bo-Yi Wu (@appleboy), thinkerou (@thinkerou), Javier Provecho (@javierprovecho)
|
||||||
|
|
||||||
## gin 0.x series authors
|
## gin 0.x series authors
|
||||||
|
|
||||||
**Maintainers:** Manu Martinez-Almeida (@manucorporat), Javier Provecho (@javierprovecho)
|
**Maintainers:** Manu Martinez-Almeida (@manucorporat), Javier Provecho (@javierprovecho)
|
||||||
|
|
||||||
|
------
|
||||||
|
|
||||||
People and companies, who have contributed, in alphabetical order.
|
People and companies, who have contributed, in alphabetical order.
|
||||||
|
|
||||||
**@858806258 (杰哥)**
|
- 178inaba <178inaba@users.noreply.github.com>
|
||||||
- Fix typo in example
|
- A. F <hello@clivern.com>
|
||||||
|
- ABHISHEK SONI <abhishek.rocks26@gmail.com>
|
||||||
|
- Abhishek Chanda <achanda@users.noreply.github.com>
|
||||||
**@achedeuzot (Klemen Sever)**
|
- Abner Chen <houjunchen@gmail.com>
|
||||||
- Fix newline debug printing
|
- AcoNCodes <acongame@gmail.com>
|
||||||
|
- Adam Dratwinski <adam.dratwinski@gmail.com>
|
||||||
|
- Adam Mckaig <adam.mckaig@gmail.com>
|
||||||
**@adammck (Adam Mckaig)**
|
- Adam Zielinski <MusicAdam@users.noreply.github.com>
|
||||||
- Add MIT license
|
- Adonis <donileo@gmail.com>
|
||||||
|
- Alan Wang <azzwacb9001@126.com>
|
||||||
|
- Albin Gilles <gilles.albin@gmail.com>
|
||||||
**@AlexanderChen1989 (Alexander)**
|
- Aleksandr Didenko <aa.didenko@yandex.ru>
|
||||||
- Typos in README
|
- Alessandro (Ale) Segala <43508+ItalyPaleAle@users.noreply.github.com>
|
||||||
|
- Alex <AWulkan@users.noreply.github.com>
|
||||||
|
- Alexander <alexanderchenmh@gmail.com>
|
||||||
**@alexanderdidenko (Aleksandr Didenko)**
|
- Alexander Lokhman <alex.lokhman@gmail.com>
|
||||||
- Add support multipart/form-data
|
- Alexander Melentyev <55826637+alexander-melentyev@users.noreply.github.com>
|
||||||
|
- Alexander Nyquist <nyquist.alexander@gmail.com>
|
||||||
|
- Allen Ren <kulong0105@gmail.com>
|
||||||
**@alexandernyquist (Alexander Nyquist)**
|
- AllinGo <tanhp@outlook.com>
|
||||||
- Using template.Must to fix multiple return issue
|
- Ammar Bandukwala <ammar@ammar.io>
|
||||||
- ★ Added support for OPTIONS verb
|
- An Xiao (Luffy) <hac@zju.edu.cn>
|
||||||
- ★ Setting response headers before calling WriteHeader
|
- Andre Dublin <81dublin@gmail.com>
|
||||||
- Improved documentation for model binding
|
- Andrew Szeto <github@jabagawee.com>
|
||||||
- ★ Added Content.Redirect()
|
- Andrey Abramov <andreyabramov.aaa@gmail.com>
|
||||||
- ★ Added tons of Unit tests
|
- Andrey Nering <andrey.nering@gmail.com>
|
||||||
|
- Andrey Smirnov <Smirnov.Andrey@gmail.com>
|
||||||
|
- Andrii Bubis <firstrow@gmail.com>
|
||||||
**@austinheap (Austin Heap)**
|
- André Bazaglia <bazaglia@users.noreply.github.com>
|
||||||
- Added travis CI integration
|
- Andy Pan <panjf2000@gmail.com>
|
||||||
|
- Antoine GIRARD <sapk@users.noreply.github.com>
|
||||||
|
- Anup Kumar Panwar <1anuppanwar@gmail.com>
|
||||||
**@andredublin (Andre Dublin)**
|
- Aravinth Sundaram <gosh.aravind@gmail.com>
|
||||||
- Fix typo in comment
|
- Artem <horechek@gmail.com>
|
||||||
|
- Ashwani <ashwanisharma686@gmail.com>
|
||||||
|
- Aurelien Regat-Barrel <arb@cyberkarma.net>
|
||||||
**@bredov (Ludwig Valda Vasquez)**
|
- Austin Heap <me@austinheap.com>
|
||||||
- Fix html templating in debug mode
|
- Barnabus <jbampton@users.noreply.github.com>
|
||||||
|
- Bo-Yi Wu <appleboy.tw@gmail.com>
|
||||||
|
- Boris Borshevsky <BorisBorshevsky@gmail.com>
|
||||||
**@bluele (Jun Kimura)**
|
- Boyi Wu <p581581@gmail.com>
|
||||||
- Fixes code examples in README
|
- BradyBromley <51128276+BradyBromley@users.noreply.github.com>
|
||||||
|
- Brendan Fosberry <brendan@shopkeep.com>
|
||||||
|
- Brian Wigginton <brianwigginton@gmail.com>
|
||||||
**@chad-russell**
|
- Carlos Eduardo <carlosedp@gmail.com>
|
||||||
- ★ Support for serializing gin.H into XML
|
- Chad Russell <chaddouglasrussell@gmail.com>
|
||||||
|
- Charles <cxjava@gmail.com>
|
||||||
|
- Christian Muehlhaeuser <muesli@gmail.com>
|
||||||
**@dickeyxxx (Jeff Dickey)**
|
- Christian Persson <saser@live.se>
|
||||||
- Typos in README
|
- Christopher Harrington <ironiridis@gmail.com>
|
||||||
- Add example about serving static files
|
- Damon Zhao <yijun.zhao@outlook.com>
|
||||||
|
- Dan Markham <dmarkham@gmail.com>
|
||||||
|
- Dang Nguyen <hoangdang.me@gmail.com>
|
||||||
**@donileo (Adonis)**
|
- Daniel Krom <kromdan@gmail.com>
|
||||||
- Add NoMethod handler
|
- Daniel M. Lambea <dmlambea@gmail.com>
|
||||||
|
- Danieliu <liudanking@gmail.com>
|
||||||
|
- David Irvine <aviddiviner@gmail.com>
|
||||||
**@dutchcoders (DutchCoders)**
|
- David Zhang <crispgm@gmail.com>
|
||||||
- ★ Fix security bug that allows client to spoof ip
|
- Davor Kapsa <dvrkps@users.noreply.github.com>
|
||||||
- Fix typo. r.HTMLTemplates -> SetHTMLTemplate
|
- DeathKing <DeathKing@users.noreply.github.com>
|
||||||
|
- Dennis Cho <47404603+forest747@users.noreply.github.com>
|
||||||
|
- Dmitry Dorogin <dmirogin@ya.ru>
|
||||||
**@el3ctro- (Joshua Loper)**
|
- Dmitry Kutakov <vkd.castle@gmail.com>
|
||||||
- Fix typo in example
|
- Dmitry Sedykh <dmitrys@d3h.local>
|
||||||
|
- Don2Quixote <35610661+Don2Quixote@users.noreply.github.com>
|
||||||
|
- Donn Pebe <iam@donnpebe.com>
|
||||||
**@ethankan (Ethan Kan)**
|
- Dustin Decker <dustindecker@protonmail.com>
|
||||||
- Unsigned integers in binding
|
- Eason Lin <easonlin404@gmail.com>
|
||||||
|
- Edward Betts <edward@4angle.com>
|
||||||
|
- Egor Seredin <4819888+agmt@users.noreply.github.com>
|
||||||
**(Evgeny Persienko)**
|
- Emmanuel Goh <emmanuel@visenze.com>
|
||||||
- Validate sub structures
|
- Equim <sayaka@ekyu.moe>
|
||||||
|
- Eren A. Akyol <eren@redmc.me>
|
||||||
|
- Eric_Lee <xplzv@126.com>
|
||||||
**@frankbille (Frank Bille)**
|
- Erik Bender <erik.bender@develerik.dev>
|
||||||
- Add support for HTTP Realm Auth
|
- Ethan Kan <ethankan@neoplot.com>
|
||||||
|
- Evgeny Persienko <e.persienko@office.ngs.ru>
|
||||||
|
- Faisal Alam <ifaisalalam@gmail.com>
|
||||||
**@fmd (Fareed Dudhia)**
|
- Fareed Dudhia <fareeddudhia@googlemail.com>
|
||||||
- Fix typo. SetHTTPTemplate -> SetHTMLTemplate
|
- Filip Figiel <figiel.filip@gmail.com>
|
||||||
|
- Florian Polster <couchpolster@icqmail.com>
|
||||||
|
- Frank Bille <github@frankbille.dk>
|
||||||
**@ironiridis (Christopher Harrington)**
|
- Franz Bettag <franz@bett.ag>
|
||||||
- Remove old reference
|
- Ganlv <ganlvtech@users.noreply.github.com>
|
||||||
|
- Gaozhen Ying <yinggaozhen@hotmail.com>
|
||||||
|
- George Gabolaev <gabolaev98@gmail.com>
|
||||||
**@jammie-stackhouse (Jamie Stackhouse)**
|
- George Kirilenko <necryin@users.noreply.github.com>
|
||||||
- Add more shortcuts for router methods
|
- Georges Varouchas <georges.varouchas@gmail.com>
|
||||||
|
- Gordon Tyler <gordon@doxxx.net>
|
||||||
|
- Harindu Perera <harinduenator@gmail.com>
|
||||||
**@jasonrhansen**
|
- Helios <674876158@qq.com>
|
||||||
- Fix spelling and grammar errors in documentation
|
- Henry Kwan <piengeng@users.noreply.github.com>
|
||||||
|
- Henry Yee <henry@yearning.io>
|
||||||
|
- Himanshu Mishra <OrkoHunter@users.noreply.github.com>
|
||||||
**@JasonSoft (Jason Lee)**
|
- Hiroyuki Tanaka <h.tanaka.0325@gmail.com>
|
||||||
- Fix typo in comment
|
- Ibraheem Ahmed <ibrah1440@gmail.com>
|
||||||
|
- Ignacio Galindo <joiggama@gmail.com>
|
||||||
|
- Igor H. Vieira <zignd.igor@gmail.com>
|
||||||
**@joiggama (Ignacio Galindo)**
|
- Ildar1111 <54001462+Ildar1111@users.noreply.github.com>
|
||||||
- Add utf-8 charset header on renders
|
- Iskander (Alex) Sharipov <iskander.sharipov@intel.com>
|
||||||
|
- Ismail Gjevori <isgjevori@protonmail.com>
|
||||||
|
- Ivan Chen <allenivan@gmail.com>
|
||||||
**@julienschmidt (Julien Schmidt)**
|
- JINNOUCHI Yasushi <delphinus@remora.cx>
|
||||||
- gofmt the code examples
|
- James Pettyjohn <japettyjohn@users.noreply.github.com>
|
||||||
|
- Jamie Stackhouse <jamie.stackhouse@redspace.com>
|
||||||
|
- Jason Lee <jawc@hotmail.com>
|
||||||
**@kelcecil (Kel Cecil)**
|
- Javier Provecho <j.provecho@dartekstudios.com>
|
||||||
- Fix readme typo
|
- Javier Provecho <javier.provecho@bq.com>
|
||||||
|
- Javier Provecho <javiertitan@gmail.com>
|
||||||
|
- Javier Provecho Fernandez <j.provecho@dartekstudios.com>
|
||||||
**@kyledinh (Kyle Dinh)**
|
- Javier Provecho Fernandez <javiertitan@gmail.com>
|
||||||
- Adds RunTLS()
|
- Jean-Christophe Lebreton <jclebreton@gmail.com>
|
||||||
|
- Jeff <laojianzi1994@gmail.com>
|
||||||
|
- Jeremy Loy <jeremy.b.loy@icloud.com>
|
||||||
**@LinusU (Linus Unnebäck)**
|
- Jim Filippou <p3160253@aueb.gr>
|
||||||
- Small fixes in README
|
- Jimmy Pettersson <jimmy@expertmaker.com>
|
||||||
|
- John Bampton <jbampton@users.noreply.github.com>
|
||||||
|
- Johnny Dallas <johnnydallas0308@gmail.com>
|
||||||
**@loongmxbt (Saint Asky)**
|
- Johnny Dallas <theonlyjohnny@theonlyjohnny.sh>
|
||||||
- Fix typo in example
|
- Jonathan (JC) Chen <jc@dijonkitchen.org>
|
||||||
|
- Josep Jesus Bigorra Algaba <42377845+averageflow@users.noreply.github.com>
|
||||||
|
- Josh Horowitz <joshua.m.horowitz@gmail.com>
|
||||||
**@lucas-clemente (Lucas Clemente)**
|
- Joshua Loper <josh.el3@gmail.com>
|
||||||
- ★ work around path.Join removing trailing slashes from routes
|
- Julien Schmidt <github@julienschmidt.com>
|
||||||
|
- Jun Kimura <jksmphone@gmail.com>
|
||||||
|
- Justin Beckwith <justin.beckwith@gmail.com>
|
||||||
**@mattn (Yasuhiro Matsumoto)**
|
- Justin Israel <justinisrael@gmail.com>
|
||||||
- Improve color logger
|
- Justin Mayhew <mayhew@live.ca>
|
||||||
|
- Jérôme Laforge <jerome-laforge@users.noreply.github.com>
|
||||||
|
- Kacper Bąk <56700396+53jk1@users.noreply.github.com>
|
||||||
**@mdigger (Dmitry Sedykh)**
|
- Kamron Batman <kamronbatman@users.noreply.github.com>
|
||||||
- Fixes Form binding when content-type is x-www-form-urlencoded
|
- Kane Rogers <kane@cleanstream.com.au>
|
||||||
- No repeat call c.Writer.Status() in gin.Logger
|
- Kaushik Neelichetty <kaushikneelichetty6132@gmail.com>
|
||||||
- Fixes Content-Type for json render
|
- Keiji Yoshida <yoshida.keiji.84@gmail.com>
|
||||||
|
- Kel Cecil <kel.cecil@listhub.com>
|
||||||
|
- Kevin Mulvey <kmulvey@linux.com>
|
||||||
**@mirzac (Mirza Ceric)**
|
- Kevin Zhu <ipandtcp@gmail.com>
|
||||||
- Fix debug printing
|
- Kirill Motkov <motkov.kirill@gmail.com>
|
||||||
|
- Klemen Sever <ksever@student.42.fr>
|
||||||
|
- Kristoffer A. Iversen <kristoffer.a.iversen@gmail.com>
|
||||||
**@mopemope (Yutaka Matsubara)**
|
- Krzysztof Szafrański <k.p.szafranski@gmail.com>
|
||||||
- ★ Adds Godep support (Dependencies Manager)
|
- Kumar McMillan <kumar.mcmillan@gmail.com>
|
||||||
- Fix variadic parameter in the flexible render API
|
- Kyle Mcgill <email@kylescottmcgill.com>
|
||||||
- Fix Corrupted plain render
|
- Lanco <35420416+lancoLiu@users.noreply.github.com>
|
||||||
- Add Pluggable View Renderer Example
|
- Levi Olson <olson.levi@gmail.com>
|
||||||
|
- Lin Kao-Yuan <mosdeo@gmail.com>
|
||||||
|
- Linus Unnebäck <linus@folkdatorn.se>
|
||||||
**@msemenistyi (Mykyta Semenistyi)**
|
- Lucas Clemente <lucas@clemente.io>
|
||||||
- update Readme.md. Add code to String method
|
- Ludwig Valda Vasquez <bredov@gmail.com>
|
||||||
|
- Luis GG <lggomez@users.noreply.github.com>
|
||||||
|
- MW Lim <williamchange@gmail.com>
|
||||||
**@msoedov (Sasha Myasoedov)**
|
- Maksimov Sergey <konjoot@gmail.com>
|
||||||
- ★ Adds tons of unit tests.
|
- Manjusaka <lizheao940510@gmail.com>
|
||||||
|
- Manu MA <manu.mtza@gmail.com>
|
||||||
|
- Manu MA <manu.valladolid@gmail.com>
|
||||||
**@ngerakines (Nick Gerakines)**
|
- Manu Mtz-Almeida <manu.valladolid@gmail.com>
|
||||||
- ★ Improves API, c.GET() doesn't panic
|
- Manu Mtz.-Almeida <manu.valladolid@gmail.com>
|
||||||
- Adds MustGet() method
|
- Manuel Alonso <manuelalonso@invisionapp.com>
|
||||||
|
- Mara Kim <hacker.root@gmail.com>
|
||||||
|
- Mario Kostelac <mario@intercom.io>
|
||||||
**@r8k (Rajiv Kilaparti)**
|
- Martin Karlsch <martin@karlsch.com>
|
||||||
- Fix Port usage in README.
|
- Matt Newberry <mnewberry@opentable.com>
|
||||||
|
- Matt Williams <gh@mattyw.net>
|
||||||
|
- Matthieu MOREL <mmorel-35@users.noreply.github.com>
|
||||||
**@rayrod2030 (Ray Rodriguez)**
|
- Max Hilbrunner <mhilbrunner@users.noreply.github.com>
|
||||||
- Fix typo in example
|
- Maxime Soulé <btik-git@scoubidou.com>
|
||||||
|
- MetalBreaker <johnymichelson@gmail.com>
|
||||||
|
- Michael Puncel <mpuncel@squareup.com>
|
||||||
**@rns**
|
- MichaelDeSteven <51652084+MichaelDeSteven@users.noreply.github.com>
|
||||||
- Fix typo in example
|
- Mike <38686456+icy4ever@users.noreply.github.com>
|
||||||
|
- Mike Stipicevic <mst@ableton.com>
|
||||||
|
- Miki Tebeka <miki.tebeka@gmail.com>
|
||||||
**@RobAWilkinson (Robert Wilkinson)**
|
- Miles <MilesLin@users.noreply.github.com>
|
||||||
- Add example of forms and params
|
- Mirza Ceric <mirza.ceric@b2match.com>
|
||||||
|
- Mykyta Semenistyi <nikeiwe@gmail.com>
|
||||||
|
- Naoki Takano <honten@tinkermode.com>
|
||||||
**@rogierlommers (Rogier Lommers)**
|
- Ngalim Siregar <ngalim.siregar@gmail.com>
|
||||||
- Add updated static serve example
|
- Ni Hao <supernihaooo@qq.com>
|
||||||
|
- Nick Gerakines <nick@gerakines.net>
|
||||||
|
- Nikifor Seryakov <nikandfor@gmail.com>
|
||||||
**@se77en (Damon Zhao)**
|
- Notealot <714804968@qq.com>
|
||||||
- Improve color logging
|
- Olivier Mengué <dolmen@cpan.org>
|
||||||
|
- Olivier Robardet <orobardet@users.noreply.github.com>
|
||||||
|
- Pablo Moncada <pablo.moncada@bq.com>
|
||||||
**@silasb (Silas Baronda)**
|
- Pablo Moncada <pmoncadaisla@gmail.com>
|
||||||
- Fixing quotes in README
|
- Panmax <967168@qq.com>
|
||||||
|
- Peperoncino <2wua4nlyi@gmail.com>
|
||||||
|
- Philipp Meinen <philipp@bind.ch>
|
||||||
**@SkuliOskarsson (Skuli Oskarsson)**
|
- Pierre Massat <pierre@massat.io>
|
||||||
- Fixes some texts in README II
|
- Qt <golang.chen@gmail.com>
|
||||||
|
- Quentin ROYER <aydendevg@gmail.com>
|
||||||
|
- README Bot <35302948+codetriage-readme-bot@users.noreply.github.com>
|
||||||
**@slimmy (Jimmy Pettersson)**
|
- Rafal Zajac <rzajac@gmail.com>
|
||||||
- Added messages for required bindings
|
- Rahul Datta Roy <rahuldroy@users.noreply.github.com>
|
||||||
|
- Rajiv Kilaparti <rajivk085@gmail.com>
|
||||||
|
- Raphael Gavache <raphael.gavache@datadoghq.com>
|
||||||
**@smira (Andrey Smirnov)**
|
- Ray Rodriguez <rayrod2030@gmail.com>
|
||||||
- Add support for ignored/unexported fields in binding
|
- Regner Blok-Andersen <shadowdf@gmail.com>
|
||||||
|
- Remco <remco@dutchcoders.io>
|
||||||
|
- Rex Lee(李俊) <duguying2008@gmail.com>
|
||||||
**@superalsrk (SRK.Lyu)**
|
- Richard Lee <dlackty@gmail.com>
|
||||||
- Update httprouter godeps
|
- Riverside <wangyb65@gmail.com>
|
||||||
|
- Robert Wilkinson <wilkinson.robert.a@gmail.com>
|
||||||
|
- Rogier Lommers <rogier@lommers.org>
|
||||||
**@tebeka (Miki Tebeka)**
|
- Rohan Pai <me@rohanpai.com>
|
||||||
- Use net/http constants instead of numeric values
|
- Romain Beuque <rbeuque74@gmail.com>
|
||||||
|
- Roman Belyakovsky <ihryamzik@gmail.com>
|
||||||
|
- Roman Zaynetdinov <627197+zaynetro@users.noreply.github.com>
|
||||||
**@techjanitor**
|
- Roman Zaynetdinov <roman.zaynetdinov@lekane.com>
|
||||||
- Update context.go reserved IPs
|
- Ronald Petty <ronald.petty@rx-m.com>
|
||||||
|
- Ross Wolf <31489089+rw-access@users.noreply.github.com>
|
||||||
|
- Roy Lou <roylou@gmail.com>
|
||||||
**@yosssi (Keiji Yoshida)**
|
- Rubi <14269809+codenoid@users.noreply.github.com>
|
||||||
- Fix link in README
|
- Ryan <46182144+ryanker@users.noreply.github.com>
|
||||||
|
- Ryan J. Yoder <me@ryanjyoder.com>
|
||||||
|
- SRK.Lyu <superalsrk@gmail.com>
|
||||||
**@yuyabee**
|
- Sai <sairoutine@gmail.com>
|
||||||
- Fixed README
|
- Samuel Abreu <sdepaula@gmail.com>
|
||||||
|
- Santhosh Kumar <santhoshkumarr1096@gmail.com>
|
||||||
|
- Sasha Melentyev <sasha@melentyev.io>
|
||||||
|
- Sasha Myasoedov <msoedov@gmail.com>
|
||||||
|
- Segev Finer <segev208@gmail.com>
|
||||||
|
- Sergey Egorov <egorovhome@gmail.com>
|
||||||
|
- Sergey Fedchenko <seregayoga@bk.ru>
|
||||||
|
- Sergey Gonimar <sergey.gonimar@gmail.com>
|
||||||
|
- Sergey Ponomarev <me@sergey-ponomarev.ru>
|
||||||
|
- Serica <943914044@qq.com>
|
||||||
|
- Shamus Taylor <Shamus03@me.com>
|
||||||
|
- Shilin Wang <jarvisfironman@gmail.com>
|
||||||
|
- Shuo <openset.wang@gmail.com>
|
||||||
|
- Skuli Oskarsson <skuli@codeiak.io>
|
||||||
|
- Snawoot <vladislav-ex-github@vm-0.com>
|
||||||
|
- Sridhar Ratnakumar <srid@srid.ca>
|
||||||
|
- Steeve Chailloux <steeve@chaahk.com>
|
||||||
|
- Sudhir Mishra <sudhirxps@gmail.com>
|
||||||
|
- Suhas Karanth <sudo-suhas@users.noreply.github.com>
|
||||||
|
- TaeJun Park <miking38@gmail.com>
|
||||||
|
- Tatsuya Hoshino <tatsuya7.hoshino7@gmail.com>
|
||||||
|
- Tevic <tevic.tt@gmail.com>
|
||||||
|
- Tevin Jeffrey <tev.jeffrey@gmail.com>
|
||||||
|
- The Gitter Badger <badger@gitter.im>
|
||||||
|
- Thibault Jamet <tjamet@users.noreply.github.com>
|
||||||
|
- Thomas Boerger <thomas@webhippie.de>
|
||||||
|
- Thomas Schaffer <loopfz@gmail.com>
|
||||||
|
- Tommy Chu <tommychu2256@gmail.com>
|
||||||
|
- Tudor Roman <tudurom@gmail.com>
|
||||||
|
- Uwe Dauernheim <djui@users.noreply.github.com>
|
||||||
|
- Valentine Oragbakosi <valentine13400@gmail.com>
|
||||||
|
- Vas N <pnvasanth@users.noreply.github.com>
|
||||||
|
- Vasilyuk Vasiliy <By-Vasiliy@users.noreply.github.com>
|
||||||
|
- Victor Castell <victor@victorcastell.com>
|
||||||
|
- Vince Yuan <vince.yuan@gmail.com>
|
||||||
|
- Vyacheslav Dubinin <vyacheslav.dubinin@gmail.com>
|
||||||
|
- Waynerv <ampedee@gmail.com>
|
||||||
|
- Weilin Shi <934587911@qq.com>
|
||||||
|
- Xudong Cai <fifsky@gmail.com>
|
||||||
|
- Yasuhiro Matsumoto <mattn.jp@gmail.com>
|
||||||
|
- Yehezkiel Syamsuhadi <ybs@ybs.im>
|
||||||
|
- Yoshiki Nakagawa <yyoshiki41@gmail.com>
|
||||||
|
- Yoshiyuki Kinjo <yskkin+github@gmail.com>
|
||||||
|
- Yue Yang <g1enyy0ung@gmail.com>
|
||||||
|
- ZYunH <zyunhjob@163.com>
|
||||||
|
- Zach Newburgh <zach.newburgh@gmail.com>
|
||||||
|
- Zasda Yusuf Mikail <zasdaym@gmail.com>
|
||||||
|
- ZhangYunHao <zyunhjob@163.com>
|
||||||
|
- ZhiFeng Hu <hufeng1987@gmail.com>
|
||||||
|
- Zhu Xi <zhuxi910511@163.com>
|
||||||
|
- a2tt <usera2tt@gmail.com>
|
||||||
|
- ahuigo <1781999+ahuigo@users.noreply.github.com>
|
||||||
|
- ali <anio@users.noreply.github.com>
|
||||||
|
- aljun <salameryy@163.com>
|
||||||
|
- andrea <crypto.andrea@protonmail.ch>
|
||||||
|
- andriikushch <andrii.kushch@gmail.com>
|
||||||
|
- anoty <anjunyou@foxmail.com>
|
||||||
|
- awkj <hzzbiu@gmail.com>
|
||||||
|
- axiaoxin <254606826@qq.com>
|
||||||
|
- bbiao <bbbiao@gmail.com>
|
||||||
|
- bestgopher <84328409@qq.com>
|
||||||
|
- betahu <zhong.wenhuang@foxmail.com>
|
||||||
|
- bigwheel <k.bigwheel+eng@gmail.com>
|
||||||
|
- bn4t <17193640+bn4t@users.noreply.github.com>
|
||||||
|
- bullgare <bullgare@gmail.com>
|
||||||
|
- chainhelen <chainhelen@gmail.com>
|
||||||
|
- chenyang929 <chenyang929code@gmail.com>
|
||||||
|
- chriswhelix <chris.williams@helix.com>
|
||||||
|
- collinmsn <4130944@qq.com>
|
||||||
|
- cssivision <cssivision@gmail.com>
|
||||||
|
- danielalves <alves.lopes.dan@gmail.com>
|
||||||
|
- delphinus <delphinus@remora.cx>
|
||||||
|
- dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
|
||||||
|
- dickeyxxx <jeff@dickeyxxx.com>
|
||||||
|
- edebernis <emeric.debernis@gmail.com>
|
||||||
|
- error10 <error@ioerror.us>
|
||||||
|
- esplo <esplo@users.noreply.github.com>
|
||||||
|
- eudore <30709860+eudore@users.noreply.github.com>
|
||||||
|
- ffhelicopter <32922889+ffhelicopter@users.noreply.github.com>
|
||||||
|
- filikos <11477309+filikos@users.noreply.github.com>
|
||||||
|
- forging2012 <forging2012@users.noreply.github.com>
|
||||||
|
- goqihoo <goqihoo@gmail.com>
|
||||||
|
- grapeVine <treeui.old@gmail.com>
|
||||||
|
- guonaihong <guonaihong@qq.com>
|
||||||
|
- heige <daheige@users.noreply.github.com>
|
||||||
|
- heige <zhuwei313@hotmail.com>
|
||||||
|
- hellojukay <hellojukay@163.com>
|
||||||
|
- henrylee2cn <henrylee2cn@gmail.com>
|
||||||
|
- htobenothing <htobenothing@gmail.com>
|
||||||
|
- iamhesir <78344375+iamhesir@users.noreply.github.com>
|
||||||
|
- ijaa <kailiu2013@gmail.com>
|
||||||
|
- ishanray <ishan.iipm@gmail.com>
|
||||||
|
- ishanray <ishanray@users.noreply.github.com>
|
||||||
|
- itcloudy <272685110@qq.com>
|
||||||
|
- jarodsong6 <jarodsong6@gmail.com>
|
||||||
|
- jasonrhansen <jasonrodneyhansen@gmail.com>
|
||||||
|
- jincheng9 <perfume0607@gmail.com>
|
||||||
|
- joeADSP <75027008+joeADSP@users.noreply.github.com>
|
||||||
|
- junfengye <junfeng.yejf@gmail.com>
|
||||||
|
- kaiiak <aNxFi37X@outlook.com>
|
||||||
|
- kebo <kevinke2020@outlook.com>
|
||||||
|
- keke <19yamashita15@gmail.com>
|
||||||
|
- kishor kunal raj <68464660+kishorkunal-raj@users.noreply.github.com>
|
||||||
|
- kyledinh <kyledinh@gmail.com>
|
||||||
|
- lantw44 <lantw44@gmail.com>
|
||||||
|
- likakuli <1154584512@qq.com>
|
||||||
|
- linfangrong <linfangrong.liuxin@qq.com>
|
||||||
|
- linzi <873804682@qq.com>
|
||||||
|
- llgoer <yanghuxiao@vip.qq.com>
|
||||||
|
- long-road <13412081338@163.com>
|
||||||
|
- mbesancon <mathieu.besancon@gmail.com>
|
||||||
|
- mehdy <mehdy.khoshnoody@gmail.com>
|
||||||
|
- metal A-wing <freedom.awing.777@gmail.com>
|
||||||
|
- micanzhang <micanzhang@gmail.com>
|
||||||
|
- minarc <ragnhildmowinckel@gmail.com>
|
||||||
|
- mllu <mornlyn@gmail.com>
|
||||||
|
- mopemoepe <yutaka.matsubara@gmail.com>
|
||||||
|
- msoedov <msoedov@gmail.com>
|
||||||
|
- mstmdev <mstmdev@gmail.com>
|
||||||
|
- novaeye <fcoffee@gmail.com>
|
||||||
|
- olebedev <oolebedev@gmail.com>
|
||||||
|
- phithon <phith0n@users.noreply.github.com>
|
||||||
|
- pjgg <pablo.gonzalez.granados@gmail.com>
|
||||||
|
- qm012 <67568757+qm012@users.noreply.github.com>
|
||||||
|
- raymonder jin <rayjingithub@gmail.com>
|
||||||
|
- rns <ruslan.shvedov@gmail.com>
|
||||||
|
- root@andrea:~# <crypto.andrea@protonmail.ch>
|
||||||
|
- sekky0905 <20237968+sekky0905@users.noreply.github.com>
|
||||||
|
- senhtry <w169q169@gmail.com>
|
||||||
|
- shadrus <shadrus@gmail.com>
|
||||||
|
- silasb <silas.baronda@gmail.com>
|
||||||
|
- solos <lxl1217@gmail.com>
|
||||||
|
- songjiayang <songjiayang@users.noreply.github.com>
|
||||||
|
- sope <shenshouer@163.com>
|
||||||
|
- srt180 <30768686+srt180@users.noreply.github.com>
|
||||||
|
- stackerzzq <foo_stacker@yeah.net>
|
||||||
|
- sunshineplan <sunshineplan@users.noreply.github.com>
|
||||||
|
- syssam <s.y.s.sam.sys@gmail.com>
|
||||||
|
- techjanitor <puntme@gmail.com>
|
||||||
|
- techjanitor <techjanitor@users.noreply.github.com>
|
||||||
|
- thinkerou <thinkerou@gmail.com>
|
||||||
|
- thinkgo <49174849+thinkgos@users.noreply.github.com>
|
||||||
|
- tsirolnik <tsirolnik@users.noreply.github.com>
|
||||||
|
- tyltr <31768692+tylitianrui@users.noreply.github.com>
|
||||||
|
- vinhha96 <anhvinha1@gmail.com>
|
||||||
|
- voidman <retmain@foxmail.com>
|
||||||
|
- vz <vzvway@gmail.com>
|
||||||
|
- wei <wei840222@gmail.com>
|
||||||
|
- weibaohui <weibaohui@yeah.net>
|
||||||
|
- whirosan <whirosan@users.noreply.github.com>
|
||||||
|
- willnewrelic <will@newrelic.com>
|
||||||
|
- wssccc <wssccc@qq.com>
|
||||||
|
- wuhuizuo <wuhuizuo@126.com>
|
||||||
|
- xyb <xyb4638@gmail.com>
|
||||||
|
- y-yagi <yuuji.yaginuma@gmail.com>
|
||||||
|
- yiranzai <wuqingdzx@gmail.com>
|
||||||
|
- youzeliang <youzel@126.com>
|
||||||
|
- yugu <chenzilong_1227@foxmail.com>
|
||||||
|
- yuyabe <yuyabee@gmail.com>
|
||||||
|
- zebozhuang <zebozhuang@163.com>
|
||||||
|
- zero11-0203 <93071220+zero11-0203@users.noreply.github.com>
|
||||||
|
- zesani <7sin@outlook.co.th>
|
||||||
|
- zhanweidu <zhanweidu@163.com>
|
||||||
|
- zhing <zqwillseven@gmail.com>
|
||||||
|
- ziheng <zihenglv@gmail.com>
|
||||||
|
- zzjin <zzjin@users.noreply.github.com>
|
||||||
|
- 森 優太 <59682979+uta-mori@users.noreply.github.com>
|
||||||
|
- 杰哥 <858806258@qq.com>
|
||||||
|
- 涛叔 <hi@taoshu.in>
|
||||||
|
- 市民233 <mengrenxiong@gmail.com>
|
||||||
|
- 尹宝强 <wmdandme@gmail.com>
|
||||||
|
- 梦溪笔谈 <loongmxbt@gmail.com>
|
||||||
|
- 飞雪无情 <ls8707@gmail.com>
|
||||||
|
- 寻寻觅觅的Gopher <zoujh99@qq.com>
|
||||||
|
|||||||
1116
BENCHMARKS.md
1116
BENCHMARKS.md
File diff suppressed because it is too large
Load Diff
428
CHANGELOG.md
428
CHANGELOG.md
@ -1,8 +1,398 @@
|
|||||||
|
# Gin ChangeLog
|
||||||
|
|
||||||
### Gin 1.4.0
|
## Gin v1.10.0
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* feat(auth): add proxy-server authentication (#3877) (@EndlessParadox1)
|
||||||
|
* feat(bind): ShouldBindBodyWith shortcut and change doc (#3871) (@RedCrazyGhost)
|
||||||
|
* feat(binding): Support custom BindUnmarshaler for binding. (#3933) (@dkkb)
|
||||||
|
* feat(binding): support override default binding implement (#3514) (@ssfyn)
|
||||||
|
* feat(engine): Added `OptionFunc` and `With` (#3572) (@flc1125)
|
||||||
|
* feat(logger): ability to skip logs based on user-defined logic (#3593) (@palvaneh)
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
|
||||||
|
* Revert "fix(uri): query binding bug (#3236)" (#3899) (@appleboy)
|
||||||
|
* fix(binding): binding error while not upload file (#3819) (#3820) (@clearcodecn)
|
||||||
|
* fix(binding): dereference pointer to struct (#3199) (@echovl)
|
||||||
|
* fix(context): make context Value method adhere to Go standards (#3897) (@FarmerChillax)
|
||||||
|
* fix(engine): fix unit test (#3878) (@flc1125)
|
||||||
|
* fix(header): Allow header according to RFC 7231 (HTTP 405) (#3759) (@Crocmagnon)
|
||||||
|
* fix(route): Add fullPath in context copy (#3784) (@KarthikReddyPuli)
|
||||||
|
* fix(router): catch-all conflicting wildcard (#3812) (@FirePing32)
|
||||||
|
* fix(sec): upgrade golang.org/x/crypto to 0.17.0 (#3832) (@chncaption)
|
||||||
|
* fix(tree): correctly expand the capacity of params (#3502) (@georgijd-form3)
|
||||||
|
* fix(uri): query binding bug (#3236) (@illiafox)
|
||||||
|
* fix: Add pointer support for url query params (#3659) (#3666) (@omkar-foss)
|
||||||
|
* fix: protect Context.Keys map when call Copy method (#3873) (@kingcanfish)
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
* chore(CI): update release args (#3595) (@qloog)
|
||||||
|
* chore(IP): add TrustedPlatform constant for Fly.io. (#3839) (@ab)
|
||||||
|
* chore(debug): add ability to override the debugPrint statement (#2337) (@josegonzalez)
|
||||||
|
* chore(deps): update dependencies to latest versions (#3835) (@appleboy)
|
||||||
|
* chore(header): Add support for RFC 9512: application/yaml (#3851) (@vincentbernat)
|
||||||
|
* chore(http): use white color for HTTP 1XX (#3741) (@viralparmarme)
|
||||||
|
* chore(optimize): the ShouldBindUri method of the Context struct (#3911) (@1911860538)
|
||||||
|
* chore(perf): Optimize the Copy method of the Context struct (#3859) (@1911860538)
|
||||||
|
* chore(refactor): modify interface check way (#3855) (@demoManito)
|
||||||
|
* chore(request): check reader if it's nil before reading (#3419) (@noahyao1024)
|
||||||
|
* chore(security): upgrade Protobuf for CVE-2024-24786 (#3893) (@Fotkurz)
|
||||||
|
* chore: refactor CI and update dependencies (#3848) (@appleboy)
|
||||||
|
* chore: refactor configuration files for better readability (#3951) (@appleboy)
|
||||||
|
* chore: update GitHub Actions configuration (#3792) (@appleboy)
|
||||||
|
* chore: update changelog categories and improve documentation (#3917) (@appleboy)
|
||||||
|
* chore: update dependencies to latest versions (#3694) (@appleboy)
|
||||||
|
* chore: update external dependencies to latest versions (#3950) (@appleboy)
|
||||||
|
* chore: update various Go dependencies to latest versions (#3901) (@appleboy)
|
||||||
|
|
||||||
|
### Build process updates
|
||||||
|
|
||||||
|
* build(codecov): Added a codecov configuration (#3891) (@flc1125)
|
||||||
|
* ci(Makefile): vet command add .PHONY (#3915) (@imalasong)
|
||||||
|
* ci(lint): update tooling and workflows for consistency (#3834) (@appleboy)
|
||||||
|
* ci(release): refactor changelog regex patterns and exclusions (#3914) (@appleboy)
|
||||||
|
* ci(testing): add go1.22 version (#3842) (@appleboy)
|
||||||
|
|
||||||
|
### Documentation updates
|
||||||
|
|
||||||
|
* docs(context): Added deprecation comments to BindWith (#3880) (@flc1125)
|
||||||
|
* docs(middleware): comments to function `BasicAuthForProxy` (#3881) (@EndlessParadox1)
|
||||||
|
* docs: Add document to constant `AuthProxyUserKey` and `BasicAuthForProxy`. (#3887) (@EndlessParadox1)
|
||||||
|
* docs: fix typo in comment (#3868) (@testwill)
|
||||||
|
* docs: fix typo in function documentation (#3872) (@TotomiEcio)
|
||||||
|
* docs: remove redundant comments (#3765) (@WeiTheShinobi)
|
||||||
|
* feat: update version constant to v1.10.0 (#3952) (@appleboy)
|
||||||
|
|
||||||
|
### Others
|
||||||
|
|
||||||
|
* Upgrade golang.org/x/net -> v0.13.0 (#3684) (@cpcf)
|
||||||
|
* test(git): gitignore add develop tools (#3370) (@demoManito)
|
||||||
|
* test(http): use constant instead of numeric literal (#3863) (@testwill)
|
||||||
|
* test(path): Optimize unit test execution results (#3883) (@flc1125)
|
||||||
|
* test(render): increased unit tests coverage (#3691) (@araujo88)
|
||||||
|
|
||||||
|
## Gin v1.9.1
|
||||||
|
|
||||||
|
### BUG FIXES
|
||||||
|
|
||||||
|
* fix Request.Context() checks [#3512](https://github.com/gin-gonic/gin/pull/3512)
|
||||||
|
|
||||||
|
### SECURITY
|
||||||
|
|
||||||
|
* fix lack of escaping of filename in Content-Disposition [#3556](https://github.com/gin-gonic/gin/pull/3556)
|
||||||
|
|
||||||
|
### ENHANCEMENTS
|
||||||
|
|
||||||
|
* refactor: use bytes.ReplaceAll directly [#3455](https://github.com/gin-gonic/gin/pull/3455)
|
||||||
|
* convert strings and slices using the officially recommended way [#3344](https://github.com/gin-gonic/gin/pull/3344)
|
||||||
|
* improve render code coverage [#3525](https://github.com/gin-gonic/gin/pull/3525)
|
||||||
|
|
||||||
|
### DOCS
|
||||||
|
|
||||||
|
* docs: changed documentation link for trusted proxies [#3575](https://github.com/gin-gonic/gin/pull/3575)
|
||||||
|
* chore: improve linting, testing, and GitHub Actions setup [#3583](https://github.com/gin-gonic/gin/pull/3583)
|
||||||
|
|
||||||
|
## Gin v1.9.0
|
||||||
|
|
||||||
|
### BREAK CHANGES
|
||||||
|
|
||||||
|
* Stop useless panicking in context and render [#2150](https://github.com/gin-gonic/gin/pull/2150)
|
||||||
|
|
||||||
|
### BUG FIXES
|
||||||
|
|
||||||
|
* fix(router): tree bug where loop index is not decremented. [#3460](https://github.com/gin-gonic/gin/pull/3460)
|
||||||
|
* fix(context): panic on NegotiateFormat - index out of range [#3397](https://github.com/gin-gonic/gin/pull/3397)
|
||||||
|
* Add escape logic for header [#3500](https://github.com/gin-gonic/gin/pull/3500) and [#3503](https://github.com/gin-gonic/gin/pull/3503)
|
||||||
|
|
||||||
|
### SECURITY
|
||||||
|
|
||||||
|
* Fix the GO-2022-0969 and GO-2022-0288 vulnerabilities [#3333](https://github.com/gin-gonic/gin/pull/3333)
|
||||||
|
* fix(security): vulnerability GO-2023-1571 [#3505](https://github.com/gin-gonic/gin/pull/3505)
|
||||||
|
|
||||||
|
### ENHANCEMENTS
|
||||||
|
|
||||||
|
* feat: add sonic json support [#3184](https://github.com/gin-gonic/gin/pull/3184)
|
||||||
|
* chore(file): Creates a directory named path [#3316](https://github.com/gin-gonic/gin/pull/3316)
|
||||||
|
* fix: modify interface check way [#3327](https://github.com/gin-gonic/gin/pull/3327)
|
||||||
|
* remove deprecated of package io/ioutil [#3395](https://github.com/gin-gonic/gin/pull/3395)
|
||||||
|
* refactor: avoid calling strings.ToLower twice [#3343](https://github.com/gin-gonic/gin/pull/3433)
|
||||||
|
* console logger HTTP status code bug fixed [#3453](https://github.com/gin-gonic/gin/pull/3453)
|
||||||
|
* chore(yaml): upgrade dependency to v3 version [#3456](https://github.com/gin-gonic/gin/pull/3456)
|
||||||
|
* chore(router): match method added to routergroup for multiple HTTP methods supporting [#3464](https://github.com/gin-gonic/gin/pull/3464)
|
||||||
|
* chore(http): add support for go1.20 http.rwUnwrapper to gin.responseWriter [#3489](https://github.com/gin-gonic/gin/pull/3489)
|
||||||
|
|
||||||
|
### DOCS
|
||||||
|
|
||||||
|
* docs: update markdown format [#3260](https://github.com/gin-gonic/gin/pull/3260)
|
||||||
|
* docs(readme): Add the TOML rendering example [#3400](https://github.com/gin-gonic/gin/pull/3400)
|
||||||
|
* docs(readme): move more example to docs/doc.md [#3449](https://github.com/gin-gonic/gin/pull/3449)
|
||||||
|
* docs: update markdown format [#3446](https://github.com/gin-gonic/gin/pull/3446)
|
||||||
|
|
||||||
|
## Gin v1.8.2
|
||||||
|
|
||||||
|
### BUG FIXES
|
||||||
|
|
||||||
|
* fix(route): redirectSlash bug ([#3227]((https://github.com/gin-gonic/gin/pull/3227)))
|
||||||
|
* fix(engine): missing route params for CreateTestContext ([#2778]((https://github.com/gin-gonic/gin/pull/2778))) ([#2803]((https://github.com/gin-gonic/gin/pull/2803)))
|
||||||
|
|
||||||
|
### SECURITY
|
||||||
|
|
||||||
|
* Fix the GO-2022-1144 vulnerability ([#3432]((https://github.com/gin-gonic/gin/pull/3432)))
|
||||||
|
|
||||||
|
## Gin v1.8.1
|
||||||
|
|
||||||
|
### ENHANCEMENTS
|
||||||
|
|
||||||
|
* feat(context): add ContextWithFallback feature flag [#3172](https://github.com/gin-gonic/gin/pull/3172)
|
||||||
|
|
||||||
|
## Gin v1.8.0
|
||||||
|
|
||||||
|
### BREAK CHANGES
|
||||||
|
|
||||||
|
* TrustedProxies: Add default IPv6 support and refactor [#2967](https://github.com/gin-gonic/gin/pull/2967). Please replace `RemoteIP() (net.IP, bool)` with `RemoteIP() net.IP`
|
||||||
|
* gin.Context with fallback value from gin.Context.Request.Context() [#2751](https://github.com/gin-gonic/gin/pull/2751)
|
||||||
|
|
||||||
|
### BUG FIXES
|
||||||
|
|
||||||
|
* Fixed SetOutput() panics on go 1.17 [#2861](https://github.com/gin-gonic/gin/pull/2861)
|
||||||
|
* Fix: wrong when wildcard follows named param [#2983](https://github.com/gin-gonic/gin/pull/2983)
|
||||||
|
* Fix: missing sameSite when do context.reset() [#3123](https://github.com/gin-gonic/gin/pull/3123)
|
||||||
|
|
||||||
|
### ENHANCEMENTS
|
||||||
|
|
||||||
|
* Use Header() instead of deprecated HeaderMap [#2694](https://github.com/gin-gonic/gin/pull/2694)
|
||||||
|
* RouterGroup.Handle regular match optimization of http method [#2685](https://github.com/gin-gonic/gin/pull/2685)
|
||||||
|
* Add support go-json, another drop-in json replacement [#2680](https://github.com/gin-gonic/gin/pull/2680)
|
||||||
|
* Use errors.New to replace fmt.Errorf will much better [#2707](https://github.com/gin-gonic/gin/pull/2707)
|
||||||
|
* Use Duration.Truncate for truncating precision [#2711](https://github.com/gin-gonic/gin/pull/2711)
|
||||||
|
* Get client IP when using Cloudflare [#2723](https://github.com/gin-gonic/gin/pull/2723)
|
||||||
|
* Optimize code adjust [#2700](https://github.com/gin-gonic/gin/pull/2700/files)
|
||||||
|
* Optimize code and reduce code cyclomatic complexity [#2737](https://github.com/gin-gonic/gin/pull/2737)
|
||||||
|
* Improve sliceValidateError.Error performance [#2765](https://github.com/gin-gonic/gin/pull/2765)
|
||||||
|
* Support custom struct tag [#2720](https://github.com/gin-gonic/gin/pull/2720)
|
||||||
|
* Improve router group tests [#2787](https://github.com/gin-gonic/gin/pull/2787)
|
||||||
|
* Fallback Context.Deadline() Context.Done() Context.Err() to Context.Request.Context() [#2769](https://github.com/gin-gonic/gin/pull/2769)
|
||||||
|
* Some codes optimize [#2830](https://github.com/gin-gonic/gin/pull/2830) [#2834](https://github.com/gin-gonic/gin/pull/2834) [#2838](https://github.com/gin-gonic/gin/pull/2838) [#2837](https://github.com/gin-gonic/gin/pull/2837) [#2788](https://github.com/gin-gonic/gin/pull/2788) [#2848](https://github.com/gin-gonic/gin/pull/2848) [#2851](https://github.com/gin-gonic/gin/pull/2851) [#2701](https://github.com/gin-gonic/gin/pull/2701)
|
||||||
|
* TrustedProxies: Add default IPv6 support and refactor [#2967](https://github.com/gin-gonic/gin/pull/2967)
|
||||||
|
* Test(route): expose performRequest func [#3012](https://github.com/gin-gonic/gin/pull/3012)
|
||||||
|
* Support h2c with prior knowledge [#1398](https://github.com/gin-gonic/gin/pull/1398)
|
||||||
|
* Feat attachment filename support utf8 [#3071](https://github.com/gin-gonic/gin/pull/3071)
|
||||||
|
* Feat: add StaticFileFS [#2749](https://github.com/gin-gonic/gin/pull/2749)
|
||||||
|
* Feat(context): return GIN Context from Value method [#2825](https://github.com/gin-gonic/gin/pull/2825)
|
||||||
|
* Feat: automatically SetMode to TestMode when run go test [#3139](https://github.com/gin-gonic/gin/pull/3139)
|
||||||
|
* Add TOML bining for gin [#3081](https://github.com/gin-gonic/gin/pull/3081)
|
||||||
|
* IPv6 add default trusted proxies [#3033](https://github.com/gin-gonic/gin/pull/3033)
|
||||||
|
|
||||||
|
### DOCS
|
||||||
|
|
||||||
|
* Add note about nomsgpack tag to the readme [#2703](https://github.com/gin-gonic/gin/pull/2703)
|
||||||
|
|
||||||
|
## Gin v1.7.7
|
||||||
|
|
||||||
|
### BUG FIXES
|
||||||
|
|
||||||
|
* Fixed X-Forwarded-For unsafe handling of CVE-2020-28483 [#2844](https://github.com/gin-gonic/gin/pull/2844), closed issue [#2862](https://github.com/gin-gonic/gin/issues/2862).
|
||||||
|
* Tree: updated the code logic for `latestNode` [#2897](https://github.com/gin-gonic/gin/pull/2897), closed issue [#2894](https://github.com/gin-gonic/gin/issues/2894) [#2878](https://github.com/gin-gonic/gin/issues/2878).
|
||||||
|
* Tree: fixed the misplacement of adding slashes [#2847](https://github.com/gin-gonic/gin/pull/2847), closed issue [#2843](https://github.com/gin-gonic/gin/issues/2843).
|
||||||
|
* Tree: fixed tsr with mixed static and wildcard paths [#2924](https://github.com/gin-gonic/gin/pull/2924), closed issue [#2918](https://github.com/gin-gonic/gin/issues/2918).
|
||||||
|
|
||||||
|
### ENHANCEMENTS
|
||||||
|
|
||||||
|
* TrustedProxies: make it backward-compatible [#2887](https://github.com/gin-gonic/gin/pull/2887), closed issue [#2819](https://github.com/gin-gonic/gin/issues/2819).
|
||||||
|
* TrustedPlatform: provide custom options for another CDN services [#2906](https://github.com/gin-gonic/gin/pull/2906).
|
||||||
|
|
||||||
|
### DOCS
|
||||||
|
|
||||||
|
* NoMethod: added usage annotation ([#2832](https://github.com/gin-gonic/gin/pull/2832#issuecomment-929954463)).
|
||||||
|
|
||||||
|
## Gin v1.7.6
|
||||||
|
|
||||||
|
### BUG FIXES
|
||||||
|
|
||||||
|
* bump new release to fix v1.7.5 release error by using v1.7.4 codes.
|
||||||
|
|
||||||
|
## Gin v1.7.4
|
||||||
|
|
||||||
|
### BUG FIXES
|
||||||
|
|
||||||
|
* bump new release to fix checksum mismatch
|
||||||
|
|
||||||
|
## Gin v1.7.3
|
||||||
|
|
||||||
|
### BUG FIXES
|
||||||
|
|
||||||
|
* fix level 1 router match [#2767](https://github.com/gin-gonic/gin/issues/2767), [#2796](https://github.com/gin-gonic/gin/issues/2796)
|
||||||
|
|
||||||
|
## Gin v1.7.2
|
||||||
|
|
||||||
|
### BUG FIXES
|
||||||
|
|
||||||
|
* Fix conflict between param and exact path [#2706](https://github.com/gin-gonic/gin/issues/2706). Close issue [#2682](https://github.com/gin-gonic/gin/issues/2682) [#2696](https://github.com/gin-gonic/gin/issues/2696).
|
||||||
|
|
||||||
|
## Gin v1.7.1
|
||||||
|
|
||||||
|
### BUG FIXES
|
||||||
|
|
||||||
|
* fix: data race with trustedCIDRs from [#2674](https://github.com/gin-gonic/gin/issues/2674)([#2675](https://github.com/gin-gonic/gin/pull/2675))
|
||||||
|
|
||||||
|
## Gin v1.7.0
|
||||||
|
|
||||||
|
### BUG FIXES
|
||||||
|
|
||||||
|
* fix compile error from [#2572](https://github.com/gin-gonic/gin/pull/2572) ([#2600](https://github.com/gin-gonic/gin/pull/2600))
|
||||||
|
* fix: print headers without Authorization header on broken pipe ([#2528](https://github.com/gin-gonic/gin/pull/2528))
|
||||||
|
* fix(tree): reassign fullpath when register new node ([#2366](https://github.com/gin-gonic/gin/pull/2366))
|
||||||
|
|
||||||
|
### ENHANCEMENTS
|
||||||
|
|
||||||
|
* Support params and exact routes without creating conflicts ([#2663](https://github.com/gin-gonic/gin/pull/2663))
|
||||||
|
* chore: improve render string performance ([#2365](https://github.com/gin-gonic/gin/pull/2365))
|
||||||
|
* Sync route tree to httprouter latest code ([#2368](https://github.com/gin-gonic/gin/pull/2368))
|
||||||
|
* chore: rename getQueryCache/getFormCache to initQueryCache/initFormCa ([#2375](https://github.com/gin-gonic/gin/pull/2375))
|
||||||
|
* chore(performance): improve countParams ([#2378](https://github.com/gin-gonic/gin/pull/2378))
|
||||||
|
* Remove some functions that have the same effect as the bytes package ([#2387](https://github.com/gin-gonic/gin/pull/2387))
|
||||||
|
* update:SetMode function ([#2321](https://github.com/gin-gonic/gin/pull/2321))
|
||||||
|
* remove an unused type SecureJSONPrefix ([#2391](https://github.com/gin-gonic/gin/pull/2391))
|
||||||
|
* Add a redirect sample for POST method ([#2389](https://github.com/gin-gonic/gin/pull/2389))
|
||||||
|
* Add CustomRecovery builtin middleware ([#2322](https://github.com/gin-gonic/gin/pull/2322))
|
||||||
|
* binding: avoid 2038 problem on 32-bit architectures ([#2450](https://github.com/gin-gonic/gin/pull/2450))
|
||||||
|
* Prevent panic in Context.GetQuery() when there is no Request ([#2412](https://github.com/gin-gonic/gin/pull/2412))
|
||||||
|
* Add GetUint and GetUint64 method on gin.context ([#2487](https://github.com/gin-gonic/gin/pull/2487))
|
||||||
|
* update content-disposition header to MIME-style ([#2512](https://github.com/gin-gonic/gin/pull/2512))
|
||||||
|
* reduce allocs and improve the render `WriteString` ([#2508](https://github.com/gin-gonic/gin/pull/2508))
|
||||||
|
* implement ".Unwrap() error" on Error type ([#2525](https://github.com/gin-gonic/gin/pull/2525)) ([#2526](https://github.com/gin-gonic/gin/pull/2526))
|
||||||
|
* Allow bind with a map[string]string ([#2484](https://github.com/gin-gonic/gin/pull/2484))
|
||||||
|
* chore: update tree ([#2371](https://github.com/gin-gonic/gin/pull/2371))
|
||||||
|
* Support binding for slice/array obj [Rewrite] ([#2302](https://github.com/gin-gonic/gin/pull/2302))
|
||||||
|
* basic auth: fix timing oracle ([#2609](https://github.com/gin-gonic/gin/pull/2609))
|
||||||
|
* Add mixed param and non-param paths (port of httprouter[#329](https://github.com/gin-gonic/gin/pull/329)) ([#2663](https://github.com/gin-gonic/gin/pull/2663))
|
||||||
|
* feat(engine): add trustedproxies and remoteIP ([#2632](https://github.com/gin-gonic/gin/pull/2632))
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
### BUG FIXES
|
||||||
|
|
||||||
|
* 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
|
||||||
|
|
||||||
|
### BUG FIXES
|
||||||
|
|
||||||
|
* 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)
|
||||||
|
|
||||||
|
### BUG FIXES
|
||||||
|
|
||||||
|
* Unix Socket Handling [#2280](https://github.com/gin-gonic/gin/pull/2280)
|
||||||
|
* Use json marshall in context json to fix breaking new line issue. Fixes #2209 [#2228](https://github.com/gin-gonic/gin/pull/2228)
|
||||||
|
* fix accept incoming network connections [#2216](https://github.com/gin-gonic/gin/pull/2216)
|
||||||
|
* Fixed a bug in the calculation of the maximum number of parameters [#2166](https://github.com/gin-gonic/gin/pull/2166)
|
||||||
|
* [FIX] allow empty headers on DataFromReader [#2121](https://github.com/gin-gonic/gin/pull/2121)
|
||||||
|
* Add mutex for protect Context.Keys map [#1391](https://github.com/gin-gonic/gin/pull/1391)
|
||||||
|
|
||||||
|
### ENHANCEMENTS
|
||||||
|
|
||||||
|
* Add mitigation for log injection [#2277](https://github.com/gin-gonic/gin/pull/2277)
|
||||||
|
* tree: range over nodes values [#2229](https://github.com/gin-gonic/gin/pull/2229)
|
||||||
|
* tree: remove duplicate assignment [#2222](https://github.com/gin-gonic/gin/pull/2222)
|
||||||
|
* chore: upgrade go-isatty and json-iterator/go [#2215](https://github.com/gin-gonic/gin/pull/2215)
|
||||||
|
* path: sync code with httprouter [#2212](https://github.com/gin-gonic/gin/pull/2212)
|
||||||
|
* Use zero-copy approach to convert types between string and byte slice [#2206](https://github.com/gin-gonic/gin/pull/2206)
|
||||||
|
* Reuse bytes when cleaning the URL paths [#2179](https://github.com/gin-gonic/gin/pull/2179)
|
||||||
|
* tree: remove one else statement [#2177](https://github.com/gin-gonic/gin/pull/2177)
|
||||||
|
* tree: sync httprouter update (#2173) (#2172) [#2171](https://github.com/gin-gonic/gin/pull/2171)
|
||||||
|
* tree: sync part httprouter codes and reduce if/else [#2163](https://github.com/gin-gonic/gin/pull/2163)
|
||||||
|
* use http method constant [#2155](https://github.com/gin-gonic/gin/pull/2155)
|
||||||
|
* upgrade go-validator to v10 [#2149](https://github.com/gin-gonic/gin/pull/2149)
|
||||||
|
* Refactor redirect request in gin.go [#1970](https://github.com/gin-gonic/gin/pull/1970)
|
||||||
|
* Add build tag nomsgpack [#1852](https://github.com/gin-gonic/gin/pull/1852)
|
||||||
|
|
||||||
|
### DOCS
|
||||||
|
|
||||||
|
* docs(path): improve comments [#2223](https://github.com/gin-gonic/gin/pull/2223)
|
||||||
|
* Renew README to fit the modification of SetCookie method [#2217](https://github.com/gin-gonic/gin/pull/2217)
|
||||||
|
* Fix spelling [#2202](https://github.com/gin-gonic/gin/pull/2202)
|
||||||
|
* Remove broken link from README. [#2198](https://github.com/gin-gonic/gin/pull/2198)
|
||||||
|
* Update docs on Context.Done(), Context.Deadline() and Context.Err() [#2196](https://github.com/gin-gonic/gin/pull/2196)
|
||||||
|
* Update validator to v10 [#2190](https://github.com/gin-gonic/gin/pull/2190)
|
||||||
|
* upgrade go-validator to v10 for README [#2189](https://github.com/gin-gonic/gin/pull/2189)
|
||||||
|
* Update to currently output [#2188](https://github.com/gin-gonic/gin/pull/2188)
|
||||||
|
* Fix "Custom Validators" example [#2186](https://github.com/gin-gonic/gin/pull/2186)
|
||||||
|
* Add project to README [#2165](https://github.com/gin-gonic/gin/pull/2165)
|
||||||
|
* docs(benchmarks): for gin v1.5 [#2153](https://github.com/gin-gonic/gin/pull/2153)
|
||||||
|
* Changed wording for clarity in README.md [#2122](https://github.com/gin-gonic/gin/pull/2122)
|
||||||
|
|
||||||
|
### MISC
|
||||||
|
|
||||||
|
* ci support go1.14 [#2262](https://github.com/gin-gonic/gin/pull/2262)
|
||||||
|
* chore: upgrade depend version [#2231](https://github.com/gin-gonic/gin/pull/2231)
|
||||||
|
* Drop support go1.10 [#2147](https://github.com/gin-gonic/gin/pull/2147)
|
||||||
|
* fix comment in `mode.go` [#2129](https://github.com/gin-gonic/gin/pull/2129)
|
||||||
|
|
||||||
|
## Gin v1.5.0
|
||||||
|
|
||||||
|
- [FIX] Use DefaultWriter and DefaultErrorWriter for debug messages [#1891](https://github.com/gin-gonic/gin/pull/1891)
|
||||||
|
- [NEW] Now you can parse the inline lowercase start structure [#1893](https://github.com/gin-gonic/gin/pull/1893)
|
||||||
|
- [FIX] Some code improvements [#1909](https://github.com/gin-gonic/gin/pull/1909)
|
||||||
|
- [FIX] Use encode replace json marshal increase json encoder speed [#1546](https://github.com/gin-gonic/gin/pull/1546)
|
||||||
|
- [NEW] Hold matched route full path in the Context [#1826](https://github.com/gin-gonic/gin/pull/1826)
|
||||||
|
- [FIX] Fix context.Params race condition on Copy() [#1841](https://github.com/gin-gonic/gin/pull/1841)
|
||||||
|
- [NEW] Add context param query cache [#1450](https://github.com/gin-gonic/gin/pull/1450)
|
||||||
|
- [FIX] Improve GetQueryMap performance [#1918](https://github.com/gin-gonic/gin/pull/1918)
|
||||||
|
- [FIX] Improve get post data [#1920](https://github.com/gin-gonic/gin/pull/1920)
|
||||||
|
- [FIX] Use context instead of x/net/context [#1922](https://github.com/gin-gonic/gin/pull/1922)
|
||||||
|
- [FIX] Attempt to fix PostForm cache bug [#1931](https://github.com/gin-gonic/gin/pull/1931)
|
||||||
|
- [NEW] Add support of multipart multi files [#1949](https://github.com/gin-gonic/gin/pull/1949)
|
||||||
|
- [NEW] Support bind http header param [#1957](https://github.com/gin-gonic/gin/pull/1957)
|
||||||
|
- [FIX] Drop support for go1.8 and go1.9 [#1933](https://github.com/gin-gonic/gin/pull/1933)
|
||||||
|
- [FIX] Bugfix for the FullPath feature [#1919](https://github.com/gin-gonic/gin/pull/1919)
|
||||||
|
- [FIX] Gin1.5 bytes.Buffer to strings.Builder [#1939](https://github.com/gin-gonic/gin/pull/1939)
|
||||||
|
- [FIX] Upgrade github.com/ugorji/go/codec [#1969](https://github.com/gin-gonic/gin/pull/1969)
|
||||||
|
- [NEW] Support bind unix time [#1980](https://github.com/gin-gonic/gin/pull/1980)
|
||||||
|
- [FIX] Simplify code [#2004](https://github.com/gin-gonic/gin/pull/2004)
|
||||||
|
- [NEW] Support negative Content-Length in DataFromReader [#1981](https://github.com/gin-gonic/gin/pull/1981)
|
||||||
|
- [FIX] Identify terminal on a RISC-V architecture for auto-colored logs [#2019](https://github.com/gin-gonic/gin/pull/2019)
|
||||||
|
- [BREAKING] `Context.JSONP()` now expects a semicolon (`;`) at the end [#2007](https://github.com/gin-gonic/gin/pull/2007)
|
||||||
|
- [BREAKING] Upgrade default `binding.Validator` to v9 (see [its changelog](https://github.com/go-playground/validator/releases/tag/v9.0.0)) [#1015](https://github.com/gin-gonic/gin/pull/1015)
|
||||||
|
- [NEW] Add `DisallowUnknownFields()` in `Context.BindJSON()` [#2028](https://github.com/gin-gonic/gin/pull/2028)
|
||||||
|
- [NEW] Use specific `net.Listener` with `Engine.RunListener()` [#2023](https://github.com/gin-gonic/gin/pull/2023)
|
||||||
|
- [FIX] Fix some typo [#2079](https://github.com/gin-gonic/gin/pull/2079) [#2080](https://github.com/gin-gonic/gin/pull/2080)
|
||||||
|
- [FIX] Relocate binding body tests [#2086](https://github.com/gin-gonic/gin/pull/2086)
|
||||||
|
- [FIX] Use Writer in Context.Status [#1606](https://github.com/gin-gonic/gin/pull/1606)
|
||||||
|
- [FIX] `Engine.RunUnix()` now returns the error if it can't change the file mode [#2093](https://github.com/gin-gonic/gin/pull/2093)
|
||||||
|
- [FIX] `RouterGroup.StaticFS()` leaked files. Now it closes them. [#2118](https://github.com/gin-gonic/gin/pull/2118)
|
||||||
|
- [FIX] `Context.Request.FormFile` leaked file. Now it closes it. [#2114](https://github.com/gin-gonic/gin/pull/2114)
|
||||||
|
- [FIX] Ignore walking on `form:"-"` mapping [#1943](https://github.com/gin-gonic/gin/pull/1943)
|
||||||
|
|
||||||
|
### Gin v1.4.0
|
||||||
|
|
||||||
- [NEW] Support for [Go Modules](https://github.com/golang/go/wiki/Modules) [#1569](https://github.com/gin-gonic/gin/pull/1569)
|
- [NEW] Support for [Go Modules](https://github.com/golang/go/wiki/Modules) [#1569](https://github.com/gin-gonic/gin/pull/1569)
|
||||||
- [NEW] Refactor of form mapping multipart requesta [#1829](https://github.com/gin-gonic/gin/pull/1829)
|
- [NEW] Refactor of form mapping multipart request [#1829](https://github.com/gin-gonic/gin/pull/1829)
|
||||||
- [FIX] Truncate Latency precision in long running request [#1830](https://github.com/gin-gonic/gin/pull/1830)
|
- [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)
|
- [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] Supporting file binding [#1264](https://github.com/gin-gonic/gin/pull/1264)
|
||||||
@ -15,7 +405,7 @@
|
|||||||
- [NEW] Refactor form mappings [#1749](https://github.com/gin-gonic/gin/pull/1749)
|
- [NEW] Refactor form mappings [#1749](https://github.com/gin-gonic/gin/pull/1749)
|
||||||
- [NEW] Added flag to context.Stream indicates if client disconnected in middle of stream [#1252](https://github.com/gin-gonic/gin/pull/1252)
|
- [NEW] Added flag to context.Stream indicates if client disconnected in middle of stream [#1252](https://github.com/gin-gonic/gin/pull/1252)
|
||||||
- [FIX] Moved [examples](https://github.com/gin-gonic/examples) to stand alone Repo [#1775](https://github.com/gin-gonic/gin/pull/1775)
|
- [FIX] Moved [examples](https://github.com/gin-gonic/examples) to stand alone Repo [#1775](https://github.com/gin-gonic/gin/pull/1775)
|
||||||
- [NEW] Extend context.File to allow for the content-dispositon attachments via a new method context.Attachment [#1260](https://github.com/gin-gonic/gin/pull/1260)
|
- [NEW] Extend context.File to allow for the content-disposition attachments via a new method context.Attachment [#1260](https://github.com/gin-gonic/gin/pull/1260)
|
||||||
- [FIX] Support HTTP content negotiation wildcards [#1112](https://github.com/gin-gonic/gin/pull/1112)
|
- [FIX] Support HTTP content negotiation wildcards [#1112](https://github.com/gin-gonic/gin/pull/1112)
|
||||||
- [NEW] Add prefix from X-Forwarded-Prefix in redirectTrailingSlash [#1238](https://github.com/gin-gonic/gin/pull/1238)
|
- [NEW] Add prefix from X-Forwarded-Prefix in redirectTrailingSlash [#1238](https://github.com/gin-gonic/gin/pull/1238)
|
||||||
- [FIX] context.Copy() race condition [#1020](https://github.com/gin-gonic/gin/pull/1020)
|
- [FIX] context.Copy() race condition [#1020](https://github.com/gin-gonic/gin/pull/1020)
|
||||||
@ -56,7 +446,7 @@
|
|||||||
- [NEW] Upgrade dependency libraries [#1491](https://github.com/gin-gonic/gin/pull/1491)
|
- [NEW] Upgrade dependency libraries [#1491](https://github.com/gin-gonic/gin/pull/1491)
|
||||||
|
|
||||||
|
|
||||||
### Gin 1.3.0
|
## Gin v1.3.0
|
||||||
|
|
||||||
- [NEW] Add [`func (*Context) QueryMap`](https://godoc.org/github.com/gin-gonic/gin#Context.QueryMap), [`func (*Context) GetQueryMap`](https://godoc.org/github.com/gin-gonic/gin#Context.GetQueryMap), [`func (*Context) PostFormMap`](https://godoc.org/github.com/gin-gonic/gin#Context.PostFormMap) and [`func (*Context) GetPostFormMap`](https://godoc.org/github.com/gin-gonic/gin#Context.GetPostFormMap) to support `type map[string]string` as query string or form parameters, see [#1383](https://github.com/gin-gonic/gin/pull/1383)
|
- [NEW] Add [`func (*Context) QueryMap`](https://godoc.org/github.com/gin-gonic/gin#Context.QueryMap), [`func (*Context) GetQueryMap`](https://godoc.org/github.com/gin-gonic/gin#Context.GetQueryMap), [`func (*Context) PostFormMap`](https://godoc.org/github.com/gin-gonic/gin#Context.PostFormMap) and [`func (*Context) GetPostFormMap`](https://godoc.org/github.com/gin-gonic/gin#Context.GetPostFormMap) to support `type map[string]string` as query string or form parameters, see [#1383](https://github.com/gin-gonic/gin/pull/1383)
|
||||||
- [NEW] Add [`func (*Context) AsciiJSON`](https://godoc.org/github.com/gin-gonic/gin#Context.AsciiJSON), see [#1358](https://github.com/gin-gonic/gin/pull/1358)
|
- [NEW] Add [`func (*Context) AsciiJSON`](https://godoc.org/github.com/gin-gonic/gin#Context.AsciiJSON), see [#1358](https://github.com/gin-gonic/gin/pull/1358)
|
||||||
@ -78,7 +468,7 @@
|
|||||||
- [FIX] Gin Mode `""` when calling [`func Mode`](https://godoc.org/github.com/gin-gonic/gin#Mode) now returns `const DebugMode`, see [#1250](https://github.com/gin-gonic/gin/pull/1250)
|
- [FIX] Gin Mode `""` when calling [`func Mode`](https://godoc.org/github.com/gin-gonic/gin#Mode) now returns `const DebugMode`, see [#1250](https://github.com/gin-gonic/gin/pull/1250)
|
||||||
- [FIX] `Flush()` now doesn't overwrite `responseWriter` status code, see [#1460](https://github.com/gin-gonic/gin/pull/1460)
|
- [FIX] `Flush()` now doesn't overwrite `responseWriter` status code, see [#1460](https://github.com/gin-gonic/gin/pull/1460)
|
||||||
|
|
||||||
### Gin 1.2.0
|
## Gin 1.2.0
|
||||||
|
|
||||||
- [NEW] Switch from godeps to govendor
|
- [NEW] Switch from godeps to govendor
|
||||||
- [NEW] Add support for Let's Encrypt via gin-gonic/autotls
|
- [NEW] Add support for Let's Encrypt via gin-gonic/autotls
|
||||||
@ -101,15 +491,15 @@
|
|||||||
- [FIX] Use X-Forwarded-For before X-Real-Ip
|
- [FIX] Use X-Forwarded-For before X-Real-Ip
|
||||||
- [FIX] time.Time binding (#904)
|
- [FIX] time.Time binding (#904)
|
||||||
|
|
||||||
### Gin 1.1.4
|
## Gin 1.1.4
|
||||||
|
|
||||||
- [NEW] Support google appengine for IsTerminal func
|
- [NEW] Support google appengine for IsTerminal func
|
||||||
|
|
||||||
### Gin 1.1.3
|
## Gin 1.1.3
|
||||||
|
|
||||||
- [FIX] Reverted Logger: skip ANSI color commands
|
- [FIX] Reverted Logger: skip ANSI color commands
|
||||||
|
|
||||||
### Gin 1.1
|
## Gin 1.1
|
||||||
|
|
||||||
- [NEW] Implement QueryArray and PostArray methods
|
- [NEW] Implement QueryArray and PostArray methods
|
||||||
- [NEW] Refactor GetQuery and GetPostForm
|
- [NEW] Refactor GetQuery and GetPostForm
|
||||||
@ -119,7 +509,7 @@
|
|||||||
- [FIX] Changed imports to gopkg instead of github in README (#733)
|
- [FIX] Changed imports to gopkg instead of github in README (#733)
|
||||||
- [FIX] Logger: skip ANSI color commands if output is not a tty
|
- [FIX] Logger: skip ANSI color commands if output is not a tty
|
||||||
|
|
||||||
### Gin 1.0rc2 (...)
|
## Gin 1.0rc2 (...)
|
||||||
|
|
||||||
- [PERFORMANCE] Fast path for writing Content-Type.
|
- [PERFORMANCE] Fast path for writing Content-Type.
|
||||||
- [PERFORMANCE] Much faster 404 routing
|
- [PERFORMANCE] Much faster 404 routing
|
||||||
@ -154,7 +544,7 @@
|
|||||||
- [FIX] MIT license in every file
|
- [FIX] MIT license in every file
|
||||||
|
|
||||||
|
|
||||||
### Gin 1.0rc1 (May 22, 2015)
|
## Gin 1.0rc1 (May 22, 2015)
|
||||||
|
|
||||||
- [PERFORMANCE] Zero allocation router
|
- [PERFORMANCE] Zero allocation router
|
||||||
- [PERFORMANCE] Faster JSON, XML and text rendering
|
- [PERFORMANCE] Faster JSON, XML and text rendering
|
||||||
@ -162,7 +552,7 @@
|
|||||||
- [PERFORMANCE] Misc code optimizations. Inlining, tail call optimizations
|
- [PERFORMANCE] Misc code optimizations. Inlining, tail call optimizations
|
||||||
- [NEW] Built-in support for golang.org/x/net/context
|
- [NEW] Built-in support for golang.org/x/net/context
|
||||||
- [NEW] Any(path, handler). Create a route that matches any path
|
- [NEW] Any(path, handler). Create a route that matches any path
|
||||||
- [NEW] Refactored rendering pipeline (faster and static typeded)
|
- [NEW] Refactored rendering pipeline (faster and static typed)
|
||||||
- [NEW] Refactored errors API
|
- [NEW] Refactored errors API
|
||||||
- [NEW] IndentedJSON() prints pretty JSON
|
- [NEW] IndentedJSON() prints pretty JSON
|
||||||
- [NEW] Added gin.DefaultWriter
|
- [NEW] Added gin.DefaultWriter
|
||||||
@ -198,7 +588,7 @@
|
|||||||
- [FIX] Better support for Google App Engine (using log instead of fmt)
|
- [FIX] Better support for Google App Engine (using log instead of fmt)
|
||||||
|
|
||||||
|
|
||||||
### Gin 0.6 (Mar 9, 2015)
|
## Gin 0.6 (Mar 9, 2015)
|
||||||
|
|
||||||
- [NEW] Support multipart/form-data
|
- [NEW] Support multipart/form-data
|
||||||
- [NEW] NoMethod handler
|
- [NEW] NoMethod handler
|
||||||
@ -208,14 +598,14 @@
|
|||||||
- [FIX] Improve color logger
|
- [FIX] Improve color logger
|
||||||
|
|
||||||
|
|
||||||
### Gin 0.5 (Feb 7, 2015)
|
## Gin 0.5 (Feb 7, 2015)
|
||||||
|
|
||||||
- [NEW] Content Negotiation
|
- [NEW] Content Negotiation
|
||||||
- [FIX] Solved security bug that allow a client to spoof ip
|
- [FIX] Solved security bug that allow a client to spoof ip
|
||||||
- [FIX] Fix unexported/ignored fields in binding
|
- [FIX] Fix unexported/ignored fields in binding
|
||||||
|
|
||||||
|
|
||||||
### Gin 0.4 (Aug 21, 2014)
|
## Gin 0.4 (Aug 21, 2014)
|
||||||
|
|
||||||
- [NEW] Development mode
|
- [NEW] Development mode
|
||||||
- [NEW] Unit tests
|
- [NEW] Unit tests
|
||||||
@ -224,14 +614,14 @@
|
|||||||
- [FIX] Improved documentation for model binding
|
- [FIX] Improved documentation for model binding
|
||||||
|
|
||||||
|
|
||||||
### Gin 0.3 (Jul 18, 2014)
|
## Gin 0.3 (Jul 18, 2014)
|
||||||
|
|
||||||
- [PERFORMANCE] Normal log and error log are printed in the same call.
|
- [PERFORMANCE] Normal log and error log are printed in the same call.
|
||||||
- [PERFORMANCE] Improve performance of NoRouter()
|
- [PERFORMANCE] Improve performance of NoRouter()
|
||||||
- [PERFORMANCE] Improve context's memory locality, reduce CPU cache faults.
|
- [PERFORMANCE] Improve context's memory locality, reduce CPU cache faults.
|
||||||
- [NEW] Flexible rendering API
|
- [NEW] Flexible rendering API
|
||||||
- [NEW] Add Context.File()
|
- [NEW] Add Context.File()
|
||||||
- [NEW] Add shorcut RunTLS() for http.ListenAndServeTLS
|
- [NEW] Add shortcut RunTLS() for http.ListenAndServeTLS
|
||||||
- [FIX] Rename NotFound404() to NoRoute()
|
- [FIX] Rename NotFound404() to NoRoute()
|
||||||
- [FIX] Errors in context are purged
|
- [FIX] Errors in context are purged
|
||||||
- [FIX] Adds HEAD method in Static file serving
|
- [FIX] Adds HEAD method in Static file serving
|
||||||
@ -242,7 +632,7 @@
|
|||||||
- [FIX] Check application/x-www-form-urlencoded when parsing form
|
- [FIX] Check application/x-www-form-urlencoded when parsing form
|
||||||
|
|
||||||
|
|
||||||
### Gin 0.2b (Jul 08, 2014)
|
## Gin 0.2b (Jul 08, 2014)
|
||||||
- [PERFORMANCE] Using sync.Pool to allocatio/gc overhead
|
- [PERFORMANCE] Using sync.Pool to allocatio/gc overhead
|
||||||
- [NEW] Travis CI integration
|
- [NEW] Travis CI integration
|
||||||
- [NEW] Completely new logger
|
- [NEW] Completely new logger
|
||||||
@ -254,14 +644,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 GitHub Actions.
|
||||||
- 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.
|
||||||
|
|||||||
30
Makefile
30
Makefile
@ -1,20 +1,17 @@
|
|||||||
GO ?= go
|
GO ?= go
|
||||||
GOFMT ?= gofmt "-s"
|
GOFMT ?= gofmt "-s"
|
||||||
PACKAGES ?= $(shell $(GO) list ./... | grep -v /vendor/)
|
GO_VERSION=$(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f2)
|
||||||
VETPACKAGES ?= $(shell $(GO) list ./... | grep -v /vendor/ | grep -v /examples/)
|
PACKAGES ?= $(shell $(GO) list ./...)
|
||||||
GOFILES := $(shell find . -name "*.go" -type f -not -path "./vendor/*")
|
VETPACKAGES ?= $(shell $(GO) list ./... | grep -v /examples/)
|
||||||
|
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 $(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; \
|
||||||
@ -45,14 +42,10 @@ fmt-check:
|
|||||||
exit 1; \
|
exit 1; \
|
||||||
fi;
|
fi;
|
||||||
|
|
||||||
|
.PHONY: vet
|
||||||
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 \
|
||||||
@ -76,5 +69,10 @@ misspell:
|
|||||||
|
|
||||||
.PHONY: tools
|
.PHONY: tools
|
||||||
tools:
|
tools:
|
||||||
go install golang.org/x/lint/golint; \
|
@if [ $(GO_VERSION) -gt 15 ]; then \
|
||||||
go install github.com/client9/misspell/cmd/misspell;
|
$(GO) install golang.org/x/lint/golint@latest; \
|
||||||
|
$(GO) install github.com/client9/misspell/cmd/misspell@latest; \
|
||||||
|
elif [ $(GO_VERSION) -lt 16 ]; then \
|
||||||
|
$(GO) install golang.org/x/lint/golint; \
|
||||||
|
$(GO) install github.com/client9/misspell/cmd/misspell; \
|
||||||
|
fi
|
||||||
|
|||||||
38
auth.go
38
auth.go
@ -9,11 +9,16 @@ 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.
|
||||||
const AuthUserKey = "user"
|
const AuthUserKey = "user"
|
||||||
|
|
||||||
|
// AuthProxyUserKey is the cookie name for proxy_user credential in basic auth for proxy.
|
||||||
|
const AuthProxyUserKey = "proxy_user"
|
||||||
|
|
||||||
// Accounts defines a key/value for user/pass list of authorized logins.
|
// Accounts defines a key/value for user/pass list of authorized logins.
|
||||||
type Accounts map[string]string
|
type Accounts map[string]string
|
||||||
|
|
||||||
@ -29,7 +34,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(bytesconv.StringToBytes(pair.value), bytesconv.StringToBytes(authValue)) == 1 {
|
||||||
return pair.user, true
|
return pair.user, true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -69,8 +74,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 +90,27 @@ 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 {
|
// BasicAuthForProxy returns a Basic HTTP Proxy-Authorization middleware.
|
||||||
if subtle.ConstantTimeEq(int32(len(given)), int32(len(actual))) == 1 {
|
// If the realm is empty, "Proxy Authorization Required" will be used by default.
|
||||||
return subtle.ConstantTimeCompare([]byte(given), []byte(actual)) == 1
|
func BasicAuthForProxy(accounts Accounts, realm string) HandlerFunc {
|
||||||
|
if realm == "" {
|
||||||
|
realm = "Proxy Authorization Required"
|
||||||
|
}
|
||||||
|
realm = "Basic realm=" + strconv.Quote(realm)
|
||||||
|
pairs := processAccounts(accounts)
|
||||||
|
return func(c *Context) {
|
||||||
|
proxyUser, found := pairs.searchCredential(c.requestHeader("Proxy-Authorization"))
|
||||||
|
if !found {
|
||||||
|
// Credentials doesn't match, we return 407 and abort handlers chain.
|
||||||
|
c.Header("Proxy-Authenticate", realm)
|
||||||
|
c.AbortWithStatus(http.StatusProxyAuthRequired)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// The proxy_user credentials was found, set proxy_user's id to key AuthProxyUserKey in this context, the proxy_user's id can be read later using
|
||||||
|
// c.MustGet(gin.AuthProxyUserKey).
|
||||||
|
c.Set(AuthProxyUserKey, proxyUser)
|
||||||
}
|
}
|
||||||
// Securely compare actual to itself to keep constant time, but always return false.
|
|
||||||
return subtle.ConstantTimeCompare([]byte(actual), []byte(actual)) == 1 && false
|
|
||||||
}
|
}
|
||||||
|
|||||||
44
auth_test.go
44
auth_test.go
@ -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()
|
||||||
@ -144,3 +137,40 @@ func TestBasicAuth401WithCustomRealm(t *testing.T) {
|
|||||||
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
||||||
assert.Equal(t, "Basic realm=\"My Custom \\\"Realm\\\"\"", w.Header().Get("WWW-Authenticate"))
|
assert.Equal(t, "Basic realm=\"My Custom \\\"Realm\\\"\"", w.Header().Get("WWW-Authenticate"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBasicAuthForProxySucceed(t *testing.T) {
|
||||||
|
accounts := Accounts{"admin": "password"}
|
||||||
|
router := New()
|
||||||
|
router.Use(BasicAuthForProxy(accounts, ""))
|
||||||
|
router.Any("/*proxyPath", func(c *Context) {
|
||||||
|
c.String(http.StatusOK, c.MustGet(AuthProxyUserKey).(string))
|
||||||
|
})
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("GET", "/test", nil)
|
||||||
|
req.Header.Set("Proxy-Authorization", authorizationHeader("admin", "password"))
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
assert.Equal(t, "admin", w.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBasicAuthForProxy407(t *testing.T) {
|
||||||
|
called := false
|
||||||
|
accounts := Accounts{"foo": "bar"}
|
||||||
|
router := New()
|
||||||
|
router.Use(BasicAuthForProxy(accounts, ""))
|
||||||
|
router.Any("/*proxyPath", func(c *Context) {
|
||||||
|
called = true
|
||||||
|
c.String(http.StatusOK, c.MustGet(AuthProxyUserKey).(string))
|
||||||
|
})
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("GET", "/test", nil)
|
||||||
|
req.Header.Set("Proxy-Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:password")))
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.False(t, called)
|
||||||
|
assert.Equal(t, http.StatusProxyAuthRequired, w.Code)
|
||||||
|
assert.Equal(t, "Basic realm=\"Proxy Authorization Required\"", w.Header().Get("Proxy-Authenticate"))
|
||||||
|
}
|
||||||
|
|||||||
@ -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.
|
||||||
|
|
||||||
|
//go:build !nomsgpack
|
||||||
|
|
||||||
package binding
|
package binding
|
||||||
|
|
||||||
import "net/http"
|
import "net/http"
|
||||||
@ -19,6 +21,8 @@ const (
|
|||||||
MIMEMSGPACK = "application/x-msgpack"
|
MIMEMSGPACK = "application/x-msgpack"
|
||||||
MIMEMSGPACK2 = "application/msgpack"
|
MIMEMSGPACK2 = "application/msgpack"
|
||||||
MIMEYAML = "application/x-yaml"
|
MIMEYAML = "application/x-yaml"
|
||||||
|
MIMEYAML2 = "application/yaml"
|
||||||
|
MIMETOML = "application/toml"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Binding describes the interface which needs to be implemented for binding the
|
// Binding describes the interface which needs to be implemented for binding the
|
||||||
@ -26,64 +30,67 @@ const (
|
|||||||
// the form POST.
|
// the form POST.
|
||||||
type Binding interface {
|
type Binding interface {
|
||||||
Name() string
|
Name() string
|
||||||
Bind(*http.Request, interface{}) error
|
Bind(*http.Request, any) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// BindingBody adds BindBody method to Binding. BindBody is similar with Bind,
|
// BindingBody adds BindBody method to Binding. BindBody is similar with Bind,
|
||||||
// but it reads the body from supplied bytes instead of req.Body.
|
// but it reads the body from supplied bytes instead of req.Body.
|
||||||
type BindingBody interface {
|
type BindingBody interface {
|
||||||
Binding
|
Binding
|
||||||
BindBody([]byte, interface{}) error
|
BindBody([]byte, any) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// BindingUri adds BindUri method to Binding. BindUri is similar with Bind,
|
// BindingUri adds BindUri method to Binding. BindUri is similar with Bind,
|
||||||
// but it read the Params.
|
// but it reads the Params.
|
||||||
type BindingUri interface {
|
type BindingUri interface {
|
||||||
Name() string
|
Name() string
|
||||||
BindUri(map[string][]string, interface{}) error
|
BindUri(map[string][]string, any) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// StructValidator is the minimal interface which needs to be implemented in
|
// 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
|
// 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
|
// of the request. Gin provides a default implementation for this using
|
||||||
// https://github.com/go-playground/validator/tree/v8.18.2.
|
// https://github.com/go-playground/validator/tree/v10.6.1.
|
||||||
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.
|
||||||
ValidateStruct(interface{}) error
|
ValidateStruct(any) error
|
||||||
|
|
||||||
// Engine returns the underlying validator engine which powers the
|
// Engine returns the underlying validator engine which powers the
|
||||||
// StructValidator implementation.
|
// StructValidator implementation.
|
||||||
Engine() interface{}
|
Engine() any
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validator is the default validator which implements the StructValidator
|
// Validator is the default validator which implements the StructValidator
|
||||||
// interface. It uses https://github.com/go-playground/validator/tree/v8.18.2
|
// interface. It uses https://github.com/go-playground/validator/tree/v10.6.1
|
||||||
// under the hood.
|
// under the hood.
|
||||||
var Validator StructValidator = &defaultValidator{}
|
var Validator StructValidator = &defaultValidator{}
|
||||||
|
|
||||||
// These implement the Binding interface and can be used to bind the data
|
// These implement the Binding interface and can be used to bind the data
|
||||||
// present in the request to struct instances.
|
// present in the request to struct instances.
|
||||||
var (
|
var (
|
||||||
JSON = jsonBinding{}
|
JSON BindingBody = jsonBinding{}
|
||||||
XML = xmlBinding{}
|
XML BindingBody = xmlBinding{}
|
||||||
Form = formBinding{}
|
Form Binding = formBinding{}
|
||||||
Query = queryBinding{}
|
Query Binding = queryBinding{}
|
||||||
FormPost = formPostBinding{}
|
FormPost Binding = formPostBinding{}
|
||||||
FormMultipart = formMultipartBinding{}
|
FormMultipart Binding = formMultipartBinding{}
|
||||||
ProtoBuf = protobufBinding{}
|
ProtoBuf BindingBody = protobufBinding{}
|
||||||
MsgPack = msgpackBinding{}
|
MsgPack BindingBody = msgpackBinding{}
|
||||||
YAML = yamlBinding{}
|
YAML BindingBody = yamlBinding{}
|
||||||
Uri = uriBinding{}
|
Uri BindingUri = uriBinding{}
|
||||||
|
Header Binding = headerBinding{}
|
||||||
|
TOML BindingBody = tomlBinding{}
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,8 +103,10 @@ func Default(method, contentType string) Binding {
|
|||||||
return ProtoBuf
|
return ProtoBuf
|
||||||
case MIMEMSGPACK, MIMEMSGPACK2:
|
case MIMEMSGPACK, MIMEMSGPACK2:
|
||||||
return MsgPack
|
return MsgPack
|
||||||
case MIMEYAML:
|
case MIMEYAML, MIMEYAML2:
|
||||||
return YAML
|
return YAML
|
||||||
|
case MIMETOML:
|
||||||
|
return TOML
|
||||||
case MIMEMultipartPOSTForm:
|
case MIMEMultipartPOSTForm:
|
||||||
return FormMultipart
|
return FormMultipart
|
||||||
default: // case MIMEPOSTForm:
|
default: // case MIMEPOSTForm:
|
||||||
@ -105,7 +114,7 @@ func Default(method, contentType string) Binding {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func validate(obj interface{}) error {
|
func validate(obj any) error {
|
||||||
if Validator == nil {
|
if Validator == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.
|
||||||
|
|
||||||
|
//go: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))
|
||||||
|
}
|
||||||
116
binding/binding_nomsgpack.go
Normal file
116
binding/binding_nomsgpack.go
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
//go: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"
|
||||||
|
MIMEYAML2 = "application/yaml"
|
||||||
|
MIMETOML = "application/toml"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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, any) 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, any) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// BindingUri adds BindUri method to Binding. BindUri is similar with Bind,
|
||||||
|
// but it reads the Params.
|
||||||
|
type BindingUri interface {
|
||||||
|
Name() string
|
||||||
|
BindUri(map[string][]string, any) 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/v10.6.1.
|
||||||
|
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(any) error
|
||||||
|
|
||||||
|
// Engine returns the underlying validator engine which powers the
|
||||||
|
// StructValidator implementation.
|
||||||
|
Engine() any
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validator is the default validator which implements the StructValidator
|
||||||
|
// interface. It uses https://github.com/go-playground/validator/tree/v10.6.1
|
||||||
|
// 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{}
|
||||||
|
TOML = tomlBinding{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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, MIMEYAML2:
|
||||||
|
return YAML
|
||||||
|
case MIMEMultipartPOSTForm:
|
||||||
|
return FormMultipart
|
||||||
|
case MIMETOML:
|
||||||
|
return TOML
|
||||||
|
default: // case MIMEPOSTForm:
|
||||||
|
return Form
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validate(obj any) error {
|
||||||
|
if Validator == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return Validator.ValidateStruct(obj)
|
||||||
|
}
|
||||||
@ -9,23 +9,32 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin/testdata/protoexample"
|
"github.com/gin-gonic/gin/testdata/protoexample"
|
||||||
"github.com/golang/protobuf/proto"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/ugorji/go/codec"
|
"google.golang.org/protobuf/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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 {
|
||||||
@ -51,12 +60,23 @@ type FooDefaultBarStruct struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type FooStructUseNumber struct {
|
type FooStructUseNumber struct {
|
||||||
Foo interface{} `json:"foo" binding:"required"`
|
Foo any `json:"foo" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FooStructDisallowUnknownFields struct {
|
||||||
|
Foo any `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 {
|
||||||
@ -72,7 +92,7 @@ type FooStructForTimeTypeFailLocation struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type FooStructForMapType struct {
|
type FooStructForMapType struct {
|
||||||
MapFoo map[string]interface{} `form:"map_foo"`
|
MapFoo map[string]any `form:"map_foo"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FooStructForIgnoreFormTag struct {
|
type FooStructForIgnoreFormTag struct {
|
||||||
@ -85,7 +105,7 @@ type InvalidNameType struct {
|
|||||||
|
|
||||||
type InvalidNameMapType struct {
|
type InvalidNameMapType struct {
|
||||||
TestName struct {
|
TestName struct {
|
||||||
MapFoo map[string]interface{} `form:"map_foo"`
|
MapFoo map[string]any `form:"map_foo"`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,7 +127,7 @@ type FooStructForStructPointerType struct {
|
|||||||
|
|
||||||
type FooStructForSliceMapType struct {
|
type FooStructForSliceMapType struct {
|
||||||
// Unknown type: not support map
|
// Unknown type: not support map
|
||||||
SliceMapFoo []map[string]interface{} `form:"slice_map_foo"`
|
SliceMapFoo []map[string]any `form:"slice_map_foo"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FooStructForBoolType struct {
|
type FooStructForBoolType struct {
|
||||||
@ -120,7 +140,7 @@ type FooStructForStringPtrType struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type FooStructForMapPtrType struct {
|
type FooStructForMapPtrType struct {
|
||||||
PtrBar *map[string]interface{} `form:"ptr_bar"`
|
PtrBar *map[string]any `form:"ptr_bar"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBindingDefault(t *testing.T) {
|
func TestBindingDefault(t *testing.T) {
|
||||||
@ -142,11 +162,13 @@ 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))
|
||||||
|
assert.Equal(t, YAML, Default("POST", MIMEYAML2))
|
||||||
|
assert.Equal(t, YAML, Default("PUT", MIMEYAML2))
|
||||||
|
|
||||||
|
assert.Equal(t, TOML, Default("POST", MIMETOML))
|
||||||
|
assert.Equal(t, TOML, Default("PUT", MIMETOML))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBindingJSONNilBody(t *testing.T) {
|
func TestBindingJSONNilBody(t *testing.T) {
|
||||||
@ -163,6 +185,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",
|
||||||
@ -177,6 +213,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",
|
||||||
"/", "/",
|
"/", "/",
|
||||||
@ -189,6 +237,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",
|
||||||
"/", "/",
|
"/", "/",
|
||||||
@ -204,7 +264,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")
|
||||||
@ -218,8 +281,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",
|
||||||
"", "")
|
"", "")
|
||||||
@ -295,6 +361,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", "/",
|
||||||
@ -325,6 +422,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",
|
||||||
@ -339,6 +458,20 @@ func TestBindingXMLFail(t *testing.T) {
|
|||||||
"<map><foo>bar<foo></map>", "<map><bar>foo</bar></map>")
|
"<map><foo>bar<foo></map>", "<map><bar>foo</bar></map>")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBindingTOML(t *testing.T) {
|
||||||
|
testBodyBinding(t,
|
||||||
|
TOML, "toml",
|
||||||
|
"/", "/",
|
||||||
|
`foo="bar"`, `bar="foo"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBindingTOMLFail(t *testing.T) {
|
||||||
|
testBodyBindingFail(t,
|
||||||
|
TOML, "toml",
|
||||||
|
"/", "/",
|
||||||
|
`foo=\n"bar"`, `bar="foo"`)
|
||||||
|
}
|
||||||
|
|
||||||
func TestBindingYAML(t *testing.T) {
|
func TestBindingYAML(t *testing.T) {
|
||||||
testBodyBinding(t,
|
testBodyBinding(t,
|
||||||
YAML, "yaml",
|
YAML, "yaml",
|
||||||
@ -346,6 +479,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",
|
||||||
@ -396,7 +536,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)
|
||||||
@ -420,7 +561,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)
|
||||||
@ -509,17 +651,18 @@ 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")
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
fileActual, _ := ioutil.ReadAll(f)
|
fileActual, _ := io.ReadAll(f)
|
||||||
|
|
||||||
// file from multipart
|
// file from multipart
|
||||||
mf, _ := obj.File.Open()
|
mf, _ := obj.File.Open()
|
||||||
defer mf.Close()
|
defer mf.Close()
|
||||||
fileExpect, _ := ioutil.ReadAll(mf)
|
fileExpect, _ := io.ReadAll(mf)
|
||||||
|
|
||||||
assert.Equal(t, FormMultipart.Name(), "multipart/form-data")
|
assert.Equal(t, FormMultipart.Name(), "multipart/form-data")
|
||||||
assert.Equal(t, obj.Foo, "bar")
|
assert.Equal(t, obj.Foo, "bar")
|
||||||
@ -585,26 +728,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"}`)
|
||||||
@ -623,9 +746,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
|
||||||
@ -634,9 +757,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
|
||||||
@ -645,6 +768,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]any `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())
|
||||||
@ -659,11 +807,11 @@ func TestUriBinding(t *testing.T) {
|
|||||||
assert.Equal(t, "thinkerou", tag.Name)
|
assert.Equal(t, "thinkerou", tag.Name)
|
||||||
|
|
||||||
type NotSupportStruct struct {
|
type NotSupportStruct struct {
|
||||||
Name map[string]interface{} `uri:"name"`
|
Name map[string]any `uri:"name"`
|
||||||
}
|
}
|
||||||
var not NotSupportStruct
|
var not NotSupportStruct
|
||||||
assert.Error(t, b.BindUri(m, ¬))
|
assert.Error(t, b.BindUri(m, ¬))
|
||||||
assert.Equal(t, map[string]interface{}(nil), not.Name)
|
assert.Equal(t, map[string]any(nil), not.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUriInnerBinding(t *testing.T) {
|
func TestUriInnerBinding(t *testing.T) {
|
||||||
@ -688,6 +836,22 @@ 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())
|
||||||
@ -785,6 +949,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)
|
||||||
@ -792,6 +958,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())
|
||||||
@ -924,9 +1108,7 @@ func testFormBindingForType(t *testing.T, method, path, badPath, body, badBody s
|
|||||||
assert.Equal(t,
|
assert.Equal(t,
|
||||||
struct {
|
struct {
|
||||||
Idx int "form:\"idx\""
|
Idx int "form:\"idx\""
|
||||||
}(struct {
|
}{Idx: 123},
|
||||||
Idx int "form:\"idx\""
|
|
||||||
}{Idx: 123}),
|
|
||||||
obj.StructFoo)
|
obj.StructFoo)
|
||||||
case "StructPointer":
|
case "StructPointer":
|
||||||
obj := FooStructForStructPointerType{}
|
obj := FooStructForStructPointerType{}
|
||||||
@ -935,9 +1117,7 @@ func testFormBindingForType(t *testing.T, method, path, badPath, body, badBody s
|
|||||||
assert.Equal(t,
|
assert.Equal(t,
|
||||||
struct {
|
struct {
|
||||||
Name string "form:\"name\""
|
Name string "form:\"name\""
|
||||||
}(struct {
|
}{Name: "thinkerou"},
|
||||||
Name string "form:\"name\""
|
|
||||||
}{Name: "thinkerou"}),
|
|
||||||
*obj.StructPointerFoo)
|
*obj.StructPointerFoo)
|
||||||
case "Map":
|
case "Map":
|
||||||
obj := FooStructForMapType{}
|
obj := FooStructForMapType{}
|
||||||
@ -1028,6 +1208,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())
|
||||||
|
|
||||||
@ -1065,6 +1285,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())
|
||||||
|
|
||||||
@ -1109,11 +1348,18 @@ func testProtoBodyBindingFail(t *testing.T, b Binding, name, path, badPath, body
|
|||||||
obj := protoexample.Test{}
|
obj := protoexample.Test{}
|
||||||
req := requestWithBody("POST", path, body)
|
req := requestWithBody("POST", path, body)
|
||||||
|
|
||||||
req.Body = ioutil.NopCloser(&hook{})
|
req.Body = io.NopCloser(&hook{})
|
||||||
req.Header.Add("Content-Type", MIMEPROTOBUF)
|
req.Header.Add("Content-Type", MIMEPROTOBUF)
|
||||||
err := b.Bind(req, &obj)
|
err := b.Bind(req, &obj)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
invalidobj := FooStruct{}
|
||||||
|
req.Body = io.NopCloser(strings.NewReader(`{"msg":"hello"}`))
|
||||||
|
req.Header.Add("Content-Type", MIMEPROTOBUF)
|
||||||
|
err = b.Bind(req, &invalidobj)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Equal(t, err.Error(), "obj is not ProtoMessage")
|
||||||
|
|
||||||
obj = protoexample.Test{}
|
obj = protoexample.Test{}
|
||||||
req = requestWithBody("POST", badPath, badBody)
|
req = requestWithBody("POST", badPath, badBody)
|
||||||
req.Header.Add("Content-Type", MIMEPROTOBUF)
|
req.Header.Add("Content-Type", MIMEPROTOBUF)
|
||||||
@ -1121,23 +1367,6 @@ func testProtoBodyBindingFail(t *testing.T, b Binding, name, path, badPath, body
|
|||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testMsgPackBodyBinding(t *testing.T, b Binding, name, path, badPath, body, badBody string) {
|
|
||||||
assert.Equal(t, name, b.Name())
|
|
||||||
|
|
||||||
obj := FooStruct{}
|
|
||||||
req := requestWithBody("POST", path, body)
|
|
||||||
req.Header.Add("Content-Type", MIMEMSGPACK)
|
|
||||||
err := b.Bind(req, &obj)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, "bar", obj.Foo)
|
|
||||||
|
|
||||||
obj = FooStruct{}
|
|
||||||
req = requestWithBody("POST", badPath, badBody)
|
|
||||||
req.Header.Add("Content-Type", MIMEMSGPACK)
|
|
||||||
err = MsgPack.Bind(req, &obj)
|
|
||||||
assert.Error(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func requestWithBody(method, path, body string) (req *http.Request) {
|
func requestWithBody(method, path, body string) (req *http.Request) {
|
||||||
req, _ = http.NewRequest(method, path, bytes.NewBufferString(body))
|
req, _ = http.NewRequest(method, path, bytes.NewBufferString(body))
|
||||||
return
|
return
|
||||||
|
|||||||
@ -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,36 +18,83 @@ type defaultValidator struct {
|
|||||||
validate *validator.Validate
|
validate *validator.Validate
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ StructValidator = &defaultValidator{}
|
type SliceValidationError []error
|
||||||
|
|
||||||
|
// Error concatenates all error elements in SliceValidationError into a single string separated by \n.
|
||||||
|
func (err SliceValidationError) Error() string {
|
||||||
|
n := len(err)
|
||||||
|
switch n {
|
||||||
|
case 0:
|
||||||
|
return ""
|
||||||
|
default:
|
||||||
|
var b strings.Builder
|
||||||
|
if err[0] != nil {
|
||||||
|
fmt.Fprintf(&b, "[%d]: %s", 0, err[0].Error())
|
||||||
|
}
|
||||||
|
if n > 1 {
|
||||||
|
for i := 1; i < n; i++ {
|
||||||
|
if err[i] != nil {
|
||||||
|
b.WriteString("\n")
|
||||||
|
fmt.Fprintf(&b, "[%d]: %s", i, err[i].Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ StructValidator = (*defaultValidator)(nil)
|
||||||
|
|
||||||
// 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 any) error {
|
||||||
value := reflect.ValueOf(obj)
|
if obj == nil {
|
||||||
valueType := value.Kind()
|
|
||||||
if valueType == reflect.Ptr {
|
|
||||||
valueType = value.Elem().Kind()
|
|
||||||
}
|
|
||||||
if valueType == reflect.Struct {
|
|
||||||
v.lazyinit()
|
|
||||||
if err := v.validate.Struct(obj); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
value := reflect.ValueOf(obj)
|
||||||
|
switch value.Kind() {
|
||||||
|
case reflect.Ptr:
|
||||||
|
if value.Elem().Kind() != reflect.Struct {
|
||||||
|
return v.ValidateStruct(value.Elem().Interface())
|
||||||
|
}
|
||||||
|
return v.validateStruct(obj)
|
||||||
|
case reflect.Struct:
|
||||||
|
return v.validateStruct(obj)
|
||||||
|
case reflect.Slice, reflect.Array:
|
||||||
|
count := value.Len()
|
||||||
|
validateRet := make(SliceValidationError, 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateStruct receives struct type
|
||||||
|
func (v *defaultValidator) validateStruct(obj any) 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
|
||||||
// Validator instance. This is useful if you want to register custom validations
|
// Validator instance. This is useful if you want to register custom validations
|
||||||
// or struct level validations. See validator GoDoc for more info -
|
// or struct level validations. See validator GoDoc for more info -
|
||||||
// https://godoc.org/gopkg.in/go-playground/validator.v8
|
// https://pkg.go.dev/github.com/go-playground/validator/v10
|
||||||
func (v *defaultValidator) Engine() interface{} {
|
func (v *defaultValidator) Engine() any {
|
||||||
v.lazyinit()
|
v.lazyinit()
|
||||||
return v.validate
|
return v.validate
|
||||||
}
|
}
|
||||||
|
|
||||||
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")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
24
binding/default_validator_benchmark_test.go
Normal file
24
binding/default_validator_benchmark_test.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
// Copyright 2022 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"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func BenchmarkSliceValidationError(b *testing.B) {
|
||||||
|
const size int = 100
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
e := make(SliceValidationError, size)
|
||||||
|
for j := 0; j < size; j++ {
|
||||||
|
e[j] = errors.New(strconv.Itoa(j))
|
||||||
|
}
|
||||||
|
if len(e.Error()) == 0 {
|
||||||
|
b.Errorf("error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
88
binding/default_validator_test.go
Normal file
88
binding/default_validator_test.go
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
// 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 TestSliceValidationError(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
err SliceValidationError
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"has nil elements", SliceValidationError{errors.New("test error"), nil}, "[0]: test error"},
|
||||||
|
{"has zero elements", SliceValidationError{}, ""},
|
||||||
|
{"has one element", SliceValidationError{errors.New("test one error")}, "[0]: test one error"},
|
||||||
|
{"has two elements",
|
||||||
|
SliceValidationError{
|
||||||
|
errors.New("first error"),
|
||||||
|
errors.New("second error"),
|
||||||
|
},
|
||||||
|
"[0]: first error\n[1]: second error",
|
||||||
|
},
|
||||||
|
{"has many elements",
|
||||||
|
SliceValidationError{
|
||||||
|
errors.New("first error"),
|
||||||
|
errors.New("second error"),
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
errors.New("last error"),
|
||||||
|
},
|
||||||
|
"[0]: first error\n[1]: second error\n[5]: last error",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := tt.err.Error(); got != tt.want {
|
||||||
|
t.Errorf("SliceValidationError.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 any
|
||||||
|
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,11 @@
|
|||||||
package binding
|
package binding
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"mime/multipart"
|
"errors"
|
||||||
"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{}
|
||||||
@ -20,15 +19,13 @@ func (formBinding) Name() string {
|
|||||||
return "form"
|
return "form"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (formBinding) Bind(req *http.Request, obj interface{}) error {
|
func (formBinding) Bind(req *http.Request, obj any) error {
|
||||||
if err := req.ParseForm(); err != nil {
|
if err := req.ParseForm(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := req.ParseMultipartForm(defaultMemory); err != nil {
|
if err := req.ParseMultipartForm(defaultMemory); err != nil && !errors.Is(err, http.ErrNotMultipart) {
|
||||||
if err != http.ErrNotMultipart {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if err := mapForm(obj, req.Form); err != nil {
|
if err := mapForm(obj, req.Form); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -39,7 +36,7 @@ func (formPostBinding) Name() string {
|
|||||||
return "form-urlencoded"
|
return "form-urlencoded"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (formPostBinding) Bind(req *http.Request, obj interface{}) error {
|
func (formPostBinding) Bind(req *http.Request, obj any) error {
|
||||||
if err := req.ParseForm(); err != nil {
|
if err := req.ParseForm(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -53,7 +50,7 @@ func (formMultipartBinding) Name() string {
|
|||||||
return "multipart/form-data"
|
return "multipart/form-data"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (formMultipartBinding) Bind(req *http.Request, obj interface{}) error {
|
func (formMultipartBinding) Bind(req *http.Request, obj any) error {
|
||||||
if err := req.ParseMultipartForm(defaultMemory); err != nil {
|
if err := req.ParseMultipartForm(defaultMemory); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -63,27 +60,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)
|
|
||||||
}
|
|
||||||
|
|||||||
@ -7,33 +7,62 @@ package binding
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"mime/multipart"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"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 {
|
// ErrConvertMapStringSlice can not convert to map[string][]string
|
||||||
|
ErrConvertMapStringSlice = errors.New("can not convert to map slices of strings")
|
||||||
|
|
||||||
|
// ErrConvertToMapString can not convert to map[string]string
|
||||||
|
ErrConvertToMapString = errors.New("can not convert to map of strings")
|
||||||
|
)
|
||||||
|
|
||||||
|
func mapURI(ptr any, m map[string][]string) error {
|
||||||
return mapFormByTag(ptr, m, "uri")
|
return mapFormByTag(ptr, m, "uri")
|
||||||
}
|
}
|
||||||
|
|
||||||
func mapForm(ptr interface{}, form map[string][]string) error {
|
func mapForm(ptr any, form map[string][]string) error {
|
||||||
return mapFormByTag(ptr, form, "form")
|
return mapFormByTag(ptr, form, "form")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func MapFormWithTag(ptr any, form map[string][]string, tag string) error {
|
||||||
|
return mapFormByTag(ptr, form, tag)
|
||||||
|
}
|
||||||
|
|
||||||
var emptyField = reflect.StructField{}
|
var emptyField = reflect.StructField{}
|
||||||
|
|
||||||
func mapFormByTag(ptr interface{}, form map[string][]string, tag string) error {
|
func mapFormByTag(ptr any, form map[string][]string, tag string) error {
|
||||||
|
// Check if ptr is a map
|
||||||
|
ptrVal := reflect.ValueOf(ptr)
|
||||||
|
var pointed any
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
// setter tries to set value on a walking by fields of a struct
|
// setter tries to set value on a walking by fields of a struct
|
||||||
type setter interface {
|
type setter interface {
|
||||||
TrySet(value reflect.Value, field reflect.StructField, key string, opt setOptions) (isSetted bool, err error)
|
TrySet(value reflect.Value, field reflect.StructField, key string, opt setOptions) (isSet bool, err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type formSource map[string][]string
|
type formSource map[string][]string
|
||||||
@ -41,17 +70,21 @@ type formSource map[string][]string
|
|||||||
var _ setter = formSource(nil)
|
var _ setter = formSource(nil)
|
||||||
|
|
||||||
// TrySet tries to set a value by request's form source (like map[string][]string)
|
// TrySet tries to set a value by request's form source (like map[string][]string)
|
||||||
func (form formSource) TrySet(value reflect.Value, field reflect.StructField, tagValue string, opt setOptions) (isSetted bool, err error) {
|
func (form formSource) TrySet(value reflect.Value, field reflect.StructField, tagValue string, opt setOptions) (isSet bool, err error) {
|
||||||
return setByForm(value, field, form, tagValue, opt)
|
return setByForm(value, field, form, tagValue, opt)
|
||||||
}
|
}
|
||||||
|
|
||||||
func mappingByPtr(ptr interface{}, setter setter, tag string) error {
|
func mappingByPtr(ptr any, setter setter, tag string) error {
|
||||||
_, err := mapping(reflect.ValueOf(ptr), emptyField, setter, tag)
|
_, err := mapping(reflect.ValueOf(ptr), emptyField, setter, tag)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
var vKind = value.Kind()
|
if field.Tag.Get(tag) == "-" { // just ignoring this field
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
vKind := value.Kind()
|
||||||
|
|
||||||
if vKind == reflect.Ptr {
|
if vKind == reflect.Ptr {
|
||||||
var isNew bool
|
var isNew bool
|
||||||
@ -60,16 +93,17 @@ func mapping(value reflect.Value, field reflect.StructField, setter setter, tag
|
|||||||
isNew = true
|
isNew = true
|
||||||
vPtr = reflect.New(value.Type().Elem())
|
vPtr = reflect.New(value.Type().Elem())
|
||||||
}
|
}
|
||||||
isSetted, err := mapping(vPtr.Elem(), field, setter, tag)
|
isSet, err := mapping(vPtr.Elem(), field, setter, tag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
if isNew && isSetted {
|
if isNew && isSet {
|
||||||
value.Set(vPtr)
|
value.Set(vPtr)
|
||||||
}
|
}
|
||||||
return isSetted, nil
|
return isSet, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if vKind != reflect.Struct || !field.Anonymous {
|
||||||
ok, err := tryToSetValue(value, field, setter, tag)
|
ok, err := tryToSetValue(value, field, setter, tag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@ -77,22 +111,24 @@ func mapping(value reflect.Value, field reflect.StructField, setter setter, tag
|
|||||||
if ok {
|
if ok {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if vKind == reflect.Struct {
|
if vKind == reflect.Struct {
|
||||||
tValue := value.Type()
|
tValue := value.Type()
|
||||||
|
|
||||||
var isSetted bool
|
var isSet 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), sf, setter, tag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
isSetted = isSetted || ok
|
isSet = isSet || ok
|
||||||
}
|
}
|
||||||
return isSetted, nil
|
return isSet, nil
|
||||||
}
|
}
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
@ -109,9 +145,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 +156,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
|
||||||
}
|
}
|
||||||
@ -134,7 +165,24 @@ func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter
|
|||||||
return setter.TrySet(value, field, tagValue, setOpt)
|
return setter.TrySet(value, field, tagValue, setOpt)
|
||||||
}
|
}
|
||||||
|
|
||||||
func setByForm(value reflect.Value, field reflect.StructField, form map[string][]string, tagValue string, opt setOptions) (isSetted bool, err error) {
|
// BindUnmarshaler is the interface used to wrap the UnmarshalParam method.
|
||||||
|
type BindUnmarshaler interface {
|
||||||
|
// UnmarshalParam decodes and assigns a value from an form or query param.
|
||||||
|
UnmarshalParam(param string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// trySetCustom tries to set a custom type value
|
||||||
|
// If the value implements the BindUnmarshaler interface, it will be used to set the value, we will return `true`
|
||||||
|
// to skip the default value setting.
|
||||||
|
func trySetCustom(val string, value reflect.Value) (isSet bool, err error) {
|
||||||
|
switch v := value.Addr().Interface().(type) {
|
||||||
|
case BindUnmarshaler:
|
||||||
|
return true, v.UnmarshalParam(val)
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setByForm(value reflect.Value, field reflect.StructField, form map[string][]string, tagValue string, opt setOptions) (isSet bool, err error) {
|
||||||
vs, ok := form[tagValue]
|
vs, ok := form[tagValue]
|
||||||
if !ok && !opt.isDefaultExists {
|
if !ok && !opt.isDefaultExists {
|
||||||
return false, nil
|
return false, nil
|
||||||
@ -163,6 +211,9 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
|
|||||||
if len(vs) > 0 {
|
if len(vs) > 0 {
|
||||||
val = vs[0]
|
val = vs[0]
|
||||||
}
|
}
|
||||||
|
if ok, err := trySetCustom(val, value); ok {
|
||||||
|
return ok, err
|
||||||
|
}
|
||||||
return true, setWithProperType(val, value, field)
|
return true, setWithProperType(val, value, field)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -180,7 +231,7 @@ func setWithProperType(val string, value reflect.Value, field reflect.StructFiel
|
|||||||
case reflect.Int64:
|
case reflect.Int64:
|
||||||
switch value.Interface().(type) {
|
switch value.Interface().(type) {
|
||||||
case time.Duration:
|
case time.Duration:
|
||||||
return setTimeDuration(val, value, field)
|
return setTimeDuration(val, value)
|
||||||
}
|
}
|
||||||
return setIntField(val, 64, value)
|
return setIntField(val, 64, value)
|
||||||
case reflect.Uint:
|
case reflect.Uint:
|
||||||
@ -205,10 +256,17 @@ func setWithProperType(val string, value reflect.Value, field reflect.StructFiel
|
|||||||
switch value.Interface().(type) {
|
switch value.Interface().(type) {
|
||||||
case time.Time:
|
case time.Time:
|
||||||
return setTimeField(val, field, value)
|
return setTimeField(val, field, value)
|
||||||
|
case multipart.FileHeader:
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
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())
|
||||||
|
case reflect.Ptr:
|
||||||
|
if !value.Elem().IsValid() {
|
||||||
|
value.Set(reflect.New(value.Type().Elem()))
|
||||||
|
}
|
||||||
|
return setWithProperType(val, value.Elem(), field)
|
||||||
default:
|
default:
|
||||||
return errUnknownType
|
return errUnknownType
|
||||||
}
|
}
|
||||||
@ -265,6 +323,23 @@ 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
|
||||||
@ -312,7 +387,7 @@ func setSlice(vals []string, value reflect.Value, field reflect.StructField) err
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func setTimeDuration(val string, value reflect.Value, field reflect.StructField) error {
|
func setTimeDuration(val string, value reflect.Value) error {
|
||||||
d, err := time.ParseDuration(val)
|
d, err := time.ParseDuration(val)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -322,9 +397,32 @@ func setTimeDuration(val string, value reflect.Value, field reflect.StructField)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func head(str, sep string) (head string, tail string) {
|
func head(str, sep string) (head string, tail string) {
|
||||||
idx := strings.Index(str, sep)
|
head, tail, _ = strings.Cut(str, sep)
|
||||||
if idx < 0 {
|
return head, tail
|
||||||
return str, ""
|
}
|
||||||
}
|
|
||||||
return str[:idx], str[idx+len(sep):]
|
func setFormMap(ptr any, form map[string][]string) error {
|
||||||
|
el := reflect.TypeOf(ptr).Elem()
|
||||||
|
|
||||||
|
if el.Kind() == reflect.Slice {
|
||||||
|
ptrMap, ok := ptr.(map[string][]string)
|
||||||
|
if !ok {
|
||||||
|
return ErrConvertMapStringSlice
|
||||||
|
}
|
||||||
|
for k, v := range form {
|
||||||
|
ptrMap[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ptrMap, ok := ptr.(map[string]string)
|
||||||
|
if !ok {
|
||||||
|
return ErrConvertToMapString
|
||||||
|
}
|
||||||
|
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()
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,11 @@
|
|||||||
package binding
|
package binding
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"mime/multipart"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -18,9 +22,9 @@ func TestMappingBaseTypes(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tt := range []struct {
|
for _, tt := range []struct {
|
||||||
name string
|
name string
|
||||||
value interface{}
|
value any
|
||||||
form string
|
form string
|
||||||
expect interface{}
|
expect any
|
||||||
}{
|
}{
|
||||||
{"base type", struct{ F int }{}, "9", int(9)},
|
{"base type", struct{ F int }{}, "9", int(9)},
|
||||||
{"base type", struct{ F int8 }{}, "9", int8(9)},
|
{"base type", struct{ F int8 }{}, "9", int8(9)},
|
||||||
@ -43,6 +47,7 @@ func TestMappingBaseTypes(t *testing.T) {
|
|||||||
{"zero value", struct{ F uint }{}, "", uint(0)},
|
{"zero value", struct{ F uint }{}, "", uint(0)},
|
||||||
{"zero value", struct{ F bool }{}, "", false},
|
{"zero value", struct{ F bool }{}, "", false},
|
||||||
{"zero value", struct{ F float32 }{}, "", float32(0)},
|
{"zero value", struct{ F float32 }{}, "", float32(0)},
|
||||||
|
{"file value", struct{ F *multipart.FileHeader }{}, "", &multipart.FileHeader{}},
|
||||||
} {
|
} {
|
||||||
tp := reflect.TypeOf(tt.value)
|
tp := reflect.TypeOf(tt.value)
|
||||||
testName := tt.name + ":" + tp.Field(0).Type.String()
|
testName := tt.name + ":" + tp.Field(0).Type.String()
|
||||||
@ -114,7 +119,7 @@ func TestMappingPrivateField(t *testing.T) {
|
|||||||
}
|
}
|
||||||
err := mappingByPtr(&s, formSource{"field": {"6"}}, "form")
|
err := mappingByPtr(&s, formSource{"field": {"6"}}, "form")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, int(0), s.f)
|
assert.Equal(t, 0, s.f)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMappingUnknownFieldType(t *testing.T) {
|
func TestMappingUnknownFieldType(t *testing.T) {
|
||||||
@ -131,9 +136,9 @@ func TestMappingURI(t *testing.T) {
|
|||||||
var s struct {
|
var s struct {
|
||||||
F int `uri:"field"`
|
F int `uri:"field"`
|
||||||
}
|
}
|
||||||
err := mapUri(&s, map[string][]string{"field": {"6"}})
|
err := mapURI(&s, map[string][]string{"field": {"6"}})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, int(6), s.F)
|
assert.Equal(t, 6, s.F)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMappingForm(t *testing.T) {
|
func TestMappingForm(t *testing.T) {
|
||||||
@ -142,7 +147,16 @@ func TestMappingForm(t *testing.T) {
|
|||||||
}
|
}
|
||||||
err := mapForm(&s, map[string][]string{"field": {"6"}})
|
err := mapForm(&s, map[string][]string{"field": {"6"}})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, int(6), s.F)
|
assert.Equal(t, 6, s.F)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMapFormWithTag(t *testing.T) {
|
||||||
|
var s struct {
|
||||||
|
F int `externalTag:"field"`
|
||||||
|
}
|
||||||
|
err := MapFormWithTag(&s, map[string][]string{"field": {"6"}}, "externalTag")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 6, s.F)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMappingTime(t *testing.T) {
|
func TestMappingTime(t *testing.T) {
|
||||||
@ -190,7 +204,7 @@ func TestMappingTime(t *testing.T) {
|
|||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMapiingTimeDuration(t *testing.T) {
|
func TestMappingTimeDuration(t *testing.T) {
|
||||||
var s struct {
|
var s struct {
|
||||||
D time.Duration
|
D time.Duration
|
||||||
}
|
}
|
||||||
@ -260,6 +274,39 @@ func TestMappingStructField(t *testing.T) {
|
|||||||
assert.Equal(t, 9, s.J.I)
|
assert.Equal(t, 9, s.J.I)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMappingPtrField(t *testing.T) {
|
||||||
|
type ptrStruct struct {
|
||||||
|
Key int64 `json:"key"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ptrRequest struct {
|
||||||
|
Items []*ptrStruct `json:"items" form:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// With 0 items.
|
||||||
|
var req0 ptrRequest
|
||||||
|
err = mappingByPtr(&req0, formSource{}, "form")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Empty(t, req0.Items)
|
||||||
|
|
||||||
|
// With 1 item.
|
||||||
|
var req1 ptrRequest
|
||||||
|
err = mappingByPtr(&req1, formSource{"items": {`{"key": 1}`}}, "form")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, req1.Items, 1)
|
||||||
|
assert.EqualValues(t, 1, req1.Items[0].Key)
|
||||||
|
|
||||||
|
// With 2 items.
|
||||||
|
var req2 ptrRequest
|
||||||
|
err = mappingByPtr(&req2, formSource{"items": {`{"key": 1}`, `{"key": 2}`}}, "form")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, req2.Items, 2)
|
||||||
|
assert.EqualValues(t, 1, req2.Items[0].Key)
|
||||||
|
assert.EqualValues(t, 2, req2.Items[1].Key)
|
||||||
|
}
|
||||||
|
|
||||||
func TestMappingMapField(t *testing.T) {
|
func TestMappingMapField(t *testing.T) {
|
||||||
var s struct {
|
var s struct {
|
||||||
M map[string]int
|
M map[string]int
|
||||||
@ -269,3 +316,109 @@ func TestMappingMapField(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, map[string]int{"one": 1}, s.M)
|
assert.Equal(t, map[string]int{"one": 1}, s.M)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMappingIgnoredCircularRef(t *testing.T) {
|
||||||
|
type S struct {
|
||||||
|
S *S `form:"-"`
|
||||||
|
}
|
||||||
|
var s S
|
||||||
|
|
||||||
|
err := mappingByPtr(&s, formSource{}, "form")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
type customUnmarshalParamHex int
|
||||||
|
|
||||||
|
func (f *customUnmarshalParamHex) UnmarshalParam(param string) error {
|
||||||
|
v, err := strconv.ParseInt(param, 16, 64)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*f = customUnmarshalParamHex(v)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMappingCustomUnmarshalParamHexWithFormTag(t *testing.T) {
|
||||||
|
var s struct {
|
||||||
|
Foo customUnmarshalParamHex `form:"foo"`
|
||||||
|
}
|
||||||
|
err := mappingByPtr(&s, formSource{"foo": {`f5`}}, "form")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.EqualValues(t, 245, s.Foo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMappingCustomUnmarshalParamHexWithURITag(t *testing.T) {
|
||||||
|
var s struct {
|
||||||
|
Foo customUnmarshalParamHex `uri:"foo"`
|
||||||
|
}
|
||||||
|
err := mappingByPtr(&s, formSource{"foo": {`f5`}}, "uri")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.EqualValues(t, 245, s.Foo)
|
||||||
|
}
|
||||||
|
|
||||||
|
type customUnmarshalParamType struct {
|
||||||
|
Protocol string
|
||||||
|
Path string
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *customUnmarshalParamType) UnmarshalParam(param string) error {
|
||||||
|
parts := strings.Split(param, ":")
|
||||||
|
if len(parts) != 3 {
|
||||||
|
return fmt.Errorf("invalid format")
|
||||||
|
}
|
||||||
|
f.Protocol = parts[0]
|
||||||
|
f.Path = parts[1]
|
||||||
|
f.Name = parts[2]
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMappingCustomStructTypeWithFormTag(t *testing.T) {
|
||||||
|
var s struct {
|
||||||
|
FileData customUnmarshalParamType `form:"data"`
|
||||||
|
}
|
||||||
|
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "form")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.EqualValues(t, "file", s.FileData.Protocol)
|
||||||
|
assert.EqualValues(t, "/foo", s.FileData.Path)
|
||||||
|
assert.EqualValues(t, "happiness", s.FileData.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMappingCustomStructTypeWithURITag(t *testing.T) {
|
||||||
|
var s struct {
|
||||||
|
FileData customUnmarshalParamType `uri:"data"`
|
||||||
|
}
|
||||||
|
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "uri")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.EqualValues(t, "file", s.FileData.Protocol)
|
||||||
|
assert.EqualValues(t, "/foo", s.FileData.Path)
|
||||||
|
assert.EqualValues(t, "happiness", s.FileData.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMappingCustomPointerStructTypeWithFormTag(t *testing.T) {
|
||||||
|
var s struct {
|
||||||
|
FileData *customUnmarshalParamType `form:"data"`
|
||||||
|
}
|
||||||
|
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "form")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.EqualValues(t, "file", s.FileData.Protocol)
|
||||||
|
assert.EqualValues(t, "/foo", s.FileData.Path)
|
||||||
|
assert.EqualValues(t, "happiness", s.FileData.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMappingCustomPointerStructTypeWithURITag(t *testing.T) {
|
||||||
|
var s struct {
|
||||||
|
FileData *customUnmarshalParamType `uri:"data"`
|
||||||
|
}
|
||||||
|
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "uri")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.EqualValues(t, "file", s.FileData.Protocol)
|
||||||
|
assert.EqualValues(t, "/foo", s.FileData.Path)
|
||||||
|
assert.EqualValues(t, "happiness", s.FileData.Name)
|
||||||
|
}
|
||||||
|
|||||||
38
binding/header.go
Normal file
38
binding/header.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
// Copyright 2022 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 (
|
||||||
|
"net/http"
|
||||||
|
"net/textproto"
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
type headerBinding struct{}
|
||||||
|
|
||||||
|
func (headerBinding) Name() string {
|
||||||
|
return "header"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (headerBinding) Bind(req *http.Request, obj any) error {
|
||||||
|
|
||||||
|
if err := mapHeader(obj, req.Header); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return validate(obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapHeader(ptr any, 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) (bool, error) {
|
||||||
|
return setByForm(value, field, hs, textproto.CanonicalMIMEHeaderKey(tagValue), opt)
|
||||||
|
}
|
||||||
@ -6,7 +6,7 @@ package binding
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
@ -15,31 +15,40 @@ import (
|
|||||||
|
|
||||||
// EnableDecoderUseNumber is used to call the UseNumber method on the JSON
|
// EnableDecoderUseNumber is used to call the UseNumber method on the JSON
|
||||||
// Decoder instance. UseNumber causes the Decoder to unmarshal a number into an
|
// Decoder instance. UseNumber causes the Decoder to unmarshal a number into an
|
||||||
// interface{} as a Number instead of as a float64.
|
// any 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 {
|
||||||
return "json"
|
return "json"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (jsonBinding) Bind(req *http.Request, obj interface{}) error {
|
func (jsonBinding) Bind(req *http.Request, obj any) error {
|
||||||
if req == nil || req.Body == nil {
|
if req == nil || req.Body == nil {
|
||||||
return fmt.Errorf("invalid request")
|
return errors.New("invalid request")
|
||||||
}
|
}
|
||||||
return decodeJSON(req.Body, obj)
|
return decodeJSON(req.Body, obj)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (jsonBinding) BindBody(body []byte, obj interface{}) error {
|
func (jsonBinding) BindBody(body []byte, obj any) error {
|
||||||
return decodeJSON(bytes.NewReader(body), obj)
|
return decodeJSON(bytes.NewReader(body), obj)
|
||||||
}
|
}
|
||||||
|
|
||||||
func decodeJSON(r io.Reader, obj interface{}) error {
|
func decodeJSON(r io.Reader, obj any) error {
|
||||||
decoder := json.NewDecoder(r)
|
decoder := json.NewDecoder(r)
|
||||||
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.
|
||||||
|
|
||||||
|
//go:build !nomsgpack
|
||||||
|
|
||||||
package binding
|
package binding
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -18,15 +20,15 @@ func (msgpackBinding) Name() string {
|
|||||||
return "msgpack"
|
return "msgpack"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (msgpackBinding) Bind(req *http.Request, obj interface{}) error {
|
func (msgpackBinding) Bind(req *http.Request, obj any) error {
|
||||||
return decodeMsgPack(req.Body, obj)
|
return decodeMsgPack(req.Body, obj)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (msgpackBinding) BindBody(body []byte, obj interface{}) error {
|
func (msgpackBinding) BindBody(body []byte, obj any) error {
|
||||||
return decodeMsgPack(bytes.NewReader(body), obj)
|
return decodeMsgPack(bytes.NewReader(body), obj)
|
||||||
}
|
}
|
||||||
|
|
||||||
func decodeMsgPack(r io.Reader, obj interface{}) error {
|
func decodeMsgPack(r io.Reader, obj any) error {
|
||||||
cdc := new(codec.MsgpackHandle)
|
cdc := new(codec.MsgpackHandle)
|
||||||
if err := codec.NewDecoder(r, cdc).Decode(&obj); err != nil {
|
if err := codec.NewDecoder(r, cdc).Decode(&obj); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
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.
|
||||||
|
|
||||||
|
//go: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 any) []byte {
|
||||||
|
var bs bytes.Buffer
|
||||||
|
h := &codec.MsgpackHandle{}
|
||||||
|
err := codec.NewEncoder(&bs, h).Encode(obj)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return bs.Bytes()
|
||||||
|
}
|
||||||
74
binding/multipart_form_mapping.go
Normal file
74
binding/multipart_form_mapping.go
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
// 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)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrMultiFileHeader multipart.FileHeader invalid
|
||||||
|
ErrMultiFileHeader = errors.New("unsupported field type for multipart.FileHeader")
|
||||||
|
|
||||||
|
// ErrMultiFileHeaderLenInvalid array for []*multipart.FileHeader len invalid
|
||||||
|
ErrMultiFileHeaderLenInvalid = errors.New("unsupported len of array for []*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) (bool, 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) (isSet 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))
|
||||||
|
isSet, err = setArrayOfMultipartFormFiles(slice, field, files)
|
||||||
|
if err != nil || !isSet {
|
||||||
|
return isSet, err
|
||||||
|
}
|
||||||
|
value.Set(slice)
|
||||||
|
return true, nil
|
||||||
|
case reflect.Array:
|
||||||
|
return setArrayOfMultipartFormFiles(value, field, files)
|
||||||
|
}
|
||||||
|
return false, ErrMultiFileHeader
|
||||||
|
}
|
||||||
|
|
||||||
|
func setArrayOfMultipartFormFiles(value reflect.Value, field reflect.StructField, files []*multipart.FileHeader) (isSet bool, err error) {
|
||||||
|
if value.Len() != len(files) {
|
||||||
|
return false, ErrMultiFileHeaderLenInvalid
|
||||||
|
}
|
||||||
|
for i := range files {
|
||||||
|
set, err := setByMultipartFormFile(value.Index(i), field, files[i:i+1])
|
||||||
|
if err != nil || !set {
|
||||||
|
return set, 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"
|
||||||
|
"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 any
|
||||||
|
}{
|
||||||
|
{"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)
|
||||||
|
|
||||||
|
fl, err := fh.Open()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
body, err := io.ReadAll(fl)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, string(file.Content), string(body))
|
||||||
|
|
||||||
|
err = fl.Close()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
@ -5,10 +5,11 @@
|
|||||||
package binding
|
package binding
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
"errors"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/golang/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
type protobufBinding struct{}
|
type protobufBinding struct{}
|
||||||
@ -17,16 +18,20 @@ func (protobufBinding) Name() string {
|
|||||||
return "protobuf"
|
return "protobuf"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b protobufBinding) Bind(req *http.Request, obj interface{}) error {
|
func (b protobufBinding) Bind(req *http.Request, obj any) error {
|
||||||
buf, err := ioutil.ReadAll(req.Body)
|
buf, err := io.ReadAll(req.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return b.BindBody(buf, obj)
|
return b.BindBody(buf, obj)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (protobufBinding) BindBody(body []byte, obj interface{}) error {
|
func (protobufBinding) BindBody(body []byte, obj any) error {
|
||||||
if err := proto.Unmarshal(body, obj.(proto.Message)); err != nil {
|
msg, ok := obj.(proto.Message)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("obj is not ProtoMessage")
|
||||||
|
}
|
||||||
|
if err := proto.Unmarshal(body, msg); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// Here it's same to return validate(obj), but util now we can't add
|
// Here it's same to return validate(obj), but util now we can't add
|
||||||
|
|||||||
@ -12,7 +12,7 @@ func (queryBinding) Name() string {
|
|||||||
return "query"
|
return "query"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (queryBinding) Bind(req *http.Request, obj interface{}) error {
|
func (queryBinding) Bind(req *http.Request, obj any) error {
|
||||||
values := req.URL.Query()
|
values := req.URL.Query()
|
||||||
if err := mapForm(obj, values); err != nil {
|
if err := mapForm(obj, values); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
35
binding/toml.go
Normal file
35
binding/toml.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
// Copyright 2022 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"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/pelletier/go-toml/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type tomlBinding struct{}
|
||||||
|
|
||||||
|
func (tomlBinding) Name() string {
|
||||||
|
return "toml"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tomlBinding) Bind(req *http.Request, obj any) error {
|
||||||
|
return decodeToml(req.Body, obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tomlBinding) BindBody(body []byte, obj any) error {
|
||||||
|
return decodeToml(bytes.NewReader(body), obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeToml(r io.Reader, obj any) error {
|
||||||
|
decoder := toml.NewDecoder(r)
|
||||||
|
if err := decoder.Decode(obj); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return decoder.Decode(obj)
|
||||||
|
}
|
||||||
22
binding/toml_test.go
Normal file
22
binding/toml_test.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
// Copyright 2022 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 TestTOMLBindingBindBody(t *testing.T) {
|
||||||
|
var s struct {
|
||||||
|
Foo string `toml:"foo"`
|
||||||
|
}
|
||||||
|
tomlBody := `foo="FOO"`
|
||||||
|
err := tomlBinding{}.BindBody([]byte(tomlBody), &s)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "FOO", s.Foo)
|
||||||
|
}
|
||||||
@ -10,8 +10,8 @@ func (uriBinding) Name() string {
|
|||||||
return "uri"
|
return "uri"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (uriBinding) BindUri(m map[string][]string, obj interface{}) error {
|
func (uriBinding) BindUri(m map[string][]string, obj any) error {
|
||||||
if err := mapUri(obj, m); err != nil {
|
if err := mapURI(obj, m); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return validate(obj)
|
return validate(obj)
|
||||||
|
|||||||
@ -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 {
|
||||||
@ -60,7 +59,7 @@ type structNoValidationValues struct {
|
|||||||
StructSlice []substructNoValidation
|
StructSlice []substructNoValidation
|
||||||
InterfaceSlice []testInterface
|
InterfaceSlice []testInterface
|
||||||
|
|
||||||
UniversalInterface interface{}
|
UniversalInterface any
|
||||||
CustomInterface testInterface
|
CustomInterface testInterface
|
||||||
|
|
||||||
FloatMap map[string]float32
|
FloatMap map[string]float32
|
||||||
@ -170,7 +169,7 @@ func TestValidateNoValidationPointers(t *testing.T) {
|
|||||||
//assert.Equal(t, origin, test)
|
//assert.Equal(t, origin, test)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Object map[string]interface{}
|
type Object map[string]any
|
||||||
|
|
||||||
func TestValidatePrimitives(t *testing.T) {
|
func TestValidatePrimitives(t *testing.T) {
|
||||||
obj := Object{"foo": "bar", "bar": 1}
|
obj := Object{"foo": "bar", "bar": 1}
|
||||||
@ -193,6 +192,30 @@ func TestValidatePrimitives(t *testing.T) {
|
|||||||
assert.Equal(t, "value", str)
|
assert.Equal(t, "value", str)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type structModifyValidation struct {
|
||||||
|
Integer int
|
||||||
|
}
|
||||||
|
|
||||||
|
func toZero(sl validator.StructLevel) {
|
||||||
|
var s *structModifyValidation = sl.Top().Interface().(*structModifyValidation)
|
||||||
|
s.Integer = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateAndModifyStruct(t *testing.T) {
|
||||||
|
// This validates that pointers to structs are passed to the validator
|
||||||
|
// giving us the ability to modify the struct being validated.
|
||||||
|
engine, ok := Validator.Engine().(*validator.Validate)
|
||||||
|
assert.True(t, ok)
|
||||||
|
|
||||||
|
engine.RegisterStructValidation(toZero, structModifyValidation{})
|
||||||
|
|
||||||
|
s := structModifyValidation{Integer: 1}
|
||||||
|
errs := validate(&s)
|
||||||
|
|
||||||
|
assert.Nil(t, errs)
|
||||||
|
assert.Equal(t, s, structModifyValidation{Integer: 0})
|
||||||
|
}
|
||||||
|
|
||||||
// structCustomValidation is a helper struct we use to check that
|
// structCustomValidation is a helper struct we use to check that
|
||||||
// custom validation can be registered on it.
|
// custom validation can be registered on it.
|
||||||
// The `notone` binding directive is for custom validation and registered later.
|
// The `notone` binding directive is for custom validation and registered later.
|
||||||
@ -200,15 +223,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
|
||||||
|
|||||||
@ -17,14 +17,14 @@ func (xmlBinding) Name() string {
|
|||||||
return "xml"
|
return "xml"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (xmlBinding) Bind(req *http.Request, obj interface{}) error {
|
func (xmlBinding) Bind(req *http.Request, obj any) error {
|
||||||
return decodeXML(req.Body, obj)
|
return decodeXML(req.Body, obj)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (xmlBinding) BindBody(body []byte, obj interface{}) error {
|
func (xmlBinding) BindBody(body []byte, obj any) error {
|
||||||
return decodeXML(bytes.NewReader(body), obj)
|
return decodeXML(bytes.NewReader(body), obj)
|
||||||
}
|
}
|
||||||
func decodeXML(r io.Reader, obj interface{}) error {
|
func decodeXML(r io.Reader, obj any) error {
|
||||||
decoder := xml.NewDecoder(r)
|
decoder := xml.NewDecoder(r)
|
||||||
if err := decoder.Decode(obj); err != nil {
|
if err := decoder.Decode(obj); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
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)
|
||||||
|
}
|
||||||
@ -9,7 +9,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
type yamlBinding struct{}
|
type yamlBinding struct{}
|
||||||
@ -18,15 +18,15 @@ func (yamlBinding) Name() string {
|
|||||||
return "yaml"
|
return "yaml"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (yamlBinding) Bind(req *http.Request, obj interface{}) error {
|
func (yamlBinding) Bind(req *http.Request, obj any) error {
|
||||||
return decodeYAML(req.Body, obj)
|
return decodeYAML(req.Body, obj)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (yamlBinding) BindBody(body []byte, obj interface{}) error {
|
func (yamlBinding) BindBody(body []byte, obj any) error {
|
||||||
return decodeYAML(bytes.NewReader(body), obj)
|
return decodeYAML(bytes.NewReader(body), obj)
|
||||||
}
|
}
|
||||||
|
|
||||||
func decodeYAML(r io.Reader, obj interface{}) error {
|
func decodeYAML(r io.Reader, obj any) error {
|
||||||
decoder := yaml.NewDecoder(r)
|
decoder := yaml.NewDecoder(r)
|
||||||
if err := decoder.Decode(obj); err != nil {
|
if err := decoder.Decode(obj); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
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)
|
||||||
|
}
|
||||||
14
codecov.yml
14
codecov.yml
@ -1,5 +1,13 @@
|
|||||||
coverage:
|
coverage:
|
||||||
notify:
|
require_ci_to_pass: true
|
||||||
gitter:
|
|
||||||
|
status:
|
||||||
|
project:
|
||||||
default:
|
default:
|
||||||
url: https://webhooks.gitter.im/e/d90dcdeeab2f1e357165
|
target: 99%
|
||||||
|
threshold: 99%
|
||||||
|
|
||||||
|
patch:
|
||||||
|
default:
|
||||||
|
target: 99%
|
||||||
|
threshold: 95%
|
||||||
548
context.go
548
context.go
File diff suppressed because it is too large
Load Diff
37
context_1.18_test.go
Normal file
37
context_1.18_test.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
// Copyright 2021 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.
|
||||||
|
|
||||||
|
//go:build !go1.19
|
||||||
|
|
||||||
|
package gin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestContextFormFileFailed18(t *testing.T) {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
mw := multipart.NewWriter(buf)
|
||||||
|
defer func(mw *multipart.Writer) {
|
||||||
|
err := mw.Close()
|
||||||
|
if err != nil {
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
}(mw)
|
||||||
|
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||||
|
c.Request, _ = http.NewRequest("POST", "/", nil)
|
||||||
|
c.Request.Header.Set("Content-Type", mw.FormDataContentType())
|
||||||
|
c.engine.MaxMultipartMemory = 8 << 20
|
||||||
|
assert.Panics(t, func() {
|
||||||
|
f, err := c.FormFile("file")
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, f)
|
||||||
|
})
|
||||||
|
}
|
||||||
30
context_1.19_test.go
Normal file
30
context_1.19_test.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
// Copyright 2022 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.
|
||||||
|
|
||||||
|
//go:build go1.19
|
||||||
|
|
||||||
|
package gin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestContextFormFileFailed19(t *testing.T) {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
mw := multipart.NewWriter(buf)
|
||||||
|
mw.Close()
|
||||||
|
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||||
|
c.Request, _ = http.NewRequest("POST", "/", nil)
|
||||||
|
c.Request.Header.Set("Content-Type", mw.FormDataContentType())
|
||||||
|
c.engine.MaxMultipartMemory = 8 << 20
|
||||||
|
f, err := c.FormFile("file")
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, f)
|
||||||
|
}
|
||||||
@ -1,11 +1,11 @@
|
|||||||
// +build appengine
|
|
||||||
|
|
||||||
// Copyright 2017 Manu Martinez-Almeida. All rights reserved.
|
// Copyright 2017 Manu Martinez-Almeida. All rights reserved.
|
||||||
// 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.
|
||||||
|
|
||||||
|
//go:build appengine
|
||||||
|
|
||||||
package gin
|
package gin
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
defaultAppEngine = true
|
defaultPlatform = PlatformGoogleAppEngine
|
||||||
}
|
}
|
||||||
|
|||||||
1028
context_test.go
1028
context_test.go
File diff suppressed because it is too large
Load Diff
32
debug.go
32
debug.go
@ -5,17 +5,15 @@
|
|||||||
package gin
|
package gin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"os"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
)
|
)
|
||||||
|
|
||||||
const ginSupportMinGoVer = 8
|
const ginSupportMinGoVer = 18
|
||||||
|
|
||||||
// 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.
|
||||||
@ -26,6 +24,9 @@ func IsDebugging() bool {
|
|||||||
// DebugPrintRouteFunc indicates debug log output format.
|
// DebugPrintRouteFunc indicates debug log output format.
|
||||||
var DebugPrintRouteFunc func(httpMethod, absolutePath, handlerName string, nuHandlers int)
|
var DebugPrintRouteFunc func(httpMethod, absolutePath, handlerName string, nuHandlers int)
|
||||||
|
|
||||||
|
// DebugPrintFunc indicates debug log output format.
|
||||||
|
var DebugPrintFunc func(format string, values ...interface{})
|
||||||
|
|
||||||
func debugPrintRoute(httpMethod, absolutePath string, handlers HandlersChain) {
|
func debugPrintRoute(httpMethod, absolutePath string, handlers HandlersChain) {
|
||||||
if IsDebugging() {
|
if IsDebugging() {
|
||||||
nuHandlers := len(handlers)
|
nuHandlers := len(handlers)
|
||||||
@ -40,7 +41,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())
|
||||||
@ -50,13 +51,20 @@ func debugPrintLoadTemplate(tmpl *template.Template) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func debugPrint(format string, values ...interface{}) {
|
func debugPrint(format string, values ...any) {
|
||||||
if IsDebugging() {
|
if !IsDebugging() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if DebugPrintFunc != nil {
|
||||||
|
DebugPrintFunc(format, values...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
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...)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getMinVer(v string) (uint64, error) {
|
func getMinVer(v string) (uint64, error) {
|
||||||
@ -69,8 +77,8 @@ 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.8 or later and Go 1.9 will be required soon.
|
debugPrint(`[WARNING] Now Gin requires Go 1.18+.
|
||||||
|
|
||||||
`)
|
`)
|
||||||
}
|
}
|
||||||
@ -98,7 +106,7 @@ 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 && IsDebugging() {
|
||||||
debugPrint("[ERROR] %v\n", err)
|
fmt.Fprintf(DefaultErrorWriter, "[GIN-debug] [ERROR] %v\n", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,13 +5,14 @@
|
|||||||
package gin
|
package gin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@ -20,7 +21,7 @@ import (
|
|||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
// func debugRoute(httpMethod, absolutePath string, handlers HandlersChain) {
|
// func debugRoute(httpMethod, absolutePath string, handlers HandlersChain) {
|
||||||
// func debugPrint(format string, values ...interface{}) {
|
// func debugPrint(format string, values ...any) {
|
||||||
|
|
||||||
func TestIsDebugging(t *testing.T) {
|
func TestIsDebugging(t *testing.T) {
|
||||||
SetMode(DebugMode)
|
SetMode(DebugMode)
|
||||||
@ -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)
|
||||||
@ -90,8 +103,8 @@ func TestDebugPrintWARNINGDefault(t *testing.T) {
|
|||||||
SetMode(TestMode)
|
SetMode(TestMode)
|
||||||
})
|
})
|
||||||
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.8 or later and Go 1.9 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.18+.\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,21 +124,21 @@ 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)
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
var buf bytes.Buffer
|
var buf strings.Builder
|
||||||
wg.Done()
|
wg.Done()
|
||||||
_, err := io.Copy(&buf, reader)
|
_, err := io.Copy(&buf, reader)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|||||||
@ -12,8 +12,10 @@ import (
|
|||||||
|
|
||||||
// BindWith binds the passed struct pointer using the specified binding engine.
|
// BindWith binds the passed struct pointer using the specified binding engine.
|
||||||
// See the binding package.
|
// See the binding package.
|
||||||
func (c *Context) BindWith(obj interface{}, b binding.Binding) error {
|
//
|
||||||
log.Println(`BindWith(\"interface{}, binding.Binding\") error is going to
|
// Deprecated: Use MustBindWith or ShouldBindWith.
|
||||||
|
func (c *Context) BindWith(obj any, b binding.Binding) error {
|
||||||
|
log.Println(`BindWith(\"any, binding.Binding\") error is going to
|
||||||
be deprecated, please check issue #662 and either use MustBindWith() if you
|
be deprecated, please check issue #662 and either use MustBindWith() if you
|
||||||
want HTTP 400 to be automatically returned if any error occur, or use
|
want HTTP 400 to be automatically returned if any error occur, or use
|
||||||
ShouldBindWith() if you need to manage the error.`)
|
ShouldBindWith() if you need to manage the error.`)
|
||||||
|
|||||||
2333
docs/doc.md
Normal file
2333
docs/doc.md
Normal file
File diff suppressed because it is too large
Load Diff
42
errors.go
42
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"
|
||||||
)
|
)
|
||||||
@ -34,12 +34,12 @@ const (
|
|||||||
type Error struct {
|
type Error struct {
|
||||||
Err error
|
Err error
|
||||||
Type ErrorType
|
Type ErrorType
|
||||||
Meta interface{}
|
Meta any
|
||||||
}
|
}
|
||||||
|
|
||||||
type errorMsgs []*Error
|
type errorMsgs []*Error
|
||||||
|
|
||||||
var _ error = &Error{}
|
var _ error = (*Error)(nil)
|
||||||
|
|
||||||
// SetType sets the error's type.
|
// SetType sets the error's type.
|
||||||
func (msg *Error) SetType(flags ErrorType) *Error {
|
func (msg *Error) SetType(flags ErrorType) *Error {
|
||||||
@ -48,14 +48,14 @@ func (msg *Error) SetType(flags ErrorType) *Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SetMeta sets the error's meta data.
|
// SetMeta sets the error's meta data.
|
||||||
func (msg *Error) SetMeta(data interface{}) *Error {
|
func (msg *Error) SetMeta(data any) *Error {
|
||||||
msg.Meta = data
|
msg.Meta = data
|
||||||
return msg
|
return msg
|
||||||
}
|
}
|
||||||
|
|
||||||
// JSON creates a properly formatted JSON
|
// JSON creates a properly formatted JSON
|
||||||
func (msg *Error) JSON() interface{} {
|
func (msg *Error) JSON() any {
|
||||||
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 {
|
||||||
@ -117,8 +122,9 @@ func (a errorMsgs) Last() *Error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Errors returns an array will all the error messages.
|
// Errors returns an array with all the error messages.
|
||||||
// Example:
|
// Example:
|
||||||
|
//
|
||||||
// c.Error(errors.New("first"))
|
// c.Error(errors.New("first"))
|
||||||
// c.Error(errors.New("second"))
|
// c.Error(errors.New("second"))
|
||||||
// c.Error(errors.New("third"))
|
// c.Error(errors.New("third"))
|
||||||
@ -134,18 +140,18 @@ func (a errorMsgs) Errors() []string {
|
|||||||
return errorStrings
|
return errorStrings
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a errorMsgs) JSON() interface{} {
|
func (a errorMsgs) JSON() any {
|
||||||
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([]any, 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 +164,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 {
|
||||||
|
|||||||
@ -6,6 +6,7 @@ package gin
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin/internal/json"
|
"github.com/gin-gonic/gin/internal/json"
|
||||||
@ -34,7 +35,7 @@ func TestError(t *testing.T) {
|
|||||||
jsonBytes, _ := json.Marshal(err)
|
jsonBytes, _ := json.Marshal(err)
|
||||||
assert.Equal(t, "{\"error\":\"test error\",\"meta\":\"some data\"}", string(jsonBytes))
|
assert.Equal(t, "{\"error\":\"test error\",\"meta\":\"some data\"}", string(jsonBytes))
|
||||||
|
|
||||||
err.SetMeta(H{ // nolint: errcheck
|
err.SetMeta(H{ //nolint: errcheck
|
||||||
"status": "200",
|
"status": "200",
|
||||||
"data": "some data",
|
"data": "some data",
|
||||||
})
|
})
|
||||||
@ -44,7 +45,7 @@ func TestError(t *testing.T) {
|
|||||||
"data": "some data",
|
"data": "some data",
|
||||||
}, err.JSON())
|
}, err.JSON())
|
||||||
|
|
||||||
err.SetMeta(H{ // nolint: errcheck
|
err.SetMeta(H{ //nolint: errcheck
|
||||||
"error": "custom error",
|
"error": "custom error",
|
||||||
"status": "200",
|
"status": "200",
|
||||||
"data": "some data",
|
"data": "some data",
|
||||||
@ -59,7 +60,7 @@ func TestError(t *testing.T) {
|
|||||||
status string
|
status string
|
||||||
data string
|
data string
|
||||||
}
|
}
|
||||||
err.SetMeta(customError{status: "200", data: "other data"}) // nolint: errcheck
|
err.SetMeta(customError{status: "200", data: "other data"}) //nolint: errcheck
|
||||||
assert.Equal(t, customError{status: "200", data: "other data"}, err.JSON())
|
assert.Equal(t, customError{status: "200", data: "other data"}, err.JSON())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,7 +86,7 @@ Error #02: second
|
|||||||
Error #03: third
|
Error #03: third
|
||||||
Meta: map[status:400]
|
Meta: map[status:400]
|
||||||
`, errs.String())
|
`, errs.String())
|
||||||
assert.Equal(t, []interface{}{
|
assert.Equal(t, []any{
|
||||||
H{"error": "first"},
|
H{"error": "first"},
|
||||||
H{"error": "second", "meta": "some data"},
|
H{"error": "second", "meta": "some data"},
|
||||||
H{"error": "third", "status": "400"},
|
H{"error": "third", "status": "400"},
|
||||||
@ -104,3 +105,24 @@ Error #03: third
|
|||||||
assert.Nil(t, errs.JSON())
|
assert.Nil(t, errs.JSON())
|
||||||
assert.Empty(t, errs.String())
|
assert.Empty(t, errs.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
func TestErrorUnwrap(t *testing.T) {
|
||||||
|
innerErr := TestErr("some 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))
|
||||||
|
}
|
||||||
|
|||||||
10
fs.go
10
fs.go
@ -9,7 +9,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
type onlyfilesFS struct {
|
type onlyFilesFS struct {
|
||||||
fs http.FileSystem
|
fs http.FileSystem
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -17,7 +17,7 @@ type neuteredReaddirFile struct {
|
|||||||
http.File
|
http.File
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dir returns a http.Filesystem that can be used by http.FileServer(). It is used internally
|
// Dir returns a http.FileSystem that can be used by http.FileServer(). It is used internally
|
||||||
// in router.Static().
|
// in router.Static().
|
||||||
// if listDirectory == true, then it works the same as http.Dir() otherwise it returns
|
// if listDirectory == true, then it works the same as http.Dir() otherwise it returns
|
||||||
// a filesystem that prevents http.FileServer() to list the directory files.
|
// a filesystem that prevents http.FileServer() to list the directory files.
|
||||||
@ -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
|
||||||
@ -39,7 +39,7 @@ func (fs onlyfilesFS) Open(name string) (http.File, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Readdir overrides the http.File default implementation.
|
// Readdir overrides the http.File default implementation.
|
||||||
func (f neuteredReaddirFile) Readdir(count int) ([]os.FileInfo, error) {
|
func (f neuteredReaddirFile) Readdir(_ int) ([]os.FileInfo, error) {
|
||||||
// this disables directory listing
|
// this disables directory listing
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|||||||
382
gin.go
382
gin.go
@ -11,9 +11,14 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin/internal/bytesconv"
|
||||||
"github.com/gin-gonic/gin/render"
|
"github.com/gin-gonic/gin/render"
|
||||||
|
"golang.org/x/net/http2"
|
||||||
|
"golang.org/x/net/http2/h2c"
|
||||||
)
|
)
|
||||||
|
|
||||||
const defaultMultipartMemory = 32 << 20 // 32 MB
|
const defaultMultipartMemory = 32 << 20 // 32 MB
|
||||||
@ -21,16 +26,34 @@ 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 defaultPlatform string
|
||||||
|
|
||||||
|
var defaultTrustedCIDRs = []*net.IPNet{
|
||||||
|
{ // 0.0.0.0/0 (IPv4)
|
||||||
|
IP: net.IP{0x0, 0x0, 0x0, 0x0},
|
||||||
|
Mask: net.IPMask{0x0, 0x0, 0x0, 0x0},
|
||||||
|
},
|
||||||
|
{ // ::/0 (IPv6)
|
||||||
|
IP: net.IP{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
|
||||||
|
Mask: net.IPMask{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var regSafePrefix = regexp.MustCompile("[^a-zA-Z0-9/-]+")
|
||||||
|
var regRemoveRepeatedChar = regexp.MustCompile("/{2,}")
|
||||||
|
|
||||||
// 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.
|
// OptionFunc defines the function to change the default configuration
|
||||||
|
type OptionFunc func(*Engine)
|
||||||
|
|
||||||
|
// HandlersChain defines a HandlerFunc slice.
|
||||||
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. i.e. 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]
|
||||||
@ -46,22 +69,34 @@ type RouteInfo struct {
|
|||||||
HandlerFunc HandlerFunc
|
HandlerFunc HandlerFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
// RoutesInfo defines a RouteInfo array.
|
// RoutesInfo defines a RouteInfo slice.
|
||||||
type RoutesInfo []RouteInfo
|
type RoutesInfo []RouteInfo
|
||||||
|
|
||||||
|
// Trusted platforms
|
||||||
|
const (
|
||||||
|
// PlatformGoogleAppEngine when running on Google App Engine. Trust X-Appengine-Remote-Addr
|
||||||
|
// for determining the client's IP
|
||||||
|
PlatformGoogleAppEngine = "X-Appengine-Remote-Addr"
|
||||||
|
// PlatformCloudflare when using Cloudflare's CDN. Trust CF-Connecting-IP for determining
|
||||||
|
// the client's IP
|
||||||
|
PlatformCloudflare = "CF-Connecting-IP"
|
||||||
|
// PlatformFlyIO when running on Fly.io. Trust Fly-Client-IP for determining the client's IP
|
||||||
|
PlatformFlyIO = "Fly-Client-IP"
|
||||||
|
)
|
||||||
|
|
||||||
// Engine is the framework's instance, it contains the muxer, middleware and configuration settings.
|
// Engine is the framework's instance, it contains the muxer, middleware and configuration settings.
|
||||||
// Create an instance of Engine, by using New() or Default()
|
// Create an instance of Engine, by using New() or Default()
|
||||||
type Engine struct {
|
type Engine struct {
|
||||||
RouterGroup
|
RouterGroup
|
||||||
|
|
||||||
// Enables automatic redirection if the current route can't be matched but a
|
// RedirectTrailingSlash enables automatic redirection if the current route can't be matched but a
|
||||||
// handler for the path with (without) the trailing slash exists.
|
// handler for the path with (without) the trailing slash exists.
|
||||||
// For example if /foo/ is requested but a route only exists for /foo, the
|
// For example if /foo/ is requested but a route only exists for /foo, the
|
||||||
// client is redirected to /foo with http status code 301 for GET requests
|
// client is redirected to /foo with http status code 301 for GET requests
|
||||||
// and 307 for all other request methods.
|
// and 307 for all other request methods.
|
||||||
RedirectTrailingSlash bool
|
RedirectTrailingSlash bool
|
||||||
|
|
||||||
// If enabled, the router tries to fix the current request path, if no
|
// RedirectFixedPath if enabled, the router tries to fix the current request path, if no
|
||||||
// handle is registered for it.
|
// handle is registered for it.
|
||||||
// First superfluous path elements like ../ or // are removed.
|
// First superfluous path elements like ../ or // are removed.
|
||||||
// Afterwards the router does a case-insensitive lookup of the cleaned path.
|
// Afterwards the router does a case-insensitive lookup of the cleaned path.
|
||||||
@ -72,33 +107,60 @@ type Engine struct {
|
|||||||
// RedirectTrailingSlash is independent of this option.
|
// RedirectTrailingSlash is independent of this option.
|
||||||
RedirectFixedPath bool
|
RedirectFixedPath bool
|
||||||
|
|
||||||
// If enabled, the router checks if another method is allowed for the
|
// HandleMethodNotAllowed if enabled, the router checks if another method is allowed for the
|
||||||
// current route, if the current request can not be routed.
|
// current route, if the current request can not be routed.
|
||||||
// If this is the case, the request is answered with 'Method Not Allowed'
|
// If this is the case, the request is answered with 'Method Not Allowed'
|
||||||
// and HTTP status code 405.
|
// and HTTP status code 405.
|
||||||
// If no other Method is allowed, the request is delegated to the NotFound
|
// If no other Method is allowed, the request is delegated to the NotFound
|
||||||
// handler.
|
// handler.
|
||||||
HandleMethodNotAllowed bool
|
HandleMethodNotAllowed bool
|
||||||
|
|
||||||
|
// ForwardedByClientIP if enabled, client IP will be parsed from the request's headers that
|
||||||
|
// match those stored at `(*gin.Engine).RemoteIPHeaders`. If no IP was
|
||||||
|
// fetched, it falls back to the IP obtained from
|
||||||
|
// `(*gin.Context).Request.RemoteAddr`.
|
||||||
ForwardedByClientIP bool
|
ForwardedByClientIP bool
|
||||||
|
|
||||||
// #726 #755 If enabled, it will thrust some headers starting with
|
// AppEngine was deprecated.
|
||||||
|
// Deprecated: USE `TrustedPlatform` WITH VALUE `gin.PlatformGoogleAppEngine` INSTEAD
|
||||||
|
// #726 #755 If enabled, it will trust some headers starting with
|
||||||
// 'X-AppEngine...' for better integration with that PaaS.
|
// 'X-AppEngine...' for better integration with that PaaS.
|
||||||
AppEngine bool
|
AppEngine bool
|
||||||
|
|
||||||
// If enabled, the url.RawPath will be used to find parameters.
|
// UseRawPath if enabled, the url.RawPath will be used to find parameters.
|
||||||
UseRawPath bool
|
UseRawPath bool
|
||||||
|
|
||||||
// If true, the path value will be unescaped.
|
// UnescapePathValues if true, the path value will be unescaped.
|
||||||
// If UseRawPath is false (by default), the UnescapePathValues effectively is true,
|
// If UseRawPath is false (by default), the UnescapePathValues effectively is true,
|
||||||
// as url.Path gonna be used, which is already unescaped.
|
// as url.Path gonna be used, which is already unescaped.
|
||||||
UnescapePathValues bool
|
UnescapePathValues bool
|
||||||
|
|
||||||
// Value of 'maxMemory' param that is given to http.Request's ParseMultipartForm
|
// RemoveExtraSlash a parameter can be parsed from the URL even with extra slashes.
|
||||||
|
// See the PR #1817 and issue #1644
|
||||||
|
RemoveExtraSlash bool
|
||||||
|
|
||||||
|
// RemoteIPHeaders list of headers used to obtain the client IP when
|
||||||
|
// `(*gin.Engine).ForwardedByClientIP` is `true` and
|
||||||
|
// `(*gin.Context).Request.RemoteAddr` is matched by at least one of the
|
||||||
|
// network origins of list defined by `(*gin.Engine).SetTrustedProxies()`.
|
||||||
|
RemoteIPHeaders []string
|
||||||
|
|
||||||
|
// TrustedPlatform if set to a constant of value gin.Platform*, trusts the headers set by
|
||||||
|
// that platform, for example to determine the client IP
|
||||||
|
TrustedPlatform string
|
||||||
|
|
||||||
|
// MaxMultipartMemory value of 'maxMemory' param that is given to http.Request's ParseMultipartForm
|
||||||
// method call.
|
// method call.
|
||||||
MaxMultipartMemory int64
|
MaxMultipartMemory int64
|
||||||
|
|
||||||
|
// UseH2C enable h2c support.
|
||||||
|
UseH2C bool
|
||||||
|
|
||||||
|
// ContextWithFallback enable fallback Context.Deadline(), Context.Done(), Context.Err() and Context.Value() when Context.Request.Context() is not nil.
|
||||||
|
ContextWithFallback 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,19 +169,23 @@ type Engine struct {
|
|||||||
noMethod HandlersChain
|
noMethod HandlersChain
|
||||||
pool sync.Pool
|
pool sync.Pool
|
||||||
trees methodTrees
|
trees methodTrees
|
||||||
|
maxParams uint16
|
||||||
|
maxSections uint16
|
||||||
|
trustedProxies []string
|
||||||
|
trustedCIDRs []*net.IPNet
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ IRouter = &Engine{}
|
var _ IRouter = (*Engine)(nil)
|
||||||
|
|
||||||
// New returns a new blank Engine instance without any middleware attached.
|
// New returns a new blank Engine instance without any middleware attached.
|
||||||
// By default the configuration is:
|
// By default, the configuration is:
|
||||||
// - RedirectTrailingSlash: true
|
// - RedirectTrailingSlash: true
|
||||||
// - RedirectFixedPath: false
|
// - RedirectFixedPath: false
|
||||||
// - HandleMethodNotAllowed: false
|
// - HandleMethodNotAllowed: false
|
||||||
// - ForwardedByClientIP: true
|
// - ForwardedByClientIP: true
|
||||||
// - UseRawPath: false
|
// - UseRawPath: false
|
||||||
// - UnescapePathValues: true
|
// - UnescapePathValues: true
|
||||||
func New() *Engine {
|
func New(opts ...OptionFunc) *Engine {
|
||||||
debugPrintWARNINGNew()
|
debugPrintWARNINGNew()
|
||||||
engine := &Engine{
|
engine := &Engine{
|
||||||
RouterGroup: RouterGroup{
|
RouterGroup: RouterGroup{
|
||||||
@ -132,42 +198,57 @@ func New() *Engine {
|
|||||||
RedirectFixedPath: false,
|
RedirectFixedPath: false,
|
||||||
HandleMethodNotAllowed: false,
|
HandleMethodNotAllowed: false,
|
||||||
ForwardedByClientIP: true,
|
ForwardedByClientIP: true,
|
||||||
AppEngine: defaultAppEngine,
|
RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"},
|
||||||
|
TrustedPlatform: defaultPlatform,
|
||||||
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);",
|
||||||
|
trustedProxies: []string{"0.0.0.0/0", "::/0"},
|
||||||
|
trustedCIDRs: defaultTrustedCIDRs,
|
||||||
}
|
}
|
||||||
engine.RouterGroup.engine = engine
|
engine.RouterGroup.engine = engine
|
||||||
engine.pool.New = func() interface{} {
|
engine.pool.New = func() any {
|
||||||
return engine.allocateContext()
|
return engine.allocateContext(engine.maxParams)
|
||||||
}
|
}
|
||||||
return engine
|
return engine.With(opts...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default returns an Engine instance with the Logger and Recovery middleware already attached.
|
// Default returns an Engine instance with the Logger and Recovery middleware already attached.
|
||||||
func Default() *Engine {
|
func Default(opts ...OptionFunc) *Engine {
|
||||||
debugPrintWARNINGDefault()
|
debugPrintWARNINGDefault()
|
||||||
engine := New()
|
engine := New()
|
||||||
engine.Use(Logger(), Recovery())
|
engine.Use(Logger(), Recovery())
|
||||||
|
return engine.With(opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (engine *Engine) Handler() http.Handler {
|
||||||
|
if !engine.UseH2C {
|
||||||
return engine
|
return engine
|
||||||
|
}
|
||||||
|
|
||||||
|
h2s := &http2.Server{}
|
||||||
|
return h2c.NewHandler(engine, h2s)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (engine *Engine) allocateContext() *Context {
|
func (engine *Engine) allocateContext(maxParams uint16) *Context {
|
||||||
return &Context{engine: engine}
|
v := make(Params, 0, maxParams)
|
||||||
|
skippedNodes := make([]skippedNode, 0, engine.maxSections)
|
||||||
|
return &Context{engine: engine, params: &v, skippedNodes: &skippedNodes}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delims sets template left and right delims and returns a Engine instance.
|
// Delims sets template left and right delims and returns an Engine instance.
|
||||||
func (engine *Engine) Delims(left, right string) *Engine {
|
func (engine *Engine) Delims(left, right string) *Engine {
|
||||||
engine.delims = render.Delims{Left: left, Right: right}
|
engine.delims = render.Delims{Left: left, Right: right}
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -213,19 +294,19 @@ func (engine *Engine) SetFuncMap(funcMap template.FuncMap) {
|
|||||||
engine.FuncMap = funcMap
|
engine.FuncMap = funcMap
|
||||||
}
|
}
|
||||||
|
|
||||||
// NoRoute adds handlers for NoRoute. It return a 404 code by default.
|
// NoRoute adds handlers for NoRoute. It returns a 404 code by default.
|
||||||
func (engine *Engine) NoRoute(handlers ...HandlerFunc) {
|
func (engine *Engine) NoRoute(handlers ...HandlerFunc) {
|
||||||
engine.noRoute = handlers
|
engine.noRoute = handlers
|
||||||
engine.rebuild404Handlers()
|
engine.rebuild404Handlers()
|
||||||
}
|
}
|
||||||
|
|
||||||
// NoMethod sets the handlers called when... TODO.
|
// NoMethod sets the handlers called when Engine.HandleMethodNotAllowed = true.
|
||||||
func (engine *Engine) NoMethod(handlers ...HandlerFunc) {
|
func (engine *Engine) NoMethod(handlers ...HandlerFunc) {
|
||||||
engine.noMethod = handlers
|
engine.noMethod = handlers
|
||||||
engine.rebuild405Handlers()
|
engine.rebuild405Handlers()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use attaches a global middleware to the router. ie. the middleware attached though Use() will be
|
// Use attaches a global middleware to the router. i.e. the middleware attached through Use() will be
|
||||||
// included in the handlers chain for every single request. Even 404, 405, static files...
|
// included in the handlers chain for every single request. Even 404, 405, static files...
|
||||||
// For example, this is the right place for a logger or error management middleware.
|
// For example, this is the right place for a logger or error management middleware.
|
||||||
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
|
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
|
||||||
@ -235,6 +316,15 @@ func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
|
|||||||
return engine
|
return engine
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// With returns a Engine with the configuration set in the OptionFunc.
|
||||||
|
func (engine *Engine) With(opts ...OptionFunc) *Engine {
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(engine)
|
||||||
|
}
|
||||||
|
|
||||||
|
return engine
|
||||||
|
}
|
||||||
|
|
||||||
func (engine *Engine) rebuild404Handlers() {
|
func (engine *Engine) rebuild404Handlers() {
|
||||||
engine.allNoRoute = engine.combineHandlers(engine.noRoute)
|
engine.allNoRoute = engine.combineHandlers(engine.noRoute)
|
||||||
}
|
}
|
||||||
@ -249,12 +339,22 @@ 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)
|
||||||
|
|
||||||
|
if paramsCount := countParams(path); paramsCount > engine.maxParams {
|
||||||
|
engine.maxParams = paramsCount
|
||||||
|
}
|
||||||
|
|
||||||
|
if sectionsCount := countSections(path); sectionsCount > engine.maxSections {
|
||||||
|
engine.maxSections = sectionsCount
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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:
|
||||||
@ -289,12 +389,120 @@ func iterate(path, method string, routes RoutesInfo, root *node) RoutesInfo {
|
|||||||
func (engine *Engine) Run(addr ...string) (err error) {
|
func (engine *Engine) Run(addr ...string) (err error) {
|
||||||
defer func() { debugPrintError(err) }()
|
defer func() { debugPrintError(err) }()
|
||||||
|
|
||||||
|
if engine.isUnsafeTrustedProxies() {
|
||||||
|
debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
|
||||||
|
"Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.")
|
||||||
|
}
|
||||||
|
|
||||||
address := resolveAddress(addr)
|
address := resolveAddress(addr)
|
||||||
debugPrint("Listening and serving HTTP on %s\n", address)
|
debugPrint("Listening and serving HTTP on %s\n", address)
|
||||||
err = http.ListenAndServe(address, engine)
|
err = http.ListenAndServe(address, engine.Handler())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (engine *Engine) prepareTrustedCIDRs() ([]*net.IPNet, error) {
|
||||||
|
if engine.trustedProxies == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cidr := make([]*net.IPNet, 0, len(engine.trustedProxies))
|
||||||
|
for _, trustedProxy := range engine.trustedProxies {
|
||||||
|
if !strings.Contains(trustedProxy, "/") {
|
||||||
|
ip := parseIP(trustedProxy)
|
||||||
|
if ip == nil {
|
||||||
|
return cidr, &net.ParseError{Type: "IP address", Text: trustedProxy}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch len(ip) {
|
||||||
|
case net.IPv4len:
|
||||||
|
trustedProxy += "/32"
|
||||||
|
case net.IPv6len:
|
||||||
|
trustedProxy += "/128"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, cidrNet, err := net.ParseCIDR(trustedProxy)
|
||||||
|
if err != nil {
|
||||||
|
return cidr, err
|
||||||
|
}
|
||||||
|
cidr = append(cidr, cidrNet)
|
||||||
|
}
|
||||||
|
return cidr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTrustedProxies set a list of network origins (IPv4 addresses,
|
||||||
|
// IPv4 CIDRs, IPv6 addresses or IPv6 CIDRs) from which to trust
|
||||||
|
// request's headers that contain alternative client IP when
|
||||||
|
// `(*gin.Engine).ForwardedByClientIP` is `true`. `TrustedProxies`
|
||||||
|
// feature is enabled by default, and it also trusts all proxies
|
||||||
|
// by default. If you want to disable this feature, use
|
||||||
|
// Engine.SetTrustedProxies(nil), then Context.ClientIP() will
|
||||||
|
// return the remote address directly.
|
||||||
|
func (engine *Engine) SetTrustedProxies(trustedProxies []string) error {
|
||||||
|
engine.trustedProxies = trustedProxies
|
||||||
|
return engine.parseTrustedProxies()
|
||||||
|
}
|
||||||
|
|
||||||
|
// isUnsafeTrustedProxies checks if Engine.trustedCIDRs contains all IPs, it's not safe if it has (returns true)
|
||||||
|
func (engine *Engine) isUnsafeTrustedProxies() bool {
|
||||||
|
return engine.isTrustedProxy(net.ParseIP("0.0.0.0")) || engine.isTrustedProxy(net.ParseIP("::"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseTrustedProxies parse Engine.trustedProxies to Engine.trustedCIDRs
|
||||||
|
func (engine *Engine) parseTrustedProxies() error {
|
||||||
|
trustedCIDRs, err := engine.prepareTrustedCIDRs()
|
||||||
|
engine.trustedCIDRs = trustedCIDRs
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// isTrustedProxy will check whether the IP address is included in the trusted list according to Engine.trustedCIDRs
|
||||||
|
func (engine *Engine) isTrustedProxy(ip net.IP) bool {
|
||||||
|
if engine.trustedCIDRs == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, cidr := range engine.trustedCIDRs {
|
||||||
|
if cidr.Contains(ip) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateHeader will parse X-Forwarded-For header and return the trusted client IP address
|
||||||
|
func (engine *Engine) validateHeader(header string) (clientIP string, valid bool) {
|
||||||
|
if header == "" {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
items := strings.Split(header, ",")
|
||||||
|
for i := len(items) - 1; i >= 0; i-- {
|
||||||
|
ipStr := strings.TrimSpace(items[i])
|
||||||
|
ip := net.ParseIP(ipStr)
|
||||||
|
if ip == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// X-Forwarded-For is appended by proxy
|
||||||
|
// Check IPs in reverse order and stop when find untrusted proxy
|
||||||
|
if (i == 0) || (!engine.isTrustedProxy(ip)) {
|
||||||
|
return ipStr, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseIP parse a string representation of an IP and returns a net.IP with the
|
||||||
|
// minimum byte representation or nil if input is invalid.
|
||||||
|
func parseIP(ip string) net.IP {
|
||||||
|
parsedIP := net.ParseIP(ip)
|
||||||
|
|
||||||
|
if ipv4 := parsedIP.To4(); ipv4 != nil {
|
||||||
|
// return ip in a 4-byte representation
|
||||||
|
return ipv4
|
||||||
|
}
|
||||||
|
|
||||||
|
// return ip in a 16-byte representation or nil
|
||||||
|
return parsedIP
|
||||||
|
}
|
||||||
|
|
||||||
// RunTLS attaches the router to a http.Server and starts listening and serving HTTPS (secure) requests.
|
// RunTLS attaches the router to a http.Server and starts listening and serving HTTPS (secure) requests.
|
||||||
// It is a shortcut for http.ListenAndServeTLS(addr, certFile, keyFile, router)
|
// It is a shortcut for http.ListenAndServeTLS(addr, certFile, keyFile, router)
|
||||||
// Note: this method will block the calling goroutine indefinitely unless an error happens.
|
// Note: this method will block the calling goroutine indefinitely unless an error happens.
|
||||||
@ -302,25 +510,35 @@ func (engine *Engine) RunTLS(addr, certFile, keyFile string) (err error) {
|
|||||||
debugPrint("Listening and serving HTTPS on %s\n", addr)
|
debugPrint("Listening and serving HTTPS on %s\n", addr)
|
||||||
defer func() { debugPrintError(err) }()
|
defer func() { debugPrintError(err) }()
|
||||||
|
|
||||||
err = http.ListenAndServeTLS(addr, certFile, keyFile, engine)
|
if engine.isUnsafeTrustedProxies() {
|
||||||
|
debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
|
||||||
|
"Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = http.ListenAndServeTLS(addr, certFile, keyFile, engine.Handler())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunUnix attaches the router to a http.Server and starts listening and serving HTTP requests
|
// RunUnix attaches the router to a http.Server and starts listening and serving HTTP requests
|
||||||
// through the specified unix socket (ie. a file).
|
// through the specified unix socket (i.e. a file).
|
||||||
// Note: this method will block the calling goroutine indefinitely unless an error happens.
|
// Note: this method will block the calling goroutine indefinitely unless an error happens.
|
||||||
func (engine *Engine) RunUnix(file string) (err error) {
|
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)
|
if engine.isUnsafeTrustedProxies() {
|
||||||
|
debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
|
||||||
|
"Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.")
|
||||||
|
}
|
||||||
|
|
||||||
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.Handler())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -331,13 +549,33 @@ func (engine *Engine) RunFd(fd int) (err error) {
|
|||||||
debugPrint("Listening and serving HTTP on fd@%d", fd)
|
debugPrint("Listening and serving HTTP on fd@%d", fd)
|
||||||
defer func() { debugPrintError(err) }()
|
defer func() { debugPrintError(err) }()
|
||||||
|
|
||||||
|
if engine.isUnsafeTrustedProxies() {
|
||||||
|
debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
|
||||||
|
"Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.")
|
||||||
|
}
|
||||||
|
|
||||||
f := os.NewFile(uintptr(fd), fmt.Sprintf("fd@%d", fd))
|
f := os.NewFile(uintptr(fd), fmt.Sprintf("fd@%d", fd))
|
||||||
listener, err := net.FileListener(f)
|
listener, err := net.FileListener(f)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
err = http.Serve(listener, engine)
|
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) }()
|
||||||
|
|
||||||
|
if engine.isUnsafeTrustedProxies() {
|
||||||
|
debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
|
||||||
|
"Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = http.Serve(listener, engine.Handler())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -353,9 +591,9 @@ func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|||||||
engine.pool.Put(c)
|
engine.pool.Put(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleContext re-enter a context that has been rewritten.
|
// HandleContext re-enters a context that has been rewritten.
|
||||||
// This can be done by setting c.Request.URL.Path to your new target.
|
// This can be done by setting c.Request.URL.Path to your new target.
|
||||||
// Disclaimer: You can loop yourself to death with this, use wisely.
|
// Disclaimer: You can loop yourself to deal with this, use wisely.
|
||||||
func (engine *Engine) HandleContext(c *Context) {
|
func (engine *Engine) HandleContext(c *Context) {
|
||||||
oldIndexValue := c.index
|
oldIndexValue := c.index
|
||||||
c.reset()
|
c.reset()
|
||||||
@ -373,6 +611,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++ {
|
||||||
@ -381,16 +623,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, c.skippedNodes, 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 != http.MethodConnect && rPath != "/" {
|
||||||
if tsr && engine.RedirectTrailingSlash {
|
if value.tsr && engine.RedirectTrailingSlash {
|
||||||
redirectTrailingSlash(c)
|
redirectTrailingSlash(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -402,17 +647,25 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if engine.HandleMethodNotAllowed {
|
if engine.HandleMethodNotAllowed {
|
||||||
|
// According to RFC 7231 section 6.5.5, MUST generate an Allow header field in response
|
||||||
|
// containing a list of the target resource's currently supported methods.
|
||||||
|
allowed := make([]string, 0, len(t)-1)
|
||||||
for _, tree := range engine.trees {
|
for _, tree := range engine.trees {
|
||||||
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, c.skippedNodes, unescape); value.handlers != nil {
|
||||||
|
allowed = append(allowed, tree.method)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(allowed) > 0 {
|
||||||
c.handlers = engine.allNoMethod
|
c.handlers = engine.allNoMethod
|
||||||
|
c.writermem.Header().Set("Allow", strings.Join(allowed, ", "))
|
||||||
serveError(c, http.StatusMethodNotAllowed, default405Body)
|
serveError(c, http.StatusMethodNotAllowed, default405Body)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
c.handlers = engine.allNoRoute
|
c.handlers = engine.allNoRoute
|
||||||
serveError(c, http.StatusNotFound, default404Body)
|
serveError(c, http.StatusNotFound, default404Body)
|
||||||
}
|
}
|
||||||
@ -434,27 +687,22 @@ 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) {
|
||||||
req := c.Request
|
req := c.Request
|
||||||
p := req.URL.Path
|
p := req.URL.Path
|
||||||
if prefix := path.Clean(c.Request.Header.Get("X-Forwarded-Prefix")); prefix != "." {
|
if prefix := path.Clean(c.Request.Header.Get("X-Forwarded-Prefix")); prefix != "." {
|
||||||
|
prefix = regSafePrefix.ReplaceAllString(prefix, "")
|
||||||
|
prefix = regRemoveRepeatedChar.ReplaceAllString(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 {
|
||||||
@ -462,15 +710,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()
|
||||||
|
}
|
||||||
|
|||||||
@ -37,7 +37,7 @@ func SetHTMLTemplate(templ *template.Template) {
|
|||||||
engine().SetHTMLTemplate(templ)
|
engine().SetHTMLTemplate(templ)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NoRoute adds handlers for NoRoute. It return a 404 code by default.
|
// NoRoute adds handlers for NoRoute. It returns a 404 code by default.
|
||||||
func NoRoute(handlers ...gin.HandlerFunc) {
|
func NoRoute(handlers ...gin.HandlerFunc) {
|
||||||
engine().NoRoute(handlers...)
|
engine().NoRoute(handlers...)
|
||||||
}
|
}
|
||||||
@ -108,6 +108,7 @@ func StaticFile(relativePath, filepath string) gin.IRoutes {
|
|||||||
// of the Router's NotFound handler.
|
// of the Router's NotFound handler.
|
||||||
// To use the operating system's file system implementation,
|
// To use the operating system's file system implementation,
|
||||||
// use :
|
// use :
|
||||||
|
//
|
||||||
// router.Static("/static", "/var/www")
|
// router.Static("/static", "/var/www")
|
||||||
func Static(relativePath, root string) gin.IRoutes {
|
func Static(relativePath, root string) gin.IRoutes {
|
||||||
return engine().Static(relativePath, root)
|
return engine().Static(relativePath, root)
|
||||||
@ -118,7 +119,7 @@ func StaticFS(relativePath string, fs http.FileSystem) gin.IRoutes {
|
|||||||
return engine().StaticFS(relativePath, fs)
|
return engine().StaticFS(relativePath, fs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use attaches a global middleware to the router. ie. the middlewares attached though Use() will be
|
// Use attaches a global middleware to the router. i.e. the middlewares attached through Use() will be
|
||||||
// included in the handlers chain for every single request. Even 404, 405, static files...
|
// included in the handlers chain for every single request. Even 404, 405, static files...
|
||||||
// For example, this is the right place for a logger or error management middleware.
|
// For example, this is the right place for a logger or error management middleware.
|
||||||
func Use(middlewares ...gin.HandlerFunc) gin.IRoutes {
|
func Use(middlewares ...gin.HandlerFunc) gin.IRoutes {
|
||||||
@ -145,7 +146,7 @@ func RunTLS(addr, certFile, keyFile string) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RunUnix attaches to a http.Server and starts listening and serving HTTP requests
|
// RunUnix attaches to a http.Server and starts listening and serving HTTP requests
|
||||||
// through the specified unix socket (ie. a file)
|
// through the specified unix socket (i.e. a file)
|
||||||
// Note: this method will block the calling goroutine indefinitely unless an error happens.
|
// Note: this method will block the calling goroutine indefinitely unless an error happens.
|
||||||
func RunUnix(file string) (err error) {
|
func RunUnix(file string) (err error) {
|
||||||
return engine().RunUnix(file)
|
return engine().RunUnix(file)
|
||||||
|
|||||||
@ -9,11 +9,13 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io/ioutil"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@ -21,7 +23,15 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func testRequest(t *testing.T, url string) {
|
// params[0]=url example:http://127.0.0.1:8080/index (cannot be empty)
|
||||||
|
// params[1]=response status (custom compare status) default:"200 OK"
|
||||||
|
// params[2]=response body (custom compare content) default:"it worked"
|
||||||
|
func testRequest(t *testing.T, params ...string) {
|
||||||
|
|
||||||
|
if len(params) == 0 {
|
||||||
|
t.Fatal("url cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
tr := &http.Transport{
|
tr := &http.Transport{
|
||||||
TLSClientConfig: &tls.Config{
|
TLSClientConfig: &tls.Config{
|
||||||
InsecureSkipVerify: true,
|
InsecureSkipVerify: true,
|
||||||
@ -29,14 +39,27 @@ func testRequest(t *testing.T, url string) {
|
|||||||
}
|
}
|
||||||
client := &http.Client{Transport: tr}
|
client := &http.Client{Transport: tr}
|
||||||
|
|
||||||
resp, err := client.Get(url)
|
resp, err := client.Get(params[0])
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
body, ioerr := ioutil.ReadAll(resp.Body)
|
body, ioerr := io.ReadAll(resp.Body)
|
||||||
assert.NoError(t, ioerr)
|
assert.NoError(t, ioerr)
|
||||||
assert.Equal(t, "it worked", string(body), "resp body should match")
|
|
||||||
assert.Equal(t, "200 OK", resp.Status, "should get a 200")
|
var responseStatus = "200 OK"
|
||||||
|
if len(params) > 1 && params[1] != "" {
|
||||||
|
responseStatus = params[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
var responseBody = "it worked"
|
||||||
|
if len(params) > 2 && params[2] != "" {
|
||||||
|
responseBody = params[2]
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, responseStatus, resp.Status, "should get a "+responseStatus)
|
||||||
|
if responseStatus == "200 OK" {
|
||||||
|
assert.Equal(t, responseBody, string(body), "resp body should match")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRunEmpty(t *testing.T) {
|
func TestRunEmpty(t *testing.T) {
|
||||||
@ -54,6 +77,81 @@ func TestRunEmpty(t *testing.T) {
|
|||||||
testRequest(t, "http://localhost:8080/example")
|
testRequest(t, "http://localhost:8080/example")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBadTrustedCIDRs(t *testing.T) {
|
||||||
|
router := New()
|
||||||
|
assert.Error(t, router.SetTrustedProxies([]string{"hello/world"}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/* legacy tests
|
||||||
|
func TestBadTrustedCIDRsForRun(t *testing.T) {
|
||||||
|
os.Setenv("PORT", "")
|
||||||
|
router := New()
|
||||||
|
router.TrustedProxies = []string{"hello/world"}
|
||||||
|
assert.Error(t, router.Run(":8080"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBadTrustedCIDRsForRunUnix(t *testing.T) {
|
||||||
|
router := New()
|
||||||
|
router.TrustedProxies = []string{"hello/world"}
|
||||||
|
|
||||||
|
unixTestSocket := filepath.Join(os.TempDir(), "unix_unit_test")
|
||||||
|
|
||||||
|
defer os.Remove(unixTestSocket)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
|
||||||
|
assert.Error(t, router.RunUnix(unixTestSocket))
|
||||||
|
}()
|
||||||
|
// have to wait for the goroutine to start and run the server
|
||||||
|
// otherwise the main thread will complete
|
||||||
|
time.Sleep(5 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBadTrustedCIDRsForRunFd(t *testing.T) {
|
||||||
|
router := New()
|
||||||
|
router.TrustedProxies = []string{"hello/world"}
|
||||||
|
|
||||||
|
addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
listener, err := net.ListenTCP("tcp", addr)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
socketFile, err := listener.File()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
|
||||||
|
assert.Error(t, router.RunFd(int(socketFile.Fd())))
|
||||||
|
}()
|
||||||
|
// have to wait for the goroutine to start and run the server
|
||||||
|
// otherwise the main thread will complete
|
||||||
|
time.Sleep(5 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBadTrustedCIDRsForRunListener(t *testing.T) {
|
||||||
|
router := New()
|
||||||
|
router.TrustedProxies = []string{"hello/world"}
|
||||||
|
|
||||||
|
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.Error(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)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBadTrustedCIDRsForRunTLS(t *testing.T) {
|
||||||
|
os.Setenv("PORT", "")
|
||||||
|
router := New()
|
||||||
|
router.TrustedProxies = []string{"hello/world"}
|
||||||
|
assert.Error(t, router.RunTLS(":8080", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem"))
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
func TestRunTLS(t *testing.T) {
|
func TestRunTLS(t *testing.T) {
|
||||||
router := New()
|
router := New()
|
||||||
go func() {
|
go func() {
|
||||||
@ -90,7 +188,8 @@ func TestPusher(t *testing.T) {
|
|||||||
go func() {
|
go func() {
|
||||||
router.GET("/pusher", func(c *Context) {
|
router.GET("/pusher", func(c *Context) {
|
||||||
if pusher := c.Writer.Pusher(); pusher != nil {
|
if pusher := c.Writer.Pusher(); pusher != nil {
|
||||||
pusher.Push("/assets/app.js", nil)
|
err := pusher.Push("/assets/app.js", nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
c.String(http.StatusOK, "it worked")
|
c.String(http.StatusOK, "it worked")
|
||||||
})
|
})
|
||||||
@ -145,15 +244,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")
|
||||||
@ -179,7 +282,16 @@ func TestFileDescriptor(t *testing.T) {
|
|||||||
listener, err := net.ListenTCP("tcp", addr)
|
listener, err := net.ListenTCP("tcp", addr)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
socketFile, err := listener.File()
|
socketFile, err := listener.File()
|
||||||
|
if isWindows() {
|
||||||
|
// not supported by windows, it is unimplemented now
|
||||||
|
assert.Error(t, err)
|
||||||
|
} else {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if socketFile == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
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") })
|
||||||
@ -207,6 +319,43 @@ func TestBadFileDescriptor(t *testing.T) {
|
|||||||
assert.Error(t, router.RunFd(0))
|
assert.Error(t, router.RunFd(0))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestListener(t *testing.T) {
|
||||||
|
router := New()
|
||||||
|
addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
listener, err := net.ListenTCP("tcp", addr)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
go func() {
|
||||||
|
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
|
||||||
|
assert.NoError(t, router.RunListener(listener))
|
||||||
|
}()
|
||||||
|
// have to wait for the goroutine to start and run the server
|
||||||
|
// otherwise the main thread will complete
|
||||||
|
time.Sleep(5 * time.Millisecond)
|
||||||
|
|
||||||
|
c, err := net.Dial("tcp", listener.Addr().String())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
fmt.Fprintf(c, "GET /example HTTP/1.0\r\n\r\n")
|
||||||
|
scanner := bufio.NewScanner(c)
|
||||||
|
var response string
|
||||||
|
for scanner.Scan() {
|
||||||
|
response += scanner.Text()
|
||||||
|
}
|
||||||
|
assert.Contains(t, response, "HTTP/1.0 200", "should get a 200")
|
||||||
|
assert.Contains(t, response, "it worked", "resp body should match")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBadListener(t *testing.T) {
|
||||||
|
router := New()
|
||||||
|
addr, err := net.ResolveTCPAddr("tcp", "localhost:10086")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
listener, err := net.ListenTCP("tcp", addr)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
listener.Close()
|
||||||
|
assert.Error(t, router.RunListener(listener))
|
||||||
|
}
|
||||||
|
|
||||||
func TestWithHttptestWithAutoSelectedPort(t *testing.T) {
|
func TestWithHttptestWithAutoSelectedPort(t *testing.T) {
|
||||||
router := New()
|
router := New()
|
||||||
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
|
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
|
||||||
@ -253,7 +402,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()
|
||||||
@ -262,3 +411,153 @@ func testGetRequestHandler(t *testing.T, h http.Handler, url string) {
|
|||||||
assert.Equal(t, "it worked", w.Body.String(), "resp body should match")
|
assert.Equal(t, "it worked", w.Body.String(), "resp body should match")
|
||||||
assert.Equal(t, 200, w.Code, "should get a 200")
|
assert.Equal(t, 200, w.Code, "should get a 200")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTreeRunDynamicRouting(t *testing.T) {
|
||||||
|
router := New()
|
||||||
|
router.GET("/aa/*xx", func(c *Context) { c.String(http.StatusOK, "/aa/*xx") })
|
||||||
|
router.GET("/ab/*xx", func(c *Context) { c.String(http.StatusOK, "/ab/*xx") })
|
||||||
|
router.GET("/", func(c *Context) { c.String(http.StatusOK, "home") })
|
||||||
|
router.GET("/:cc", func(c *Context) { c.String(http.StatusOK, "/:cc") })
|
||||||
|
router.GET("/c1/:dd/e", func(c *Context) { c.String(http.StatusOK, "/c1/:dd/e") })
|
||||||
|
router.GET("/c1/:dd/e1", func(c *Context) { c.String(http.StatusOK, "/c1/:dd/e1") })
|
||||||
|
router.GET("/c1/:dd/f1", func(c *Context) { c.String(http.StatusOK, "/c1/:dd/f1") })
|
||||||
|
router.GET("/c1/:dd/f2", func(c *Context) { c.String(http.StatusOK, "/c1/:dd/f2") })
|
||||||
|
router.GET("/:cc/cc", func(c *Context) { c.String(http.StatusOK, "/:cc/cc") })
|
||||||
|
router.GET("/:cc/:dd/ee", func(c *Context) { c.String(http.StatusOK, "/:cc/:dd/ee") })
|
||||||
|
router.GET("/:cc/:dd/f", func(c *Context) { c.String(http.StatusOK, "/:cc/:dd/f") })
|
||||||
|
router.GET("/:cc/:dd/:ee/ff", func(c *Context) { c.String(http.StatusOK, "/:cc/:dd/:ee/ff") })
|
||||||
|
router.GET("/:cc/:dd/:ee/:ff/gg", func(c *Context) { c.String(http.StatusOK, "/:cc/:dd/:ee/:ff/gg") })
|
||||||
|
router.GET("/:cc/:dd/:ee/:ff/:gg/hh", func(c *Context) { c.String(http.StatusOK, "/:cc/:dd/:ee/:ff/:gg/hh") })
|
||||||
|
router.GET("/get/test/abc/", func(c *Context) { c.String(http.StatusOK, "/get/test/abc/") })
|
||||||
|
router.GET("/get/:param/abc/", func(c *Context) { c.String(http.StatusOK, "/get/:param/abc/") })
|
||||||
|
router.GET("/something/:paramname/thirdthing", func(c *Context) { c.String(http.StatusOK, "/something/:paramname/thirdthing") })
|
||||||
|
router.GET("/something/secondthing/test", func(c *Context) { c.String(http.StatusOK, "/something/secondthing/test") })
|
||||||
|
router.GET("/get/abc", func(c *Context) { c.String(http.StatusOK, "/get/abc") })
|
||||||
|
router.GET("/get/:param", func(c *Context) { c.String(http.StatusOK, "/get/:param") })
|
||||||
|
router.GET("/get/abc/123abc", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abc") })
|
||||||
|
router.GET("/get/abc/:param", func(c *Context) { c.String(http.StatusOK, "/get/abc/:param") })
|
||||||
|
router.GET("/get/abc/123abc/xxx8", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abc/xxx8") })
|
||||||
|
router.GET("/get/abc/123abc/:param", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abc/:param") })
|
||||||
|
router.GET("/get/abc/123abc/xxx8/1234", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abc/xxx8/1234") })
|
||||||
|
router.GET("/get/abc/123abc/xxx8/:param", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abc/xxx8/:param") })
|
||||||
|
router.GET("/get/abc/123abc/xxx8/1234/ffas", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abc/xxx8/1234/ffas") })
|
||||||
|
router.GET("/get/abc/123abc/xxx8/1234/:param", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abc/xxx8/1234/:param") })
|
||||||
|
router.GET("/get/abc/123abc/xxx8/1234/kkdd/12c", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abc/xxx8/1234/kkdd/12c") })
|
||||||
|
router.GET("/get/abc/123abc/xxx8/1234/kkdd/:param", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abc/xxx8/1234/kkdd/:param") })
|
||||||
|
router.GET("/get/abc/:param/test", func(c *Context) { c.String(http.StatusOK, "/get/abc/:param/test") })
|
||||||
|
router.GET("/get/abc/123abd/:param", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abd/:param") })
|
||||||
|
router.GET("/get/abc/123abddd/:param", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abddd/:param") })
|
||||||
|
router.GET("/get/abc/123/:param", func(c *Context) { c.String(http.StatusOK, "/get/abc/123/:param") })
|
||||||
|
router.GET("/get/abc/123abg/:param", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abg/:param") })
|
||||||
|
router.GET("/get/abc/123abf/:param", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abf/:param") })
|
||||||
|
router.GET("/get/abc/123abfff/:param", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abfff/:param") })
|
||||||
|
|
||||||
|
ts := httptest.NewServer(router)
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
testRequest(t, ts.URL+"/", "", "home")
|
||||||
|
testRequest(t, ts.URL+"/aa/aa", "", "/aa/*xx")
|
||||||
|
testRequest(t, ts.URL+"/ab/ab", "", "/ab/*xx")
|
||||||
|
testRequest(t, ts.URL+"/all", "", "/:cc")
|
||||||
|
testRequest(t, ts.URL+"/all/cc", "", "/:cc/cc")
|
||||||
|
testRequest(t, ts.URL+"/a/cc", "", "/:cc/cc")
|
||||||
|
testRequest(t, ts.URL+"/c1/d/e", "", "/c1/:dd/e")
|
||||||
|
testRequest(t, ts.URL+"/c1/d/e1", "", "/c1/:dd/e1")
|
||||||
|
testRequest(t, ts.URL+"/c1/d/ee", "", "/:cc/:dd/ee")
|
||||||
|
testRequest(t, ts.URL+"/c1/d/f", "", "/:cc/:dd/f")
|
||||||
|
testRequest(t, ts.URL+"/c/d/ee", "", "/:cc/:dd/ee")
|
||||||
|
testRequest(t, ts.URL+"/c/d/e/ff", "", "/:cc/:dd/:ee/ff")
|
||||||
|
testRequest(t, ts.URL+"/c/d/e/f/gg", "", "/:cc/:dd/:ee/:ff/gg")
|
||||||
|
testRequest(t, ts.URL+"/c/d/e/f/g/hh", "", "/:cc/:dd/:ee/:ff/:gg/hh")
|
||||||
|
testRequest(t, ts.URL+"/cc/dd/ee/ff/gg/hh", "", "/:cc/:dd/:ee/:ff/:gg/hh")
|
||||||
|
testRequest(t, ts.URL+"/a", "", "/:cc")
|
||||||
|
testRequest(t, ts.URL+"/d", "", "/:cc")
|
||||||
|
testRequest(t, ts.URL+"/ad", "", "/:cc")
|
||||||
|
testRequest(t, ts.URL+"/dd", "", "/:cc")
|
||||||
|
testRequest(t, ts.URL+"/aa", "", "/:cc")
|
||||||
|
testRequest(t, ts.URL+"/aaa", "", "/:cc")
|
||||||
|
testRequest(t, ts.URL+"/aaa/cc", "", "/:cc/cc")
|
||||||
|
testRequest(t, ts.URL+"/ab", "", "/:cc")
|
||||||
|
testRequest(t, ts.URL+"/abb", "", "/:cc")
|
||||||
|
testRequest(t, ts.URL+"/abb/cc", "", "/:cc/cc")
|
||||||
|
testRequest(t, ts.URL+"/dddaa", "", "/:cc")
|
||||||
|
testRequest(t, ts.URL+"/allxxxx", "", "/:cc")
|
||||||
|
testRequest(t, ts.URL+"/alldd", "", "/:cc")
|
||||||
|
testRequest(t, ts.URL+"/cc/cc", "", "/:cc/cc")
|
||||||
|
testRequest(t, ts.URL+"/ccc/cc", "", "/:cc/cc")
|
||||||
|
testRequest(t, ts.URL+"/deedwjfs/cc", "", "/:cc/cc")
|
||||||
|
testRequest(t, ts.URL+"/acllcc/cc", "", "/:cc/cc")
|
||||||
|
testRequest(t, ts.URL+"/get/test/abc/", "", "/get/test/abc/")
|
||||||
|
testRequest(t, ts.URL+"/get/testaa/abc/", "", "/get/:param/abc/")
|
||||||
|
testRequest(t, ts.URL+"/get/te/abc/", "", "/get/:param/abc/")
|
||||||
|
testRequest(t, ts.URL+"/get/xx/abc/", "", "/get/:param/abc/")
|
||||||
|
testRequest(t, ts.URL+"/get/tt/abc/", "", "/get/:param/abc/")
|
||||||
|
testRequest(t, ts.URL+"/get/a/abc/", "", "/get/:param/abc/")
|
||||||
|
testRequest(t, ts.URL+"/get/t/abc/", "", "/get/:param/abc/")
|
||||||
|
testRequest(t, ts.URL+"/get/aa/abc/", "", "/get/:param/abc/")
|
||||||
|
testRequest(t, ts.URL+"/get/abas/abc/", "", "/get/:param/abc/")
|
||||||
|
testRequest(t, ts.URL+"/something/secondthing/test", "", "/something/secondthing/test")
|
||||||
|
testRequest(t, ts.URL+"/something/secondthingaaaa/thirdthing", "", "/something/:paramname/thirdthing")
|
||||||
|
testRequest(t, ts.URL+"/something/abcdad/thirdthing", "", "/something/:paramname/thirdthing")
|
||||||
|
testRequest(t, ts.URL+"/something/se/thirdthing", "", "/something/:paramname/thirdthing")
|
||||||
|
testRequest(t, ts.URL+"/something/s/thirdthing", "", "/something/:paramname/thirdthing")
|
||||||
|
testRequest(t, ts.URL+"/something/secondthing/thirdthing", "", "/something/:paramname/thirdthing")
|
||||||
|
testRequest(t, ts.URL+"/get/abc", "", "/get/abc")
|
||||||
|
testRequest(t, ts.URL+"/get/a", "", "/get/:param")
|
||||||
|
testRequest(t, ts.URL+"/get/abz", "", "/get/:param")
|
||||||
|
testRequest(t, ts.URL+"/get/12a", "", "/get/:param")
|
||||||
|
testRequest(t, ts.URL+"/get/abcd", "", "/get/:param")
|
||||||
|
testRequest(t, ts.URL+"/get/abc/123abc", "", "/get/abc/123abc")
|
||||||
|
testRequest(t, ts.URL+"/get/abc/12", "", "/get/abc/:param")
|
||||||
|
testRequest(t, ts.URL+"/get/abc/123ab", "", "/get/abc/:param")
|
||||||
|
testRequest(t, ts.URL+"/get/abc/xyz", "", "/get/abc/:param")
|
||||||
|
testRequest(t, ts.URL+"/get/abc/123abcddxx", "", "/get/abc/:param")
|
||||||
|
testRequest(t, ts.URL+"/get/abc/123abc/xxx8", "", "/get/abc/123abc/xxx8")
|
||||||
|
testRequest(t, ts.URL+"/get/abc/123abc/x", "", "/get/abc/123abc/:param")
|
||||||
|
testRequest(t, ts.URL+"/get/abc/123abc/xxx", "", "/get/abc/123abc/:param")
|
||||||
|
testRequest(t, ts.URL+"/get/abc/123abc/abc", "", "/get/abc/123abc/:param")
|
||||||
|
testRequest(t, ts.URL+"/get/abc/123abc/xxx8xxas", "", "/get/abc/123abc/:param")
|
||||||
|
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234", "", "/get/abc/123abc/xxx8/1234")
|
||||||
|
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1", "", "/get/abc/123abc/xxx8/:param")
|
||||||
|
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/123", "", "/get/abc/123abc/xxx8/:param")
|
||||||
|
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/78k", "", "/get/abc/123abc/xxx8/:param")
|
||||||
|
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234xxxd", "", "/get/abc/123abc/xxx8/:param")
|
||||||
|
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234/ffas", "", "/get/abc/123abc/xxx8/1234/ffas")
|
||||||
|
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234/f", "", "/get/abc/123abc/xxx8/1234/:param")
|
||||||
|
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234/ffa", "", "/get/abc/123abc/xxx8/1234/:param")
|
||||||
|
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234/kka", "", "/get/abc/123abc/xxx8/1234/:param")
|
||||||
|
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234/ffas321", "", "/get/abc/123abc/xxx8/1234/:param")
|
||||||
|
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234/kkdd/12c", "", "/get/abc/123abc/xxx8/1234/kkdd/12c")
|
||||||
|
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234/kkdd/1", "", "/get/abc/123abc/xxx8/1234/kkdd/:param")
|
||||||
|
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234/kkdd/12", "", "/get/abc/123abc/xxx8/1234/kkdd/:param")
|
||||||
|
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234/kkdd/12b", "", "/get/abc/123abc/xxx8/1234/kkdd/:param")
|
||||||
|
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234/kkdd/34", "", "/get/abc/123abc/xxx8/1234/kkdd/:param")
|
||||||
|
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234/kkdd/12c2e3", "", "/get/abc/123abc/xxx8/1234/kkdd/:param")
|
||||||
|
testRequest(t, ts.URL+"/get/abc/12/test", "", "/get/abc/:param/test")
|
||||||
|
testRequest(t, ts.URL+"/get/abc/123abdd/test", "", "/get/abc/:param/test")
|
||||||
|
testRequest(t, ts.URL+"/get/abc/123abdddf/test", "", "/get/abc/:param/test")
|
||||||
|
testRequest(t, ts.URL+"/get/abc/123ab/test", "", "/get/abc/:param/test")
|
||||||
|
testRequest(t, ts.URL+"/get/abc/123abgg/test", "", "/get/abc/:param/test")
|
||||||
|
testRequest(t, ts.URL+"/get/abc/123abff/test", "", "/get/abc/:param/test")
|
||||||
|
testRequest(t, ts.URL+"/get/abc/123abffff/test", "", "/get/abc/:param/test")
|
||||||
|
testRequest(t, ts.URL+"/get/abc/123abd/test", "", "/get/abc/123abd/:param")
|
||||||
|
testRequest(t, ts.URL+"/get/abc/123abddd/test", "", "/get/abc/123abddd/:param")
|
||||||
|
testRequest(t, ts.URL+"/get/abc/123/test22", "", "/get/abc/123/:param")
|
||||||
|
testRequest(t, ts.URL+"/get/abc/123abg/test", "", "/get/abc/123abg/:param")
|
||||||
|
testRequest(t, ts.URL+"/get/abc/123abf/testss", "", "/get/abc/123abf/:param")
|
||||||
|
testRequest(t, ts.URL+"/get/abc/123abfff/te", "", "/get/abc/123abfff/:param")
|
||||||
|
// 404 not found
|
||||||
|
testRequest(t, ts.URL+"/c/d/e", "404 Not Found")
|
||||||
|
testRequest(t, ts.URL+"/c/d/e1", "404 Not Found")
|
||||||
|
testRequest(t, ts.URL+"/c/d/eee", "404 Not Found")
|
||||||
|
testRequest(t, ts.URL+"/c1/d/eee", "404 Not Found")
|
||||||
|
testRequest(t, ts.URL+"/c1/d/e2", "404 Not Found")
|
||||||
|
testRequest(t, ts.URL+"/cc/dd/ee/ff/gg/hh1", "404 Not Found")
|
||||||
|
testRequest(t, ts.URL+"/a/dd", "404 Not Found")
|
||||||
|
testRequest(t, ts.URL+"/addr/dd/aa", "404 Not Found")
|
||||||
|
testRequest(t, ts.URL+"/something/secondthing/121", "404 Not Found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func isWindows() bool {
|
||||||
|
return runtime.GOOS == "windows"
|
||||||
|
}
|
||||||
|
|||||||
266
gin_test.go
266
gin_test.go
@ -8,16 +8,19 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io/ioutil"
|
"io"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"golang.org/x/net/http2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func formatAsDate(t time.Time) string {
|
func formatAsDate(t time.Time) string {
|
||||||
@ -41,7 +44,7 @@ func setupHTMLFiles(t *testing.T, mode string, tls bool, loadMethod func(*Engine
|
|||||||
c.HTML(http.StatusOK, "hello.tmpl", map[string]string{"name": "world"})
|
c.HTML(http.StatusOK, "hello.tmpl", map[string]string{"name": "world"})
|
||||||
})
|
})
|
||||||
router.GET("/raw", func(c *Context) {
|
router.GET("/raw", func(c *Context) {
|
||||||
c.HTML(http.StatusOK, "raw.tmpl", map[string]interface{}{
|
c.HTML(http.StatusOK, "raw.tmpl", map[string]any{
|
||||||
"now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC),
|
"now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -71,10 +74,48 @@ func TestLoadHTMLGlobDebugMode(t *testing.T) {
|
|||||||
|
|
||||||
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
|
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, _ := ioutil.ReadAll(res.Body)
|
resp, _ := io.ReadAll(res.Body)
|
||||||
|
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestH2c(t *testing.T) {
|
||||||
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
r := Default()
|
||||||
|
r.UseH2C = true
|
||||||
|
r.GET("/", func(c *Context) {
|
||||||
|
c.String(200, "<h1>Hello world</h1>")
|
||||||
|
})
|
||||||
|
go func() {
|
||||||
|
err := http.Serve(ln, r.Handler())
|
||||||
|
if err != nil {
|
||||||
|
t.Log(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
defer ln.Close()
|
||||||
|
|
||||||
|
url := "http://" + ln.Addr().String() + "/"
|
||||||
|
|
||||||
|
httpClient := http.Client{
|
||||||
|
Transport: &http2.Transport{
|
||||||
|
AllowHTTP: true,
|
||||||
|
DialTLS: func(netw, addr string, cfg *tls.Config) (net.Conn, error) {
|
||||||
|
return net.Dial(netw, addr)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := httpClient.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, _ := io.ReadAll(res.Body)
|
||||||
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
|
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,10 +132,10 @@ func TestLoadHTMLGlobTestMode(t *testing.T) {
|
|||||||
|
|
||||||
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
|
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, _ := ioutil.ReadAll(res.Body)
|
resp, _ := io.ReadAll(res.Body)
|
||||||
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
|
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,10 +152,10 @@ func TestLoadHTMLGlobReleaseMode(t *testing.T) {
|
|||||||
|
|
||||||
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
|
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, _ := ioutil.ReadAll(res.Body)
|
resp, _ := io.ReadAll(res.Body)
|
||||||
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
|
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,10 +179,10 @@ func TestLoadHTMLGlobUsingTLS(t *testing.T) {
|
|||||||
client := &http.Client{Transport: tr}
|
client := &http.Client{Transport: tr}
|
||||||
res, err := client.Get(fmt.Sprintf("%s/test", ts.URL))
|
res, err := client.Get(fmt.Sprintf("%s/test", ts.URL))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, _ := ioutil.ReadAll(res.Body)
|
resp, _ := io.ReadAll(res.Body)
|
||||||
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
|
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,11 +199,11 @@ func TestLoadHTMLGlobFromFuncMap(t *testing.T) {
|
|||||||
|
|
||||||
res, err := http.Get(fmt.Sprintf("%s/raw", ts.URL))
|
res, err := http.Get(fmt.Sprintf("%s/raw", ts.URL))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, _ := ioutil.ReadAll(res.Body)
|
resp, _ := io.ReadAll(res.Body)
|
||||||
assert.Equal(t, "Date: 2017/07/01\n", string(resp))
|
assert.Equal(t, "Date: 2017/07/01", string(resp))
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@ -189,10 +230,10 @@ func TestLoadHTMLFilesTestMode(t *testing.T) {
|
|||||||
|
|
||||||
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
|
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, _ := ioutil.ReadAll(res.Body)
|
resp, _ := io.ReadAll(res.Body)
|
||||||
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
|
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -209,10 +250,10 @@ func TestLoadHTMLFilesDebugMode(t *testing.T) {
|
|||||||
|
|
||||||
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
|
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, _ := ioutil.ReadAll(res.Body)
|
resp, _ := io.ReadAll(res.Body)
|
||||||
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
|
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -229,10 +270,10 @@ func TestLoadHTMLFilesReleaseMode(t *testing.T) {
|
|||||||
|
|
||||||
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
|
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, _ := ioutil.ReadAll(res.Body)
|
resp, _ := io.ReadAll(res.Body)
|
||||||
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
|
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -256,10 +297,10 @@ func TestLoadHTMLFilesUsingTLS(t *testing.T) {
|
|||||||
client := &http.Client{Transport: tr}
|
client := &http.Client{Transport: tr}
|
||||||
res, err := client.Get(fmt.Sprintf("%s/test", ts.URL))
|
res, err := client.Get(fmt.Sprintf("%s/test", ts.URL))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, _ := ioutil.ReadAll(res.Body)
|
resp, _ := io.ReadAll(res.Body)
|
||||||
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
|
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -276,11 +317,11 @@ func TestLoadHTMLFilesFuncMap(t *testing.T) {
|
|||||||
|
|
||||||
res, err := http.Get(fmt.Sprintf("%s/raw", ts.URL))
|
res, err := http.Get(fmt.Sprintf("%s/raw", ts.URL))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, _ := ioutil.ReadAll(res.Body)
|
resp, _ := io.ReadAll(res.Body)
|
||||||
assert.Equal(t, "Date: 2017/07/01\n", string(resp))
|
assert.Equal(t, "Date: 2017/07/01", string(resp))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAddRoute(t *testing.T) {
|
func TestAddRoute(t *testing.T) {
|
||||||
@ -394,7 +435,6 @@ func TestNoMethodWithoutGlobalHandlers(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRebuild404Handlers(t *testing.T) {
|
func TestRebuild404Handlers(t *testing.T) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNoMethodWithGlobalHandlers(t *testing.T) {
|
func TestNoMethodWithGlobalHandlers(t *testing.T) {
|
||||||
@ -428,7 +468,7 @@ func TestNoMethodWithGlobalHandlers(t *testing.T) {
|
|||||||
compareFunc(t, router.allNoMethod[2], middleware0)
|
compareFunc(t, router.allNoMethod[2], middleware0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func compareFunc(t *testing.T, a, b interface{}) {
|
func compareFunc(t *testing.T, a, b any) {
|
||||||
sf1 := reflect.ValueOf(a)
|
sf1 := reflect.ValueOf(a)
|
||||||
sf2 := reflect.ValueOf(b)
|
sf2 := reflect.ValueOf(b)
|
||||||
if sf1.Pointer() != sf2.Pointer() {
|
if sf1.Pointer() != sf2.Pointer() {
|
||||||
@ -490,7 +530,7 @@ func TestEngineHandleContext(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
assert.NotPanics(t, func() {
|
assert.NotPanics(t, func() {
|
||||||
w := performRequest(r, "GET", "/")
|
w := PerformRequest(r, "GET", "/")
|
||||||
assert.Equal(t, 301, w.Code)
|
assert.Equal(t, 301, w.Code)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -523,7 +563,7 @@ func TestEngineHandleContextManyReEntries(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
assert.NotPanics(t, func() {
|
assert.NotPanics(t, func() {
|
||||||
w := performRequest(r, "GET", "/"+strconv.Itoa(expectValue-1)) // include 0 value
|
w := PerformRequest(r, "GET", "/"+strconv.Itoa(expectValue-1)) // include 0 value
|
||||||
assert.Equal(t, 200, w.Code)
|
assert.Equal(t, 200, w.Code)
|
||||||
assert.Equal(t, expectValue, w.Body.Len())
|
assert.Equal(t, expectValue, w.Body.Len())
|
||||||
})
|
})
|
||||||
@ -532,6 +572,119 @@ func TestEngineHandleContextManyReEntries(t *testing.T) {
|
|||||||
assert.Equal(t, int64(expectValue), middlewareCounter)
|
assert.Equal(t, int64(expectValue), middlewareCounter)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPrepareTrustedCIRDsWith(t *testing.T) {
|
||||||
|
r := New()
|
||||||
|
|
||||||
|
// valid ipv4 cidr
|
||||||
|
{
|
||||||
|
expectedTrustedCIDRs := []*net.IPNet{parseCIDR("0.0.0.0/0")}
|
||||||
|
err := r.SetTrustedProxies([]string{"0.0.0.0/0"})
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// invalid ipv4 cidr
|
||||||
|
{
|
||||||
|
err := r.SetTrustedProxies([]string{"192.168.1.33/33"})
|
||||||
|
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// valid ipv4 address
|
||||||
|
{
|
||||||
|
expectedTrustedCIDRs := []*net.IPNet{parseCIDR("192.168.1.33/32")}
|
||||||
|
|
||||||
|
err := r.SetTrustedProxies([]string{"192.168.1.33"})
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// invalid ipv4 address
|
||||||
|
{
|
||||||
|
err := r.SetTrustedProxies([]string{"192.168.1.256"})
|
||||||
|
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// valid ipv6 address
|
||||||
|
{
|
||||||
|
expectedTrustedCIDRs := []*net.IPNet{parseCIDR("2002:0000:0000:1234:abcd:ffff:c0a8:0101/128")}
|
||||||
|
err := r.SetTrustedProxies([]string{"2002:0000:0000:1234:abcd:ffff:c0a8:0101"})
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// invalid ipv6 address
|
||||||
|
{
|
||||||
|
err := r.SetTrustedProxies([]string{"gggg:0000:0000:1234:abcd:ffff:c0a8:0101"})
|
||||||
|
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// valid ipv6 cidr
|
||||||
|
{
|
||||||
|
expectedTrustedCIDRs := []*net.IPNet{parseCIDR("::/0")}
|
||||||
|
err := r.SetTrustedProxies([]string{"::/0"})
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// invalid ipv6 cidr
|
||||||
|
{
|
||||||
|
err := r.SetTrustedProxies([]string{"gggg:0000:0000:1234:abcd:ffff:c0a8:0101/129"})
|
||||||
|
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// valid combination
|
||||||
|
{
|
||||||
|
expectedTrustedCIDRs := []*net.IPNet{
|
||||||
|
parseCIDR("::/0"),
|
||||||
|
parseCIDR("192.168.0.0/16"),
|
||||||
|
parseCIDR("172.16.0.1/32"),
|
||||||
|
}
|
||||||
|
err := r.SetTrustedProxies([]string{
|
||||||
|
"::/0",
|
||||||
|
"192.168.0.0/16",
|
||||||
|
"172.16.0.1",
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// invalid combination
|
||||||
|
{
|
||||||
|
err := r.SetTrustedProxies([]string{
|
||||||
|
"::/0",
|
||||||
|
"192.168.0.0/16",
|
||||||
|
"172.16.0.256",
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// nil value
|
||||||
|
{
|
||||||
|
err := r.SetTrustedProxies(nil)
|
||||||
|
|
||||||
|
assert.Nil(t, r.trustedCIDRs)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseCIDR(cidr string) *net.IPNet {
|
||||||
|
_, parsedCIDR, err := net.ParseCIDR(cidr)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
return parsedCIDR
|
||||||
|
}
|
||||||
|
|
||||||
func assertRoutePresent(t *testing.T, gotRoutes RoutesInfo, wantRoute RouteInfo) {
|
func assertRoutePresent(t *testing.T, gotRoutes RoutesInfo, wantRoute RouteInfo) {
|
||||||
for _, gotRoute := range gotRoutes {
|
for _, gotRoute := range gotRoutes {
|
||||||
if gotRoute.Path == wantRoute.Path && gotRoute.Method == wantRoute.Method {
|
if gotRoute.Path == wantRoute.Path && gotRoute.Method == wantRoute.Method {
|
||||||
@ -544,3 +697,60 @@ func assertRoutePresent(t *testing.T, gotRoutes RoutesInfo, wantRoute RouteInfo)
|
|||||||
|
|
||||||
func handlerTest1(c *Context) {}
|
func handlerTest1(c *Context) {}
|
||||||
func handlerTest2(c *Context) {}
|
func handlerTest2(c *Context) {}
|
||||||
|
|
||||||
|
func TestNewOptionFunc(t *testing.T) {
|
||||||
|
var fc = func(e *Engine) {
|
||||||
|
e.GET("/test1", handlerTest1)
|
||||||
|
e.GET("/test2", handlerTest2)
|
||||||
|
|
||||||
|
e.Use(func(c *Context) {
|
||||||
|
c.Next()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
r := New(fc)
|
||||||
|
|
||||||
|
routes := r.Routes()
|
||||||
|
assertRoutePresent(t, routes, RouteInfo{Path: "/test1", Method: "GET", Handler: "github.com/gin-gonic/gin.handlerTest1"})
|
||||||
|
assertRoutePresent(t, routes, RouteInfo{Path: "/test2", Method: "GET", Handler: "github.com/gin-gonic/gin.handlerTest2"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithOptionFunc(t *testing.T) {
|
||||||
|
r := New()
|
||||||
|
|
||||||
|
r.With(func(e *Engine) {
|
||||||
|
e.GET("/test1", handlerTest1)
|
||||||
|
e.GET("/test2", handlerTest2)
|
||||||
|
|
||||||
|
e.Use(func(c *Context) {
|
||||||
|
c.Next()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
routes := r.Routes()
|
||||||
|
assertRoutePresent(t, routes, RouteInfo{Path: "/test1", Method: "GET", Handler: "github.com/gin-gonic/gin.handlerTest1"})
|
||||||
|
assertRoutePresent(t, routes, RouteInfo{Path: "/test2", Method: "GET", Handler: "github.com/gin-gonic/gin.handlerTest2"})
|
||||||
|
}
|
||||||
|
|
||||||
|
type Birthday string
|
||||||
|
|
||||||
|
func (b *Birthday) UnmarshalParam(param string) error {
|
||||||
|
*b = Birthday(strings.Replace(param, "-", "/", -1))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCustomUnmarshalStruct(t *testing.T) {
|
||||||
|
route := Default()
|
||||||
|
var request struct {
|
||||||
|
Birthday Birthday `form:"birthday"`
|
||||||
|
}
|
||||||
|
route.GET("/test", func(ctx *Context) {
|
||||||
|
_ = ctx.BindQuery(&request)
|
||||||
|
ctx.JSON(200, request.Birthday)
|
||||||
|
})
|
||||||
|
req := httptest.NewRequest("GET", "/test?birthday=2000-01-01", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
route.ServeHTTP(w, req)
|
||||||
|
assert.Equal(t, 200, w.Code)
|
||||||
|
assert.Equal(t, `"2000/01/01"`, w.Body.String())
|
||||||
|
}
|
||||||
|
|||||||
@ -5,12 +5,12 @@
|
|||||||
package gin
|
package gin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@ -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())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -389,7 +389,7 @@ func TestGithubAPI(t *testing.T) {
|
|||||||
|
|
||||||
for _, route := range githubAPI {
|
for _, route := range githubAPI {
|
||||||
path, values := exampleFromPath(route.path)
|
path, values := exampleFromPath(route.path)
|
||||||
w := performRequest(router, route.method, path)
|
w := PerformRequest(router, route.method, path)
|
||||||
|
|
||||||
// TEST
|
// TEST
|
||||||
assert.Contains(t, w.Body.String(), "\"status\":\"good\"")
|
assert.Contains(t, w.Body.String(), "\"status\":\"good\"")
|
||||||
@ -401,7 +401,7 @@ func TestGithubAPI(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func exampleFromPath(path string) (string, Params) {
|
func exampleFromPath(path string) (string, Params) {
|
||||||
output := new(bytes.Buffer)
|
output := new(strings.Builder)
|
||||||
params := make(Params, 0, 6)
|
params := make(Params, 0, 6)
|
||||||
start := -1
|
start := -1
|
||||||
for i, c := range path {
|
for i, c := range path {
|
||||||
@ -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.
|
||||||
|
|||||||
46
go.mod
46
go.mod
@ -1,18 +1,38 @@
|
|||||||
module github.com/gin-gonic/gin
|
module github.com/gin-gonic/gin
|
||||||
|
|
||||||
go 1.12
|
go 1.20
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3
|
github.com/bytedance/sonic v1.11.6
|
||||||
github.com/golang/protobuf v1.3.0
|
github.com/gin-contrib/sse v0.1.0
|
||||||
github.com/json-iterator/go v1.1.5
|
github.com/go-playground/validator/v10 v10.20.0
|
||||||
github.com/mattn/go-isatty v0.0.6
|
github.com/goccy/go-json v0.10.2
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/json-iterator/go v1.1.12
|
||||||
github.com/modern-go/reflect2 v1.0.1 // indirect
|
github.com/mattn/go-isatty v0.0.20
|
||||||
github.com/stretchr/testify v1.3.0
|
github.com/pelletier/go-toml/v2 v2.2.2
|
||||||
github.com/ugorji/go/codec v0.0.0-20190204201341-e444a5086c43
|
github.com/stretchr/testify v1.9.0
|
||||||
golang.org/x/net v0.0.0-20190301231341-16b79f2e4e95
|
github.com/ugorji/go/codec v1.2.12
|
||||||
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
|
golang.org/x/net v0.25.0
|
||||||
gopkg.in/go-playground/validator.v8 v8.18.2
|
google.golang.org/protobuf v1.34.1
|
||||||
gopkg.in/yaml.v2 v2.2.2
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||||
|
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||||
|
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||||
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
golang.org/x/arch v0.8.0 // indirect
|
||||||
|
golang.org/x/crypto v0.23.0 // indirect
|
||||||
|
golang.org/x/sys v0.20.0 // indirect
|
||||||
|
golang.org/x/text v0.15.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
105
go.sum
105
go.sum
@ -1,39 +1,84 @@
|
|||||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
||||||
|
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||||
|
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||||
|
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||||
|
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||||
|
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||||
|
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||||
|
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||||
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/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||||
github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0=
|
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||||
github.com/json-iterator/go v1.1.5 h1:gL2yXlmiIo4+t+y32d4WGwOjKGYcGOuyrg46vadswDE=
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
github.com/mattn/go-isatty v0.0.6 h1:SrwhHcpV4nWrMGdNcC2kXpMfcBVYGDuTArqyhocJgvA=
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
github.com/mattn/go-isatty v0.0.6/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
|
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
|
||||||
|
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||||
|
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||||
|
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
|
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||||
|
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||||
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||||
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/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
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.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/ugorji/go v1.1.2/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/ugorji/go/codec v0.0.0-20190204201341-e444a5086c43 h1:BasDe+IErOQKrMVXab7UayvSlIpiyGwRvuX3EKYY7UA=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/ugorji/go/codec v0.0.0-20190204201341-e444a5086c43/go.mod h1:iT03XoTwV7xq/+UGwKO3UbC1nNNlopQiY61beSdrtOA=
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
golang.org/x/net v0.0.0-20190301231341-16b79f2e4e95 h1:fY7Dsw114eJN4boqzVSbpVHO6rTdhq6/GnXeu+PKnzU=
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
golang.org/x/net v0.0.0-20190301231341-16b79f2e4e95/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8=
|
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
|
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||||
|
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||||
|
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||||
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
|
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||||
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||||
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||||
|
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||||
|
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
||||||
|
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||||
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/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
|
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
|
|||||||
26
internal/bytesconv/bytesconv_1.19.go
Normal file
26
internal/bytesconv/bytesconv_1.19.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
//go:build !go1.20
|
||||||
|
|
||||||
|
package bytesconv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StringToBytes converts string to byte slice without a memory allocation.
|
||||||
|
func StringToBytes(s string) []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))
|
||||||
|
}
|
||||||
23
internal/bytesconv/bytesconv_1.20.go
Normal file
23
internal/bytesconv/bytesconv_1.20.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
// Copyright 2023 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.
|
||||||
|
|
||||||
|
//go:build go1.20
|
||||||
|
|
||||||
|
package bytesconv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StringToBytes converts string to byte slice without a memory allocation.
|
||||||
|
// For more details, see https://github.com/golang/go/issues/53003#issuecomment-1140276077.
|
||||||
|
func StringToBytes(s string) []byte {
|
||||||
|
return unsafe.Slice(unsafe.StringData(s), len(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
// BytesToString converts byte slice to string without a memory allocation.
|
||||||
|
// For more details, see https://github.com/golang/go/issues/53003#issuecomment-1140276077.
|
||||||
|
func BytesToString(b []byte) string {
|
||||||
|
return unsafe.String(unsafe.SliceData(b), len(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)
|
||||||
|
}
|
||||||
|
}
|
||||||
22
internal/json/go_json.go
Normal file
22
internal/json/go_json.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
// Copyright 2017 Bo-Yi Wu. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build go_json
|
||||||
|
|
||||||
|
package json
|
||||||
|
|
||||||
|
import json "github.com/goccy/go-json"
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Marshal is exported by gin/json package.
|
||||||
|
Marshal = json.Marshal
|
||||||
|
// Unmarshal is exported by gin/json package.
|
||||||
|
Unmarshal = json.Unmarshal
|
||||||
|
// MarshalIndent is exported by gin/json package.
|
||||||
|
MarshalIndent = json.MarshalIndent
|
||||||
|
// NewDecoder is exported by gin/json package.
|
||||||
|
NewDecoder = json.NewDecoder
|
||||||
|
// NewEncoder is exported by gin/json package.
|
||||||
|
NewEncoder = json.NewEncoder
|
||||||
|
)
|
||||||
@ -2,7 +2,7 @@
|
|||||||
// 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 !jsoniter
|
//go:build !jsoniter && !go_json && !(sonic && avx && (linux || windows || darwin) && amd64)
|
||||||
|
|
||||||
package json
|
package json
|
||||||
|
|
||||||
|
|||||||
@ -2,11 +2,11 @@
|
|||||||
// 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 jsoniter
|
//go:build jsoniter
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
23
internal/json/sonic.go
Normal file
23
internal/json/sonic.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
// Copyright 2022 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.
|
||||||
|
|
||||||
|
//go:build sonic && avx && (linux || windows || darwin) && amd64
|
||||||
|
|
||||||
|
package json
|
||||||
|
|
||||||
|
import "github.com/bytedance/sonic"
|
||||||
|
|
||||||
|
var (
|
||||||
|
json = sonic.ConfigStd
|
||||||
|
// Marshal is exported by gin/json package.
|
||||||
|
Marshal = json.Marshal
|
||||||
|
// Unmarshal is exported by gin/json package.
|
||||||
|
Unmarshal = json.Unmarshal
|
||||||
|
// MarshalIndent is exported by gin/json package.
|
||||||
|
MarshalIndent = json.MarshalIndent
|
||||||
|
// NewDecoder is exported by gin/json package.
|
||||||
|
NewDecoder = json.NewDecoder
|
||||||
|
// NewEncoder is exported by gin/json package.
|
||||||
|
NewEncoder = json.NewEncoder
|
||||||
|
)
|
||||||
69
logger.go
69
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
|
||||||
@ -43,11 +44,18 @@ type LoggerConfig struct {
|
|||||||
// Optional. Default value is gin.DefaultWriter.
|
// Optional. Default value is gin.DefaultWriter.
|
||||||
Output io.Writer
|
Output io.Writer
|
||||||
|
|
||||||
// SkipPaths is a url path array which logs are not written.
|
// SkipPaths is an url path array which logs are not written.
|
||||||
// Optional.
|
// Optional.
|
||||||
SkipPaths []string
|
SkipPaths []string
|
||||||
|
|
||||||
|
// Skip is a Skipper that indicates which logs should not be written.
|
||||||
|
// Optional.
|
||||||
|
Skip Skipper
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skipper is a function to skip logs based on provided Context
|
||||||
|
type Skipper func(c *Context) bool
|
||||||
|
|
||||||
// LogFormatter gives the signature of the formatter function passed to LoggerWithFormatter
|
// LogFormatter gives the signature of the formatter function passed to LoggerWithFormatter
|
||||||
type LogFormatter func(params LogFormatterParams) string
|
type LogFormatter func(params LogFormatterParams) string
|
||||||
|
|
||||||
@ -69,12 +77,12 @@ type LogFormatterParams struct {
|
|||||||
Path string
|
Path string
|
||||||
// ErrorMessage is set if error has occurred in processing the request.
|
// ErrorMessage is set if error has occurred in processing the request.
|
||||||
ErrorMessage string
|
ErrorMessage string
|
||||||
// isTerm shows whether does gin's output descriptor refers to a terminal.
|
// isTerm shows whether gin's output descriptor refers to a terminal.
|
||||||
isTerm bool
|
isTerm bool
|
||||||
// BodySize is the size of the Response Body
|
// BodySize is the size of the Response Body
|
||||||
BodySize int
|
BodySize int
|
||||||
// Keys are the keys set on the request's context.
|
// Keys are the keys set on the request's context.
|
||||||
Keys map[string]interface{}
|
Keys map[string]any
|
||||||
}
|
}
|
||||||
|
|
||||||
// StatusCodeColor is the ANSI color for appropriately logging http status code to a terminal.
|
// StatusCodeColor is the ANSI color for appropriately logging http status code to a terminal.
|
||||||
@ -82,6 +90,8 @@ func (p *LogFormatterParams) StatusCodeColor() string {
|
|||||||
code := p.StatusCode
|
code := p.StatusCode
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
|
case code >= http.StatusContinue && code < http.StatusOK:
|
||||||
|
return white
|
||||||
case code >= http.StatusOK && code < http.StatusMultipleChoices:
|
case code >= http.StatusOK && code < http.StatusMultipleChoices:
|
||||||
return green
|
return green
|
||||||
case code >= http.StatusMultipleChoices && code < http.StatusBadRequest:
|
case code >= http.StatusMultipleChoices && code < http.StatusBadRequest:
|
||||||
@ -98,19 +108,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
|
||||||
@ -137,10 +147,9 @@ var defaultLogFormatter = func(param LogFormatterParams) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if param.Latency > time.Minute {
|
if param.Latency > time.Minute {
|
||||||
// Truncate in a golang < 1.8 safe way
|
param.Latency = param.Latency.Truncate(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,
|
||||||
@ -161,12 +170,12 @@ func ForceConsoleColor() {
|
|||||||
consoleColorMode = forceColor
|
consoleColorMode = forceColor
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrorLogger returns a handlerfunc for any error type.
|
// ErrorLogger returns a HandlerFunc for any error type.
|
||||||
func ErrorLogger() HandlerFunc {
|
func ErrorLogger() HandlerFunc {
|
||||||
return ErrorLoggerT(ErrorTypeAny)
|
return ErrorLoggerT(ErrorTypeAny)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrorLoggerT returns a handlerfunc for a given error type.
|
// ErrorLoggerT returns a HandlerFunc for a given error type.
|
||||||
func ErrorLoggerT(typ ErrorType) HandlerFunc {
|
func ErrorLoggerT(typ ErrorType) HandlerFunc {
|
||||||
return func(c *Context) {
|
return func(c *Context) {
|
||||||
c.Next()
|
c.Next()
|
||||||
@ -178,7 +187,7 @@ func ErrorLoggerT(typ ErrorType) HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Logger instances a Logger middleware that will write the logs to gin.DefaultWriter.
|
// Logger instances a Logger middleware that will write the logs to gin.DefaultWriter.
|
||||||
// By default gin.DefaultWriter = os.Stdout.
|
// By default, gin.DefaultWriter = os.Stdout.
|
||||||
func Logger() HandlerFunc {
|
func Logger() HandlerFunc {
|
||||||
return LoggerWithConfig(LoggerConfig{})
|
return LoggerWithConfig(LoggerConfig{})
|
||||||
}
|
}
|
||||||
@ -239,8 +248,11 @@ func LoggerWithConfig(conf LoggerConfig) HandlerFunc {
|
|||||||
// Process request
|
// Process request
|
||||||
c.Next()
|
c.Next()
|
||||||
|
|
||||||
// Log only when path is not being skipped
|
// Log only when it is not being skipped
|
||||||
if _, ok := skip[path]; !ok {
|
if _, ok := skip[path]; ok || (conf.Skip != nil && conf.Skip(c)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
param := LogFormatterParams{
|
param := LogFormatterParams{
|
||||||
Request: c.Request,
|
Request: c.Request,
|
||||||
isTerm: isTerm,
|
isTerm: isTerm,
|
||||||
@ -266,5 +278,4 @@ func LoggerWithConfig(conf LoggerConfig) HandlerFunc {
|
|||||||
|
|
||||||
fmt.Fprint(out, formatter(param))
|
fmt.Fprint(out, formatter(param))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
132
logger_test.go
132
logger_test.go
@ -5,10 +5,10 @@
|
|||||||
package gin
|
package gin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -20,7 +20,7 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestLogger(t *testing.T) {
|
func TestLogger(t *testing.T) {
|
||||||
buffer := new(bytes.Buffer)
|
buffer := new(strings.Builder)
|
||||||
router := New()
|
router := New()
|
||||||
router.Use(LoggerWithWriter(buffer))
|
router.Use(LoggerWithWriter(buffer))
|
||||||
router.GET("/example", func(c *Context) {})
|
router.GET("/example", func(c *Context) {})
|
||||||
@ -31,7 +31,7 @@ func TestLogger(t *testing.T) {
|
|||||||
router.HEAD("/example", func(c *Context) {})
|
router.HEAD("/example", func(c *Context) {})
|
||||||
router.OPTIONS("/example", func(c *Context) {})
|
router.OPTIONS("/example", func(c *Context) {})
|
||||||
|
|
||||||
performRequest(router, "GET", "/example?a=100")
|
PerformRequest(router, "GET", "/example?a=100")
|
||||||
assert.Contains(t, buffer.String(), "200")
|
assert.Contains(t, buffer.String(), "200")
|
||||||
assert.Contains(t, buffer.String(), "GET")
|
assert.Contains(t, buffer.String(), "GET")
|
||||||
assert.Contains(t, buffer.String(), "/example")
|
assert.Contains(t, buffer.String(), "/example")
|
||||||
@ -41,50 +41,50 @@ func TestLogger(t *testing.T) {
|
|||||||
// like integration tests because they test the whole logging process rather
|
// like integration tests because they test the whole logging process rather
|
||||||
// than individual functions. Im not sure where these should go.
|
// than individual functions. Im not sure where these should go.
|
||||||
buffer.Reset()
|
buffer.Reset()
|
||||||
performRequest(router, "POST", "/example")
|
PerformRequest(router, "POST", "/example")
|
||||||
assert.Contains(t, buffer.String(), "200")
|
assert.Contains(t, buffer.String(), "200")
|
||||||
assert.Contains(t, buffer.String(), "POST")
|
assert.Contains(t, buffer.String(), "POST")
|
||||||
assert.Contains(t, buffer.String(), "/example")
|
assert.Contains(t, buffer.String(), "/example")
|
||||||
|
|
||||||
buffer.Reset()
|
buffer.Reset()
|
||||||
performRequest(router, "PUT", "/example")
|
PerformRequest(router, "PUT", "/example")
|
||||||
assert.Contains(t, buffer.String(), "200")
|
assert.Contains(t, buffer.String(), "200")
|
||||||
assert.Contains(t, buffer.String(), "PUT")
|
assert.Contains(t, buffer.String(), "PUT")
|
||||||
assert.Contains(t, buffer.String(), "/example")
|
assert.Contains(t, buffer.String(), "/example")
|
||||||
|
|
||||||
buffer.Reset()
|
buffer.Reset()
|
||||||
performRequest(router, "DELETE", "/example")
|
PerformRequest(router, "DELETE", "/example")
|
||||||
assert.Contains(t, buffer.String(), "200")
|
assert.Contains(t, buffer.String(), "200")
|
||||||
assert.Contains(t, buffer.String(), "DELETE")
|
assert.Contains(t, buffer.String(), "DELETE")
|
||||||
assert.Contains(t, buffer.String(), "/example")
|
assert.Contains(t, buffer.String(), "/example")
|
||||||
|
|
||||||
buffer.Reset()
|
buffer.Reset()
|
||||||
performRequest(router, "PATCH", "/example")
|
PerformRequest(router, "PATCH", "/example")
|
||||||
assert.Contains(t, buffer.String(), "200")
|
assert.Contains(t, buffer.String(), "200")
|
||||||
assert.Contains(t, buffer.String(), "PATCH")
|
assert.Contains(t, buffer.String(), "PATCH")
|
||||||
assert.Contains(t, buffer.String(), "/example")
|
assert.Contains(t, buffer.String(), "/example")
|
||||||
|
|
||||||
buffer.Reset()
|
buffer.Reset()
|
||||||
performRequest(router, "HEAD", "/example")
|
PerformRequest(router, "HEAD", "/example")
|
||||||
assert.Contains(t, buffer.String(), "200")
|
assert.Contains(t, buffer.String(), "200")
|
||||||
assert.Contains(t, buffer.String(), "HEAD")
|
assert.Contains(t, buffer.String(), "HEAD")
|
||||||
assert.Contains(t, buffer.String(), "/example")
|
assert.Contains(t, buffer.String(), "/example")
|
||||||
|
|
||||||
buffer.Reset()
|
buffer.Reset()
|
||||||
performRequest(router, "OPTIONS", "/example")
|
PerformRequest(router, "OPTIONS", "/example")
|
||||||
assert.Contains(t, buffer.String(), "200")
|
assert.Contains(t, buffer.String(), "200")
|
||||||
assert.Contains(t, buffer.String(), "OPTIONS")
|
assert.Contains(t, buffer.String(), "OPTIONS")
|
||||||
assert.Contains(t, buffer.String(), "/example")
|
assert.Contains(t, buffer.String(), "/example")
|
||||||
|
|
||||||
buffer.Reset()
|
buffer.Reset()
|
||||||
performRequest(router, "GET", "/notfound")
|
PerformRequest(router, "GET", "/notfound")
|
||||||
assert.Contains(t, buffer.String(), "404")
|
assert.Contains(t, buffer.String(), "404")
|
||||||
assert.Contains(t, buffer.String(), "GET")
|
assert.Contains(t, buffer.String(), "GET")
|
||||||
assert.Contains(t, buffer.String(), "/notfound")
|
assert.Contains(t, buffer.String(), "/notfound")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoggerWithConfig(t *testing.T) {
|
func TestLoggerWithConfig(t *testing.T) {
|
||||||
buffer := new(bytes.Buffer)
|
buffer := new(strings.Builder)
|
||||||
router := New()
|
router := New()
|
||||||
router.Use(LoggerWithConfig(LoggerConfig{Output: buffer}))
|
router.Use(LoggerWithConfig(LoggerConfig{Output: buffer}))
|
||||||
router.GET("/example", func(c *Context) {})
|
router.GET("/example", func(c *Context) {})
|
||||||
@ -95,7 +95,7 @@ func TestLoggerWithConfig(t *testing.T) {
|
|||||||
router.HEAD("/example", func(c *Context) {})
|
router.HEAD("/example", func(c *Context) {})
|
||||||
router.OPTIONS("/example", func(c *Context) {})
|
router.OPTIONS("/example", func(c *Context) {})
|
||||||
|
|
||||||
performRequest(router, "GET", "/example?a=100")
|
PerformRequest(router, "GET", "/example?a=100")
|
||||||
assert.Contains(t, buffer.String(), "200")
|
assert.Contains(t, buffer.String(), "200")
|
||||||
assert.Contains(t, buffer.String(), "GET")
|
assert.Contains(t, buffer.String(), "GET")
|
||||||
assert.Contains(t, buffer.String(), "/example")
|
assert.Contains(t, buffer.String(), "/example")
|
||||||
@ -105,50 +105,50 @@ func TestLoggerWithConfig(t *testing.T) {
|
|||||||
// like integration tests because they test the whole logging process rather
|
// like integration tests because they test the whole logging process rather
|
||||||
// than individual functions. Im not sure where these should go.
|
// than individual functions. Im not sure where these should go.
|
||||||
buffer.Reset()
|
buffer.Reset()
|
||||||
performRequest(router, "POST", "/example")
|
PerformRequest(router, "POST", "/example")
|
||||||
assert.Contains(t, buffer.String(), "200")
|
assert.Contains(t, buffer.String(), "200")
|
||||||
assert.Contains(t, buffer.String(), "POST")
|
assert.Contains(t, buffer.String(), "POST")
|
||||||
assert.Contains(t, buffer.String(), "/example")
|
assert.Contains(t, buffer.String(), "/example")
|
||||||
|
|
||||||
buffer.Reset()
|
buffer.Reset()
|
||||||
performRequest(router, "PUT", "/example")
|
PerformRequest(router, "PUT", "/example")
|
||||||
assert.Contains(t, buffer.String(), "200")
|
assert.Contains(t, buffer.String(), "200")
|
||||||
assert.Contains(t, buffer.String(), "PUT")
|
assert.Contains(t, buffer.String(), "PUT")
|
||||||
assert.Contains(t, buffer.String(), "/example")
|
assert.Contains(t, buffer.String(), "/example")
|
||||||
|
|
||||||
buffer.Reset()
|
buffer.Reset()
|
||||||
performRequest(router, "DELETE", "/example")
|
PerformRequest(router, "DELETE", "/example")
|
||||||
assert.Contains(t, buffer.String(), "200")
|
assert.Contains(t, buffer.String(), "200")
|
||||||
assert.Contains(t, buffer.String(), "DELETE")
|
assert.Contains(t, buffer.String(), "DELETE")
|
||||||
assert.Contains(t, buffer.String(), "/example")
|
assert.Contains(t, buffer.String(), "/example")
|
||||||
|
|
||||||
buffer.Reset()
|
buffer.Reset()
|
||||||
performRequest(router, "PATCH", "/example")
|
PerformRequest(router, "PATCH", "/example")
|
||||||
assert.Contains(t, buffer.String(), "200")
|
assert.Contains(t, buffer.String(), "200")
|
||||||
assert.Contains(t, buffer.String(), "PATCH")
|
assert.Contains(t, buffer.String(), "PATCH")
|
||||||
assert.Contains(t, buffer.String(), "/example")
|
assert.Contains(t, buffer.String(), "/example")
|
||||||
|
|
||||||
buffer.Reset()
|
buffer.Reset()
|
||||||
performRequest(router, "HEAD", "/example")
|
PerformRequest(router, "HEAD", "/example")
|
||||||
assert.Contains(t, buffer.String(), "200")
|
assert.Contains(t, buffer.String(), "200")
|
||||||
assert.Contains(t, buffer.String(), "HEAD")
|
assert.Contains(t, buffer.String(), "HEAD")
|
||||||
assert.Contains(t, buffer.String(), "/example")
|
assert.Contains(t, buffer.String(), "/example")
|
||||||
|
|
||||||
buffer.Reset()
|
buffer.Reset()
|
||||||
performRequest(router, "OPTIONS", "/example")
|
PerformRequest(router, "OPTIONS", "/example")
|
||||||
assert.Contains(t, buffer.String(), "200")
|
assert.Contains(t, buffer.String(), "200")
|
||||||
assert.Contains(t, buffer.String(), "OPTIONS")
|
assert.Contains(t, buffer.String(), "OPTIONS")
|
||||||
assert.Contains(t, buffer.String(), "/example")
|
assert.Contains(t, buffer.String(), "/example")
|
||||||
|
|
||||||
buffer.Reset()
|
buffer.Reset()
|
||||||
performRequest(router, "GET", "/notfound")
|
PerformRequest(router, "GET", "/notfound")
|
||||||
assert.Contains(t, buffer.String(), "404")
|
assert.Contains(t, buffer.String(), "404")
|
||||||
assert.Contains(t, buffer.String(), "GET")
|
assert.Contains(t, buffer.String(), "GET")
|
||||||
assert.Contains(t, buffer.String(), "/notfound")
|
assert.Contains(t, buffer.String(), "/notfound")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoggerWithFormatter(t *testing.T) {
|
func TestLoggerWithFormatter(t *testing.T) {
|
||||||
buffer := new(bytes.Buffer)
|
buffer := new(strings.Builder)
|
||||||
|
|
||||||
d := DefaultWriter
|
d := DefaultWriter
|
||||||
DefaultWriter = buffer
|
DefaultWriter = buffer
|
||||||
@ -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,
|
||||||
@ -169,7 +169,7 @@ func TestLoggerWithFormatter(t *testing.T) {
|
|||||||
)
|
)
|
||||||
}))
|
}))
|
||||||
router.GET("/example", func(c *Context) {})
|
router.GET("/example", func(c *Context) {})
|
||||||
performRequest(router, "GET", "/example?a=100")
|
PerformRequest(router, "GET", "/example?a=100")
|
||||||
|
|
||||||
// output test
|
// output test
|
||||||
assert.Contains(t, buffer.String(), "[FORMATTER TEST]")
|
assert.Contains(t, buffer.String(), "[FORMATTER TEST]")
|
||||||
@ -181,10 +181,12 @@ func TestLoggerWithFormatter(t *testing.T) {
|
|||||||
|
|
||||||
func TestLoggerWithConfigFormatting(t *testing.T) {
|
func TestLoggerWithConfigFormatting(t *testing.T) {
|
||||||
var gotParam LogFormatterParams
|
var gotParam LogFormatterParams
|
||||||
var gotKeys map[string]interface{}
|
var gotKeys map[string]any
|
||||||
buffer := new(bytes.Buffer)
|
buffer := new(strings.Builder)
|
||||||
|
|
||||||
router := New()
|
router := New()
|
||||||
|
router.engine.trustedCIDRs, _ = router.engine.prepareTrustedCIDRs()
|
||||||
|
|
||||||
router.Use(LoggerWithConfig(LoggerConfig{
|
router.Use(LoggerWithConfig(LoggerConfig{
|
||||||
Output: buffer,
|
Output: buffer,
|
||||||
Formatter: func(param LogFormatterParams) string {
|
Formatter: func(param LogFormatterParams) string {
|
||||||
@ -206,8 +208,9 @@ func TestLoggerWithConfigFormatting(t *testing.T) {
|
|||||||
// set dummy ClientIP
|
// set dummy ClientIP
|
||||||
c.Request.Header.Set("X-Forwarded-For", "20.20.20.20")
|
c.Request.Header.Set("X-Forwarded-For", "20.20.20.20")
|
||||||
gotKeys = c.Keys
|
gotKeys = c.Keys
|
||||||
|
time.Sleep(time.Millisecond)
|
||||||
})
|
})
|
||||||
performRequest(router, "GET", "/example?a=100")
|
PerformRequest(router, "GET", "/example?a=100")
|
||||||
|
|
||||||
// output test
|
// output test
|
||||||
assert.Contains(t, buffer.String(), "[FORMATTER TEST]")
|
assert.Contains(t, buffer.String(), "[FORMATTER TEST]")
|
||||||
@ -226,7 +229,6 @@ func TestLoggerWithConfigFormatting(t *testing.T) {
|
|||||||
assert.Equal(t, "/example?a=100", gotParam.Path)
|
assert.Equal(t, "/example?a=100", gotParam.Path)
|
||||||
assert.Empty(t, gotParam.ErrorMessage)
|
assert.Empty(t, gotParam.ErrorMessage)
|
||||||
assert.Equal(t, gotKeys, gotParam.Keys)
|
assert.Equal(t, gotKeys, gotParam.Keys)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDefaultLogFormatter(t *testing.T) {
|
func TestDefaultLogFormatter(t *testing.T) {
|
||||||
@ -275,12 +277,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| 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| 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))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestColorForMethod(t *testing.T) {
|
func TestColorForMethod(t *testing.T) {
|
||||||
@ -291,14 +292,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 +310,11 @@ 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, white, colorForStatus(http.StatusContinue), "1xx should be white")
|
||||||
assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 55, 109}), colorForStatus(http.StatusMovedPermanently), "3xx should be white")
|
assert.Equal(t, green, colorForStatus(http.StatusOK), "2xx should be green")
|
||||||
assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 51, 109}), colorForStatus(http.StatusNotFound), "4xx should be yellow")
|
assert.Equal(t, white, colorForStatus(http.StatusMovedPermanently), "3xx should be white")
|
||||||
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 49, 109}), colorForStatus(2), "other things should be red")
|
assert.Equal(t, yellow, colorForStatus(http.StatusNotFound), "4xx should be yellow")
|
||||||
|
assert.Equal(t, red, colorForStatus(2), "other things should be red")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestResetColor(t *testing.T) {
|
func TestResetColor(t *testing.T) {
|
||||||
@ -357,46 +359,46 @@ func TestErrorLogger(t *testing.T) {
|
|||||||
router := New()
|
router := New()
|
||||||
router.Use(ErrorLogger())
|
router.Use(ErrorLogger())
|
||||||
router.GET("/error", func(c *Context) {
|
router.GET("/error", func(c *Context) {
|
||||||
c.Error(errors.New("this is an error")) // nolint: errcheck
|
c.Error(errors.New("this is an error")) //nolint: errcheck
|
||||||
})
|
})
|
||||||
router.GET("/abort", func(c *Context) {
|
router.GET("/abort", func(c *Context) {
|
||||||
c.AbortWithError(http.StatusUnauthorized, errors.New("no authorized")) // nolint: errcheck
|
c.AbortWithError(http.StatusUnauthorized, errors.New("no authorized")) //nolint: errcheck
|
||||||
})
|
})
|
||||||
router.GET("/print", func(c *Context) {
|
router.GET("/print", func(c *Context) {
|
||||||
c.Error(errors.New("this is an error")) // nolint: errcheck
|
c.Error(errors.New("this is an error")) //nolint: errcheck
|
||||||
c.String(http.StatusInternalServerError, "hola!")
|
c.String(http.StatusInternalServerError, "hola!")
|
||||||
})
|
})
|
||||||
|
|
||||||
w := performRequest(router, "GET", "/error")
|
w := PerformRequest(router, "GET", "/error")
|
||||||
assert.Equal(t, http.StatusOK, w.Code)
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
assert.Equal(t, "{\"error\":\"this is an error\"}", w.Body.String())
|
assert.Equal(t, "{\"error\":\"this is an error\"}", w.Body.String())
|
||||||
|
|
||||||
w = performRequest(router, "GET", "/abort")
|
w = PerformRequest(router, "GET", "/abort")
|
||||||
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
||||||
assert.Equal(t, "{\"error\":\"no authorized\"}", w.Body.String())
|
assert.Equal(t, "{\"error\":\"no authorized\"}", w.Body.String())
|
||||||
|
|
||||||
w = performRequest(router, "GET", "/print")
|
w = PerformRequest(router, "GET", "/print")
|
||||||
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||||
assert.Equal(t, "hola!{\"error\":\"this is an error\"}", w.Body.String())
|
assert.Equal(t, "hola!{\"error\":\"this is an error\"}", w.Body.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoggerWithWriterSkippingPaths(t *testing.T) {
|
func TestLoggerWithWriterSkippingPaths(t *testing.T) {
|
||||||
buffer := new(bytes.Buffer)
|
buffer := new(strings.Builder)
|
||||||
router := New()
|
router := New()
|
||||||
router.Use(LoggerWithWriter(buffer, "/skipped"))
|
router.Use(LoggerWithWriter(buffer, "/skipped"))
|
||||||
router.GET("/logged", func(c *Context) {})
|
router.GET("/logged", func(c *Context) {})
|
||||||
router.GET("/skipped", func(c *Context) {})
|
router.GET("/skipped", func(c *Context) {})
|
||||||
|
|
||||||
performRequest(router, "GET", "/logged")
|
PerformRequest(router, "GET", "/logged")
|
||||||
assert.Contains(t, buffer.String(), "200")
|
assert.Contains(t, buffer.String(), "200")
|
||||||
|
|
||||||
buffer.Reset()
|
buffer.Reset()
|
||||||
performRequest(router, "GET", "/skipped")
|
PerformRequest(router, "GET", "/skipped")
|
||||||
assert.Contains(t, buffer.String(), "")
|
assert.Contains(t, buffer.String(), "")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoggerWithConfigSkippingPaths(t *testing.T) {
|
func TestLoggerWithConfigSkippingPaths(t *testing.T) {
|
||||||
buffer := new(bytes.Buffer)
|
buffer := new(strings.Builder)
|
||||||
router := New()
|
router := New()
|
||||||
router.Use(LoggerWithConfig(LoggerConfig{
|
router.Use(LoggerWithConfig(LoggerConfig{
|
||||||
Output: buffer,
|
Output: buffer,
|
||||||
@ -405,11 +407,31 @@ func TestLoggerWithConfigSkippingPaths(t *testing.T) {
|
|||||||
router.GET("/logged", func(c *Context) {})
|
router.GET("/logged", func(c *Context) {})
|
||||||
router.GET("/skipped", func(c *Context) {})
|
router.GET("/skipped", func(c *Context) {})
|
||||||
|
|
||||||
performRequest(router, "GET", "/logged")
|
PerformRequest(router, "GET", "/logged")
|
||||||
assert.Contains(t, buffer.String(), "200")
|
assert.Contains(t, buffer.String(), "200")
|
||||||
|
|
||||||
buffer.Reset()
|
buffer.Reset()
|
||||||
performRequest(router, "GET", "/skipped")
|
PerformRequest(router, "GET", "/skipped")
|
||||||
|
assert.Contains(t, buffer.String(), "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoggerWithConfigSkipper(t *testing.T) {
|
||||||
|
buffer := new(strings.Builder)
|
||||||
|
router := New()
|
||||||
|
router.Use(LoggerWithConfig(LoggerConfig{
|
||||||
|
Output: buffer,
|
||||||
|
Skip: func(c *Context) bool {
|
||||||
|
return c.Writer.Status() == http.StatusNoContent
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
router.GET("/logged", func(c *Context) { c.Status(http.StatusOK) })
|
||||||
|
router.GET("/skipped", func(c *Context) { c.Status(http.StatusNoContent) })
|
||||||
|
|
||||||
|
PerformRequest(router, "GET", "/logged")
|
||||||
|
assert.Contains(t, buffer.String(), "200")
|
||||||
|
|
||||||
|
buffer.Reset()
|
||||||
|
PerformRequest(router, "GET", "/skipped")
|
||||||
assert.Contains(t, buffer.String(), "")
|
assert.Contains(t, buffer.String(), "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -35,7 +35,7 @@ func TestMiddlewareGeneralCase(t *testing.T) {
|
|||||||
signature += " XX "
|
signature += " XX "
|
||||||
})
|
})
|
||||||
// RUN
|
// RUN
|
||||||
w := performRequest(router, "GET", "/")
|
w := PerformRequest(router, "GET", "/")
|
||||||
|
|
||||||
// TEST
|
// TEST
|
||||||
assert.Equal(t, http.StatusOK, w.Code)
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
@ -71,7 +71,7 @@ func TestMiddlewareNoRoute(t *testing.T) {
|
|||||||
signature += " X "
|
signature += " X "
|
||||||
})
|
})
|
||||||
// RUN
|
// RUN
|
||||||
w := performRequest(router, "GET", "/")
|
w := PerformRequest(router, "GET", "/")
|
||||||
|
|
||||||
// TEST
|
// TEST
|
||||||
assert.Equal(t, http.StatusNotFound, w.Code)
|
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||||
@ -108,7 +108,7 @@ func TestMiddlewareNoMethodEnabled(t *testing.T) {
|
|||||||
signature += " XX "
|
signature += " XX "
|
||||||
})
|
})
|
||||||
// RUN
|
// RUN
|
||||||
w := performRequest(router, "GET", "/")
|
w := PerformRequest(router, "GET", "/")
|
||||||
|
|
||||||
// TEST
|
// TEST
|
||||||
assert.Equal(t, http.StatusMethodNotAllowed, w.Code)
|
assert.Equal(t, http.StatusMethodNotAllowed, w.Code)
|
||||||
@ -118,7 +118,10 @@ func TestMiddlewareNoMethodEnabled(t *testing.T) {
|
|||||||
func TestMiddlewareNoMethodDisabled(t *testing.T) {
|
func TestMiddlewareNoMethodDisabled(t *testing.T) {
|
||||||
signature := ""
|
signature := ""
|
||||||
router := New()
|
router := New()
|
||||||
|
|
||||||
|
// NoMethod disabled
|
||||||
router.HandleMethodNotAllowed = false
|
router.HandleMethodNotAllowed = false
|
||||||
|
|
||||||
router.Use(func(c *Context) {
|
router.Use(func(c *Context) {
|
||||||
signature += "A"
|
signature += "A"
|
||||||
c.Next()
|
c.Next()
|
||||||
@ -144,8 +147,9 @@ func TestMiddlewareNoMethodDisabled(t *testing.T) {
|
|||||||
router.POST("/", func(c *Context) {
|
router.POST("/", func(c *Context) {
|
||||||
signature += " XX "
|
signature += " XX "
|
||||||
})
|
})
|
||||||
|
|
||||||
// RUN
|
// RUN
|
||||||
w := performRequest(router, "GET", "/")
|
w := PerformRequest(router, "GET", "/")
|
||||||
|
|
||||||
// TEST
|
// TEST
|
||||||
assert.Equal(t, http.StatusNotFound, w.Code)
|
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||||
@ -171,7 +175,7 @@ func TestMiddlewareAbort(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// RUN
|
// RUN
|
||||||
w := performRequest(router, "GET", "/")
|
w := PerformRequest(router, "GET", "/")
|
||||||
|
|
||||||
// TEST
|
// TEST
|
||||||
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
||||||
@ -186,14 +190,13 @@ func TestMiddlewareAbortHandlersChainAndNext(t *testing.T) {
|
|||||||
c.Next()
|
c.Next()
|
||||||
c.AbortWithStatus(http.StatusGone)
|
c.AbortWithStatus(http.StatusGone)
|
||||||
signature += "B"
|
signature += "B"
|
||||||
|
|
||||||
})
|
})
|
||||||
router.GET("/", func(c *Context) {
|
router.GET("/", func(c *Context) {
|
||||||
signature += "C"
|
signature += "C"
|
||||||
c.Next()
|
c.Next()
|
||||||
})
|
})
|
||||||
// RUN
|
// RUN
|
||||||
w := performRequest(router, "GET", "/")
|
w := PerformRequest(router, "GET", "/")
|
||||||
|
|
||||||
// TEST
|
// TEST
|
||||||
assert.Equal(t, http.StatusGone, w.Code)
|
assert.Equal(t, http.StatusGone, w.Code)
|
||||||
@ -208,7 +211,7 @@ func TestMiddlewareFailHandlersChain(t *testing.T) {
|
|||||||
router := New()
|
router := New()
|
||||||
router.Use(func(context *Context) {
|
router.Use(func(context *Context) {
|
||||||
signature += "A"
|
signature += "A"
|
||||||
context.AbortWithError(http.StatusInternalServerError, errors.New("foo")) // nolint: errcheck
|
context.AbortWithError(http.StatusInternalServerError, errors.New("foo")) //nolint: errcheck
|
||||||
})
|
})
|
||||||
router.Use(func(context *Context) {
|
router.Use(func(context *Context) {
|
||||||
signature += "B"
|
signature += "B"
|
||||||
@ -216,7 +219,7 @@ func TestMiddlewareFailHandlersChain(t *testing.T) {
|
|||||||
signature += "C"
|
signature += "C"
|
||||||
})
|
})
|
||||||
// RUN
|
// RUN
|
||||||
w := performRequest(router, "GET", "/")
|
w := PerformRequest(router, "GET", "/")
|
||||||
|
|
||||||
// TEST
|
// TEST
|
||||||
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||||
@ -243,7 +246,7 @@ func TestMiddlewareWrite(t *testing.T) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
w := performRequest(router, "GET", "/")
|
w := PerformRequest(router, "GET", "/")
|
||||||
|
|
||||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||||
assert.Equal(t, strings.Replace("hola\n<map><foo>bar</foo></map>{\"foo\":\"bar\"}{\"foo\":\"bar\"}event:test\ndata:message\n\n", " ", "", -1), strings.Replace(w.Body.String(), " ", "", -1))
|
assert.Equal(t, strings.Replace("hola\n<map><foo>bar</foo></map>{\"foo\":\"bar\"}{\"foo\":\"bar\"}event:test\ndata:message\n\n", " ", "", -1), strings.Replace(w.Body.String(), " ", "", -1))
|
||||||
|
|||||||
26
mode.go
26
mode.go
@ -5,6 +5,7 @@
|
|||||||
package gin
|
package gin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"flag"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
@ -23,6 +24,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
|
||||||
@ -34,6 +36,7 @@ const (
|
|||||||
// Note that both Logger and Recovery provides custom ways to configure their
|
// Note that both Logger and Recovery provides custom ways to configure their
|
||||||
// output io.Writer.
|
// output io.Writer.
|
||||||
// To support coloring in Windows use:
|
// To support coloring in Windows use:
|
||||||
|
//
|
||||||
// import "github.com/mattn/go-colorable"
|
// import "github.com/mattn/go-colorable"
|
||||||
// gin.DefaultWriter = colorable.NewColorableStdout()
|
// gin.DefaultWriter = colorable.NewColorableStdout()
|
||||||
var DefaultWriter io.Writer = os.Stdout
|
var DefaultWriter io.Writer = os.Stdout
|
||||||
@ -51,6 +54,14 @@ 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 == "" {
|
||||||
|
if flag.Lookup("test.v") != nil {
|
||||||
|
value = TestMode
|
||||||
|
} else {
|
||||||
|
value = DebugMode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch value {
|
switch value {
|
||||||
case DebugMode, "":
|
case DebugMode, "":
|
||||||
atomic.StoreInt32(&ginMode, debugCode)
|
atomic.StoreInt32(&ginMode, debugCode)
|
||||||
@ -59,10 +70,7 @@ func SetMode(value string) {
|
|||||||
case TestMode:
|
case TestMode:
|
||||||
atomic.StoreInt32(&ginMode, testCode)
|
atomic.StoreInt32(&ginMode, testCode)
|
||||||
default:
|
default:
|
||||||
panic("gin mode unknown: " + value)
|
panic("gin mode unknown: " + value + " (available mode: debug release test)")
|
||||||
}
|
|
||||||
if value == "" {
|
|
||||||
value = DebugMode
|
|
||||||
}
|
}
|
||||||
modeName.Store(value)
|
modeName.Store(value)
|
||||||
}
|
}
|
||||||
@ -72,13 +80,19 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mode returns currently gin mode.
|
// EnableJsonDecoderDisallowUnknownFields sets true for binding.EnableDecoderDisallowUnknownFields to
|
||||||
|
// call the DisallowUnknownFields method on the JSON Decoder instance.
|
||||||
|
func EnableJsonDecoderDisallowUnknownFields() {
|
||||||
|
binding.EnableDecoderDisallowUnknownFields = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mode returns current gin mode.
|
||||||
func Mode() string {
|
func Mode() string {
|
||||||
return modeName.Load().(string)
|
return modeName.Load().(string)
|
||||||
}
|
}
|
||||||
|
|||||||
15
mode_test.go
15
mode_test.go
@ -5,6 +5,7 @@
|
|||||||
package gin
|
package gin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"flag"
|
||||||
"os"
|
"os"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
@ -41,8 +42,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)
|
||||||
|
}
|
||||||
|
|||||||
55
path.go
55
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
|
||||||
|
|
||||||
|
if n+1 > stackBufSize {
|
||||||
buf = make([]byte, n+1)
|
buf = make([]byte, n+1)
|
||||||
|
} else {
|
||||||
|
buf = buf[:n+1]
|
||||||
|
}
|
||||||
buf[0] = '/'
|
buf[0] = '/'
|
||||||
}
|
}
|
||||||
|
|
||||||
trailing := n > 1 && p[n-1] == '/'
|
trailing := n > 1 && p[n-1] == '/'
|
||||||
|
|
||||||
// A bit more clunky without a 'lazybuf' like the path package, but the loop
|
// A bit more clunky without a 'lazybuf' like the path package, but the loop
|
||||||
// gets completely inlined (bufApp). So in contrast to the path package this
|
// gets completely inlined (bufApp calls).
|
||||||
// loop has no expensive function calls (except 1x make)
|
// loop has no expensive function calls (except 1x make) // So in contrast to the path package this loop has no expensive function
|
||||||
|
// calls (except make, if needed).
|
||||||
|
|
||||||
for r < n {
|
for r < n {
|
||||||
switch {
|
switch {
|
||||||
@ -69,7 +79,7 @@ func cleanPath(p string) string {
|
|||||||
// can backtrack
|
// can backtrack
|
||||||
w--
|
w--
|
||||||
|
|
||||||
if buf == nil {
|
if len(buf) == 0 {
|
||||||
for w > 1 && p[w] != '/' {
|
for w > 1 && p[w] != '/' {
|
||||||
w--
|
w--
|
||||||
}
|
}
|
||||||
@ -81,14 +91,14 @@ func cleanPath(p string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// real path element.
|
// Real path element.
|
||||||
// add slash if needed
|
// Add slash if needed
|
||||||
if w > 1 {
|
if w > 1 {
|
||||||
bufApp(&buf, p, w, '/')
|
bufApp(&buf, p, w, '/')
|
||||||
w++
|
w++
|
||||||
}
|
}
|
||||||
|
|
||||||
// copy element
|
// Copy element
|
||||||
for r < n && p[r] != '/' {
|
for r < n && p[r] != '/' {
|
||||||
bufApp(&buf, p, w, p[r])
|
bufApp(&buf, p, w, p[r])
|
||||||
w++
|
w++
|
||||||
@ -97,27 +107,44 @@ func cleanPath(p string) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// re-append trailing slash
|
// Re-append trailing slash
|
||||||
if trailing && w > 1 {
|
if trailing && w > 1 {
|
||||||
bufApp(&buf, p, w, '/')
|
bufApp(&buf, p, w, '/')
|
||||||
w++
|
w++
|
||||||
}
|
}
|
||||||
|
|
||||||
if buf == nil {
|
// If the original string was not modified (or only shortened at the end),
|
||||||
|
// return the respective substring of the original string.
|
||||||
|
// Otherwise return a new string from the buffer.
|
||||||
|
if len(buf) == 0 {
|
||||||
return p[:w]
|
return p[:w]
|
||||||
}
|
}
|
||||||
return string(buf[:w])
|
return string(buf[:w])
|
||||||
}
|
}
|
||||||
|
|
||||||
// internal helper to lazily create a buffer if necessary.
|
// Internal helper to lazily create a buffer if necessary.
|
||||||
|
// Calls to this function get inlined.
|
||||||
func bufApp(buf *[]byte, s string, w int, c byte) {
|
func bufApp(buf *[]byte, s string, w int, c byte) {
|
||||||
if *buf == nil {
|
b := *buf
|
||||||
|
if len(b) == 0 {
|
||||||
|
// No modification of the original string so far.
|
||||||
|
// If the next character is the same as in the original string, we do
|
||||||
|
// not yet have to allocate a buffer.
|
||||||
if s[w] == c {
|
if s[w] == c {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
*buf = make([]byte, len(s))
|
// Otherwise use either the stack buffer, if it is large enough, or
|
||||||
copy(*buf, s[:w])
|
// allocate a new buffer on the heap, and copy all previous characters.
|
||||||
|
length := len(s)
|
||||||
|
if length > cap(b) {
|
||||||
|
*buf = make([]byte, length)
|
||||||
|
} else {
|
||||||
|
*buf = (*buf)[:length]
|
||||||
}
|
}
|
||||||
(*buf)[w] = c
|
b = *buf
|
||||||
|
|
||||||
|
copy(b, s[:w])
|
||||||
|
}
|
||||||
|
b[w] = c
|
||||||
}
|
}
|
||||||
|
|||||||
64
path_test.go
64
path_test.go
@ -7,14 +7,17 @@ package gin
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"runtime"
|
"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,9 +80,9 @@ 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 {
|
if runtime.GOMAXPROCS(0) > 1 {
|
||||||
t.Log("skipping AllocsPerRun checks; GOMAXPROCS>1")
|
t.Skip("skipping malloc count; GOMAXPROCS>1")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range cleanTests {
|
for _, test := range cleanTests {
|
||||||
@ -87,3 +90,56 @@ func TestPathCleanMallocs(t *testing.T) {
|
|||||||
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
53
recovery.go
53
recovery.go
@ -6,9 +6,9 @@ package gin
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -26,13 +26,29 @@ var (
|
|||||||
slash = []byte("/")
|
slash = []byte("/")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// RecoveryFunc defines the function passable to CustomRecovery.
|
||||||
|
type RecoveryFunc func(c *Context, err any)
|
||||||
|
|
||||||
// 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)
|
||||||
@ -44,8 +60,11 @@ func RecoveryWithWriter(out io.Writer) HandlerFunc {
|
|||||||
// condition that warrants a panic stack trace.
|
// condition that warrants a panic stack trace.
|
||||||
var brokenPipe bool
|
var brokenPipe bool
|
||||||
if ne, ok := err.(*net.OpError); ok {
|
if ne, ok := err.(*net.OpError); ok {
|
||||||
if se, ok := ne.Err.(*os.SyscallError); ok {
|
var se *os.SyscallError
|
||||||
if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
|
if errors.As(ne, &se) {
|
||||||
|
seStr := strings.ToLower(se.Error())
|
||||||
|
if strings.Contains(seStr, "broken pipe") ||
|
||||||
|
strings.Contains(seStr, "connection reset by peer") {
|
||||||
brokenPipe = true
|
brokenPipe = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -60,23 +79,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 {
|
||||||
c.Error(err.(error)) // nolint: errcheck
|
// If the connection is dead, we can't write a status to it.
|
||||||
|
c.Error(err.(error)) //nolint: errcheck
|
||||||
c.Abort()
|
c.Abort()
|
||||||
} else {
|
} else {
|
||||||
c.AbortWithStatus(http.StatusInternalServerError)
|
handle(c, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@ -84,6 +103,10 @@ func RecoveryWithWriter(out io.Writer) HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func defaultHandleRecovery(c *Context, _ any) {
|
||||||
|
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
|
||||||
@ -99,7 +122,7 @@ func stack(skip int) []byte {
|
|||||||
// Print this much at least. If we can't find the source, it won't show.
|
// Print this much at least. If we can't find the source, it won't show.
|
||||||
fmt.Fprintf(buf, "%s:%d (0x%x)\n", file, line, pc)
|
fmt.Fprintf(buf, "%s:%d (0x%x)\n", file, line, pc)
|
||||||
if file != lastFile {
|
if file != lastFile {
|
||||||
data, err := ioutil.ReadFile(file)
|
data, err := os.ReadFile(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -133,7 +156,7 @@ func function(pc uintptr) []byte {
|
|||||||
// runtime/debug.*T·ptrmethod
|
// runtime/debug.*T·ptrmethod
|
||||||
// and want
|
// and want
|
||||||
// *T.ptrmethod
|
// *T.ptrmethod
|
||||||
// Also the package path might contains dot (e.g. code.google.com/...),
|
// Also the package path might contain dot (e.g. code.google.com/...),
|
||||||
// so first eliminate the path prefix
|
// so first eliminate the path prefix
|
||||||
if lastSlash := bytes.LastIndex(name, slash); lastSlash >= 0 {
|
if lastSlash := bytes.LastIndex(name, slash); lastSlash >= 0 {
|
||||||
name = name[lastSlash+1:]
|
name = name[lastSlash+1:]
|
||||||
@ -141,11 +164,11 @@ func function(pc uintptr) []byte {
|
|||||||
if period := bytes.Index(name, dot); period >= 0 {
|
if period := bytes.Index(name, dot); period >= 0 {
|
||||||
name = name[period+1:]
|
name = name[period+1:]
|
||||||
}
|
}
|
||||||
name = bytes.Replace(name, centerDot, dot, -1)
|
name = bytes.ReplaceAll(name, centerDot, dot)
|
||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// timeFormat returns a customized time string for logger.
|
||||||
func timeFormat(t time.Time) string {
|
func timeFormat(t time.Time) string {
|
||||||
var timeString = t.Format("2006/01/02 - 15:04:05")
|
return t.Format("2006/01/02 - 15:04:05")
|
||||||
return timeString
|
|
||||||
}
|
}
|
||||||
|
|||||||
130
recovery_test.go
130
recovery_test.go
@ -5,7 +5,6 @@
|
|||||||
package gin
|
package gin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -18,7 +17,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestPanicClean(t *testing.T) {
|
func TestPanicClean(t *testing.T) {
|
||||||
buffer := new(bytes.Buffer)
|
buffer := new(strings.Builder)
|
||||||
router := New()
|
router := New()
|
||||||
password := "my-super-secret-password"
|
password := "my-super-secret-password"
|
||||||
router.Use(RecoveryWithWriter(buffer))
|
router.Use(RecoveryWithWriter(buffer))
|
||||||
@ -27,7 +26,7 @@ func TestPanicClean(t *testing.T) {
|
|||||||
panic("Oupps, Houston, we have a problem")
|
panic("Oupps, Houston, we have a problem")
|
||||||
})
|
})
|
||||||
// RUN
|
// RUN
|
||||||
w := performRequest(router, "GET", "/recovery",
|
w := PerformRequest(router, "GET", "/recovery",
|
||||||
header{
|
header{
|
||||||
Key: "Host",
|
Key: "Host",
|
||||||
Value: "www.google.com",
|
Value: "www.google.com",
|
||||||
@ -50,25 +49,25 @@ func TestPanicClean(t *testing.T) {
|
|||||||
|
|
||||||
// TestPanicInHandler assert that panic has been recovered.
|
// TestPanicInHandler assert that panic has been recovered.
|
||||||
func TestPanicInHandler(t *testing.T) {
|
func TestPanicInHandler(t *testing.T) {
|
||||||
buffer := new(bytes.Buffer)
|
buffer := new(strings.Builder)
|
||||||
router := New()
|
router := New()
|
||||||
router.Use(RecoveryWithWriter(buffer))
|
router.Use(RecoveryWithWriter(buffer))
|
||||||
router.GET("/recovery", func(_ *Context) {
|
router.GET("/recovery", func(_ *Context) {
|
||||||
panic("Oupps, Houston, we have a problem")
|
panic("Oupps, Houston, we have a problem")
|
||||||
})
|
})
|
||||||
// RUN
|
// RUN
|
||||||
w := performRequest(router, "GET", "/recovery")
|
w := PerformRequest(router, "GET", "/recovery")
|
||||||
// TEST
|
// TEST
|
||||||
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
|
||||||
SetMode(DebugMode)
|
SetMode(DebugMode)
|
||||||
// RUN
|
// RUN
|
||||||
w = performRequest(router, "GET", "/recovery")
|
w = PerformRequest(router, "GET", "/recovery")
|
||||||
// TEST
|
// TEST
|
||||||
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||||
assert.Contains(t, buffer.String(), "GET /recovery")
|
assert.Contains(t, buffer.String(), "GET /recovery")
|
||||||
@ -85,21 +84,21 @@ func TestPanicWithAbort(t *testing.T) {
|
|||||||
panic("Oupps, Houston, we have a problem")
|
panic("Oupps, Houston, we have a problem")
|
||||||
})
|
})
|
||||||
// RUN
|
// RUN
|
||||||
w := performRequest(router, "GET", "/recovery")
|
w := PerformRequest(router, "GET", "/recovery")
|
||||||
// TEST
|
// TEST
|
||||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSource(t *testing.T) {
|
func TestSource(t *testing.T) {
|
||||||
bs := source(nil, 0)
|
bs := source(nil, 0)
|
||||||
assert.Equal(t, []byte("???"), bs)
|
assert.Equal(t, dunno, bs)
|
||||||
|
|
||||||
in := [][]byte{
|
in := [][]byte{
|
||||||
[]byte("Hello world."),
|
[]byte("Hello world."),
|
||||||
[]byte("Hi, gin.."),
|
[]byte("Hi, gin.."),
|
||||||
}
|
}
|
||||||
bs = source(in, 10)
|
bs = source(in, 10)
|
||||||
assert.Equal(t, []byte("???"), bs)
|
assert.Equal(t, dunno, bs)
|
||||||
|
|
||||||
bs = source(in, 1)
|
bs = source(in, 1)
|
||||||
assert.Equal(t, []byte("Hello world."), bs)
|
assert.Equal(t, []byte("Hello world."), bs)
|
||||||
@ -107,7 +106,7 @@ func TestSource(t *testing.T) {
|
|||||||
|
|
||||||
func TestFunction(t *testing.T) {
|
func TestFunction(t *testing.T) {
|
||||||
bs := function(1)
|
bs := function(1)
|
||||||
assert.Equal(t, []byte("???"), bs)
|
assert.Equal(t, dunno, bs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestPanicWithBrokenPipe asserts that recovery specifically handles
|
// TestPanicWithBrokenPipe asserts that recovery specifically handles
|
||||||
@ -122,8 +121,7 @@ func TestPanicWithBrokenPipe(t *testing.T) {
|
|||||||
|
|
||||||
for errno, expectMsg := range expectMsgs {
|
for errno, expectMsg := range expectMsgs {
|
||||||
t.Run(expectMsg, func(t *testing.T) {
|
t.Run(expectMsg, func(t *testing.T) {
|
||||||
|
var buf strings.Builder
|
||||||
var buf bytes.Buffer
|
|
||||||
|
|
||||||
router := New()
|
router := New()
|
||||||
router.Use(RecoveryWithWriter(&buf))
|
router.Use(RecoveryWithWriter(&buf))
|
||||||
@ -137,10 +135,114 @@ func TestPanicWithBrokenPipe(t *testing.T) {
|
|||||||
panic(e)
|
panic(e)
|
||||||
})
|
})
|
||||||
// RUN
|
// RUN
|
||||||
w := performRequest(router, "GET", "/recovery")
|
w := PerformRequest(router, "GET", "/recovery")
|
||||||
// TEST
|
// TEST
|
||||||
assert.Equal(t, expectCode, w.Code)
|
assert.Equal(t, expectCode, w.Code)
|
||||||
assert.Contains(t, strings.ToLower(buf.String()), expectMsg)
|
assert.Contains(t, strings.ToLower(buf.String()), expectMsg)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCustomRecoveryWithWriter(t *testing.T) {
|
||||||
|
errBuffer := new(strings.Builder)
|
||||||
|
buffer := new(strings.Builder)
|
||||||
|
router := New()
|
||||||
|
handleRecovery := func(c *Context, err any) {
|
||||||
|
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(strings.Builder)
|
||||||
|
buffer := new(strings.Builder)
|
||||||
|
router := New()
|
||||||
|
DefaultErrorWriter = buffer
|
||||||
|
handleRecovery := func(c *Context, err any) {
|
||||||
|
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(strings.Builder)
|
||||||
|
buffer := new(strings.Builder)
|
||||||
|
router := New()
|
||||||
|
DefaultErrorWriter = buffer
|
||||||
|
handleRecovery := func(c *Context, err any) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@ -20,7 +20,7 @@ type Delims struct {
|
|||||||
// HTMLRender interface is to be implemented by HTMLProduction and HTMLDebug.
|
// HTMLRender interface is to be implemented by HTMLProduction and HTMLDebug.
|
||||||
type HTMLRender interface {
|
type HTMLRender interface {
|
||||||
// Instance returns an HTML instance.
|
// Instance returns an HTML instance.
|
||||||
Instance(string, interface{}) Render
|
Instance(string, any) Render
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTMLProduction contains template reference and its delims.
|
// HTMLProduction contains template reference and its delims.
|
||||||
@ -41,13 +41,13 @@ type HTMLDebug struct {
|
|||||||
type HTML struct {
|
type HTML struct {
|
||||||
Template *template.Template
|
Template *template.Template
|
||||||
Name string
|
Name string
|
||||||
Data interface{}
|
Data any
|
||||||
}
|
}
|
||||||
|
|
||||||
var htmlContentType = []string{"text/html; charset=utf-8"}
|
var htmlContentType = []string{"text/html; charset=utf-8"}
|
||||||
|
|
||||||
// Instance (HTMLProduction) returns an HTML instance which it realizes Render interface.
|
// Instance (HTMLProduction) returns an HTML instance which it realizes Render interface.
|
||||||
func (r HTMLProduction) Instance(name string, data interface{}) Render {
|
func (r HTMLProduction) Instance(name string, data any) Render {
|
||||||
return HTML{
|
return HTML{
|
||||||
Template: r.Template,
|
Template: r.Template,
|
||||||
Name: name,
|
Name: name,
|
||||||
@ -56,7 +56,7 @@ func (r HTMLProduction) Instance(name string, data interface{}) Render {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Instance (HTMLDebug) returns an HTML instance which it realizes Render interface.
|
// Instance (HTMLDebug) returns an HTML instance which it realizes Render interface.
|
||||||
func (r HTMLDebug) Instance(name string, data interface{}) Render {
|
func (r HTMLDebug) Instance(name string, data any) Render {
|
||||||
return HTML{
|
return HTML{
|
||||||
Template: r.loadTemplate(),
|
Template: r.loadTemplate(),
|
||||||
Name: name,
|
Name: name,
|
||||||
|
|||||||
@ -10,54 +10,51 @@ 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"
|
||||||
)
|
)
|
||||||
|
|
||||||
// JSON contains the given interface object.
|
// JSON contains the given interface object.
|
||||||
type JSON struct {
|
type JSON struct {
|
||||||
Data interface{}
|
Data any
|
||||||
}
|
}
|
||||||
|
|
||||||
// IndentedJSON contains the given interface object.
|
// IndentedJSON contains the given interface object.
|
||||||
type IndentedJSON struct {
|
type IndentedJSON struct {
|
||||||
Data interface{}
|
Data any
|
||||||
}
|
}
|
||||||
|
|
||||||
// SecureJSON contains the given interface object and its prefix.
|
// SecureJSON contains the given interface object and its prefix.
|
||||||
type SecureJSON struct {
|
type SecureJSON struct {
|
||||||
Prefix string
|
Prefix string
|
||||||
Data interface{}
|
Data any
|
||||||
}
|
}
|
||||||
|
|
||||||
// JsonpJSON contains the given interface object its callback.
|
// JsonpJSON contains the given interface object its callback.
|
||||||
type JsonpJSON struct {
|
type JsonpJSON struct {
|
||||||
Callback string
|
Callback string
|
||||||
Data interface{}
|
Data any
|
||||||
}
|
}
|
||||||
|
|
||||||
// AsciiJSON contains the given interface object.
|
// AsciiJSON contains the given interface object.
|
||||||
type AsciiJSON struct {
|
type AsciiJSON struct {
|
||||||
Data interface{}
|
Data any
|
||||||
}
|
}
|
||||||
|
|
||||||
// SecureJSONPrefix is a string which represents SecureJSON prefix.
|
|
||||||
type SecureJSONPrefix string
|
|
||||||
|
|
||||||
// PureJSON contains the given interface object.
|
// PureJSON contains the given interface object.
|
||||||
type PureJSON struct {
|
type PureJSON struct {
|
||||||
Data interface{}
|
Data any
|
||||||
}
|
}
|
||||||
|
|
||||||
var jsonContentType = []string{"application/json; charset=utf-8"}
|
var (
|
||||||
var jsonpContentType = []string{"application/javascript; charset=utf-8"}
|
jsonContentType = []string{"application/json; charset=utf-8"}
|
||||||
var jsonAsciiContentType = []string{"application/json"}
|
jsonpContentType = []string{"application/javascript; charset=utf-8"}
|
||||||
|
jsonASCIIContentType = []string{"application/json"}
|
||||||
|
)
|
||||||
|
|
||||||
// Render (JSON) writes data with custom ContentType.
|
// Render (JSON) writes data with custom ContentType.
|
||||||
func (r JSON) Render(w http.ResponseWriter) (err error) {
|
func (r JSON) Render(w http.ResponseWriter) error {
|
||||||
if err = WriteJSON(w, r.Data); err != nil {
|
return WriteJSON(w, r.Data)
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteContentType (JSON) writes JSON ContentType.
|
// WriteContentType (JSON) writes JSON ContentType.
|
||||||
@ -66,7 +63,7 @@ func (r JSON) WriteContentType(w http.ResponseWriter) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WriteJSON marshals the given interface object and writes it with custom ContentType.
|
// WriteJSON marshals the given interface object and writes it with custom ContentType.
|
||||||
func WriteJSON(w http.ResponseWriter, obj interface{}) error {
|
func WriteJSON(w http.ResponseWriter, obj any) error {
|
||||||
writeContentType(w, jsonContentType)
|
writeContentType(w, jsonContentType)
|
||||||
jsonBytes, err := json.Marshal(obj)
|
jsonBytes, err := json.Marshal(obj)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -100,9 +97,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("]")) {
|
||||||
if err != nil {
|
if _, err = w.Write(bytesconv.StringToBytes(r.Prefix)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -129,20 +126,19 @@ func (r JsonpJSON) Render(w http.ResponseWriter) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
callback := template.JSEscapeString(r.Callback)
|
callback := template.JSEscapeString(r.Callback)
|
||||||
_, err = w.Write([]byte(callback))
|
if _, err = w.Write(bytesconv.StringToBytes(callback)); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, err = w.Write([]byte("("))
|
|
||||||
if err != nil {
|
if _, err = w.Write(bytesconv.StringToBytes("(")); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, err = w.Write(ret)
|
|
||||||
if err != nil {
|
if _, err = w.Write(ret); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, err = w.Write([]byte(")"))
|
|
||||||
if err != nil {
|
if _, err = w.Write(bytesconv.StringToBytes(");")); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,7 +159,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))
|
||||||
@ -177,7 +173,7 @@ func (r AsciiJSON) Render(w http.ResponseWriter) (err error) {
|
|||||||
|
|
||||||
// WriteContentType (AsciiJSON) writes JSON ContentType.
|
// WriteContentType (AsciiJSON) writes JSON ContentType.
|
||||||
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.
|
// Render (PureJSON) writes custom ContentType and encodes the given interface object.
|
||||||
|
|||||||
@ -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.
|
||||||
|
|
||||||
|
//go:build !nomsgpack
|
||||||
|
|
||||||
package render
|
package render
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -10,9 +12,15 @@ import (
|
|||||||
"github.com/ugorji/go/codec"
|
"github.com/ugorji/go/codec"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Check interface implemented here to support go build tag nomsgpack.
|
||||||
|
// See: https://github.com/gin-gonic/gin/pull/1852/
|
||||||
|
var (
|
||||||
|
_ Render = MsgPack{}
|
||||||
|
)
|
||||||
|
|
||||||
// MsgPack contains the given interface object.
|
// MsgPack contains the given interface object.
|
||||||
type MsgPack struct {
|
type MsgPack struct {
|
||||||
Data interface{}
|
Data any
|
||||||
}
|
}
|
||||||
|
|
||||||
var msgpackContentType = []string{"application/msgpack; charset=utf-8"}
|
var msgpackContentType = []string{"application/msgpack; charset=utf-8"}
|
||||||
@ -28,7 +36,7 @@ func (r MsgPack) Render(w http.ResponseWriter) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WriteMsgPack writes MsgPack ContentType and encodes the given interface object.
|
// WriteMsgPack writes MsgPack ContentType and encodes the given interface object.
|
||||||
func WriteMsgPack(w http.ResponseWriter, obj interface{}) error {
|
func WriteMsgPack(w http.ResponseWriter, obj any) error {
|
||||||
writeContentType(w, msgpackContentType)
|
writeContentType(w, msgpackContentType)
|
||||||
var mh codec.MsgpackHandle
|
var mh codec.MsgpackHandle
|
||||||
return codec.NewEncoder(w, &mh).Encode(obj)
|
return codec.NewEncoder(w, &mh).Encode(obj)
|
||||||
|
|||||||
@ -7,12 +7,12 @@ package render
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/golang/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ProtoBuf contains the given interface object.
|
// ProtoBuf contains the given interface object.
|
||||||
type ProtoBuf struct {
|
type ProtoBuf struct {
|
||||||
Data interface{}
|
Data any
|
||||||
}
|
}
|
||||||
|
|
||||||
var protobufContentType = []string{"application/x-protobuf"}
|
var protobufContentType = []string{"application/x-protobuf"}
|
||||||
|
|||||||
@ -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)
|
||||||
|
if r.ContentLength >= 0 {
|
||||||
|
if r.Headers == nil {
|
||||||
|
r.Headers = map[string]string{}
|
||||||
|
}
|
||||||
r.Headers["Content-Length"] = strconv.FormatInt(r.ContentLength, 10)
|
r.Headers["Content-Length"] = strconv.FormatInt(r.ContentLength, 10)
|
||||||
|
}
|
||||||
r.writeHeaders(w, r.Headers)
|
r.writeHeaders(w, r.Headers)
|
||||||
_, 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)
|
||||||
|
}
|
||||||
@ -15,22 +15,22 @@ type Render interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
_ Render = JSON{}
|
_ Render = (*JSON)(nil)
|
||||||
_ Render = IndentedJSON{}
|
_ Render = (*IndentedJSON)(nil)
|
||||||
_ Render = SecureJSON{}
|
_ Render = (*SecureJSON)(nil)
|
||||||
_ Render = JsonpJSON{}
|
_ Render = (*JsonpJSON)(nil)
|
||||||
_ Render = XML{}
|
_ Render = (*XML)(nil)
|
||||||
_ Render = String{}
|
_ Render = (*String)(nil)
|
||||||
_ Render = Redirect{}
|
_ Render = (*Redirect)(nil)
|
||||||
_ Render = Data{}
|
_ Render = (*Data)(nil)
|
||||||
_ Render = HTML{}
|
_ Render = (*HTML)(nil)
|
||||||
_ HTMLRender = HTMLDebug{}
|
_ HTMLRender = (*HTMLDebug)(nil)
|
||||||
_ HTMLRender = HTMLProduction{}
|
_ HTMLRender = (*HTMLProduction)(nil)
|
||||||
_ Render = YAML{}
|
_ Render = (*YAML)(nil)
|
||||||
_ Render = MsgPack{}
|
_ Render = (*Reader)(nil)
|
||||||
_ Render = Reader{}
|
_ Render = (*AsciiJSON)(nil)
|
||||||
_ Render = AsciiJSON{}
|
_ Render = (*ProtoBuf)(nil)
|
||||||
_ Render = ProtoBuf{}
|
_ Render = (*TOML)(nil)
|
||||||
)
|
)
|
||||||
|
|
||||||
func writeContentType(w http.ResponseWriter, value []string) {
|
func writeContentType(w http.ResponseWriter, value []string) {
|
||||||
|
|||||||
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.
|
||||||
|
|
||||||
|
//go: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]any{
|
||||||
|
"foo": "bar",
|
||||||
|
}
|
||||||
|
|
||||||
|
(MsgPack{data}).WriteContentType(w)
|
||||||
|
assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type"))
|
||||||
|
|
||||||
|
err := (MsgPack{data}).Render(w)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
h := new(codec.MsgpackHandle)
|
||||||
|
assert.NotNil(t, h)
|
||||||
|
buf := bytes.NewBuffer([]byte{})
|
||||||
|
assert.NotNil(t, buf)
|
||||||
|
err = codec.NewEncoder(buf, h).Encode(data)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, w.Body.String(), buf.String())
|
||||||
|
assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type"))
|
||||||
|
}
|
||||||
@ -5,53 +5,28 @@
|
|||||||
package render
|
package render
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"errors"
|
"errors"
|
||||||
"html/template"
|
"html/template"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/golang/protobuf/proto"
|
"github.com/gin-gonic/gin/internal/json"
|
||||||
"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"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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]any{
|
||||||
"foo": "bar",
|
"foo": "bar",
|
||||||
"html": "<b>",
|
"html": "<b>",
|
||||||
}
|
}
|
||||||
@ -66,17 +41,17 @@ func TestRenderJSON(t *testing.T) {
|
|||||||
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
|
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRenderJSONPanics(t *testing.T) {
|
func TestRenderJSONError(t *testing.T) {
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
data := make(chan int)
|
data := make(chan int)
|
||||||
|
|
||||||
// json: unsupported type: chan int
|
// json: unsupported type: chan int
|
||||||
assert.Panics(t, func() { assert.NoError(t, (JSON{data}).Render(w)) })
|
assert.Error(t, (JSON{data}).Render(w))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRenderIndentedJSON(t *testing.T) {
|
func TestRenderIndentedJSON(t *testing.T) {
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
data := map[string]interface{}{
|
data := map[string]any{
|
||||||
"foo": "bar",
|
"foo": "bar",
|
||||||
"bar": "foo",
|
"bar": "foo",
|
||||||
}
|
}
|
||||||
@ -99,7 +74,7 @@ func TestRenderIndentedJSONPanics(t *testing.T) {
|
|||||||
|
|
||||||
func TestRenderSecureJSON(t *testing.T) {
|
func TestRenderSecureJSON(t *testing.T) {
|
||||||
w1 := httptest.NewRecorder()
|
w1 := httptest.NewRecorder()
|
||||||
data := map[string]interface{}{
|
data := map[string]any{
|
||||||
"foo": "bar",
|
"foo": "bar",
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -113,7 +88,7 @@ func TestRenderSecureJSON(t *testing.T) {
|
|||||||
assert.Equal(t, "application/json; charset=utf-8", w1.Header().Get("Content-Type"))
|
assert.Equal(t, "application/json; charset=utf-8", w1.Header().Get("Content-Type"))
|
||||||
|
|
||||||
w2 := httptest.NewRecorder()
|
w2 := httptest.NewRecorder()
|
||||||
datas := []map[string]interface{}{{
|
datas := []map[string]any{{
|
||||||
"foo": "bar",
|
"foo": "bar",
|
||||||
}, {
|
}, {
|
||||||
"bar": "foo",
|
"bar": "foo",
|
||||||
@ -136,7 +111,7 @@ func TestRenderSecureJSONFail(t *testing.T) {
|
|||||||
|
|
||||||
func TestRenderJsonpJSON(t *testing.T) {
|
func TestRenderJsonpJSON(t *testing.T) {
|
||||||
w1 := httptest.NewRecorder()
|
w1 := httptest.NewRecorder()
|
||||||
data := map[string]interface{}{
|
data := map[string]any{
|
||||||
"foo": "bar",
|
"foo": "bar",
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,11 +121,11 @@ 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()
|
||||||
datas := []map[string]interface{}{{
|
datas := []map[string]any{{
|
||||||
"foo": "bar",
|
"foo": "bar",
|
||||||
}, {
|
}, {
|
||||||
"bar": "foo",
|
"bar": "foo",
|
||||||
@ -158,13 +133,58 @@ 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"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type errorWriter struct {
|
||||||
|
bufString string
|
||||||
|
*httptest.ResponseRecorder
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ http.ResponseWriter = (*errorWriter)(nil)
|
||||||
|
|
||||||
|
func (w *errorWriter) Write(buf []byte) (int, error) {
|
||||||
|
if string(buf) == w.bufString {
|
||||||
|
return 0, errors.New(`write "` + w.bufString + `" error`)
|
||||||
|
}
|
||||||
|
return w.ResponseRecorder.Write(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRenderJsonpJSONError(t *testing.T) {
|
||||||
|
ew := &errorWriter{
|
||||||
|
ResponseRecorder: httptest.NewRecorder(),
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonpJSON := JsonpJSON{
|
||||||
|
Callback: "foo",
|
||||||
|
Data: map[string]string{
|
||||||
|
"foo": "bar",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cb := template.JSEscapeString(jsonpJSON.Callback)
|
||||||
|
ew.bufString = cb
|
||||||
|
err := jsonpJSON.Render(ew) // error was returned while writing callback
|
||||||
|
assert.Equal(t, `write "`+cb+`" error`, err.Error())
|
||||||
|
|
||||||
|
ew.bufString = `(`
|
||||||
|
err = jsonpJSON.Render(ew)
|
||||||
|
assert.Equal(t, `write "`+`(`+`" error`, err.Error())
|
||||||
|
|
||||||
|
data, _ := json.Marshal(jsonpJSON.Data) // error was returned while writing data
|
||||||
|
ew.bufString = string(data)
|
||||||
|
err = jsonpJSON.Render(ew)
|
||||||
|
assert.Equal(t, `write "`+string(data)+`" error`, err.Error())
|
||||||
|
|
||||||
|
ew.bufString = `);`
|
||||||
|
err = jsonpJSON.Render(ew)
|
||||||
|
assert.Equal(t, `write "`+`);`+`" error`, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
func TestRenderJsonpJSONError2(t *testing.T) {
|
func TestRenderJsonpJSONError2(t *testing.T) {
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
data := map[string]interface{}{
|
data := map[string]any{
|
||||||
"foo": "bar",
|
"foo": "bar",
|
||||||
}
|
}
|
||||||
(JsonpJSON{"", data}).WriteContentType(w)
|
(JsonpJSON{"", data}).WriteContentType(w)
|
||||||
@ -188,7 +208,7 @@ func TestRenderJsonpJSONFail(t *testing.T) {
|
|||||||
|
|
||||||
func TestRenderAsciiJSON(t *testing.T) {
|
func TestRenderAsciiJSON(t *testing.T) {
|
||||||
w1 := httptest.NewRecorder()
|
w1 := httptest.NewRecorder()
|
||||||
data1 := map[string]interface{}{
|
data1 := map[string]any{
|
||||||
"lang": "GO语言",
|
"lang": "GO语言",
|
||||||
"tag": "<br>",
|
"tag": "<br>",
|
||||||
}
|
}
|
||||||
@ -200,7 +220,7 @@ func TestRenderAsciiJSON(t *testing.T) {
|
|||||||
assert.Equal(t, "application/json", w1.Header().Get("Content-Type"))
|
assert.Equal(t, "application/json", w1.Header().Get("Content-Type"))
|
||||||
|
|
||||||
w2 := httptest.NewRecorder()
|
w2 := httptest.NewRecorder()
|
||||||
data2 := float64(3.1415926)
|
data2 := 3.1415926
|
||||||
|
|
||||||
err = (AsciiJSON{data2}).Render(w2)
|
err = (AsciiJSON{data2}).Render(w2)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
@ -217,7 +237,7 @@ func TestRenderAsciiJSONFail(t *testing.T) {
|
|||||||
|
|
||||||
func TestRenderPureJSON(t *testing.T) {
|
func TestRenderPureJSON(t *testing.T) {
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
data := map[string]interface{}{
|
data := map[string]any{
|
||||||
"foo": "bar",
|
"foo": "bar",
|
||||||
"html": "<b>",
|
"html": "<b>",
|
||||||
}
|
}
|
||||||
@ -227,7 +247,7 @@ func TestRenderPureJSON(t *testing.T) {
|
|||||||
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
|
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
|
||||||
}
|
}
|
||||||
|
|
||||||
type xmlmap map[string]interface{}
|
type xmlmap map[string]any
|
||||||
|
|
||||||
// Allows type H to be used with xml.Marshal
|
// Allows type H to be used with xml.Marshal
|
||||||
func (h xmlmap) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
func (h xmlmap) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||||
@ -260,18 +280,18 @@ b:
|
|||||||
d: [3, 4]
|
d: [3, 4]
|
||||||
`
|
`
|
||||||
(YAML{data}).WriteContentType(w)
|
(YAML{data}).WriteContentType(w)
|
||||||
assert.Equal(t, "application/x-yaml; charset=utf-8", w.Header().Get("Content-Type"))
|
assert.Equal(t, "application/yaml; charset=utf-8", w.Header().Get("Content-Type"))
|
||||||
|
|
||||||
err := (YAML{data}).Render(w)
|
err := (YAML{data}).Render(w)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, "\"\\na : Easy!\\nb:\\n\\tc: 2\\n\\td: [3, 4]\\n\\t\"\n", w.Body.String())
|
assert.Equal(t, "|4-\n a : Easy!\n b:\n \tc: 2\n \td: [3, 4]\n \t\n", w.Body.String())
|
||||||
assert.Equal(t, "application/x-yaml; charset=utf-8", w.Header().Get("Content-Type"))
|
assert.Equal(t, "application/yaml; charset=utf-8", w.Header().Get("Content-Type"))
|
||||||
}
|
}
|
||||||
|
|
||||||
type fail struct{}
|
type fail struct{}
|
||||||
|
|
||||||
// Hook MarshalYAML
|
// Hook MarshalYAML
|
||||||
func (ft *fail) MarshalYAML() (interface{}, error) {
|
func (ft *fail) MarshalYAML() (any, error) {
|
||||||
return nil, errors.New("fail")
|
return nil, errors.New("fail")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -281,6 +301,27 @@ func TestRenderYAMLFail(t *testing.T) {
|
|||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRenderTOML(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
data := map[string]any{
|
||||||
|
"foo": "bar",
|
||||||
|
"html": "<b>",
|
||||||
|
}
|
||||||
|
(TOML{data}).WriteContentType(w)
|
||||||
|
assert.Equal(t, "application/toml; charset=utf-8", w.Header().Get("Content-Type"))
|
||||||
|
|
||||||
|
err := (TOML{data}).Render(w)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "foo = 'bar'\nhtml = '<b>'\n", w.Body.String())
|
||||||
|
assert.Equal(t, "application/toml; charset=utf-8", w.Header().Get("Content-Type"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRenderTOMLFail(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
err := (TOML{net.IPv4bcast}).Render(w)
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
// test Protobuf rendering
|
// test Protobuf rendering
|
||||||
func TestRenderProtoBuf(t *testing.T) {
|
func TestRenderProtoBuf(t *testing.T) {
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
@ -347,7 +388,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)
|
||||||
@ -372,13 +426,13 @@ func TestRenderString(t *testing.T) {
|
|||||||
|
|
||||||
(String{
|
(String{
|
||||||
Format: "hello %s %d",
|
Format: "hello %s %d",
|
||||||
Data: []interface{}{},
|
Data: []any{},
|
||||||
}).WriteContentType(w)
|
}).WriteContentType(w)
|
||||||
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"))
|
||||||
|
|
||||||
err := (String{
|
err := (String{
|
||||||
Format: "hola %s %d",
|
Format: "hola %s %d",
|
||||||
Data: []interface{}{"manu", 2},
|
Data: []any{"manu", 2},
|
||||||
}).Render(w)
|
}).Render(w)
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
@ -391,7 +445,7 @@ func TestRenderStringLenZero(t *testing.T) {
|
|||||||
|
|
||||||
err := (String{
|
err := (String{
|
||||||
Format: "hola %s %d",
|
Format: "hola %s %d",
|
||||||
Data: []interface{}{},
|
Data: []any{},
|
||||||
}).Render(w)
|
}).Render(w)
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
@ -404,7 +458,7 @@ func TestRenderHTMLTemplate(t *testing.T) {
|
|||||||
templ := template.Must(template.New("t").Parse(`Hello {{.name}}`))
|
templ := template.Must(template.New("t").Parse(`Hello {{.name}}`))
|
||||||
|
|
||||||
htmlRender := HTMLProduction{Template: templ}
|
htmlRender := HTMLProduction{Template: templ}
|
||||||
instance := htmlRender.Instance("t", map[string]interface{}{
|
instance := htmlRender.Instance("t", map[string]any{
|
||||||
"name": "alexandernyquist",
|
"name": "alexandernyquist",
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -420,7 +474,7 @@ func TestRenderHTMLTemplateEmptyName(t *testing.T) {
|
|||||||
templ := template.Must(template.New("").Parse(`Hello {{.name}}`))
|
templ := template.Must(template.New("").Parse(`Hello {{.name}}`))
|
||||||
|
|
||||||
htmlRender := HTMLProduction{Template: templ}
|
htmlRender := HTMLProduction{Template: templ}
|
||||||
instance := htmlRender.Instance("", map[string]interface{}{
|
instance := htmlRender.Instance("", map[string]any{
|
||||||
"name": "alexandernyquist",
|
"name": "alexandernyquist",
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -433,12 +487,13 @@ func TestRenderHTMLTemplateEmptyName(t *testing.T) {
|
|||||||
|
|
||||||
func TestRenderHTMLDebugFiles(t *testing.T) {
|
func TestRenderHTMLDebugFiles(t *testing.T) {
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
htmlRender := HTMLDebug{Files: []string{"../testdata/template/hello.tmpl"},
|
htmlRender := HTMLDebug{
|
||||||
|
Files: []string{"../testdata/template/hello.tmpl"},
|
||||||
Glob: "",
|
Glob: "",
|
||||||
Delims: Delims{Left: "{[{", Right: "}]}"},
|
Delims: Delims{Left: "{[{", Right: "}]}"},
|
||||||
FuncMap: nil,
|
FuncMap: nil,
|
||||||
}
|
}
|
||||||
instance := htmlRender.Instance("hello.tmpl", map[string]interface{}{
|
instance := htmlRender.Instance("hello.tmpl", map[string]any{
|
||||||
"name": "thinkerou",
|
"name": "thinkerou",
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -451,12 +506,13 @@ func TestRenderHTMLDebugFiles(t *testing.T) {
|
|||||||
|
|
||||||
func TestRenderHTMLDebugGlob(t *testing.T) {
|
func TestRenderHTMLDebugGlob(t *testing.T) {
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
htmlRender := HTMLDebug{Files: nil,
|
htmlRender := HTMLDebug{
|
||||||
|
Files: nil,
|
||||||
Glob: "../testdata/template/hello*",
|
Glob: "../testdata/template/hello*",
|
||||||
Delims: Delims{Left: "{[{", Right: "}]}"},
|
Delims: Delims{Left: "{[{", Right: "}]}"},
|
||||||
FuncMap: nil,
|
FuncMap: nil,
|
||||||
}
|
}
|
||||||
instance := htmlRender.Instance("hello.tmpl", map[string]interface{}{
|
instance := htmlRender.Instance("hello.tmpl", map[string]any{
|
||||||
"name": "thinkerou",
|
"name": "thinkerou",
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -468,7 +524,8 @@ func TestRenderHTMLDebugGlob(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRenderHTMLDebugPanics(t *testing.T) {
|
func TestRenderHTMLDebugPanics(t *testing.T) {
|
||||||
htmlRender := HTMLDebug{Files: nil,
|
htmlRender := HTMLDebug{
|
||||||
|
Files: nil,
|
||||||
Glob: "",
|
Glob: "",
|
||||||
Delims: Delims{"{{", "}}"},
|
Delims: Delims{"{{", "}}"},
|
||||||
FuncMap: nil,
|
FuncMap: nil,
|
||||||
@ -498,3 +555,39 @@ 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"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRenderWriteError(t *testing.T) {
|
||||||
|
data := []interface{}{"value1", "value2"}
|
||||||
|
prefix := "my-prefix:"
|
||||||
|
r := SecureJSON{Data: data, Prefix: prefix}
|
||||||
|
ew := &errorWriter{
|
||||||
|
bufString: prefix,
|
||||||
|
ResponseRecorder: httptest.NewRecorder(),
|
||||||
|
}
|
||||||
|
err := r.Render(ew)
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
assert.Equal(t, `write "my-prefix:" error`, err.Error())
|
||||||
|
}
|
||||||
|
|||||||
@ -6,14 +6,15 @@ 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.
|
||||||
type String struct {
|
type String struct {
|
||||||
Format string
|
Format string
|
||||||
Data []interface{}
|
Data []any
|
||||||
}
|
}
|
||||||
|
|
||||||
var plainContentType = []string{"text/plain; charset=utf-8"}
|
var plainContentType = []string{"text/plain; charset=utf-8"}
|
||||||
@ -29,12 +30,12 @@ func (r String) WriteContentType(w http.ResponseWriter) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WriteString writes data according to its format and write custom ContentType.
|
// WriteString writes data according to its format and write custom ContentType.
|
||||||
func WriteString(w http.ResponseWriter, format string, data []interface{}) (err error) {
|
func WriteString(w http.ResponseWriter, format string, data []any) (err error) {
|
||||||
writeContentType(w, plainContentType)
|
writeContentType(w, plainContentType)
|
||||||
if len(data) > 0 {
|
if len(data) > 0 {
|
||||||
_, 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
|
||||||
}
|
}
|
||||||
|
|||||||
36
render/toml.go
Normal file
36
render/toml.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
// Copyright 2022 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"
|
||||||
|
|
||||||
|
"github.com/pelletier/go-toml/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TOML contains the given interface object.
|
||||||
|
type TOML struct {
|
||||||
|
Data any
|
||||||
|
}
|
||||||
|
|
||||||
|
var TOMLContentType = []string{"application/toml; charset=utf-8"}
|
||||||
|
|
||||||
|
// Render (TOML) marshals the given interface object and writes data with custom ContentType.
|
||||||
|
func (r TOML) Render(w http.ResponseWriter) error {
|
||||||
|
r.WriteContentType(w)
|
||||||
|
|
||||||
|
bytes, err := toml.Marshal(r.Data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = w.Write(bytes)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteContentType (TOML) writes TOML ContentType for response.
|
||||||
|
func (r TOML) WriteContentType(w http.ResponseWriter) {
|
||||||
|
writeContentType(w, TOMLContentType)
|
||||||
|
}
|
||||||
@ -11,7 +11,7 @@ import (
|
|||||||
|
|
||||||
// XML contains the given interface object.
|
// XML contains the given interface object.
|
||||||
type XML struct {
|
type XML struct {
|
||||||
Data interface{}
|
Data any
|
||||||
}
|
}
|
||||||
|
|
||||||
var xmlContentType = []string{"application/xml; charset=utf-8"}
|
var xmlContentType = []string{"application/xml; charset=utf-8"}
|
||||||
|
|||||||
@ -7,15 +7,15 @@ package render
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
// YAML contains the given interface object.
|
// YAML contains the given interface object.
|
||||||
type YAML struct {
|
type YAML struct {
|
||||||
Data interface{}
|
Data any
|
||||||
}
|
}
|
||||||
|
|
||||||
var yamlContentType = []string{"application/x-yaml; charset=utf-8"}
|
var yamlContentType = []string{"application/yaml; charset=utf-8"}
|
||||||
|
|
||||||
// Render (YAML) marshals the given interface object and writes data with custom ContentType.
|
// Render (YAML) marshals the given interface object and writes data with custom ContentType.
|
||||||
func (r YAML) Render(w http.ResponseWriter) error {
|
func (r YAML) Render(w http.ResponseWriter) error {
|
||||||
|
|||||||
@ -23,23 +23,23 @@ type ResponseWriter interface {
|
|||||||
http.Flusher
|
http.Flusher
|
||||||
http.CloseNotifier
|
http.CloseNotifier
|
||||||
|
|
||||||
// Returns the HTTP response status code of the current request.
|
// Status returns the HTTP response status code of the current request.
|
||||||
Status() int
|
Status() int
|
||||||
|
|
||||||
// Returns the number of bytes already written into the response http body.
|
// Size returns the number of bytes already written into the response http body.
|
||||||
// See Written()
|
// See Written()
|
||||||
Size() int
|
Size() int
|
||||||
|
|
||||||
// Writes the string into the response body.
|
// WriteString writes the string into the response body.
|
||||||
WriteString(string) (int, error)
|
WriteString(string) (int, error)
|
||||||
|
|
||||||
// Returns true if the response body was already written.
|
// Written returns true if the response body was already written.
|
||||||
Written() bool
|
Written() bool
|
||||||
|
|
||||||
// Forces to write the http header (status code + headers).
|
// WriteHeaderNow forces to write the http header (status code + headers).
|
||||||
WriteHeaderNow()
|
WriteHeaderNow()
|
||||||
|
|
||||||
// get the http.Pusher for server push
|
// Pusher get the http.Pusher for server push
|
||||||
Pusher() http.Pusher
|
Pusher() http.Pusher
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,7 +49,11 @@ type responseWriter struct {
|
|||||||
status int
|
status int
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ ResponseWriter = &responseWriter{}
|
var _ ResponseWriter = (*responseWriter)(nil)
|
||||||
|
|
||||||
|
func (w *responseWriter) Unwrap() http.ResponseWriter {
|
||||||
|
return w.ResponseWriter
|
||||||
|
}
|
||||||
|
|
||||||
func (w *responseWriter) reset(writer http.ResponseWriter) {
|
func (w *responseWriter) reset(writer http.ResponseWriter) {
|
||||||
w.ResponseWriter = writer
|
w.ResponseWriter = writer
|
||||||
@ -61,6 +65,7 @@ func (w *responseWriter) WriteHeader(code int) {
|
|||||||
if code > 0 && w.status != code {
|
if code > 0 && w.status != code {
|
||||||
if w.Written() {
|
if w.Written() {
|
||||||
debugPrint("[WARNING] Headers were already written. Wanted to override status code %d with %d", w.status, code)
|
debugPrint("[WARNING] Headers were already written. Wanted to override status code %d with %d", w.status, code)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
w.status = code
|
w.status = code
|
||||||
}
|
}
|
||||||
@ -107,12 +112,12 @@ func (w *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
|||||||
return w.ResponseWriter.(http.Hijacker).Hijack()
|
return w.ResponseWriter.(http.Hijacker).Hijack()
|
||||||
}
|
}
|
||||||
|
|
||||||
// CloseNotify implements the http.CloseNotify interface.
|
// CloseNotify implements the http.CloseNotifier interface.
|
||||||
func (w *responseWriter) CloseNotify() <-chan bool {
|
func (w *responseWriter) CloseNotify() <-chan bool {
|
||||||
return w.ResponseWriter.(http.CloseNotifier).CloseNotify()
|
return w.ResponseWriter.(http.CloseNotifier).CloseNotify()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flush implements the http.Flush interface.
|
// Flush implements the http.Flusher interface.
|
||||||
func (w *responseWriter) Flush() {
|
func (w *responseWriter) Flush() {
|
||||||
w.WriteHeaderNow()
|
w.WriteHeaderNow()
|
||||||
w.ResponseWriter.(http.Flusher).Flush()
|
w.ResponseWriter.(http.Flusher).Flush()
|
||||||
|
|||||||
@ -17,50 +17,58 @@ import (
|
|||||||
// func (w *responseWriter) CloseNotify() <-chan bool {
|
// func (w *responseWriter) CloseNotify() <-chan bool {
|
||||||
// func (w *responseWriter) Flush() {
|
// func (w *responseWriter) Flush() {
|
||||||
|
|
||||||
var _ ResponseWriter = &responseWriter{}
|
var (
|
||||||
var _ http.ResponseWriter = &responseWriter{}
|
_ ResponseWriter = &responseWriter{}
|
||||||
var _ http.ResponseWriter = ResponseWriter(&responseWriter{})
|
_ http.ResponseWriter = &responseWriter{}
|
||||||
var _ http.Hijacker = ResponseWriter(&responseWriter{})
|
_ http.ResponseWriter = ResponseWriter(&responseWriter{})
|
||||||
var _ http.Flusher = ResponseWriter(&responseWriter{})
|
_ http.Hijacker = ResponseWriter(&responseWriter{})
|
||||||
var _ http.CloseNotifier = ResponseWriter(&responseWriter{})
|
_ http.Flusher = ResponseWriter(&responseWriter{})
|
||||||
|
_ http.CloseNotifier = ResponseWriter(&responseWriter{})
|
||||||
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
SetMode(TestMode)
|
SetMode(TestMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestResponseWriterUnwrap(t *testing.T) {
|
||||||
|
testWriter := httptest.NewRecorder()
|
||||||
|
writer := &responseWriter{ResponseWriter: testWriter}
|
||||||
|
assert.Same(t, testWriter, writer.Unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
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 +76,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 +84,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() {
|
||||||
@ -130,3 +138,51 @@ func TestResponseWriterFlush(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, http.StatusInternalServerError, resp.StatusCode)
|
assert.Equal(t, http.StatusInternalServerError, resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestResponseWriterStatusCode(t *testing.T) {
|
||||||
|
testWriter := httptest.NewRecorder()
|
||||||
|
writer := &responseWriter{}
|
||||||
|
writer.reset(testWriter)
|
||||||
|
w := ResponseWriter(writer)
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.WriteHeaderNow()
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, w.Status())
|
||||||
|
assert.True(t, w.Written())
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
|
||||||
|
// status must be 200 although we tried to change it
|
||||||
|
assert.Equal(t, http.StatusOK, w.Status())
|
||||||
|
}
|
||||||
|
|
||||||
|
// mockPusherResponseWriter is an http.ResponseWriter that implements http.Pusher.
|
||||||
|
type mockPusherResponseWriter struct {
|
||||||
|
http.ResponseWriter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockPusherResponseWriter) Push(target string, opts *http.PushOptions) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// nonPusherResponseWriter is an http.ResponseWriter that does not implement http.Pusher.
|
||||||
|
type nonPusherResponseWriter struct {
|
||||||
|
http.ResponseWriter
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPusherWithPusher(t *testing.T) {
|
||||||
|
rw := &mockPusherResponseWriter{}
|
||||||
|
w := &responseWriter{ResponseWriter: rw}
|
||||||
|
|
||||||
|
pusher := w.Pusher()
|
||||||
|
assert.NotNil(t, pusher, "Expected pusher to be non-nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPusherWithoutPusher(t *testing.T) {
|
||||||
|
rw := &nonPusherResponseWriter{}
|
||||||
|
w := &responseWriter{ResponseWriter: rw}
|
||||||
|
|
||||||
|
pusher := w.Pusher()
|
||||||
|
assert.Nil(t, pusher, "Expected pusher to be nil")
|
||||||
|
}
|
||||||
|
|||||||
@ -11,6 +11,18 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// regEnLetter matches english letters for http method name
|
||||||
|
regEnLetter = regexp.MustCompile("^[A-Z]+$")
|
||||||
|
|
||||||
|
// anyMethods for RouterGroup Any method
|
||||||
|
anyMethods = []string{
|
||||||
|
http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch,
|
||||||
|
http.MethodHead, http.MethodOptions, http.MethodDelete, http.MethodConnect,
|
||||||
|
http.MethodTrace,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// IRouter defines all router handle interface includes single and group router.
|
// IRouter defines all router handle interface includes single and group router.
|
||||||
type IRouter interface {
|
type IRouter interface {
|
||||||
IRoutes
|
IRoutes
|
||||||
@ -30,8 +42,10 @@ type IRoutes interface {
|
|||||||
PUT(string, ...HandlerFunc) IRoutes
|
PUT(string, ...HandlerFunc) IRoutes
|
||||||
OPTIONS(string, ...HandlerFunc) IRoutes
|
OPTIONS(string, ...HandlerFunc) IRoutes
|
||||||
HEAD(string, ...HandlerFunc) IRoutes
|
HEAD(string, ...HandlerFunc) IRoutes
|
||||||
|
Match([]string, string, ...HandlerFunc) IRoutes
|
||||||
|
|
||||||
StaticFile(string, string) IRoutes
|
StaticFile(string, string) IRoutes
|
||||||
|
StaticFileFS(string, string, http.FileSystem) IRoutes
|
||||||
Static(string, string) IRoutes
|
Static(string, string) IRoutes
|
||||||
StaticFS(string, http.FileSystem) IRoutes
|
StaticFS(string, http.FileSystem) IRoutes
|
||||||
}
|
}
|
||||||
@ -45,7 +59,7 @@ type RouterGroup struct {
|
|||||||
root bool
|
root bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ IRouter = &RouterGroup{}
|
var _ IRouter = (*RouterGroup)(nil)
|
||||||
|
|
||||||
// Use adds middleware to the group, see example code in GitHub.
|
// Use adds middleware to the group, see example code in GitHub.
|
||||||
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
|
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
|
||||||
@ -87,71 +101,87 @@ func (group *RouterGroup) handle(httpMethod, relativePath string, handlers Handl
|
|||||||
// frequently used, non-standardized or custom methods (e.g. for internal
|
// frequently used, non-standardized or custom methods (e.g. for internal
|
||||||
// communication with a proxy).
|
// communication with a proxy).
|
||||||
func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers ...HandlerFunc) IRoutes {
|
func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers ...HandlerFunc) IRoutes {
|
||||||
if matches, err := regexp.MatchString("^[A-Z]+$", httpMethod); !matches || err != nil {
|
if matched := regEnLetter.MatchString(httpMethod); !matched {
|
||||||
panic("http method " + httpMethod + " is not valid")
|
panic("http method " + httpMethod + " is not valid")
|
||||||
}
|
}
|
||||||
return group.handle(httpMethod, relativePath, handlers)
|
return group.handle(httpMethod, relativePath, handlers)
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST is a shortcut for router.Handle("POST", path, handle).
|
// POST is a shortcut for router.Handle("POST", path, handlers).
|
||||||
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, handlers).
|
||||||
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, handlers).
|
||||||
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, handlers).
|
||||||
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, handlers).
|
||||||
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, handlers).
|
||||||
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, handlers).
|
||||||
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)
|
for _, method := range anyMethods {
|
||||||
group.handle("POST", relativePath, handlers)
|
group.handle(method, relativePath, handlers)
|
||||||
group.handle("PUT", relativePath, handlers)
|
}
|
||||||
group.handle("PATCH", relativePath, handlers)
|
|
||||||
group.handle("HEAD", relativePath, handlers)
|
return group.returnObj()
|
||||||
group.handle("OPTIONS", relativePath, handlers)
|
}
|
||||||
group.handle("DELETE", relativePath, handlers)
|
|
||||||
group.handle("CONNECT", relativePath, handlers)
|
// Match registers a route that matches the specified methods that you declared.
|
||||||
group.handle("TRACE", relativePath, handlers)
|
func (group *RouterGroup) Match(methods []string, relativePath string, handlers ...HandlerFunc) IRoutes {
|
||||||
|
for _, method := range methods {
|
||||||
|
group.handle(method, relativePath, handlers)
|
||||||
|
}
|
||||||
|
|
||||||
return group.returnObj()
|
return group.returnObj()
|
||||||
}
|
}
|
||||||
|
|
||||||
// StaticFile registers a single route in order to serve a single file of the local filesystem.
|
// StaticFile registers a single route in order to serve a single file of the local filesystem.
|
||||||
// router.StaticFile("favicon.ico", "./resources/favicon.ico")
|
// router.StaticFile("favicon.ico", "./resources/favicon.ico")
|
||||||
func (group *RouterGroup) StaticFile(relativePath, filepath string) IRoutes {
|
func (group *RouterGroup) StaticFile(relativePath, filepath string) IRoutes {
|
||||||
|
return group.staticFileHandler(relativePath, func(c *Context) {
|
||||||
|
c.File(filepath)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// StaticFileFS works just like `StaticFile` but a custom `http.FileSystem` can be used instead..
|
||||||
|
// router.StaticFileFS("favicon.ico", "./resources/favicon.ico", Dir{".", false})
|
||||||
|
// Gin by default uses: gin.Dir()
|
||||||
|
func (group *RouterGroup) StaticFileFS(relativePath, filepath string, fs http.FileSystem) IRoutes {
|
||||||
|
return group.staticFileHandler(relativePath, func(c *Context) {
|
||||||
|
c.FileFromFS(filepath, fs)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (group *RouterGroup) staticFileHandler(relativePath string, handler HandlerFunc) IRoutes {
|
||||||
if strings.Contains(relativePath, ":") || strings.Contains(relativePath, "*") {
|
if strings.Contains(relativePath, ":") || strings.Contains(relativePath, "*") {
|
||||||
panic("URL parameters can not be used when serving a static file")
|
panic("URL parameters can not be used when serving a static file")
|
||||||
}
|
}
|
||||||
handler := func(c *Context) {
|
|
||||||
c.File(filepath)
|
|
||||||
}
|
|
||||||
group.GET(relativePath, handler)
|
group.GET(relativePath, handler)
|
||||||
group.HEAD(relativePath, handler)
|
group.HEAD(relativePath, handler)
|
||||||
return group.returnObj()
|
return group.returnObj()
|
||||||
@ -162,13 +192,14 @@ func (group *RouterGroup) StaticFile(relativePath, filepath string) IRoutes {
|
|||||||
// of the Router's NotFound handler.
|
// of the Router's NotFound handler.
|
||||||
// To use the operating system's file system implementation,
|
// To use the operating system's file system implementation,
|
||||||
// use :
|
// use :
|
||||||
|
//
|
||||||
// router.Static("/static", "/var/www")
|
// router.Static("/static", "/var/www")
|
||||||
func (group *RouterGroup) Static(relativePath, root string) IRoutes {
|
func (group *RouterGroup) Static(relativePath, root string) IRoutes {
|
||||||
return group.StaticFS(relativePath, Dir(root, false))
|
return group.StaticFS(relativePath, Dir(root, false))
|
||||||
}
|
}
|
||||||
|
|
||||||
// StaticFS works just like `Static()` but a custom `http.FileSystem` can be used instead.
|
// StaticFS works just like `Static()` but a custom `http.FileSystem` can be used instead.
|
||||||
// Gin by default user: gin.Dir()
|
// Gin by default uses: gin.Dir()
|
||||||
func (group *RouterGroup) StaticFS(relativePath string, fs http.FileSystem) IRoutes {
|
func (group *RouterGroup) StaticFS(relativePath string, fs http.FileSystem) IRoutes {
|
||||||
if strings.Contains(relativePath, ":") || strings.Contains(relativePath, "*") {
|
if strings.Contains(relativePath, ":") || strings.Contains(relativePath, "*") {
|
||||||
panic("URL parameters can not be used when serving a static folder")
|
panic("URL parameters can not be used when serving a static folder")
|
||||||
@ -187,19 +218,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)
|
||||||
}
|
}
|
||||||
@ -207,9 +240,7 @@ func (group *RouterGroup) createStaticHandler(relativePath string, fs http.FileS
|
|||||||
|
|
||||||
func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
|
func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
|
||||||
finalSize := len(group.Handlers) + len(handlers)
|
finalSize := len(group.Handlers) + len(handlers)
|
||||||
if finalSize >= int(abortIndex) {
|
assert1(finalSize < int(abortIndex), "too many handlers")
|
||||||
panic("too many handlers")
|
|
||||||
}
|
|
||||||
mergedHandlers := make(HandlersChain, finalSize)
|
mergedHandlers := make(HandlersChain, finalSize)
|
||||||
copy(mergedHandlers, group.Handlers)
|
copy(mergedHandlers, group.Handlers)
|
||||||
copy(mergedHandlers[len(group.Handlers):], handlers)
|
copy(mergedHandlers[len(group.Handlers):], handlers)
|
||||||
|
|||||||
@ -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,36 +55,36 @@ 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:
|
||||||
panic("unknown method")
|
panic("unknown method")
|
||||||
}
|
}
|
||||||
|
|
||||||
w := performRequest(router, method, "/v1/login/test")
|
w := PerformRequest(router, method, "/v1/login/test")
|
||||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||||
assert.Equal(t, "the method was "+method+" and index 3", w.Body.String())
|
assert.Equal(t, "the method was "+method+" and index 3", w.Body.String())
|
||||||
|
|
||||||
w = performRequest(router, method, "/v1/test")
|
w = PerformRequest(router, method, "/v1/test")
|
||||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||||
assert.Equal(t, "the method was "+method+" and index 1", w.Body.String())
|
assert.Equal(t, "the method was "+method+" and index 1", w.Body.String())
|
||||||
}
|
}
|
||||||
@ -111,16 +111,31 @@ func TestRouterGroupInvalidStaticFile(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRouterGroupTooManyHandlers(t *testing.T) {
|
func TestRouterGroupInvalidStaticFileFS(t *testing.T) {
|
||||||
router := New()
|
router := New()
|
||||||
handlers1 := make([]HandlerFunc, 40)
|
assert.Panics(t, func() {
|
||||||
|
router.StaticFileFS("/path/:param", "favicon.ico", Dir(".", false))
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.Panics(t, func() {
|
||||||
|
router.StaticFileFS("/path/*param", "favicon.ico", Dir(".", false))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRouterGroupTooManyHandlers(t *testing.T) {
|
||||||
|
const (
|
||||||
|
panicValue = "too many handlers"
|
||||||
|
maximumCnt = abortIndex
|
||||||
|
)
|
||||||
|
router := New()
|
||||||
|
handlers1 := make([]HandlerFunc, maximumCnt-1)
|
||||||
router.Use(handlers1...)
|
router.Use(handlers1...)
|
||||||
|
|
||||||
handlers2 := make([]HandlerFunc, 26)
|
handlers2 := make([]HandlerFunc, maximumCnt+1)
|
||||||
assert.Panics(t, func() {
|
assert.PanicsWithValue(t, panicValue, func() {
|
||||||
router.Use(handlers2...)
|
router.Use(handlers2...)
|
||||||
})
|
})
|
||||||
assert.Panics(t, func() {
|
assert.PanicsWithValue(t, panicValue, func() {
|
||||||
router.GET("/", handlers2...)
|
router.GET("/", handlers2...)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -128,7 +143,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 +177,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))
|
||||||
@ -171,8 +186,10 @@ func testRoutesInterface(t *testing.T, r IRoutes) {
|
|||||||
assert.Equal(t, r, r.PUT("/", handler))
|
assert.Equal(t, r, r.PUT("/", handler))
|
||||||
assert.Equal(t, r, r.OPTIONS("/", handler))
|
assert.Equal(t, r, r.OPTIONS("/", handler))
|
||||||
assert.Equal(t, r, r.HEAD("/", handler))
|
assert.Equal(t, r, r.HEAD("/", handler))
|
||||||
|
assert.Equal(t, r, r.Match([]string{http.MethodPut, http.MethodPatch}, "/match", handler))
|
||||||
|
|
||||||
assert.Equal(t, r, r.StaticFile("/file", "."))
|
assert.Equal(t, r, r.StaticFile("/file", "."))
|
||||||
|
assert.Equal(t, r, r.StaticFileFS("/static2", ".", Dir(".", false)))
|
||||||
assert.Equal(t, r, r.Static("/static", "."))
|
assert.Equal(t, r, r.Static("/static", "."))
|
||||||
assert.Equal(t, r, r.StaticFS("/static2", Dir(".", false)))
|
assert.Equal(t, r, r.StaticFS("/static2", Dir(".", false)))
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user