Compare commits

..

No commits in common. "master" and "v1.9.1" have entirely different histories.

63 changed files with 1301 additions and 3553 deletions

View File

@ -12,7 +12,7 @@ on:
# The branches below must be a subset of the branches above # The branches below must be a subset of the branches above
branches: [ master ] branches: [ master ]
schedule: schedule:
- cron: "0 17 * * 5" - cron: '0 17 * * 5'
jobs: jobs:
analyze: analyze:
@ -29,15 +29,15 @@ jobs:
# Override automatic language detection by changing the below list # Override automatic language detection by changing the below list
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
# TODO: Enable for javascript later # TODO: Enable for javascript later
language: ["go"] language: [ 'go']
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v3 uses: github/codeql-action/init@v2
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # If you wish to specify custom queries, you can do so here or in a config file.
@ -46,4 +46,4 @@ jobs:
# queries: ./path/to/local/query, your-org/your-repo/queries@main # queries: ./path/to/local/query, your-org/your-repo/queries@main
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3 uses: github/codeql-action/analyze@v2

View File

@ -15,33 +15,24 @@ jobs:
lint: lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Setup go
uses: actions/checkout@v4 uses: actions/setup-go@v4
with: with:
fetch-depth: 0 go-version: '^1.18'
- name: Set up Go - name: Checkout repository
uses: actions/setup-go@v5 uses: actions/checkout@v3
with:
go-version: "^1"
- name: Setup golangci-lint - name: Setup golangci-lint
uses: golangci/golangci-lint-action@v6 uses: golangci/golangci-lint-action@v3.4.0
with: with:
version: v1.61.0 version: v1.52.2
args: --verbose args: --verbose
test: test:
needs: lint needs: lint
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, macos-latest] os: [ubuntu-latest, macos-latest]
go: ["1.23", "1.24"] go: ['1.18', '1.19', '1.20']
test-tags: test-tags: ['', '-tags nomsgpack', '-tags "sonic avx"', '-tags go_json']
[
"",
"-tags nomsgpack",
'--ldflags="-checklinkname=0" -tags "sonic avx"',
"-tags go_json",
"-race",
]
include: include:
- os: ubuntu-latest - os: ubuntu-latest
go-build: ~/.cache/go-build go-build: ~/.cache/go-build
@ -55,17 +46,16 @@ jobs:
GOPROXY: https://proxy.golang.org GOPROXY: https://proxy.golang.org
steps: steps:
- name: Set up Go ${{ matrix.go }} - name: Set up Go ${{ matrix.go }}
uses: actions/setup-go@v5 uses: actions/setup-go@v4
with: with:
go-version: ${{ matrix.go }} go-version: ${{ matrix.go }}
cache: false
- name: Checkout Code - name: Checkout Code
uses: actions/checkout@v4 uses: actions/checkout@v3
with: with:
ref: ${{ github.ref }} ref: ${{ github.ref }}
- uses: actions/cache@v4 - uses: actions/cache@v3
with: with:
path: | path: |
${{ matrix.go-build }} ${{ matrix.go-build }}
@ -78,6 +68,10 @@ jobs:
run: make test run: make test
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
uses: codecov/codecov-action@v4 uses: codecov/codecov-action@v3
with: with:
flags: ${{ matrix.os }},go-${{ matrix.go }},${{ matrix.test-tags }} flags: ${{ matrix.os }},go-${{ matrix.go }},${{ matrix.test-tags }}
- name: Format
if: matrix.go-version == '1.20.x'
run: diff -u <(echo -n) <(gofmt -d .)

View File

@ -3,7 +3,7 @@ name: Goreleaser
on: on:
push: push:
tags: tags:
- "*" - '*'
permissions: permissions:
contents: write contents: write
@ -12,20 +12,23 @@ jobs:
goreleaser: goreleaser:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout -
uses: actions/checkout@v4 name: Checkout
uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Go -
uses: actions/setup-go@v5 name: Set up Go
uses: actions/setup-go@v4
with: with:
go-version: "^1" go-version: 1.20
- name: Run GoReleaser -
uses: goreleaser/goreleaser-action@v6 name: Run GoReleaser
uses: goreleaser/goreleaser-action@v4
with: with:
# either 'goreleaser' (default) or 'goreleaser-pro' # either 'goreleaser' (default) or 'goreleaser-pro'
distribution: goreleaser distribution: goreleaser
version: latest version: latest
args: release --clean args: release --rm-dist
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

4
.gitignore vendored
View File

@ -5,7 +5,3 @@ count.out
test test
profile.out profile.out
tmp.out tmp.out
# Develop tools
.idea/
.vscode/

View File

@ -3,11 +3,12 @@ run:
linters: linters:
enable: enable:
- asciicheck - asciicheck
- depguard
- dogsled - dogsled
- durationcheck - durationcheck
- errcheck - errcheck
- errorlint - errorlint
- copyloopvar - exportloopref
- gci - gci
- gofmt - gofmt
- goimports - goimports
@ -16,10 +17,7 @@ linters:
- nakedret - nakedret
- nilerr - nilerr
- nolintlint - nolintlint
- perfsprint
- revive - revive
- testifylint
- usestdlibvars
- wastedassign - wastedassign
linters-settings: linters-settings:
@ -36,14 +34,6 @@ linters-settings:
- G112 - G112
- G201 - G201
- G203 - G203
perfsprint:
err-error: true
errorf: true
int-conversion: true
sprintf1: true
strconcat: true
testifylint:
enable-all: true
issues: issues:
exclude-rules: exclude-rules:
@ -66,6 +56,3 @@ issues:
- linters: - linters:
- revive - revive
path: _test\.go path: _test\.go
- path: gin.go
linters:
- gci

View File

@ -1,7 +1,8 @@
project_name: gin project_name: gin
builds: builds:
- # If true, skip the build. -
# If true, skip the build.
# Useful for library projects. # Useful for library projects.
# Default is false # Default is false
skip: true skip: true
@ -9,7 +10,7 @@ builds:
changelog: changelog:
# Set it to true if you wish to skip the changelog generation. # Set it to true if you wish to skip the changelog generation.
# This may result in an empty release notes on GitHub/GitLab/Gitea. # This may result in an empty release notes on GitHub/GitLab/Gitea.
disable: false skip: false
# Changelog generation implementation to use. # Changelog generation implementation to use.
# #
@ -20,7 +21,7 @@ changelog:
# - `github-native`: uses the GitHub release notes generation API, disables the groups feature. # - `github-native`: uses the GitHub release notes generation API, disables the groups feature.
# #
# Defaults to `git`. # Defaults to `git`.
use: github use: git
# Sorts the changelog by the commit's messages. # Sorts the changelog by the commit's messages.
# Could either be asc, desc or empty # Could either be asc, desc or empty
@ -37,20 +38,20 @@ changelog:
- title: Features - title: Features
regexp: "^.*feat[(\\w)]*:+.*$" regexp: "^.*feat[(\\w)]*:+.*$"
order: 0 order: 0
- title: "Bug fixes" - title: 'Bug fixes'
regexp: "^.*fix[(\\w)]*:+.*$" regexp: "^.*fix[(\\w)]*:+.*$"
order: 1 order: 1
- title: "Enhancements" - title: 'Enhancements'
regexp: "^.*chore[(\\w)]*:+.*$" regexp: "^.*chore[(\\w)]*:+.*$"
order: 2 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 - title: Others
order: 999 order: 999
filters:
# Commit messages matching the regexp listed here will be removed from
# the changelog
# Default is empty
exclude:
- '^docs'
- 'CICD'
- typo

View File

@ -1,79 +1,5 @@
# Gin ChangeLog # Gin ChangeLog
## 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 ## Gin v1.9.1
### BUG FIXES ### BUG FIXES
@ -488,7 +414,7 @@
- [FIX] Refactor render - [FIX] Refactor render
- [FIX] Reworked tests - [FIX] Reworked tests
- [FIX] logger now supports cygwin - [FIX] logger now supports cygwin
- [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

View File

@ -8,7 +8,6 @@ TESTFOLDER := $(shell $(GO) list ./... | grep -E 'gin$$|binding$$|render$$' | gr
TESTTAGS ?= "" TESTTAGS ?= ""
.PHONY: test .PHONY: test
# Run tests to verify code functionality.
test: test:
echo "mode: count" > coverage.out echo "mode: count" > coverage.out
for d in $(TESTFOLDER); do \ for d in $(TESTFOLDER); do \
@ -31,12 +30,10 @@ test:
done done
.PHONY: fmt .PHONY: fmt
# Ensure consistent code formatting.
fmt: fmt:
$(GOFMT) -w $(GOFILES) $(GOFMT) -w $(GOFILES)
.PHONY: fmt-check .PHONY: fmt-check
# format (check only).
fmt-check: fmt-check:
@diff=$$($(GOFMT) -d $(GOFILES)); \ @diff=$$($(GOFMT) -d $(GOFILES)); \
if [ -n "$$diff" ]; then \ if [ -n "$$diff" ]; then \
@ -45,37 +42,31 @@ fmt-check:
exit 1; \ exit 1; \
fi; fi;
.PHONY: vet
# Examine packages and report suspicious constructs if any.
vet: vet:
$(GO) vet $(VETPACKAGES) $(GO) vet $(VETPACKAGES)
.PHONY: lint .PHONY: lint
# Inspect source code for stylistic errors or potential bugs.
lint: lint:
@hash golint > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ @hash golint > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
$(GO) get -u golang.org/x/lint/golint; \ $(GO) get -u golang.org/x/lint/golint; \
fi fi
for PKG in $(PACKAGES); do golint -set_exit_status $$PKG || exit 1; done; for PKG in $(PACKAGES); do golint -set_exit_status $$PKG || exit 1; done;
.PHONY: misspell
# Correct commonly misspelled English words in source code.
misspell:
@hash misspell > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
$(GO) get -u github.com/client9/misspell/cmd/misspell; \
fi
misspell -w $(GOFILES)
.PHONY: misspell-check .PHONY: misspell-check
# misspell (check only).
misspell-check: misspell-check:
@hash misspell > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ @hash misspell > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
$(GO) get -u github.com/client9/misspell/cmd/misspell; \ $(GO) get -u github.com/client9/misspell/cmd/misspell; \
fi fi
misspell -error $(GOFILES) misspell -error $(GOFILES)
.PHONY: misspell
misspell:
@hash misspell > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
$(GO) get -u github.com/client9/misspell/cmd/misspell; \
fi
misspell -w $(GOFILES)
.PHONY: tools .PHONY: tools
# Install tools (golint and misspell).
tools: tools:
@if [ $(GO_VERSION) -gt 15 ]; then \ @if [ $(GO_VERSION) -gt 15 ]; then \
$(GO) install golang.org/x/lint/golint@latest; \ $(GO) install golang.org/x/lint/golint@latest; \
@ -84,23 +75,3 @@ tools:
$(GO) install golang.org/x/lint/golint; \ $(GO) install golang.org/x/lint/golint; \
$(GO) install github.com/client9/misspell/cmd/misspell; \ $(GO) install github.com/client9/misspell/cmd/misspell; \
fi fi
.PHONY: help
# Help.
help:
@echo ''
@echo 'Usage:'
@echo ' make [target]'
@echo ''
@echo 'Targets:'
@awk '/^[a-zA-Z\-\0-9]+:/ { \
helpMessage = match(lastLine, /^# (.*)/); \
if (helpMessage) { \
helpCommand = substr($$1, 0, index($$1, ":")-1); \
helpMessage = substr(lastLine, RSTART + 2, RLENGTH); \
printf " - \033[36m%-20s\033[0m %s\n", helpCommand, helpMessage; \
} \
} \
{ lastLine = $$0 }' $(MAKEFILE_LIST)
.DEFAULT_GOAL := help

View File

@ -5,50 +5,52 @@
[![Build Status](https://github.com/gin-gonic/gin/workflows/Run%20Tests/badge.svg?branch=master)](https://github.com/gin-gonic/gin/actions?query=branch%3Amaster) [![Build Status](https://github.com/gin-gonic/gin/workflows/Run%20Tests/badge.svg?branch=master)](https://github.com/gin-gonic/gin/actions?query=branch%3Amaster)
[![codecov](https://codecov.io/gh/gin-gonic/gin/branch/master/graph/badge.svg)](https://codecov.io/gh/gin-gonic/gin) [![codecov](https://codecov.io/gh/gin-gonic/gin/branch/master/graph/badge.svg)](https://codecov.io/gh/gin-gonic/gin)
[![Go Report Card](https://goreportcard.com/badge/github.com/gin-gonic/gin)](https://goreportcard.com/report/github.com/gin-gonic/gin) [![Go Report Card](https://goreportcard.com/badge/github.com/gin-gonic/gin)](https://goreportcard.com/report/github.com/gin-gonic/gin)
[![Go Reference](https://pkg.go.dev/badge/github.com/gin-gonic/gin?status.svg)](https://pkg.go.dev/github.com/gin-gonic/gin?tab=doc) [![GoDoc](https://pkg.go.dev/badge/github.com/gin-gonic/gin?status.svg)](https://pkg.go.dev/github.com/gin-gonic/gin?tab=doc)
[![Sourcegraph](https://sourcegraph.com/github.com/gin-gonic/gin/-/badge.svg)](https://sourcegraph.com/github.com/gin-gonic/gin?badge) [![Sourcegraph](https://sourcegraph.com/github.com/gin-gonic/gin/-/badge.svg)](https://sourcegraph.com/github.com/gin-gonic/gin?badge)
[![Open Source Helpers](https://www.codetriage.com/gin-gonic/gin/badges/users.svg)](https://www.codetriage.com/gin-gonic/gin) [![Open Source Helpers](https://www.codetriage.com/gin-gonic/gin/badges/users.svg)](https://www.codetriage.com/gin-gonic/gin)
[![Release](https://img.shields.io/github/release/gin-gonic/gin.svg?style=flat-square)](https://github.com/gin-gonic/gin/releases) [![Release](https://img.shields.io/github/release/gin-gonic/gin.svg?style=flat-square)](https://github.com/gin-gonic/gin/releases)
[![TODOs](https://badgen.net/https/api.tickgit.com/badgen/github.com/gin-gonic/gin)](https://www.tickgit.com/browse?repo=github.com/gin-gonic/gin) [![TODOs](https://badgen.net/https/api.tickgit.com/badgen/github.com/gin-gonic/gin)](https://www.tickgit.com/browse?repo=github.com/gin-gonic/gin)
Gin is a web framework written in [Go](https://go.dev/). It features a martini-like API with performance that is up to 40 times faster thanks to [httprouter](https://github.com/julienschmidt/httprouter). Gin is a web framework written in [Go](https://go.dev/). It features a martini-like API with performance that is up to 40 times faster thanks to [httprouter](https://github.com/julienschmidt/httprouter). If you need performance and good productivity, you will love Gin.
If you need performance and good productivity, you will love Gin.
**Gin's key features are:** **The key features of Gin are:**
- Zero allocation router - Zero allocation router
- Speed - Fast
- Middleware support - Middleware support
- Crash-free - Crash-free
- JSON validation - JSON validation
- Route grouping - Routes grouping
- Error management - Error management
- Built-in rendering - Rendering built-in
- Extensible - Extendable
## Getting started ## Getting started
### Prerequisites ### Prerequisites
Gin requires [Go](https://go.dev/) version [1.23](https://go.dev/doc/devel/release#go1.23.0) or above. - **[Go](https://go.dev/)**: any one of the **three latest major** [releases](https://go.dev/doc/devel/release) (we test it with these).
### Getting Gin ### Getting Gin
With [Go's module support](https://go.dev/wiki/Modules#how-to-use-modules), `go [build|run|test]` automatically fetches the necessary dependencies when you add the import in your code: With [Go module](https://github.com/golang/go/wiki/Modules) support, simply add the following import
```sh ```
import "github.com/gin-gonic/gin" import "github.com/gin-gonic/gin"
``` ```
Alternatively, use `go get`: to your code, and then `go [build|run|test]` will automatically fetch the necessary dependencies.
Otherwise, run the following Go command to install the `gin` package:
```sh ```sh
go get -u github.com/gin-gonic/gin $ go get -u github.com/gin-gonic/gin
``` ```
### Running Gin ### Running Gin
A basic example: First you need to import Gin package for using Gin, one simplest example likes the follow `example.go`:
```go ```go
package main package main
@ -70,29 +72,29 @@ func main() {
} }
``` ```
To run the code, use the `go run` command, like: And use the Go command to run the demo:
```sh ```
go run example.go # run example.go and visit 0.0.0.0:8080/ping on browser
$ go run example.go
``` ```
Then visit [`0.0.0.0:8080/ping`](http://0.0.0.0:8080/ping) in your browser to see the response! ### Learn more examples
### See more examples
#### Quick Start #### Quick Start
Learn and practice with the [Gin Quick Start](docs/doc.md), which includes API examples and builds tag. Learn and practice more examples, please read the [Gin Quick Start](docs/doc.md) which includes API examples and builds tag.
#### Examples #### Examples
A number of ready-to-run examples demonstrating various use cases of Gin are available in the [Gin examples](https://github.com/gin-gonic/examples) repository. A number of ready-to-run examples demonstrating various use cases of Gin on the [Gin examples](https://github.com/gin-gonic/examples) repository.
## Documentation ## Documentation
See the [API documentation on go.dev](https://pkg.go.dev/github.com/gin-gonic/gin). See [API documentation and descriptions](https://godoc.org/github.com/gin-gonic/gin) for package.
The documentation is also available on [gin-gonic.com](https://gin-gonic.com) in several languages: All documentation is available on the Gin website.
- [English](https://gin-gonic.com/docs/) - [English](https://gin-gonic.com/docs/)
- [简体中文](https://gin-gonic.com/zh-cn/docs/) - [简体中文](https://gin-gonic.com/zh-cn/docs/)
@ -102,18 +104,19 @@ The documentation is also available on [gin-gonic.com](https://gin-gonic.com) in
- [한국어](https://gin-gonic.com/ko-kr/docs/) - [한국어](https://gin-gonic.com/ko-kr/docs/)
- [Turkish](https://gin-gonic.com/tr/docs/) - [Turkish](https://gin-gonic.com/tr/docs/)
- [Persian](https://gin-gonic.com/fa/docs/) - [Persian](https://gin-gonic.com/fa/docs/)
- [Português](https://gin-gonic.com/pt/docs/)
### Articles ### Articles about Gin
A curated list of awesome Gin framework.
- [Tutorial: Developing a RESTful API with Go and Gin](https://go.dev/doc/tutorial/web-service-gin) - [Tutorial: Developing a RESTful API with Go and Gin](https://go.dev/doc/tutorial/web-service-gin)
## Benchmarks ## Benchmarks
Gin uses a custom version of [HttpRouter](https://github.com/julienschmidt/httprouter), [see all benchmarks](/BENCHMARKS.md). Gin uses a custom version of [HttpRouter](https://github.com/julienschmidt/httprouter), [see all benchmarks details](/BENCHMARKS.md).
| Benchmark name | (1) | (2) | (3) | (4) | | Benchmark name | (1) | (2) | (3) | (4) |
| ------------------------------ | --------: | --------------: | -----------: | --------------: | | ------------------------------ | ---------:| ---------------:| ------------:| ---------------:|
| BenchmarkGin_GithubAll | **43550** | **27364 ns/op** | **0 B/op** | **0 allocs/op** | | BenchmarkGin_GithubAll | **43550** | **27364 ns/op** | **0 B/op** | **0 allocs/op** |
| BenchmarkAce_GithubAll | 40543 | 29670 ns/op | 0 B/op | 0 allocs/op | | BenchmarkAce_GithubAll | 40543 | 29670 ns/op | 0 B/op | 0 allocs/op |
| BenchmarkAero_GithubAll | 57632 | 20648 ns/op | 0 B/op | 0 allocs/op | | BenchmarkAero_GithubAll | 57632 | 20648 ns/op | 0 B/op | 0 allocs/op |
@ -150,23 +153,26 @@ Gin uses a custom version of [HttpRouter](https://github.com/julienschmidt/httpr
- (3): Heap Memory (B/op), lower is better - (3): Heap Memory (B/op), lower is better
- (4): Average Allocations per Repetition (allocs/op), lower is better - (4): Average Allocations per Repetition (allocs/op), lower is better
## Middleware
## Middlewares
You can find many useful Gin middlewares at [gin-contrib](https://github.com/gin-contrib). You can find many useful Gin middlewares at [gin-contrib](https://github.com/gin-contrib).
## Uses
Here are some awesome projects that are using the [Gin](https://github.com/gin-gonic/gin) web framework. ## Users
Awesome project lists using [Gin](https://github.com/gin-gonic/gin) web framework.
* [gorush](https://github.com/appleboy/gorush): A push notification server written in Go.
* [fnproject](https://github.com/fnproject/fn): The container native, cloud agnostic serverless platform.
* [photoprism](https://github.com/photoprism/photoprism): Personal photo management powered by Go and Google TensorFlow.
* [lura](https://github.com/luraproject/lura): Ultra performant API Gateway with middlewares.
* [picfit](https://github.com/thoas/picfit): An image resizing server written in Go.
* [dkron](https://github.com/distribworks/dkron): Distributed, fault tolerant job scheduling system.
- [gorush](https://github.com/appleboy/gorush): A push notification server.
- [fnproject](https://github.com/fnproject/fn): A container native, cloud agnostic serverless platform.
- [photoprism](https://github.com/photoprism/photoprism): Personal photo management powered by Google TensorFlow.
- [lura](https://github.com/luraproject/lura): Ultra performant API Gateway with middleware.
- [picfit](https://github.com/thoas/picfit): An image resizing server.
- [dkron](https://github.com/distribworks/dkron): Distributed, fault tolerant job scheduling system.
## Contributing ## Contributing
Gin is the work of hundreds of contributors. We appreciate your help! Gin is the work of hundreds of contributors. We appreciate your help!
Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details on submitting patches and the contribution workflow. Please see [CONTRIBUTING](CONTRIBUTING.md) for details on submitting patches and the contribution workflow.

25
auth.go
View File

@ -16,9 +16,6 @@ import (
// 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
@ -92,25 +89,3 @@ func authorizationHeader(user, password string) string {
base := user + ":" + password base := user + ":" + password
return "Basic " + base64.StdEncoding.EncodeToString(bytesconv.StringToBytes(base)) return "Basic " + base64.StdEncoding.EncodeToString(bytesconv.StringToBytes(base))
} }
// BasicAuthForProxy returns a Basic HTTP Proxy-Authorization middleware.
// If the realm is empty, "Proxy Authorization Required" will be used by default.
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)
}
}

View File

@ -90,7 +90,7 @@ func TestBasicAuthSucceed(t *testing.T) {
}) })
w := httptest.NewRecorder() w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/login", nil) req, _ := http.NewRequest("GET", "/login", nil)
req.Header.Set("Authorization", authorizationHeader("admin", "password")) req.Header.Set("Authorization", authorizationHeader("admin", "password"))
router.ServeHTTP(w, req) router.ServeHTTP(w, req)
@ -109,7 +109,7 @@ func TestBasicAuth401(t *testing.T) {
}) })
w := httptest.NewRecorder() w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/login", nil) req, _ := http.NewRequest("GET", "/login", nil)
req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:password"))) req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:password")))
router.ServeHTTP(w, req) router.ServeHTTP(w, req)
@ -129,7 +129,7 @@ func TestBasicAuth401WithCustomRealm(t *testing.T) {
}) })
w := httptest.NewRecorder() w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/login", nil) req, _ := http.NewRequest("GET", "/login", nil)
req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:password"))) req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:password")))
router.ServeHTTP(w, req) router.ServeHTTP(w, req)
@ -137,40 +137,3 @@ 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(http.MethodGet, "/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(http.MethodGet, "/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"))
}

View File

@ -14,21 +14,21 @@ import (
func BenchmarkOneRoute(B *testing.B) { func BenchmarkOneRoute(B *testing.B) {
router := New() router := New()
router.GET("/ping", func(c *Context) {}) router.GET("/ping", func(c *Context) {})
runRequest(B, router, http.MethodGet, "/ping") runRequest(B, router, "GET", "/ping")
} }
func BenchmarkRecoveryMiddleware(B *testing.B) { func BenchmarkRecoveryMiddleware(B *testing.B) {
router := New() router := New()
router.Use(Recovery()) router.Use(Recovery())
router.GET("/", func(c *Context) {}) router.GET("/", func(c *Context) {})
runRequest(B, router, http.MethodGet, "/") runRequest(B, router, "GET", "/")
} }
func BenchmarkLoggerMiddleware(B *testing.B) { func BenchmarkLoggerMiddleware(B *testing.B) {
router := New() router := New()
router.Use(LoggerWithWriter(newMockWriter())) router.Use(LoggerWithWriter(newMockWriter()))
router.GET("/", func(c *Context) {}) router.GET("/", func(c *Context) {})
runRequest(B, router, http.MethodGet, "/") runRequest(B, router, "GET", "/")
} }
func BenchmarkManyHandlers(B *testing.B) { func BenchmarkManyHandlers(B *testing.B) {
@ -37,7 +37,7 @@ func BenchmarkManyHandlers(B *testing.B) {
router.Use(func(c *Context) {}) router.Use(func(c *Context) {})
router.Use(func(c *Context) {}) router.Use(func(c *Context) {})
router.GET("/ping", func(c *Context) {}) router.GET("/ping", func(c *Context) {})
runRequest(B, router, http.MethodGet, "/ping") runRequest(B, router, "GET", "/ping")
} }
func Benchmark5Params(B *testing.B) { func Benchmark5Params(B *testing.B) {
@ -45,7 +45,7 @@ func Benchmark5Params(B *testing.B) {
router := New() router := New()
router.Use(func(c *Context) {}) router.Use(func(c *Context) {})
router.GET("/param/:param1/:params2/:param3/:param4/:param5", func(c *Context) {}) router.GET("/param/:param1/:params2/:param3/:param4/:param5", func(c *Context) {})
runRequest(B, router, http.MethodGet, "/param/path/to/parameter/john/12345") runRequest(B, router, "GET", "/param/path/to/parameter/john/12345")
} }
func BenchmarkOneRouteJSON(B *testing.B) { func BenchmarkOneRouteJSON(B *testing.B) {
@ -56,7 +56,7 @@ func BenchmarkOneRouteJSON(B *testing.B) {
router.GET("/json", func(c *Context) { router.GET("/json", func(c *Context) {
c.JSON(http.StatusOK, data) c.JSON(http.StatusOK, data)
}) })
runRequest(B, router, http.MethodGet, "/json") runRequest(B, router, "GET", "/json")
} }
func BenchmarkOneRouteHTML(B *testing.B) { func BenchmarkOneRouteHTML(B *testing.B) {
@ -68,7 +68,7 @@ func BenchmarkOneRouteHTML(B *testing.B) {
router.GET("/html", func(c *Context) { router.GET("/html", func(c *Context) {
c.HTML(http.StatusOK, "index", "hola") c.HTML(http.StatusOK, "index", "hola")
}) })
runRequest(B, router, http.MethodGet, "/html") runRequest(B, router, "GET", "/html")
} }
func BenchmarkOneRouteSet(B *testing.B) { func BenchmarkOneRouteSet(B *testing.B) {
@ -76,7 +76,7 @@ func BenchmarkOneRouteSet(B *testing.B) {
router.GET("/ping", func(c *Context) { router.GET("/ping", func(c *Context) {
c.Set("key", "value") c.Set("key", "value")
}) })
runRequest(B, router, http.MethodGet, "/ping") runRequest(B, router, "GET", "/ping")
} }
func BenchmarkOneRouteString(B *testing.B) { func BenchmarkOneRouteString(B *testing.B) {
@ -84,13 +84,13 @@ func BenchmarkOneRouteString(B *testing.B) {
router.GET("/text", func(c *Context) { router.GET("/text", func(c *Context) {
c.String(http.StatusOK, "this is a plain text") c.String(http.StatusOK, "this is a plain text")
}) })
runRequest(B, router, http.MethodGet, "/text") runRequest(B, router, "GET", "/text")
} }
func BenchmarkManyRoutesFist(B *testing.B) { func BenchmarkManyRoutesFist(B *testing.B) {
router := New() router := New()
router.Any("/ping", func(c *Context) {}) router.Any("/ping", func(c *Context) {})
runRequest(B, router, http.MethodGet, "/ping") runRequest(B, router, "GET", "/ping")
} }
func BenchmarkManyRoutesLast(B *testing.B) { func BenchmarkManyRoutesLast(B *testing.B) {
@ -103,7 +103,7 @@ func Benchmark404(B *testing.B) {
router := New() router := New()
router.Any("/something", func(c *Context) {}) router.Any("/something", func(c *Context) {})
router.NoRoute(func(c *Context) {}) router.NoRoute(func(c *Context) {})
runRequest(B, router, http.MethodGet, "/ping") runRequest(B, router, "GET", "/ping")
} }
func Benchmark404Many(B *testing.B) { func Benchmark404Many(B *testing.B) {
@ -118,7 +118,7 @@ func Benchmark404Many(B *testing.B) {
router.GET("/user/:id/:mode", func(c *Context) {}) router.GET("/user/:id/:mode", func(c *Context) {})
router.NoRoute(func(c *Context) {}) router.NoRoute(func(c *Context) {})
runRequest(B, router, http.MethodGet, "/viewfake") runRequest(B, router, "GET", "/viewfake")
} }
type mockWriter struct { type mockWriter struct {

View File

@ -21,7 +21,6 @@ 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" MIMETOML = "application/toml"
) )
@ -73,19 +72,18 @@ 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 BindingBody = jsonBinding{} JSON = jsonBinding{}
XML BindingBody = xmlBinding{} XML = xmlBinding{}
Form Binding = formBinding{} Form = formBinding{}
Query Binding = queryBinding{} Query = queryBinding{}
FormPost Binding = formPostBinding{} FormPost = formPostBinding{}
FormMultipart Binding = formMultipartBinding{} FormMultipart = formMultipartBinding{}
ProtoBuf BindingBody = protobufBinding{} ProtoBuf = protobufBinding{}
MsgPack BindingBody = msgpackBinding{} MsgPack = msgpackBinding{}
YAML BindingBody = yamlBinding{} YAML = yamlBinding{}
Uri BindingUri = uriBinding{} Uri = uriBinding{}
Header Binding = headerBinding{} Header = headerBinding{}
Plain BindingBody = plainBinding{} TOML = tomlBinding{}
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
@ -104,7 +102,7 @@ func Default(method, contentType string) Binding {
return ProtoBuf return ProtoBuf
case MIMEMSGPACK, MIMEMSGPACK2: case MIMEMSGPACK, MIMEMSGPACK2:
return MsgPack return MsgPack
case MIMEYAML, MIMEYAML2: case MIMEYAML:
return YAML return YAML
case MIMETOML: case MIMETOML:
return TOML return TOML

View File

@ -8,11 +8,9 @@ package binding
import ( import (
"bytes" "bytes"
"net/http"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ugorji/go/codec" "github.com/ugorji/go/codec"
) )
@ -26,7 +24,7 @@ func TestBindingMsgPack(t *testing.T) {
buf := bytes.NewBuffer([]byte{}) buf := bytes.NewBuffer([]byte{})
assert.NotNil(t, buf) assert.NotNil(t, buf)
err := codec.NewEncoder(buf, h).Encode(test) err := codec.NewEncoder(buf, h).Encode(test)
require.NoError(t, err) assert.NoError(t, err)
data := buf.Bytes() data := buf.Bytes()
@ -40,20 +38,20 @@ func testMsgPackBodyBinding(t *testing.T, b Binding, name, path, badPath, body,
assert.Equal(t, name, b.Name()) assert.Equal(t, name, b.Name())
obj := FooStruct{} obj := FooStruct{}
req := requestWithBody(http.MethodPost, path, body) req := requestWithBody("POST", path, body)
req.Header.Add("Content-Type", MIMEMSGPACK) req.Header.Add("Content-Type", MIMEMSGPACK)
err := b.Bind(req, &obj) err := b.Bind(req, &obj)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "bar", obj.Foo) assert.Equal(t, "bar", obj.Foo)
obj = FooStruct{} obj = FooStruct{}
req = requestWithBody(http.MethodPost, badPath, badBody) req = requestWithBody("POST", badPath, badBody)
req.Header.Add("Content-Type", MIMEMSGPACK) req.Header.Add("Content-Type", MIMEMSGPACK)
err = MsgPack.Bind(req, &obj) err = MsgPack.Bind(req, &obj)
require.Error(t, err) assert.Error(t, err)
} }
func TestBindingDefaultMsgPack(t *testing.T) { func TestBindingDefaultMsgPack(t *testing.T) {
assert.Equal(t, MsgPack, Default(http.MethodPost, MIMEMSGPACK)) assert.Equal(t, MsgPack, Default("POST", MIMEMSGPACK))
assert.Equal(t, MsgPack, Default(http.MethodPut, MIMEMSGPACK2)) assert.Equal(t, MsgPack, Default("PUT", MIMEMSGPACK2))
} }

View File

@ -19,7 +19,6 @@ const (
MIMEMultipartPOSTForm = "multipart/form-data" MIMEMultipartPOSTForm = "multipart/form-data"
MIMEPROTOBUF = "application/x-protobuf" MIMEPROTOBUF = "application/x-protobuf"
MIMEYAML = "application/x-yaml" MIMEYAML = "application/x-yaml"
MIMEYAML2 = "application/yaml"
MIMETOML = "application/toml" MIMETOML = "application/toml"
) )
@ -81,7 +80,6 @@ var (
Uri = uriBinding{} Uri = uriBinding{}
Header = headerBinding{} Header = headerBinding{}
TOML = tomlBinding{} TOML = tomlBinding{}
Plain = plainBinding{}
) )
// Default returns the appropriate Binding instance based on the HTTP method // Default returns the appropriate Binding instance based on the HTTP method
@ -98,7 +96,7 @@ func Default(method, contentType string) Binding {
return XML return XML
case MIMEPROTOBUF: case MIMEPROTOBUF:
return ProtoBuf return ProtoBuf
case MIMEYAML, MIMEYAML2: case MIMEYAML:
return YAML return YAML
case MIMEMultipartPOSTForm: case MIMEMultipartPOSTForm:
return FormMultipart return FormMultipart

File diff suppressed because it is too large Load Diff

View File

@ -5,8 +5,8 @@
package binding package binding
import ( import (
"fmt"
"reflect" "reflect"
"strconv"
"strings" "strings"
"sync" "sync"
@ -22,21 +22,26 @@ type SliceValidationError []error
// Error concatenates all error elements in SliceValidationError into a single string separated by \n. // Error concatenates all error elements in SliceValidationError into a single string separated by \n.
func (err SliceValidationError) Error() string { func (err SliceValidationError) Error() string {
if len(err) == 0 { n := len(err)
switch n {
case 0:
return "" return ""
} default:
var b strings.Builder var b strings.Builder
for i := 0; i < len(err); i++ { if err[0] != nil {
if err[i] != nil { fmt.Fprintf(&b, "[%d]: %s", 0, err[0].Error())
if b.Len() > 0 { }
b.WriteString("\n") 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())
} }
b.WriteString("[" + strconv.Itoa(i) + "]: " + err[i].Error())
} }
} }
return b.String() return b.String()
} }
}
var _ StructValidator = (*defaultValidator)(nil) var _ StructValidator = (*defaultValidator)(nil)
@ -49,10 +54,7 @@ func (v *defaultValidator) ValidateStruct(obj any) error {
value := reflect.ValueOf(obj) value := reflect.ValueOf(obj)
switch value.Kind() { switch value.Kind() {
case reflect.Ptr: case reflect.Ptr:
if value.Elem().Kind() != reflect.Struct {
return v.ValidateStruct(value.Elem().Interface()) return v.ValidateStruct(value.Elem().Interface())
}
return v.validateStruct(obj)
case reflect.Struct: case reflect.Struct:
return v.validateStruct(obj) return v.validateStruct(obj)
case reflect.Slice, reflect.Array: case reflect.Slice, reflect.Array:

View File

@ -12,15 +12,11 @@ import (
func BenchmarkSliceValidationError(b *testing.B) { func BenchmarkSliceValidationError(b *testing.B) {
const size int = 100 const size int = 100
for i := 0; i < b.N; i++ {
e := make(SliceValidationError, size) e := make(SliceValidationError, size)
for j := 0; j < size; j++ { for j := 0; j < size; j++ {
e[j] = errors.New(strconv.Itoa(j)) e[j] = errors.New(strconv.Itoa(j))
} }
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
if len(e.Error()) == 0 { if len(e.Error()) == 0 {
b.Errorf("error") b.Errorf("error")
} }

View File

@ -7,7 +7,6 @@ package binding
import ( import (
"errors" "errors"
"fmt" "fmt"
"mime/multipart"
"reflect" "reflect"
"strconv" "strconv"
"strings" "strings"
@ -159,69 +158,12 @@ func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter
if k, v := head(opt, "="); k == "default" { if k, v := head(opt, "="); k == "default" {
setOpt.isDefaultExists = true setOpt.isDefaultExists = true
setOpt.defaultValue = v setOpt.defaultValue = v
// convert semicolon-separated default values to csv-separated values for processing in setByForm
if field.Type.Kind() == reflect.Slice || field.Type.Kind() == reflect.Array {
cfTag := field.Tag.Get("collection_format")
if cfTag == "" || cfTag == "multi" || cfTag == "csv" {
setOpt.defaultValue = strings.ReplaceAll(v, ";", ",")
}
}
} }
} }
return setter.TrySet(value, field, tagValue, setOpt) return setter.TrySet(value, field, tagValue, setOpt)
} }
// 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 trySplit(vs []string, field reflect.StructField) (newVs []string, err error) {
cfTag := field.Tag.Get("collection_format")
if cfTag == "" || cfTag == "multi" {
return vs, nil
}
var sep string
switch cfTag {
case "csv":
sep = ","
case "ssv":
sep = " "
case "tsv":
sep = "\t"
case "pipes":
sep = "|"
default:
return vs, fmt.Errorf("%s is not supported in the collection_format. (csv, ssv, pipes)", cfTag)
}
totalLength := 0
for _, v := range vs {
totalLength += strings.Count(v, sep) + 1
}
newVs = make([]string, 0, totalLength)
for _, v := range vs {
newVs = append(newVs, strings.Split(v, sep)...)
}
return newVs, nil
}
func setByForm(value reflect.Value, field reflect.StructField, form map[string][]string, tagValue string, opt setOptions) (isSet bool, err error) { 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 {
@ -232,46 +174,15 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
case reflect.Slice: case reflect.Slice:
if !ok { if !ok {
vs = []string{opt.defaultValue} vs = []string{opt.defaultValue}
// pre-process the default value for multi if present
cfTag := field.Tag.Get("collection_format")
if cfTag == "" || cfTag == "multi" {
vs = strings.Split(opt.defaultValue, ",")
} }
}
if ok, err = trySetCustom(vs[0], value); ok {
return ok, err
}
if vs, err = trySplit(vs, field); err != nil {
return false, err
}
return true, setSlice(vs, value, field) return true, setSlice(vs, value, field)
case reflect.Array: case reflect.Array:
if !ok { if !ok {
vs = []string{opt.defaultValue} vs = []string{opt.defaultValue}
// pre-process the default value for multi if present
cfTag := field.Tag.Get("collection_format")
if cfTag == "" || cfTag == "multi" {
vs = strings.Split(opt.defaultValue, ",")
} }
}
if ok, err = trySetCustom(vs[0], value); ok {
return ok, err
}
if vs, err = trySplit(vs, field); err != nil {
return false, err
}
if len(vs) != value.Len() { if len(vs) != value.Len() {
return false, fmt.Errorf("%q is not valid value for %s", vs, value.Type().String()) return false, fmt.Errorf("%q is not valid value for %s", vs, value.Type().String())
} }
return true, setArray(vs, value, field) return true, setArray(vs, value, field)
default: default:
var val string var val string
@ -281,12 +192,6 @@ 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 val == "" {
val = opt.defaultValue
}
}
if ok, err := trySetCustom(val, value); ok {
return ok, err
} }
return true, setWithProperType(val, value, field) return true, setWithProperType(val, value, field)
} }
@ -330,17 +235,10 @@ 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(bytesconv.StringToBytes(val), value.Addr().Interface()) return json.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
case reflect.Map: case reflect.Map:
return json.Unmarshal(bytesconv.StringToBytes(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
} }
@ -398,24 +296,18 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val
} }
switch tf := strings.ToLower(timeFormat); tf { switch tf := strings.ToLower(timeFormat); tf {
case "unix", "unixmilli", "unixmicro", "unixnano": case "unix", "unixnano":
tv, err := strconv.ParseInt(val, 10, 64) tv, err := strconv.ParseInt(val, 10, 64)
if err != nil { if err != nil {
return err return err
} }
var t time.Time d := time.Duration(1)
switch tf { if tf == "unixnano" {
case "unix": d = time.Second
t = time.Unix(tv, 0)
case "unixmilli":
t = time.UnixMilli(tv)
case "unixmicro":
t = time.UnixMicro(tv)
default:
t = time.Unix(0, tv)
} }
t := time.Unix(tv/int64(d), tv%int64(d))
value.Set(reflect.ValueOf(t)) value.Set(reflect.ValueOf(t))
return nil return nil
} }
@ -477,8 +369,11 @@ func setTimeDuration(val string, value reflect.Value) error {
} }
func head(str, sep string) (head string, tail string) { func head(str, sep string) (head string, tail string) {
head, tail, _ = strings.Cut(str, sep) idx := strings.Index(str, sep)
return head, tail if idx < 0 {
return str, ""
}
return str[:idx], str[idx+len(sep):]
} }
func setFormMap(ptr any, form map[string][]string) error { func setFormMap(ptr any, form map[string][]string) error {

View File

@ -5,17 +5,11 @@
package binding package binding
import ( import (
"encoding/hex"
"errors"
"mime/multipart"
"reflect" "reflect"
"strconv"
"strings"
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestMappingBaseTypes(t *testing.T) { func TestMappingBaseTypes(t *testing.T) {
@ -49,7 +43,6 @@ 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()
@ -60,7 +53,7 @@ func TestMappingBaseTypes(t *testing.T) {
field := val.Elem().Type().Field(0) field := val.Elem().Type().Field(0)
_, err := mapping(val, emptyField, formSource{field.Name: {tt.form}}, "form") _, err := mapping(val, emptyField, formSource{field.Name: {tt.form}}, "form")
require.NoError(t, err, testName) assert.NoError(t, err, testName)
actual := val.Elem().Field(0).Interface() actual := val.Elem().Field(0).Interface()
assert.Equal(t, tt.expect, actual, testName) assert.Equal(t, tt.expect, actual, testName)
@ -69,15 +62,13 @@ func TestMappingBaseTypes(t *testing.T) {
func TestMappingDefault(t *testing.T) { func TestMappingDefault(t *testing.T) {
var s struct { var s struct {
Str string `form:",default=defaultVal"`
Int int `form:",default=9"` Int int `form:",default=9"`
Slice []int `form:",default=9"` Slice []int `form:",default=9"`
Array [1]int `form:",default=9"` Array [1]int `form:",default=9"`
} }
err := mappingByPtr(&s, formSource{}, "form") err := mappingByPtr(&s, formSource{}, "form")
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "defaultVal", s.Str)
assert.Equal(t, 9, s.Int) assert.Equal(t, 9, s.Int)
assert.Equal(t, []int{9}, s.Slice) assert.Equal(t, []int{9}, s.Slice)
assert.Equal(t, [1]int{9}, s.Array) assert.Equal(t, [1]int{9}, s.Array)
@ -88,7 +79,7 @@ func TestMappingSkipField(t *testing.T) {
A int A int
} }
err := mappingByPtr(&s, formSource{}, "form") err := mappingByPtr(&s, formSource{}, "form")
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, 0, s.A) assert.Equal(t, 0, s.A)
} }
@ -99,7 +90,7 @@ func TestMappingIgnoreField(t *testing.T) {
B int `form:"-"` B int `form:"-"`
} }
err := mappingByPtr(&s, formSource{"A": {"9"}, "B": {"9"}}, "form") err := mappingByPtr(&s, formSource{"A": {"9"}, "B": {"9"}}, "form")
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, 9, s.A) assert.Equal(t, 9, s.A)
assert.Equal(t, 0, s.B) assert.Equal(t, 0, s.B)
@ -111,7 +102,7 @@ func TestMappingUnexportedField(t *testing.T) {
b int `form:"b"` b int `form:"b"`
} }
err := mappingByPtr(&s, formSource{"a": {"9"}, "b": {"9"}}, "form") err := mappingByPtr(&s, formSource{"a": {"9"}, "b": {"9"}}, "form")
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, 9, s.A) assert.Equal(t, 9, s.A)
assert.Equal(t, 0, s.b) assert.Equal(t, 0, s.b)
@ -122,7 +113,7 @@ func TestMappingPrivateField(t *testing.T) {
f int `form:"field"` f int `form:"field"`
} }
err := mappingByPtr(&s, formSource{"field": {"6"}}, "form") err := mappingByPtr(&s, formSource{"field": {"6"}}, "form")
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, 0, s.f) assert.Equal(t, 0, s.f)
} }
@ -132,7 +123,7 @@ func TestMappingUnknownFieldType(t *testing.T) {
} }
err := mappingByPtr(&s, formSource{"U": {"unknown"}}, "form") err := mappingByPtr(&s, formSource{"U": {"unknown"}}, "form")
require.Error(t, err) assert.Error(t, err)
assert.Equal(t, errUnknownType, err) assert.Equal(t, errUnknownType, err)
} }
@ -141,7 +132,7 @@ func TestMappingURI(t *testing.T) {
F int `uri:"field"` F int `uri:"field"`
} }
err := mapURI(&s, map[string][]string{"field": {"6"}}) err := mapURI(&s, map[string][]string{"field": {"6"}})
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, 6, s.F) assert.Equal(t, 6, s.F)
} }
@ -150,34 +141,16 @@ func TestMappingForm(t *testing.T) {
F int `form:"field"` F int `form:"field"`
} }
err := mapForm(&s, map[string][]string{"field": {"6"}}) err := mapForm(&s, map[string][]string{"field": {"6"}})
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, 6, s.F) assert.Equal(t, 6, s.F)
} }
func TestMappingFormFieldNotSent(t *testing.T) {
var s struct {
F string `form:"field,default=defVal"`
}
err := mapForm(&s, map[string][]string{})
require.NoError(t, err)
assert.Equal(t, "defVal", s.F)
}
func TestMappingFormWithEmptyToDefault(t *testing.T) {
var s struct {
F string `form:"field,default=DefVal"`
}
err := mapForm(&s, map[string][]string{"field": {""}})
require.NoError(t, err)
assert.Equal(t, "DefVal", s.F)
}
func TestMapFormWithTag(t *testing.T) { func TestMapFormWithTag(t *testing.T) {
var s struct { var s struct {
F int `externalTag:"field"` F int `externalTag:"field"`
} }
err := MapFormWithTag(&s, map[string][]string{"field": {"6"}}, "externalTag") err := MapFormWithTag(&s, map[string][]string{"field": {"6"}}, "externalTag")
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, 6, s.F) assert.Equal(t, 6, s.F)
} }
@ -192,7 +165,7 @@ func TestMappingTime(t *testing.T) {
var err error var err error
time.Local, err = time.LoadLocation("Europe/Berlin") time.Local, err = time.LoadLocation("Europe/Berlin")
require.NoError(t, err) assert.NoError(t, err)
err = mapForm(&s, map[string][]string{ err = mapForm(&s, map[string][]string{
"Time": {"2019-01-20T16:02:58Z"}, "Time": {"2019-01-20T16:02:58Z"},
@ -201,7 +174,7 @@ func TestMappingTime(t *testing.T) {
"CSTTime": {"2019-01-20"}, "CSTTime": {"2019-01-20"},
"UTCTime": {"2019-01-20"}, "UTCTime": {"2019-01-20"},
}) })
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "2019-01-20 16:02:58 +0000 UTC", s.Time.String()) assert.Equal(t, "2019-01-20 16:02:58 +0000 UTC", s.Time.String())
assert.Equal(t, "2019-01-20 00:00:00 +0100 CET", s.LocalTime.String()) assert.Equal(t, "2019-01-20 00:00:00 +0100 CET", s.LocalTime.String())
@ -216,14 +189,14 @@ func TestMappingTime(t *testing.T) {
Time time.Time `time_location:"wrong"` Time time.Time `time_location:"wrong"`
} }
err = mapForm(&wrongLoc, map[string][]string{"Time": {"2019-01-20T16:02:58Z"}}) err = mapForm(&wrongLoc, map[string][]string{"Time": {"2019-01-20T16:02:58Z"}})
require.Error(t, err) assert.Error(t, err)
// wrong time value // wrong time value
var wrongTime struct { var wrongTime struct {
Time time.Time Time time.Time
} }
err = mapForm(&wrongTime, map[string][]string{"Time": {"wrong"}}) err = mapForm(&wrongTime, map[string][]string{"Time": {"wrong"}})
require.Error(t, err) assert.Error(t, err)
} }
func TestMappingTimeDuration(t *testing.T) { func TestMappingTimeDuration(t *testing.T) {
@ -233,12 +206,12 @@ func TestMappingTimeDuration(t *testing.T) {
// ok // ok
err := mappingByPtr(&s, formSource{"D": {"5s"}}, "form") err := mappingByPtr(&s, formSource{"D": {"5s"}}, "form")
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, 5*time.Second, s.D) assert.Equal(t, 5*time.Second, s.D)
// error // error
err = mappingByPtr(&s, formSource{"D": {"wrong"}}, "form") err = mappingByPtr(&s, formSource{"D": {"wrong"}}, "form")
require.Error(t, err) assert.Error(t, err)
} }
func TestMappingSlice(t *testing.T) { func TestMappingSlice(t *testing.T) {
@ -248,17 +221,17 @@ func TestMappingSlice(t *testing.T) {
// default value // default value
err := mappingByPtr(&s, formSource{}, "form") err := mappingByPtr(&s, formSource{}, "form")
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, []int{9}, s.Slice) assert.Equal(t, []int{9}, s.Slice)
// ok // ok
err = mappingByPtr(&s, formSource{"slice": {"3", "4"}}, "form") err = mappingByPtr(&s, formSource{"slice": {"3", "4"}}, "form")
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, []int{3, 4}, s.Slice) assert.Equal(t, []int{3, 4}, s.Slice)
// error // error
err = mappingByPtr(&s, formSource{"slice": {"wrong"}}, "form") err = mappingByPtr(&s, formSource{"slice": {"wrong"}}, "form")
require.Error(t, err) assert.Error(t, err)
} }
func TestMappingArray(t *testing.T) { func TestMappingArray(t *testing.T) {
@ -268,125 +241,20 @@ func TestMappingArray(t *testing.T) {
// wrong default // wrong default
err := mappingByPtr(&s, formSource{}, "form") err := mappingByPtr(&s, formSource{}, "form")
require.Error(t, err) assert.Error(t, err)
// ok // ok
err = mappingByPtr(&s, formSource{"array": {"3", "4"}}, "form") err = mappingByPtr(&s, formSource{"array": {"3", "4"}}, "form")
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, [2]int{3, 4}, s.Array) assert.Equal(t, [2]int{3, 4}, s.Array)
// error - not enough vals // error - not enough vals
err = mappingByPtr(&s, formSource{"array": {"3"}}, "form") err = mappingByPtr(&s, formSource{"array": {"3"}}, "form")
require.Error(t, err) assert.Error(t, err)
// error - wrong value // error - wrong value
err = mappingByPtr(&s, formSource{"array": {"wrong"}}, "form") err = mappingByPtr(&s, formSource{"array": {"wrong"}}, "form")
require.Error(t, err) assert.Error(t, err)
}
func TestMappingCollectionFormat(t *testing.T) {
var s struct {
SliceMulti []int `form:"slice_multi" collection_format:"multi"`
SliceCsv []int `form:"slice_csv" collection_format:"csv"`
SliceSsv []int `form:"slice_ssv" collection_format:"ssv"`
SliceTsv []int `form:"slice_tsv" collection_format:"tsv"`
SlicePipes []int `form:"slice_pipes" collection_format:"pipes"`
ArrayMulti [2]int `form:"array_multi" collection_format:"multi"`
ArrayCsv [2]int `form:"array_csv" collection_format:"csv"`
ArraySsv [2]int `form:"array_ssv" collection_format:"ssv"`
ArrayTsv [2]int `form:"array_tsv" collection_format:"tsv"`
ArrayPipes [2]int `form:"array_pipes" collection_format:"pipes"`
}
err := mappingByPtr(&s, formSource{
"slice_multi": {"1", "2"},
"slice_csv": {"1,2"},
"slice_ssv": {"1 2"},
"slice_tsv": {"1 2"},
"slice_pipes": {"1|2"},
"array_multi": {"1", "2"},
"array_csv": {"1,2"},
"array_ssv": {"1 2"},
"array_tsv": {"1 2"},
"array_pipes": {"1|2"},
}, "form")
require.NoError(t, err)
assert.Equal(t, []int{1, 2}, s.SliceMulti)
assert.Equal(t, []int{1, 2}, s.SliceCsv)
assert.Equal(t, []int{1, 2}, s.SliceSsv)
assert.Equal(t, []int{1, 2}, s.SliceTsv)
assert.Equal(t, []int{1, 2}, s.SlicePipes)
assert.Equal(t, [2]int{1, 2}, s.ArrayMulti)
assert.Equal(t, [2]int{1, 2}, s.ArrayCsv)
assert.Equal(t, [2]int{1, 2}, s.ArraySsv)
assert.Equal(t, [2]int{1, 2}, s.ArrayTsv)
assert.Equal(t, [2]int{1, 2}, s.ArrayPipes)
}
func TestMappingCollectionFormatInvalid(t *testing.T) {
var s struct {
SliceCsv []int `form:"slice_csv" collection_format:"xxx"`
}
err := mappingByPtr(&s, formSource{
"slice_csv": {"1,2"},
}, "form")
require.Error(t, err)
var s2 struct {
ArrayCsv [2]int `form:"array_csv" collection_format:"xxx"`
}
err = mappingByPtr(&s2, formSource{
"array_csv": {"1,2"},
}, "form")
require.Error(t, err)
}
func TestMappingMultipleDefaultWithCollectionFormat(t *testing.T) {
var s struct {
SliceMulti []int `form:",default=1;2;3" collection_format:"multi"`
SliceCsv []int `form:",default=1;2;3" collection_format:"csv"`
SliceSsv []int `form:",default=1 2 3" collection_format:"ssv"`
SliceTsv []int `form:",default=1\t2\t3" collection_format:"tsv"`
SlicePipes []int `form:",default=1|2|3" collection_format:"pipes"`
ArrayMulti [2]int `form:",default=1;2" collection_format:"multi"`
ArrayCsv [2]int `form:",default=1;2" collection_format:"csv"`
ArraySsv [2]int `form:",default=1 2" collection_format:"ssv"`
ArrayTsv [2]int `form:",default=1\t2" collection_format:"tsv"`
ArrayPipes [2]int `form:",default=1|2" collection_format:"pipes"`
SliceStringMulti []string `form:",default=1;2;3" collection_format:"multi"`
SliceStringCsv []string `form:",default=1;2;3" collection_format:"csv"`
SliceStringSsv []string `form:",default=1 2 3" collection_format:"ssv"`
SliceStringTsv []string `form:",default=1\t2\t3" collection_format:"tsv"`
SliceStringPipes []string `form:",default=1|2|3" collection_format:"pipes"`
ArrayStringMulti [2]string `form:",default=1;2" collection_format:"multi"`
ArrayStringCsv [2]string `form:",default=1;2" collection_format:"csv"`
ArrayStringSsv [2]string `form:",default=1 2" collection_format:"ssv"`
ArrayStringTsv [2]string `form:",default=1\t2" collection_format:"tsv"`
ArrayStringPipes [2]string `form:",default=1|2" collection_format:"pipes"`
}
err := mappingByPtr(&s, formSource{}, "form")
require.NoError(t, err)
assert.Equal(t, []int{1, 2, 3}, s.SliceMulti)
assert.Equal(t, []int{1, 2, 3}, s.SliceCsv)
assert.Equal(t, []int{1, 2, 3}, s.SliceSsv)
assert.Equal(t, []int{1, 2, 3}, s.SliceTsv)
assert.Equal(t, []int{1, 2, 3}, s.SlicePipes)
assert.Equal(t, [2]int{1, 2}, s.ArrayMulti)
assert.Equal(t, [2]int{1, 2}, s.ArrayCsv)
assert.Equal(t, [2]int{1, 2}, s.ArraySsv)
assert.Equal(t, [2]int{1, 2}, s.ArrayTsv)
assert.Equal(t, [2]int{1, 2}, s.ArrayPipes)
assert.Equal(t, []string{"1", "2", "3"}, s.SliceStringMulti)
assert.Equal(t, []string{"1", "2", "3"}, s.SliceStringCsv)
assert.Equal(t, []string{"1", "2", "3"}, s.SliceStringSsv)
assert.Equal(t, []string{"1", "2", "3"}, s.SliceStringTsv)
assert.Equal(t, []string{"1", "2", "3"}, s.SliceStringPipes)
assert.Equal(t, [2]string{"1", "2"}, s.ArrayStringMulti)
assert.Equal(t, [2]string{"1", "2"}, s.ArrayStringCsv)
assert.Equal(t, [2]string{"1", "2"}, s.ArrayStringSsv)
assert.Equal(t, [2]string{"1", "2"}, s.ArrayStringTsv)
assert.Equal(t, [2]string{"1", "2"}, s.ArrayStringPipes)
} }
func TestMappingStructField(t *testing.T) { func TestMappingStructField(t *testing.T) {
@ -397,50 +265,17 @@ func TestMappingStructField(t *testing.T) {
} }
err := mappingByPtr(&s, formSource{"J": {`{"I": 9}`}}, "form") err := mappingByPtr(&s, formSource{"J": {`{"I": 9}`}}, "form")
require.NoError(t, err) assert.NoError(t, err)
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")
require.NoError(t, err)
assert.Empty(t, req0.Items)
// With 1 item.
var req1 ptrRequest
err = mappingByPtr(&req1, formSource{"items": {`{"key": 1}`}}, "form")
require.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")
require.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
} }
err := mappingByPtr(&s, formSource{"M": {`{"one": 1}`}}, "form") err := mappingByPtr(&s, formSource{"M": {`{"one": 1}`}}, "form")
require.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)
} }
@ -451,187 +286,5 @@ func TestMappingIgnoredCircularRef(t *testing.T) {
var s S var s S
err := mappingByPtr(&s, formSource{}, "form") err := mappingByPtr(&s, formSource{}, "form")
require.NoError(t, err) 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")
require.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")
require.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 errors.New("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")
require.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")
require.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")
require.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")
require.NoError(t, err)
assert.EqualValues(t, "file", s.FileData.Protocol)
assert.EqualValues(t, "/foo", s.FileData.Path)
assert.EqualValues(t, "happiness", s.FileData.Name)
}
type customPath []string
func (p *customPath) UnmarshalParam(param string) error {
elems := strings.Split(param, "/")
n := len(elems)
if n < 2 {
return errors.New("invalid format")
}
*p = elems
return nil
}
func TestMappingCustomSliceUri(t *testing.T) {
var s struct {
FileData customPath `uri:"path"`
}
err := mappingByPtr(&s, formSource{"path": {`bar/foo`}}, "uri")
require.NoError(t, err)
assert.EqualValues(t, "bar", s.FileData[0])
assert.EqualValues(t, "foo", s.FileData[1])
}
func TestMappingCustomSliceForm(t *testing.T) {
var s struct {
FileData customPath `form:"path"`
}
err := mappingByPtr(&s, formSource{"path": {`bar/foo`}}, "form")
require.NoError(t, err)
assert.EqualValues(t, "bar", s.FileData[0])
assert.EqualValues(t, "foo", s.FileData[1])
}
type objectID [12]byte
func (o *objectID) UnmarshalParam(param string) error {
oid, err := convertTo(param)
if err != nil {
return err
}
*o = oid
return nil
}
func convertTo(s string) (objectID, error) {
var nilObjectID objectID
if len(s) != 24 {
return nilObjectID, errors.New("invalid format")
}
var oid [12]byte
_, err := hex.Decode(oid[:], []byte(s))
if err != nil {
return nilObjectID, err
}
return oid, nil
}
func TestMappingCustomArrayUri(t *testing.T) {
var s struct {
FileData objectID `uri:"id"`
}
val := `664a062ac74a8ad104e0e80f`
err := mappingByPtr(&s, formSource{"id": {val}}, "uri")
require.NoError(t, err)
expected, _ := convertTo(val)
assert.EqualValues(t, expected, s.FileData)
}
func TestMappingCustomArrayForm(t *testing.T) {
var s struct {
FileData objectID `form:"id"`
}
val := `664a062ac74a8ad104e0e80f`
err := mappingByPtr(&s, formSource{"id": {val}}, "form")
require.NoError(t, err)
expected, _ := convertTo(val)
assert.EqualValues(t, expected, s.FileData)
} }

View File

@ -12,7 +12,6 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestFormMultipartBindingBindOneFile(t *testing.T) { func TestFormMultipartBindingBindOneFile(t *testing.T) {
@ -28,7 +27,7 @@ func TestFormMultipartBindingBindOneFile(t *testing.T) {
req := createRequestMultipartFiles(t, file) req := createRequestMultipartFiles(t, file)
err := FormMultipart.Bind(req, &s) err := FormMultipart.Bind(req, &s)
require.NoError(t, err) assert.NoError(t, err)
assertMultipartFileHeader(t, &s.FileValue, file) assertMultipartFileHeader(t, &s.FileValue, file)
assertMultipartFileHeader(t, s.FilePtr, file) assertMultipartFileHeader(t, s.FilePtr, file)
@ -54,7 +53,7 @@ func TestFormMultipartBindingBindTwoFiles(t *testing.T) {
req := createRequestMultipartFiles(t, files...) req := createRequestMultipartFiles(t, files...)
err := FormMultipart.Bind(req, &s) err := FormMultipart.Bind(req, &s)
require.NoError(t, err) assert.NoError(t, err)
assert.Len(t, s.SliceValues, len(files)) assert.Len(t, s.SliceValues, len(files))
assert.Len(t, s.SlicePtrs, len(files)) assert.Len(t, s.SlicePtrs, len(files))
@ -91,7 +90,7 @@ func TestFormMultipartBindingBindError(t *testing.T) {
} { } {
req := createRequestMultipartFiles(t, files...) req := createRequestMultipartFiles(t, files...)
err := FormMultipart.Bind(req, tt.s) err := FormMultipart.Bind(req, tt.s)
require.Error(t, err) assert.Error(t, err)
} }
} }
@ -107,17 +106,17 @@ func createRequestMultipartFiles(t *testing.T, files ...testFile) *http.Request
mw := multipart.NewWriter(&body) mw := multipart.NewWriter(&body)
for _, file := range files { for _, file := range files {
fw, err := mw.CreateFormFile(file.Fieldname, file.Filename) fw, err := mw.CreateFormFile(file.Fieldname, file.Filename)
require.NoError(t, err) assert.NoError(t, err)
n, err := fw.Write(file.Content) n, err := fw.Write(file.Content)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, len(file.Content), n) assert.Equal(t, len(file.Content), n)
} }
err := mw.Close() err := mw.Close()
require.NoError(t, err) assert.NoError(t, err)
req, err := http.NewRequest(http.MethodPost, "/", &body) req, err := http.NewRequest("POST", "/", &body)
require.NoError(t, err) assert.NoError(t, err)
req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+mw.Boundary()) req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+mw.Boundary())
return req return req
@ -128,12 +127,12 @@ func assertMultipartFileHeader(t *testing.T, fh *multipart.FileHeader, file test
assert.Equal(t, int64(len(file.Content)), fh.Size) assert.Equal(t, int64(len(file.Content)), fh.Size)
fl, err := fh.Open() fl, err := fh.Open()
require.NoError(t, err) assert.NoError(t, err)
body, err := io.ReadAll(fl) body, err := io.ReadAll(fl)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, string(file.Content), string(body)) assert.Equal(t, string(file.Content), string(body))
err = fl.Close() err = fl.Close()
require.NoError(t, err) assert.NoError(t, err)
} }

View File

@ -1,56 +0,0 @@
package binding
import (
"fmt"
"io"
"net/http"
"reflect"
"github.com/gin-gonic/gin/internal/bytesconv"
)
type plainBinding struct{}
func (plainBinding) Name() string {
return "plain"
}
func (plainBinding) Bind(req *http.Request, obj interface{}) error {
all, err := io.ReadAll(req.Body)
if err != nil {
return err
}
return decodePlain(all, obj)
}
func (plainBinding) BindBody(body []byte, obj any) error {
return decodePlain(body, obj)
}
func decodePlain(data []byte, obj any) error {
if obj == nil {
return nil
}
v := reflect.ValueOf(obj)
for v.Kind() == reflect.Ptr {
if v.IsNil() {
return nil
}
v = v.Elem()
}
if v.Kind() == reflect.String {
v.SetString(bytesconv.BytesToString(data))
return nil
}
if _, ok := v.Interface().([]byte); ok {
v.SetBytes(data)
return nil
}
return fmt.Errorf("type (%T) unknown type", v)
}

View File

@ -31,5 +31,5 @@ func decodeToml(r io.Reader, obj any) error {
if err := decoder.Decode(obj); err != nil { if err := decoder.Decode(obj); err != nil {
return err return err
} }
return validate(obj) return decoder.Decode(obj)
} }

View File

@ -11,7 +11,6 @@ import (
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
type testInterface interface { type testInterface interface {
@ -114,10 +113,10 @@ func TestValidateNoValidationValues(t *testing.T) {
test := createNoValidationValues() test := createNoValidationValues()
empty := structNoValidationValues{} empty := structNoValidationValues{}
require.NoError(t, validate(test)) assert.Nil(t, validate(test))
require.NoError(t, validate(&test)) assert.Nil(t, validate(&test))
require.NoError(t, validate(empty)) assert.Nil(t, validate(empty))
require.NoError(t, validate(&empty)) assert.Nil(t, validate(&empty))
assert.Equal(t, origin, test) assert.Equal(t, origin, test)
} }
@ -164,8 +163,8 @@ func TestValidateNoValidationPointers(t *testing.T) {
//assert.Nil(t, validate(test)) //assert.Nil(t, validate(test))
//assert.Nil(t, validate(&test)) //assert.Nil(t, validate(&test))
require.NoError(t, validate(empty)) assert.Nil(t, validate(empty))
require.NoError(t, validate(&empty)) assert.Nil(t, validate(&empty))
//assert.Equal(t, origin, test) //assert.Equal(t, origin, test)
} }
@ -174,49 +173,25 @@ 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}
require.NoError(t, validate(obj)) assert.NoError(t, validate(obj))
require.NoError(t, validate(&obj)) assert.NoError(t, validate(&obj))
assert.Equal(t, Object{"foo": "bar", "bar": 1}, obj) assert.Equal(t, Object{"foo": "bar", "bar": 1}, obj)
obj2 := []Object{{"foo": "bar", "bar": 1}, {"foo": "bar", "bar": 1}} obj2 := []Object{{"foo": "bar", "bar": 1}, {"foo": "bar", "bar": 1}}
require.NoError(t, validate(obj2)) assert.NoError(t, validate(obj2))
require.NoError(t, validate(&obj2)) assert.NoError(t, validate(&obj2))
nu := 10 nu := 10
require.NoError(t, validate(nu)) assert.NoError(t, validate(nu))
require.NoError(t, validate(&nu)) assert.NoError(t, validate(&nu))
assert.Equal(t, 10, nu) assert.Equal(t, 10, nu)
str := "value" str := "value"
require.NoError(t, validate(str)) assert.NoError(t, validate(str))
require.NoError(t, validate(&str)) assert.NoError(t, validate(&str))
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)
require.NoError(t, errs)
assert.Equal(t, structModifyValidation{Integer: 0}, s)
}
// 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.
@ -240,14 +215,14 @@ func TestValidatorEngine(t *testing.T) {
err := engine.RegisterValidation("notone", notOne) err := engine.RegisterValidation("notone", notOne)
// Check that we can register custom validation without error // Check that we can register custom validation without error
require.NoError(t, err) assert.Nil(t, err)
// Create an instance which will fail validation // Create an instance which will fail validation
withOne := structCustomValidation{Integer: 1} withOne := structCustomValidation{Integer: 1}
errs := validate(withOne) errs := validate(withOne)
// Check that we got back non-nil errs // Check that we got back non-nil errs
require.Error(t, errs) assert.NotNil(t, errs)
// Check that the error matches expectation // Check that the error matches expectation
require.Error(t, errs, "", "", "notone") assert.Error(t, errs, "", "", "notone")
} }

View File

@ -1,13 +0,0 @@
coverage:
require_ci_to_pass: true
status:
project:
default:
target: 99%
threshold: 99%
patch:
default:
target: 99%
threshold: 95%

View File

@ -7,7 +7,6 @@ package gin
import ( import (
"errors" "errors"
"io" "io"
"io/fs"
"log" "log"
"math" "math"
"mime/multipart" "mime/multipart"
@ -35,7 +34,6 @@ const (
MIMEPOSTForm = binding.MIMEPOSTForm MIMEPOSTForm = binding.MIMEPOSTForm
MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm
MIMEYAML = binding.MIMEYAML MIMEYAML = binding.MIMEYAML
MIMEYAML2 = binding.MIMEYAML2
MIMETOML = binding.MIMETOML MIMETOML = binding.MIMETOML
) )
@ -45,10 +43,6 @@ const BodyBytesKey = "_gin-gonic/gin/bodybyteskey"
// ContextKey is the key that a Context returns itself for. // ContextKey is the key that a Context returns itself for.
const ContextKey = "_gin-gonic/gin/contextkey" const ContextKey = "_gin-gonic/gin/contextkey"
type ContextKeyType int
const ContextRequestKey ContextKeyType = 0
// abortIndex represents a typical value used in abort functions. // abortIndex represents a typical value used in abort functions.
const abortIndex int8 = math.MaxInt8 >> 1 const abortIndex int8 = math.MaxInt8 >> 1
@ -119,27 +113,20 @@ func (c *Context) Copy() *Context {
cp := Context{ cp := Context{
writermem: c.writermem, writermem: c.writermem,
Request: c.Request, Request: c.Request,
Params: c.Params,
engine: c.engine, engine: c.engine,
} }
cp.writermem.ResponseWriter = nil cp.writermem.ResponseWriter = nil
cp.Writer = &cp.writermem cp.Writer = &cp.writermem
cp.index = abortIndex cp.index = abortIndex
cp.handlers = nil cp.handlers = nil
cp.fullPath = c.fullPath cp.Keys = map[string]any{}
for k, v := range c.Keys {
cKeys := c.Keys
cp.Keys = make(map[string]any, len(cKeys))
c.mu.RLock()
for k, v := range cKeys {
cp.Keys[k] = v cp.Keys[k] = v
} }
c.mu.RUnlock() paramCopy := make([]Param, len(cp.Params))
copy(paramCopy, cp.Params)
cParams := c.Params cp.Params = paramCopy
cp.Params = make([]Param, len(cParams))
copy(cp.Params, cParams)
return &cp return &cp
} }
@ -154,9 +141,6 @@ func (c *Context) HandlerName() string {
func (c *Context) HandlerNames() []string { func (c *Context) HandlerNames() []string {
hn := make([]string, 0, len(c.handlers)) hn := make([]string, 0, len(c.handlers))
for _, val := range c.handlers { for _, val := range c.handlers {
if val == nil {
continue
}
hn = append(hn, nameOfFunction(val)) hn = append(hn, nameOfFunction(val))
} }
return hn return hn
@ -187,9 +171,7 @@ func (c *Context) FullPath() string {
func (c *Context) Next() { func (c *Context) Next() {
c.index++ c.index++
for c.index < int8(len(c.handlers)) { for c.index < int8(len(c.handlers)) {
if c.handlers[c.index] != nil {
c.handlers[c.index](c) c.handlers[c.index](c)
}
c.index++ c.index++
} }
} }
@ -291,171 +273,108 @@ func (c *Context) MustGet(key string) any {
panic("Key \"" + key + "\" does not exist") panic("Key \"" + key + "\" does not exist")
} }
func getTyped[T any](c *Context, key string) (res T) { // GetString returns the value associated with the key as a string.
func (c *Context) GetString(key string) (s string) {
if val, ok := c.Get(key); ok && val != nil { if val, ok := c.Get(key); ok && val != nil {
res, _ = val.(T) s, _ = val.(string)
} }
return return
} }
// GetString returns the value associated with the key as a string.
func (c *Context) GetString(key string) (s string) {
return getTyped[string](c, key)
}
// GetBool returns the value associated with the key as a boolean. // GetBool returns the value associated with the key as a boolean.
func (c *Context) GetBool(key string) (b bool) { func (c *Context) GetBool(key string) (b bool) {
return getTyped[bool](c, key) if val, ok := c.Get(key); ok && val != nil {
b, _ = val.(bool)
}
return
} }
// GetInt returns the value associated with the key as an integer. // GetInt returns the value associated with the key as an integer.
func (c *Context) GetInt(key string) (i int) { func (c *Context) GetInt(key string) (i int) {
return getTyped[int](c, key) if val, ok := c.Get(key); ok && val != nil {
i, _ = val.(int)
}
return
} }
// GetInt8 returns the value associated with the key as an integer 8. // GetInt64 returns the value associated with the key as an integer.
func (c *Context) GetInt8(key string) (i8 int8) {
return getTyped[int8](c, key)
}
// GetInt16 returns the value associated with the key as an integer 16.
func (c *Context) GetInt16(key string) (i16 int16) {
return getTyped[int16](c, key)
}
// GetInt32 returns the value associated with the key as an integer 32.
func (c *Context) GetInt32(key string) (i32 int32) {
return getTyped[int32](c, key)
}
// GetInt64 returns the value associated with the key as an integer 64.
func (c *Context) GetInt64(key string) (i64 int64) { func (c *Context) GetInt64(key string) (i64 int64) {
return getTyped[int64](c, key) if val, ok := c.Get(key); ok && val != nil {
i64, _ = val.(int64)
}
return
} }
// GetUint returns the value associated with the key as an unsigned integer. // GetUint returns the value associated with the key as an unsigned integer.
func (c *Context) GetUint(key string) (ui uint) { func (c *Context) GetUint(key string) (ui uint) {
return getTyped[uint](c, key) if val, ok := c.Get(key); ok && val != nil {
ui, _ = val.(uint)
}
return
} }
// GetUint8 returns the value associated with the key as an unsigned integer 8. // GetUint64 returns the value associated with the key as an unsigned integer.
func (c *Context) GetUint8(key string) (ui8 uint8) {
return getTyped[uint8](c, key)
}
// GetUint16 returns the value associated with the key as an unsigned integer 16.
func (c *Context) GetUint16(key string) (ui16 uint16) {
return getTyped[uint16](c, key)
}
// GetUint32 returns the value associated with the key as an unsigned integer 32.
func (c *Context) GetUint32(key string) (ui32 uint32) {
return getTyped[uint32](c, key)
}
// GetUint64 returns the value associated with the key as an unsigned integer 64.
func (c *Context) GetUint64(key string) (ui64 uint64) { func (c *Context) GetUint64(key string) (ui64 uint64) {
return getTyped[uint64](c, key) if val, ok := c.Get(key); ok && val != nil {
ui64, _ = val.(uint64)
} }
return
// GetFloat32 returns the value associated with the key as a float32.
func (c *Context) GetFloat32(key string) (f32 float32) {
return getTyped[float32](c, key)
} }
// GetFloat64 returns the value associated with the key as a float64. // GetFloat64 returns the value associated with the key as a float64.
func (c *Context) GetFloat64(key string) (f64 float64) { func (c *Context) GetFloat64(key string) (f64 float64) {
return getTyped[float64](c, key) if val, ok := c.Get(key); ok && val != nil {
f64, _ = val.(float64)
}
return
} }
// GetTime returns the value associated with the key as time. // GetTime returns the value associated with the key as time.
func (c *Context) GetTime(key string) (t time.Time) { func (c *Context) GetTime(key string) (t time.Time) {
return getTyped[time.Time](c, key) if val, ok := c.Get(key); ok && val != nil {
t, _ = val.(time.Time)
}
return
} }
// GetDuration returns the value associated with the key as a duration. // GetDuration returns the value associated with the key as a duration.
func (c *Context) GetDuration(key string) (d time.Duration) { func (c *Context) GetDuration(key string) (d time.Duration) {
return getTyped[time.Duration](c, key) if val, ok := c.Get(key); ok && val != nil {
d, _ = val.(time.Duration)
} }
return
// GetIntSlice returns the value associated with the key as a slice of integers.
func (c *Context) GetIntSlice(key string) (is []int) {
return getTyped[[]int](c, key)
}
// GetInt8Slice returns the value associated with the key as a slice of int8 integers.
func (c *Context) GetInt8Slice(key string) (i8s []int8) {
return getTyped[[]int8](c, key)
}
// GetInt16Slice returns the value associated with the key as a slice of int16 integers.
func (c *Context) GetInt16Slice(key string) (i16s []int16) {
return getTyped[[]int16](c, key)
}
// GetInt32Slice returns the value associated with the key as a slice of int32 integers.
func (c *Context) GetInt32Slice(key string) (i32s []int32) {
return getTyped[[]int32](c, key)
}
// GetInt64Slice returns the value associated with the key as a slice of int64 integers.
func (c *Context) GetInt64Slice(key string) (i64s []int64) {
return getTyped[[]int64](c, key)
}
// GetUintSlice returns the value associated with the key as a slice of unsigned integers.
func (c *Context) GetUintSlice(key string) (uis []uint) {
return getTyped[[]uint](c, key)
}
// GetUint8Slice returns the value associated with the key as a slice of uint8 integers.
func (c *Context) GetUint8Slice(key string) (ui8s []uint8) {
return getTyped[[]uint8](c, key)
}
// GetUint16Slice returns the value associated with the key as a slice of uint16 integers.
func (c *Context) GetUint16Slice(key string) (ui16s []uint16) {
return getTyped[[]uint16](c, key)
}
// GetUint32Slice returns the value associated with the key as a slice of uint32 integers.
func (c *Context) GetUint32Slice(key string) (ui32s []uint32) {
return getTyped[[]uint32](c, key)
}
// GetUint64Slice returns the value associated with the key as a slice of uint64 integers.
func (c *Context) GetUint64Slice(key string) (ui64s []uint64) {
return getTyped[[]uint64](c, key)
}
// GetFloat32Slice returns the value associated with the key as a slice of float32 numbers.
func (c *Context) GetFloat32Slice(key string) (f32s []float32) {
return getTyped[[]float32](c, key)
}
// GetFloat64Slice returns the value associated with the key as a slice of float64 numbers.
func (c *Context) GetFloat64Slice(key string) (f64s []float64) {
return getTyped[[]float64](c, key)
} }
// GetStringSlice returns the value associated with the key as a slice of strings. // GetStringSlice returns the value associated with the key as a slice of strings.
func (c *Context) GetStringSlice(key string) (ss []string) { func (c *Context) GetStringSlice(key string) (ss []string) {
return getTyped[[]string](c, key) if val, ok := c.Get(key); ok && val != nil {
ss, _ = val.([]string)
}
return
} }
// GetStringMap returns the value associated with the key as a map of interfaces. // GetStringMap returns the value associated with the key as a map of interfaces.
func (c *Context) GetStringMap(key string) (sm map[string]any) { func (c *Context) GetStringMap(key string) (sm map[string]any) {
return getTyped[map[string]any](c, key) if val, ok := c.Get(key); ok && val != nil {
sm, _ = val.(map[string]any)
}
return
} }
// GetStringMapString returns the value associated with the key as a map of strings. // GetStringMapString returns the value associated with the key as a map of strings.
func (c *Context) GetStringMapString(key string) (sms map[string]string) { func (c *Context) GetStringMapString(key string) (sms map[string]string) {
return getTyped[map[string]string](c, key) if val, ok := c.Get(key); ok && val != nil {
sms, _ = val.(map[string]string)
}
return
} }
// GetStringMapStringSlice returns the value associated with the key as a map to a slice of strings. // GetStringMapStringSlice returns the value associated with the key as a map to a slice of strings.
func (c *Context) GetStringMapStringSlice(key string) (smss map[string][]string) { func (c *Context) GetStringMapStringSlice(key string) (smss map[string][]string) {
return getTyped[map[string][]string](c, key) if val, ok := c.Get(key); ok && val != nil {
smss, _ = val.(map[string][]string)
}
return
} }
/************************************/ /************************************/
@ -467,7 +386,7 @@ func (c *Context) GetStringMapStringSlice(key string) (smss map[string][]string)
// //
// router.GET("/user/:id", func(c *gin.Context) { // router.GET("/user/:id", func(c *gin.Context) {
// // a GET request to /user/john // // a GET request to /user/john
// id := c.Param("id") // id == "john" // id := c.Param("id") // id == "/john"
// // a GET request to /user/john/ // // a GET request to /user/john/
// id := c.Param("id") // id == "/john/" // id := c.Param("id") // id == "/john/"
// }) // })
@ -538,7 +457,7 @@ func (c *Context) QueryArray(key string) (values []string) {
func (c *Context) initQueryCache() { func (c *Context) initQueryCache() {
if c.queryCache == nil { if c.queryCache == nil {
if c.Request != nil && c.Request.URL != nil { if c.Request != nil {
c.queryCache = c.Request.URL.Query() c.queryCache = c.Request.URL.Query()
} else { } else {
c.queryCache = url.Values{} c.queryCache = url.Values{}
@ -677,22 +596,14 @@ func (c *Context) MultipartForm() (*multipart.Form, error) {
} }
// SaveUploadedFile uploads the form file to specific dst. // SaveUploadedFile uploads the form file to specific dst.
func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string, perm ...fs.FileMode) error { func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string) error {
src, err := file.Open() src, err := file.Open()
if err != nil { if err != nil {
return err return err
} }
defer src.Close() defer src.Close()
var mode os.FileMode = 0o750 if err = os.MkdirAll(filepath.Dir(dst), 0750); err != nil {
if len(perm) > 0 {
mode = perm[0]
}
dir := filepath.Dir(dst)
if err = os.MkdirAll(dir, mode); err != nil {
return err
}
if err = os.Chmod(dir, mode); err != nil {
return err return err
} }
@ -745,11 +656,6 @@ func (c *Context) BindTOML(obj any) error {
return c.MustBindWith(obj, binding.TOML) return c.MustBindWith(obj, binding.TOML)
} }
// BindPlain is a shortcut for c.MustBindWith(obj, binding.Plain).
func (c *Context) BindPlain(obj any) error {
return c.MustBindWith(obj, binding.Plain)
}
// BindHeader is a shortcut for c.MustBindWith(obj, binding.Header). // BindHeader is a shortcut for c.MustBindWith(obj, binding.Header).
func (c *Context) BindHeader(obj any) error { func (c *Context) BindHeader(obj any) error {
return c.MustBindWith(obj, binding.Header) return c.MustBindWith(obj, binding.Header)
@ -815,11 +721,6 @@ func (c *Context) ShouldBindTOML(obj any) error {
return c.ShouldBindWith(obj, binding.TOML) return c.ShouldBindWith(obj, binding.TOML)
} }
// ShouldBindPlain is a shortcut for c.ShouldBindWith(obj, binding.Plain).
func (c *Context) ShouldBindPlain(obj any) error {
return c.ShouldBindWith(obj, binding.Plain)
}
// ShouldBindHeader is a shortcut for c.ShouldBindWith(obj, binding.Header). // ShouldBindHeader is a shortcut for c.ShouldBindWith(obj, binding.Header).
func (c *Context) ShouldBindHeader(obj any) error { func (c *Context) ShouldBindHeader(obj any) error {
return c.ShouldBindWith(obj, binding.Header) return c.ShouldBindWith(obj, binding.Header)
@ -827,7 +728,7 @@ func (c *Context) ShouldBindHeader(obj any) error {
// ShouldBindUri binds the passed struct pointer using the specified binding engine. // ShouldBindUri binds the passed struct pointer using the specified binding engine.
func (c *Context) ShouldBindUri(obj any) error { func (c *Context) ShouldBindUri(obj any) error {
m := make(map[string][]string, len(c.Params)) m := make(map[string][]string)
for _, v := range c.Params { for _, v := range c.Params {
m[v.Key] = []string{v.Value} m[v.Key] = []string{v.Value}
} }
@ -862,34 +763,9 @@ func (c *Context) ShouldBindBodyWith(obj any, bb binding.BindingBody) (err error
return bb.BindBody(body, obj) return bb.BindBody(body, obj)
} }
// ShouldBindBodyWithJSON is a shortcut for c.ShouldBindBodyWith(obj, binding.JSON).
func (c *Context) ShouldBindBodyWithJSON(obj any) error {
return c.ShouldBindBodyWith(obj, binding.JSON)
}
// ShouldBindBodyWithXML is a shortcut for c.ShouldBindBodyWith(obj, binding.XML).
func (c *Context) ShouldBindBodyWithXML(obj any) error {
return c.ShouldBindBodyWith(obj, binding.XML)
}
// ShouldBindBodyWithYAML is a shortcut for c.ShouldBindBodyWith(obj, binding.YAML).
func (c *Context) ShouldBindBodyWithYAML(obj any) error {
return c.ShouldBindBodyWith(obj, binding.YAML)
}
// ShouldBindBodyWithTOML is a shortcut for c.ShouldBindBodyWith(obj, binding.TOML).
func (c *Context) ShouldBindBodyWithTOML(obj any) error {
return c.ShouldBindBodyWith(obj, binding.TOML)
}
// ShouldBindBodyWithPlain is a shortcut for c.ShouldBindBodyWith(obj, binding.Plain).
func (c *Context) ShouldBindBodyWithPlain(obj any) error {
return c.ShouldBindBodyWith(obj, binding.Plain)
}
// ClientIP implements one best effort algorithm to return the real client IP. // ClientIP implements one best effort algorithm to return the real client IP.
// It calls c.RemoteIP() under the hood, to check if the remote IP is a trusted proxy or not. // It calls c.RemoteIP() under the hood, to check if the remote IP is a trusted proxy or not.
// If it is it will then try to parse the headers defined in Engine.RemoteIPHeaders (defaulting to [X-Forwarded-For, X-Real-IP]). // If it is it will then try to parse the headers defined in Engine.RemoteIPHeaders (defaulting to [X-Forwarded-For, X-Real-Ip]).
// If the headers are not syntactically valid OR the remote IP does not correspond to a trusted proxy, // If the headers are not syntactically valid OR the remote IP does not correspond to a trusted proxy,
// the remote IP (coming from Request.RemoteAddr) is returned. // the remote IP (coming from Request.RemoteAddr) is returned.
func (c *Context) ClientIP() string { func (c *Context) ClientIP() string {
@ -997,9 +873,6 @@ func (c *Context) GetHeader(key string) string {
// GetRawData returns stream data. // GetRawData returns stream data.
func (c *Context) GetRawData() ([]byte, error) { func (c *Context) GetRawData() ([]byte, error) {
if c.Request.Body == nil {
return nil, errors.New("cannot read nil body")
}
return io.ReadAll(c.Request.Body) return io.ReadAll(c.Request.Body)
} }
@ -1254,7 +1127,7 @@ func (c *Context) Negotiate(code int, config Negotiate) {
data := chooseData(config.XMLData, config.Data) data := chooseData(config.XMLData, config.Data)
c.XML(code, data) c.XML(code, data)
case binding.MIMEYAML, binding.MIMEYAML2: case binding.MIMEYAML:
data := chooseData(config.YAMLData, config.Data) data := chooseData(config.YAMLData, config.Data)
c.YAML(code, data) c.YAML(code, data)
@ -1342,7 +1215,7 @@ func (c *Context) Err() error {
// if no value is associated with key. Successive calls to Value with // if no value is associated with key. Successive calls to Value with
// the same key returns the same result. // the same key returns the same result.
func (c *Context) Value(key any) any { func (c *Context) Value(key any) any {
if key == ContextRequestKey { if key == 0 {
return c.Request return c.Request
} }
if key == ContextKey { if key == ContextKey {

37
context_1.18_test.go Normal file
View 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
View 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)
}

File diff suppressed because it is too large Load Diff

View File

@ -10,23 +10,19 @@ import (
"runtime" "runtime"
"strconv" "strconv"
"strings" "strings"
"sync/atomic"
) )
const ginSupportMinGoVer = 21 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.
func IsDebugging() bool { func IsDebugging() bool {
return atomic.LoadInt32(&ginMode) == debugCode return ginMode == debugCode
} }
// 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)
@ -52,20 +48,13 @@ func debugPrintLoadTemplate(tmpl *template.Template) {
} }
func debugPrint(format string, values ...any) { 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(DefaultWriter, "[GIN-debug] "+format, values...) fmt.Fprintf(DefaultWriter, "[GIN-debug] "+format, values...)
} }
}
func getMinVer(v string) (uint64, error) { func getMinVer(v string) (uint64, error) {
first := strings.IndexByte(v, '.') first := strings.IndexByte(v, '.')
@ -78,7 +67,7 @@ func getMinVer(v string) (uint64, error) {
func debugPrintWARNINGDefault() { func debugPrintWARNINGDefault() {
if v, e := getMinVer(runtime.Version()); e == nil && v < ginSupportMinGoVer { if v, e := getMinVer(runtime.Version()); e == nil && v < ginSupportMinGoVer {
debugPrint(`[WARNING] Now Gin requires Go 1.23+. debugPrint(`[WARNING] Now Gin requires Go 1.18+.
`) `)
} }

View File

@ -10,7 +10,6 @@ import (
"html/template" "html/template"
"io" "io"
"log" "log"
"net/http"
"os" "os"
"runtime" "runtime"
"strings" "strings"
@ -18,7 +17,6 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
// TODO // TODO
@ -61,7 +59,7 @@ func TestDebugPrintError(t *testing.T) {
func TestDebugPrintRoutes(t *testing.T) { func TestDebugPrintRoutes(t *testing.T) {
re := captureOutput(t, func() { re := captureOutput(t, func() {
SetMode(DebugMode) SetMode(DebugMode)
debugPrintRoute(http.MethodGet, "/path/to/route/:param", HandlersChain{func(c *Context) {}, handlerNameTest}) debugPrintRoute("GET", "/path/to/route/:param", HandlersChain{func(c *Context) {}, handlerNameTest})
SetMode(TestMode) SetMode(TestMode)
}) })
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)
@ -73,7 +71,7 @@ func TestDebugPrintRouteFunc(t *testing.T) {
} }
re := captureOutput(t, func() { re := captureOutput(t, func() {
SetMode(DebugMode) SetMode(DebugMode)
debugPrintRoute(http.MethodGet, "/path/to/route/:param1/:param2", HandlersChain{func(c *Context) {}, handlerNameTest}) debugPrintRoute("GET", "/path/to/route/:param1/:param2", HandlersChain{func(c *Context) {}, handlerNameTest})
SetMode(TestMode) SetMode(TestMode)
}) })
assert.Regexp(t, `^\[GIN-debug\] GET /path/to/route/:param1/:param2 --> (.*/vendor/)?github.com/gin-gonic/gin.handlerNameTest \(2 handlers\)\n$`, re) assert.Regexp(t, `^\[GIN-debug\] GET /path/to/route/:param1/:param2 --> (.*/vendor/)?github.com/gin-gonic/gin.handlerNameTest \(2 handlers\)\n$`, re)
@ -106,7 +104,7 @@ func TestDebugPrintWARNINGDefault(t *testing.T) {
}) })
m, e := getMinVer(runtime.Version()) m, e := getMinVer(runtime.Version())
if e == nil && m < ginSupportMinGoVer { if e == nil && m < ginSupportMinGoVer {
assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.23+.\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)
} }
@ -156,13 +154,13 @@ func TestGetMinVer(t *testing.T) {
var m uint64 var m uint64
var e error var e error
_, e = getMinVer("go1") _, e = getMinVer("go1")
require.Error(t, e) assert.NotNil(t, e)
m, e = getMinVer("go1.1") m, e = getMinVer("go1.1")
assert.Equal(t, uint64(1), m) assert.Equal(t, uint64(1), m)
require.NoError(t, e) assert.Nil(t, e)
m, e = getMinVer("go1.1.1") m, e = getMinVer("go1.1.1")
require.NoError(t, e) assert.Nil(t, e)
assert.Equal(t, uint64(1), m) assert.Equal(t, uint64(1), m)
_, e = getMinVer("go1.1.1.1") _, e = getMinVer("go1.1.1.1")
require.Error(t, e) assert.NotNil(t, e)
} }

View File

@ -12,8 +12,6 @@ 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.
//
// Deprecated: Use MustBindWith or ShouldBindWith.
func (c *Context) BindWith(obj any, b binding.Binding) error { func (c *Context) BindWith(obj any, b binding.Binding) error {
log.Println(`BindWith(\"any, binding.Binding\") error is going to 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

View File

@ -18,7 +18,7 @@ func TestBindWith(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
c, _ := CreateTestContext(w) c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest(http.MethodPost, "/?foo=bar&bar=foo", bytes.NewBufferString("foo=unused")) c.Request, _ = http.NewRequest("POST", "/?foo=bar&bar=foo", bytes.NewBufferString("foo=unused"))
var obj struct { var obj struct {
Foo string `form:"foo"` Foo string `form:"foo"`

View File

@ -26,10 +26,7 @@
- [Custom Validators](#custom-validators) - [Custom Validators](#custom-validators)
- [Only Bind Query String](#only-bind-query-string) - [Only Bind Query String](#only-bind-query-string)
- [Bind Query String or Post Data](#bind-query-string-or-post-data) - [Bind Query String or Post Data](#bind-query-string-or-post-data)
- [Bind default value if none provided](#bind-default-value-if-none-provided)
- [Collection format for arrays](#collection-format-for-arrays)
- [Bind Uri](#bind-uri) - [Bind Uri](#bind-uri)
- [Bind custom unmarshaler](#bind-custom-unmarshaler)
- [Bind Header](#bind-header) - [Bind Header](#bind-header)
- [Bind HTML checkboxes](#bind-html-checkboxes) - [Bind HTML checkboxes](#bind-html-checkboxes)
- [Multipart/Urlencoded binding](#multiparturlencoded-binding) - [Multipart/Urlencoded binding](#multiparturlencoded-binding)
@ -340,16 +337,16 @@ func main() {
router := gin.Default() router := gin.Default()
// Simple group: v1 // Simple group: v1
{
v1 := router.Group("/v1") v1 := router.Group("/v1")
{
v1.POST("/login", loginEndpoint) v1.POST("/login", loginEndpoint)
v1.POST("/submit", submitEndpoint) v1.POST("/submit", submitEndpoint)
v1.POST("/read", readEndpoint) v1.POST("/read", readEndpoint)
} }
// Simple group: v2 // Simple group: v2
{
v2 := router.Group("/v2") v2 := router.Group("/v2")
{
v2.POST("/login", loginEndpoint) v2.POST("/login", loginEndpoint)
v2.POST("/submit", submitEndpoint) v2.POST("/submit", submitEndpoint)
v2.POST("/read", readEndpoint) v2.POST("/read", readEndpoint)
@ -511,44 +508,6 @@ Sample Output
::1 - [Fri, 07 Dec 2018 17:04:38 JST] "GET /ping HTTP/1.1 200 122.767µs "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.80 Safari/537.36" " ::1 - [Fri, 07 Dec 2018 17:04:38 JST] "GET /ping HTTP/1.1 200 122.767µs "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.80 Safari/537.36" "
``` ```
### Skip logging
```go
func main() {
router := gin.New()
// skip logging for desired paths by setting SkipPaths in LoggerConfig
loggerConfig := gin.LoggerConfig{SkipPaths: []string{"/metrics"}}
// skip logging based on your logic by setting Skip func in LoggerConfig
loggerConfig.Skip = func(c *gin.Context) bool {
// as an example skip non server side errors
return c.Writer.Status() < http.StatusInternalServerError
}
router.Use(gin.LoggerWithConfig(loggerConfig))
router.Use(gin.Recovery())
// skipped
router.GET("/metrics", func(c *gin.Context) {
c.Status(http.StatusNotImplemented)
})
// skipped
router.GET("/ping", func(c *gin.Context) {
c.String(http.StatusOK, "pong")
})
// not skipped
router.GET("/data", func(c *gin.Context) {
c.Status(http.StatusNotImplemented)
})
router.Run(":8080")
}
```
### Controlling Log output coloring ### Controlling Log output coloring
By default, logs output on console should be colorized depending on the detected TTY. By default, logs output on console should be colorized depending on the detected TTY.
@ -832,8 +791,6 @@ type Person struct {
Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"` Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"`
CreateTime time.Time `form:"createTime" time_format:"unixNano"` CreateTime time.Time `form:"createTime" time_format:"unixNano"`
UnixTime time.Time `form:"unixTime" time_format:"unix"` UnixTime time.Time `form:"unixTime" time_format:"unix"`
UnixMilliTime time.Time `form:"unixMilliTime" time_format:"unixmilli"`
UnixMicroTime time.Time `form:"unixMicroTime" time_format:"uNiXmIcRo"` // case does not matter for "unix*" time formats
} }
func main() { func main() {
@ -853,8 +810,6 @@ func startPage(c *gin.Context) {
log.Println(person.Birthday) log.Println(person.Birthday)
log.Println(person.CreateTime) log.Println(person.CreateTime)
log.Println(person.UnixTime) log.Println(person.UnixTime)
log.Println(person.UnixMilliTime)
log.Println(person.UnixMicroTime)
} }
c.String(http.StatusOK, "Success") c.String(http.StatusOK, "Success")
@ -864,107 +819,7 @@ func startPage(c *gin.Context) {
Test it with: Test it with:
```sh ```sh
curl -X GET "localhost:8085/testing?name=appleboy&address=xyz&birthday=1992-03-15&createTime=1562400033000000123&unixTime=1562400033&unixMilliTime=1562400033001&unixMicroTime=1562400033000012" curl -X GET "localhost:8085/testing?name=appleboy&address=xyz&birthday=1992-03-15&createTime=1562400033000000123&unixTime=1562400033"
```
### Bind default value if none provided
If the server should bind a default value to a field when the client does not provide one, specify the default value using the `default` key within the `form` tag:
```
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
type Person struct {
Name string `form:"name,default=William"`
Age int `form:"age,default=10"`
Friends []string `form:"friends,default=Will;Bill"`
Addresses [2]string `form:"addresses,default=foo bar" collection_format:"ssv"`
LapTimes []int `form:"lap_times,default=1;2;3" collection_format:"csv"`
}
func main() {
g := gin.Default()
g.POST("/person", func(c *gin.Context) {
var req Person
if err := c.ShouldBindQuery(&req); err != nil {
c.JSON(http.StatusBadRequest, err)
return
}
c.JSON(http.StatusOK, req)
})
_ = g.Run("localhost:8080")
}
```
```
curl -X POST http://localhost:8080/person
{"Name":"William","Age":10,"Friends":["Will","Bill"],"Colors":["red","blue"],"LapTimes":[1,2,3]}
```
NOTE: For default [collection values](#collection-format-for-arrays), the following rules apply:
- Since commas are used to delimit tag options, they are not supported within a default value and will result in undefined behavior
- For the collection formats "multi" and "csv", a semicolon should be used in place of a comma to delimited default values
- Since semicolons are used to delimit default values for "multi" and "csv", they are not supported within a default value for "multi" and "csv"
#### Collection format for arrays
| Format | Description | Example |
| --------------- | --------------------------------------------------------- | ----------------------- |
| multi (default) | Multiple parameter instances rather than multiple values. | key=foo&key=bar&key=baz |
| csv | Comma-separated values. | foo,bar,baz |
| ssv | Space-separated values. | foo bar baz |
| tsv | Tab-separated values. | "foo\tbar\tbaz" |
| pipes | Pipe-separated values. | foo\|bar\|baz |
```go
package main
import (
"log"
"time"
"github.com/gin-gonic/gin"
)
type Person struct {
Name string `form:"name"`
Addresses []string `form:"addresses" collection_format:"csv"`
Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"`
CreateTime time.Time `form:"createTime" time_format:"unixNano"`
UnixTime time.Time `form:"unixTime" time_format:"unix"`
}
func main() {
route := gin.Default()
route.GET("/testing", startPage)
route.Run(":8085")
}
func startPage(c *gin.Context) {
var person Person
// If `GET`, only `Form` binding engine (`query`) used.
// If `POST`, first checks the `content-type` for `JSON` or `XML`, then uses `Form` (`form-data`).
// See more at https://github.com/gin-gonic/gin/blob/master/binding/binding.go#L48
if c.ShouldBind(&person) == nil {
log.Println(person.Name)
log.Println(person.Addresses)
log.Println(person.Birthday)
log.Println(person.CreateTime)
log.Println(person.UnixTime)
}
c.String(200, "Success")
}
```
Test it with:
```sh
$ curl -X GET "localhost:8085/testing?name=appleboy&addresses=foo,bar&birthday=1992-03-15&createTime=1562400033000000123&unixTime=1562400033"
``` ```
### Bind Uri ### Bind Uri
@ -1006,46 +861,6 @@ curl -v localhost:8088/thinkerou/987fbc97-4bed-5078-9f07-9141ba07c9f3
curl -v localhost:8088/thinkerou/not-uuid curl -v localhost:8088/thinkerou/not-uuid
``` ```
### Bind custom unmarshaler
```go
package main
import (
"github.com/gin-gonic/gin"
"strings"
)
type Birthday string
func (b *Birthday) UnmarshalParam(param string) error {
*b = Birthday(strings.Replace(param, "-", "/", -1))
return nil
}
func main() {
route := gin.Default()
var request struct {
Birthday Birthday `form:"birthday"`
}
route.GET("/test", func(ctx *gin.Context) {
_ = ctx.BindQuery(&request)
ctx.JSON(200, request.Birthday)
})
route.Run(":8088")
}
```
Test it with:
```sh
curl 'localhost:8088/test?birthday=2000-01-01'
```
Result
```sh
"2000/01/01"
```
### Bind Header ### Bind Header
```go ```go
@ -2103,12 +1918,7 @@ func SomeHandler(c *gin.Context) {
} }
``` ```
For this, you can use `c.ShouldBindBodyWith` or shortcuts. For this, you can use `c.ShouldBindBodyWith`.
- `c.ShouldBindBodyWithJSON` is a shortcut for c.ShouldBindBodyWith(obj, binding.JSON).
- `c.ShouldBindBodyWithXML` is a shortcut for c.ShouldBindBodyWith(obj, binding.XML).
- `c.ShouldBindBodyWithYAML` is a shortcut for c.ShouldBindBodyWith(obj, binding.YAML).
- `c.ShouldBindBodyWithTOML` is a shortcut for c.ShouldBindBodyWith(obj, binding.TOML).
```go ```go
func SomeHandler(c *gin.Context) { func SomeHandler(c *gin.Context) {
@ -2121,7 +1931,7 @@ func SomeHandler(c *gin.Context) {
} else if errB := c.ShouldBindBodyWith(&objB, binding.JSON); errB == nil { } else if errB := c.ShouldBindBodyWith(&objB, binding.JSON); errB == nil {
c.String(http.StatusOK, `the body should be formB JSON`) c.String(http.StatusOK, `the body should be formB JSON`)
// And it can accepts other formats // And it can accepts other formats
} else if errB2 := c.ShouldBindBodyWithXML(&objB); errB2 == nil { } else if errB2 := c.ShouldBindBodyWith(&objB, binding.XML); errB2 == nil {
c.String(http.StatusOK, `the body should be formB XML`) c.String(http.StatusOK, `the body should be formB XML`)
} else { } else {
... ...
@ -2366,16 +2176,10 @@ import (
func main() { func main() {
router := gin.Default() router := gin.Default()
// Use predefined header gin.PlatformXXX // Use predefined header gin.PlatformXXX
// Google App Engine
router.TrustedPlatform = gin.PlatformGoogleAppEngine router.TrustedPlatform = gin.PlatformGoogleAppEngine
// Cloudflare // Or set your own trusted request header for another trusted proxy service
router.TrustedPlatform = gin.PlatformCloudflare // Don't set it to any suspect request header, it's unsafe
// Fly.io router.TrustedPlatform = "X-CDN-IP"
router.TrustedPlatform = gin.PlatformFlyIO
// Or, you can set your own trusted request header. But be sure your CDN
// prevents users from passing this header! For example, if your CDN puts
// the client IP in X-CDN-Client-IP:
router.TrustedPlatform = "X-CDN-Client-IP"
router.GET("/", func(c *gin.Context) { router.GET("/", func(c *gin.Context) {
// If you set TrustedPlatform, ClientIP() will resolve the // If you set TrustedPlatform, ClientIP() will resolve the

View File

@ -11,7 +11,6 @@ import (
"github.com/gin-gonic/gin/internal/json" "github.com/gin-gonic/gin/internal/json"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestError(t *testing.T) { func TestError(t *testing.T) {
@ -123,7 +122,7 @@ func TestErrorUnwrap(t *testing.T) {
}) })
// check that 'errors.Is()' and 'errors.As()' behave as expected : // check that 'errors.Is()' and 'errors.As()' behave as expected :
require.ErrorIs(t, err, innerErr) assert.True(t, errors.Is(err, innerErr))
var testErr TestErr var testErr TestErr
require.ErrorAs(t, err, &testErr) assert.True(t, errors.As(err, &testErr))
} }

52
fs.go
View File

@ -9,43 +9,37 @@ import (
"os" "os"
) )
// OnlyFilesFS implements an http.FileSystem without `Readdir` functionality. type onlyFilesFS struct {
type OnlyFilesFS struct { fs http.FileSystem
FileSystem http.FileSystem
} }
// Open passes `Open` to the upstream implementation without `Readdir` functionality. type neuteredReaddirFile struct {
func (o OnlyFilesFS) Open(name string) (http.File, error) {
f, err := o.FileSystem.Open(name)
if err != nil {
return nil, err
}
return neutralizedReaddirFile{f}, nil
}
// neutralizedReaddirFile wraps http.File with a specific implementation of `Readdir`.
type neutralizedReaddirFile struct {
http.File http.File
} }
// Readdir overrides the http.File default implementation and always returns nil. // Dir returns a http.FileSystem that can be used by http.FileServer(). It is used internally
func (n neutralizedReaddirFile) Readdir(_ int) ([]os.FileInfo, error) { // in router.Static().
// this disables directory listing // if listDirectory == true, then it works the same as http.Dir() otherwise it returns
return nil, nil // a filesystem that prevents http.FileServer() to list the directory files.
}
// Dir returns an http.FileSystem that can be used by http.FileServer().
// It is used internally in router.Static().
// 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.
func Dir(root string, listDirectory bool) http.FileSystem { func Dir(root string, listDirectory bool) http.FileSystem {
fs := http.Dir(root) fs := http.Dir(root)
if listDirectory { if listDirectory {
return fs return fs
} }
return &onlyFilesFS{fs}
return &OnlyFilesFS{FileSystem: fs} }
// Open conforms to http.Filesystem.
func (fs onlyFilesFS) Open(name string) (http.File, error) {
f, err := fs.fs.Open(name)
if err != nil {
return nil, err
}
return neuteredReaddirFile{f}, nil
}
// Readdir overrides the http.File default implementation.
func (f neuteredReaddirFile) Readdir(_ int) ([]os.FileInfo, error) {
// this disables directory listing
return nil, nil
} }

View File

@ -1,72 +0,0 @@
package gin
import (
"errors"
"net/http"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type mockFileSystem struct {
open func(name string) (http.File, error)
}
func (m *mockFileSystem) Open(name string) (http.File, error) {
return m.open(name)
}
func TestOnlyFilesFS_Open(t *testing.T) {
var testFile *os.File
mockFS := &mockFileSystem{
open: func(name string) (http.File, error) {
return testFile, nil
},
}
fs := &OnlyFilesFS{FileSystem: mockFS}
file, err := fs.Open("foo")
require.NoError(t, err)
assert.Equal(t, testFile, file.(neutralizedReaddirFile).File)
}
func TestOnlyFilesFS_Open_err(t *testing.T) {
testError := errors.New("mock")
mockFS := &mockFileSystem{
open: func(_ string) (http.File, error) {
return nil, testError
},
}
fs := &OnlyFilesFS{FileSystem: mockFS}
file, err := fs.Open("foo")
require.ErrorIs(t, err, testError)
assert.Nil(t, file)
}
func Test_neuteredReaddirFile_Readdir(t *testing.T) {
n := neutralizedReaddirFile{}
res, err := n.Readdir(0)
require.NoError(t, err)
assert.Nil(t, res)
}
func TestDir_listDirectory(t *testing.T) {
testRoot := "foo"
fs := Dir(testRoot, true)
assert.Equal(t, http.Dir(testRoot), fs)
}
func TestDir(t *testing.T) {
testRoot := "foo"
fs := Dir(testRoot, false)
assert.Equal(t, &OnlyFilesFS{FileSystem: http.Dir(testRoot)}, fs)
}

114
gin.go
View File

@ -17,16 +17,11 @@ import (
"github.com/gin-gonic/gin/internal/bytesconv" "github.com/gin-gonic/gin/internal/bytesconv"
"github.com/gin-gonic/gin/render" "github.com/gin-gonic/gin/render"
"github.com/quic-go/quic-go/http3"
"golang.org/x/net/http2" "golang.org/x/net/http2"
"golang.org/x/net/http2/h2c" "golang.org/x/net/http2/h2c"
) )
const defaultMultipartMemory = 32 << 20 // 32 MB const defaultMultipartMemory = 32 << 20 // 32 MB
const escapedColon = "\\:"
const colon = ":"
const backslash = "\\"
var ( var (
default404Body = []byte("404 page not found") default404Body = []byte("404 page not found")
@ -52,9 +47,6 @@ 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)
// OptionFunc defines the function to change the default configuration
type OptionFunc func(*Engine)
// HandlersChain defines a HandlerFunc slice. // HandlersChain defines a HandlerFunc slice.
type HandlersChain []HandlerFunc type HandlersChain []HandlerFunc
@ -85,8 +77,6 @@ const (
// PlatformCloudflare when using Cloudflare's CDN. Trust CF-Connecting-IP for determining // PlatformCloudflare when using Cloudflare's CDN. Trust CF-Connecting-IP for determining
// the client's IP // the client's IP
PlatformCloudflare = "CF-Connecting-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.
@ -190,7 +180,7 @@ var _ IRouter = (*Engine)(nil)
// - ForwardedByClientIP: true // - ForwardedByClientIP: true
// - UseRawPath: false // - UseRawPath: false
// - UnescapePathValues: true // - UnescapePathValues: true
func New(opts ...OptionFunc) *Engine { func New() *Engine {
debugPrintWARNINGNew() debugPrintWARNINGNew()
engine := &Engine{ engine := &Engine{
RouterGroup: RouterGroup{ RouterGroup: RouterGroup{
@ -219,15 +209,15 @@ func New(opts ...OptionFunc) *Engine {
engine.pool.New = func() any { engine.pool.New = func() any {
return engine.allocateContext(engine.maxParams) return engine.allocateContext(engine.maxParams)
} }
return engine.With(opts...) return engine
} }
// 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(opts ...OptionFunc) *Engine { func Default() *Engine {
debugPrintWARNINGDefault() debugPrintWARNINGDefault()
engine := New() engine := New()
engine.Use(Logger(), Recovery()) engine.Use(Logger(), Recovery())
return engine.With(opts...) return engine
} }
func (engine *Engine) Handler() http.Handler { func (engine *Engine) Handler() http.Handler {
@ -321,15 +311,6 @@ 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)
} }
@ -353,6 +334,7 @@ func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
} }
root.addRoute(path, handlers) root.addRoute(path, handlers)
// Update maxParams
if paramsCount := countParams(path); paramsCount > engine.maxParams { if paramsCount := countParams(path); paramsCount > engine.maxParams {
engine.maxParams = paramsCount engine.maxParams = paramsCount
} }
@ -388,6 +370,23 @@ func iterate(path, method string, routes RoutesInfo, root *node) RoutesInfo {
return routes return routes
} }
// Run attaches the router to a http.Server and starts listening and serving HTTP requests.
// It is a shortcut for http.ListenAndServe(addr, router)
// Note: this method will block the calling goroutine indefinitely unless an error happens.
func (engine *Engine) Run(addr ...string) (err error) {
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://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.")
}
address := resolveAddress(addr)
debugPrint("Listening and serving HTTP on %s\n", address)
err = http.ListenAndServe(address, engine.Handler())
return
}
func (engine *Engine) prepareTrustedCIDRs() ([]*net.IPNet, error) { func (engine *Engine) prepareTrustedCIDRs() ([]*net.IPNet, error) {
if engine.trustedProxies == nil { if engine.trustedProxies == nil {
return nil, nil return nil, nil
@ -477,26 +476,6 @@ func (engine *Engine) validateHeader(header string) (clientIP string, valid bool
return "", false return "", false
} }
// updateRouteTree do update to the route tree recursively
func updateRouteTree(n *node) {
n.path = strings.ReplaceAll(n.path, escapedColon, colon)
n.fullPath = strings.ReplaceAll(n.fullPath, escapedColon, colon)
n.indices = strings.ReplaceAll(n.indices, backslash, colon)
if n.children == nil {
return
}
for _, child := range n.children {
updateRouteTree(child)
}
}
// updateRouteTrees do update to the route trees
func (engine *Engine) updateRouteTrees() {
for _, tree := range engine.trees {
updateRouteTree(tree.root)
}
}
// parseIP parse a string representation of an IP and returns a net.IP with the // parseIP parse a string representation of an IP and returns a net.IP with the
// minimum byte representation or nil if input is invalid. // minimum byte representation or nil if input is invalid.
func parseIP(ip string) net.IP { func parseIP(ip string) net.IP {
@ -511,23 +490,6 @@ func parseIP(ip string) net.IP {
return parsedIP return parsedIP
} }
// Run attaches the router to a http.Server and starts listening and serving HTTP requests.
// It is a shortcut for http.ListenAndServe(addr, router)
// Note: this method will block the calling goroutine indefinitely unless an error happens.
func (engine *Engine) Run(addr ...string) (err error) {
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.")
}
engine.updateRouteTrees()
address := resolveAddress(addr)
debugPrint("Listening and serving HTTP on %s\n", address)
err = http.ListenAndServe(address, engine.Handler())
return
}
// 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.
@ -537,7 +499,7 @@ func (engine *Engine) RunTLS(addr, certFile, keyFile string) (err error) {
if engine.isUnsafeTrustedProxies() { if engine.isUnsafeTrustedProxies() {
debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" + 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.") "Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.")
} }
err = http.ListenAndServeTLS(addr, certFile, keyFile, engine.Handler()) err = http.ListenAndServeTLS(addr, certFile, keyFile, engine.Handler())
@ -589,22 +551,6 @@ func (engine *Engine) RunFd(fd int) (err error) {
return return
} }
// RunQUIC attaches the router to a http.Server and starts listening and serving QUIC requests.
// It is a shortcut for http3.ListenAndServeQUIC(addr, certFile, keyFile, router)
// Note: this method will block the calling goroutine indefinitely unless an error happens.
func (engine *Engine) RunQUIC(addr, certFile, keyFile string) (err error) {
debugPrint("Listening and serving QUIC on %s\n", 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 = http3.ListenAndServeQUIC(addr, certFile, keyFile, engine.Handler())
return
}
// RunListener attaches the router to a http.Server and starts listening and serving HTTP requests // RunListener attaches the router to a http.Server and starts listening and serving HTTP requests
// through the specified net.Listener // through the specified net.Listener
func (engine *Engine) RunListener(listener net.Listener) (err error) { func (engine *Engine) RunListener(listener net.Listener) (err error) {
@ -637,12 +583,10 @@ func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// Disclaimer: You can loop yourself to deal 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
oldHandlers := c.handlers
c.reset() c.reset()
engine.handleHTTPRequest(c) engine.handleHTTPRequest(c)
c.index = oldIndexValue c.index = oldIndexValue
c.handlers = oldHandlers
} }
func (engine *Engine) handleHTTPRequest(c *Context) { func (engine *Engine) handleHTTPRequest(c *Context) {
@ -689,26 +633,18 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
break break
} }
if engine.HandleMethodNotAllowed && len(t) > 0 { 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 value := tree.root.getValue(rPath, nil, c.skippedNodes, unescape); value.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)
} }

View File

@ -21,7 +21,6 @@ import (
"time" "time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
// params[0]=url example:http://127.0.0.1:8080/index (cannot be empty) // params[0]=url example:http://127.0.0.1:8080/index (cannot be empty)
@ -41,11 +40,11 @@ func testRequest(t *testing.T, params ...string) {
client := &http.Client{Transport: tr} client := &http.Client{Transport: tr}
resp, err := client.Get(params[0]) resp, err := client.Get(params[0])
require.NoError(t, err) assert.NoError(t, err)
defer resp.Body.Close() defer resp.Body.Close()
body, ioerr := io.ReadAll(resp.Body) body, ioerr := io.ReadAll(resp.Body)
require.NoError(t, ioerr) assert.NoError(t, ioerr)
var responseStatus = "200 OK" var responseStatus = "200 OK"
if len(params) > 1 && params[1] != "" { if len(params) > 1 && params[1] != "" {
@ -74,13 +73,13 @@ func TestRunEmpty(t *testing.T) {
// otherwise the main thread will complete // otherwise the main thread will complete
time.Sleep(5 * time.Millisecond) time.Sleep(5 * time.Millisecond)
require.Error(t, router.Run(":8080")) assert.Error(t, router.Run(":8080"))
testRequest(t, "http://localhost:8080/example") testRequest(t, "http://localhost:8080/example")
} }
func TestBadTrustedCIDRs(t *testing.T) { func TestBadTrustedCIDRs(t *testing.T) {
router := New() router := New()
require.Error(t, router.SetTrustedProxies([]string{"hello/world"})) assert.Error(t, router.SetTrustedProxies([]string{"hello/world"}))
} }
/* legacy tests /* legacy tests
@ -88,7 +87,7 @@ func TestBadTrustedCIDRsForRun(t *testing.T) {
os.Setenv("PORT", "") os.Setenv("PORT", "")
router := New() router := New()
router.TrustedProxies = []string{"hello/world"} router.TrustedProxies = []string{"hello/world"}
require.Error(t, router.Run(":8080")) assert.Error(t, router.Run(":8080"))
} }
func TestBadTrustedCIDRsForRunUnix(t *testing.T) { func TestBadTrustedCIDRsForRunUnix(t *testing.T) {
@ -101,7 +100,7 @@ func TestBadTrustedCIDRsForRunUnix(t *testing.T) {
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") })
require.Error(t, router.RunUnix(unixTestSocket)) assert.Error(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
@ -113,15 +112,15 @@ func TestBadTrustedCIDRsForRunFd(t *testing.T) {
router.TrustedProxies = []string{"hello/world"} router.TrustedProxies = []string{"hello/world"}
addr, err := net.ResolveTCPAddr("tcp", "localhost:0") addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
require.NoError(t, err) assert.NoError(t, err)
listener, err := net.ListenTCP("tcp", addr) listener, err := net.ListenTCP("tcp", addr)
require.NoError(t, err) assert.NoError(t, err)
socketFile, err := listener.File() socketFile, err := listener.File()
require.NoError(t, err) assert.NoError(t, err)
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") })
require.Error(t, router.RunFd(int(socketFile.Fd()))) assert.Error(t, router.RunFd(int(socketFile.Fd())))
}() }()
// 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
@ -133,12 +132,12 @@ func TestBadTrustedCIDRsForRunListener(t *testing.T) {
router.TrustedProxies = []string{"hello/world"} router.TrustedProxies = []string{"hello/world"}
addr, err := net.ResolveTCPAddr("tcp", "localhost:0") addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
require.NoError(t, err) assert.NoError(t, err)
listener, err := net.ListenTCP("tcp", addr) listener, err := net.ListenTCP("tcp", addr)
require.NoError(t, err) assert.NoError(t, err)
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") })
require.Error(t, router.RunListener(listener)) assert.Error(t, router.RunListener(listener))
}() }()
// 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
@ -149,7 +148,7 @@ func TestBadTrustedCIDRsForRunTLS(t *testing.T) {
os.Setenv("PORT", "") os.Setenv("PORT", "")
router := New() router := New()
router.TrustedProxies = []string{"hello/world"} router.TrustedProxies = []string{"hello/world"}
require.Error(t, router.RunTLS(":8080", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem")) assert.Error(t, router.RunTLS(":8080", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem"))
} }
*/ */
@ -165,7 +164,7 @@ func TestRunTLS(t *testing.T) {
// otherwise the main thread will complete // otherwise the main thread will complete
time.Sleep(5 * time.Millisecond) time.Sleep(5 * time.Millisecond)
require.Error(t, router.RunTLS(":8443", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem")) assert.Error(t, router.RunTLS(":8443", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem"))
testRequest(t, "https://localhost:8443/example") testRequest(t, "https://localhost:8443/example")
} }
@ -202,7 +201,7 @@ func TestPusher(t *testing.T) {
// otherwise the main thread will complete // otherwise the main thread will complete
time.Sleep(5 * time.Millisecond) time.Sleep(5 * time.Millisecond)
require.Error(t, router.RunTLS(":8449", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem")) assert.Error(t, router.RunTLS(":8449", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem"))
testRequest(t, "https://localhost:8449/pusher") testRequest(t, "https://localhost:8449/pusher")
} }
@ -217,14 +216,14 @@ func TestRunEmptyWithEnv(t *testing.T) {
// otherwise the main thread will complete // otherwise the main thread will complete
time.Sleep(5 * time.Millisecond) time.Sleep(5 * time.Millisecond)
require.Error(t, router.Run(":3123")) assert.Error(t, router.Run(":3123"))
testRequest(t, "http://localhost:3123/example") testRequest(t, "http://localhost:3123/example")
} }
func TestRunTooMuchParams(t *testing.T) { func TestRunTooMuchParams(t *testing.T) {
router := New() router := New()
assert.Panics(t, func() { assert.Panics(t, func() {
require.NoError(t, router.Run("2", "2")) assert.NoError(t, router.Run("2", "2"))
}) })
} }
@ -238,7 +237,7 @@ func TestRunWithPort(t *testing.T) {
// otherwise the main thread will complete // otherwise the main thread will complete
time.Sleep(5 * time.Millisecond) time.Sleep(5 * time.Millisecond)
require.Error(t, router.Run(":5150")) assert.Error(t, router.Run(":5150"))
testRequest(t, "http://localhost:5150/example") testRequest(t, "http://localhost:5150/example")
} }
@ -258,7 +257,7 @@ func TestUnixSocket(t *testing.T) {
time.Sleep(5 * time.Millisecond) time.Sleep(5 * time.Millisecond)
c, err := net.Dial("unix", unixTestSocket) c, err := net.Dial("unix", unixTestSocket)
require.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")
scanner := bufio.NewScanner(c) scanner := bufio.NewScanner(c)
@ -272,38 +271,22 @@ func TestUnixSocket(t *testing.T) {
func TestBadUnixSocket(t *testing.T) { func TestBadUnixSocket(t *testing.T) {
router := New() router := New()
require.Error(t, router.RunUnix("#/tmp/unix_unit_test")) assert.Error(t, router.RunUnix("#/tmp/unix_unit_test"))
}
func TestRunQUIC(t *testing.T) {
router := New()
go func() {
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
assert.NoError(t, router.RunQUIC(":8443", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem"))
}()
// have to wait for the goroutine to start and run the server
// otherwise the main thread will complete
time.Sleep(5 * time.Millisecond)
require.Error(t, router.RunQUIC(":8443", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem"))
testRequest(t, "https://localhost:8443/example")
} }
func TestFileDescriptor(t *testing.T) { func TestFileDescriptor(t *testing.T) {
router := New() router := New()
addr, err := net.ResolveTCPAddr("tcp", "localhost:0") addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
require.NoError(t, err) assert.NoError(t, err)
listener, err := net.ListenTCP("tcp", addr) listener, err := net.ListenTCP("tcp", addr)
require.NoError(t, err) assert.NoError(t, err)
socketFile, err := listener.File() socketFile, err := listener.File()
if isWindows() { if isWindows() {
// not supported by windows, it is unimplemented now // not supported by windows, it is unimplemented now
require.Error(t, err) assert.Error(t, err)
} else { } else {
require.NoError(t, err) assert.NoError(t, err)
} }
if socketFile == nil { if socketFile == nil {
@ -319,7 +302,7 @@ func TestFileDescriptor(t *testing.T) {
time.Sleep(5 * time.Millisecond) time.Sleep(5 * time.Millisecond)
c, err := net.Dial("tcp", listener.Addr().String()) c, err := net.Dial("tcp", listener.Addr().String())
require.NoError(t, err) assert.NoError(t, err)
fmt.Fprintf(c, "GET /example HTTP/1.0\r\n\r\n") fmt.Fprintf(c, "GET /example HTTP/1.0\r\n\r\n")
scanner := bufio.NewScanner(c) scanner := bufio.NewScanner(c)
@ -333,15 +316,15 @@ func TestFileDescriptor(t *testing.T) {
func TestBadFileDescriptor(t *testing.T) { func TestBadFileDescriptor(t *testing.T) {
router := New() router := New()
require.Error(t, router.RunFd(0)) assert.Error(t, router.RunFd(0))
} }
func TestListener(t *testing.T) { func TestListener(t *testing.T) {
router := New() router := New()
addr, err := net.ResolveTCPAddr("tcp", "localhost:0") addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
require.NoError(t, err) assert.NoError(t, err)
listener, err := net.ListenTCP("tcp", addr) listener, err := net.ListenTCP("tcp", addr)
require.NoError(t, err) assert.NoError(t, err)
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.RunListener(listener)) assert.NoError(t, router.RunListener(listener))
@ -351,7 +334,7 @@ func TestListener(t *testing.T) {
time.Sleep(5 * time.Millisecond) time.Sleep(5 * time.Millisecond)
c, err := net.Dial("tcp", listener.Addr().String()) c, err := net.Dial("tcp", listener.Addr().String())
require.NoError(t, err) assert.NoError(t, err)
fmt.Fprintf(c, "GET /example HTTP/1.0\r\n\r\n") fmt.Fprintf(c, "GET /example HTTP/1.0\r\n\r\n")
scanner := bufio.NewScanner(c) scanner := bufio.NewScanner(c)
@ -366,11 +349,11 @@ func TestListener(t *testing.T) {
func TestBadListener(t *testing.T) { func TestBadListener(t *testing.T) {
router := New() router := New()
addr, err := net.ResolveTCPAddr("tcp", "localhost:10086") addr, err := net.ResolveTCPAddr("tcp", "localhost:10086")
require.NoError(t, err) assert.NoError(t, err)
listener, err := net.ListenTCP("tcp", addr) listener, err := net.ListenTCP("tcp", addr)
require.NoError(t, err) assert.NoError(t, err)
listener.Close() listener.Close()
require.Error(t, router.RunListener(listener)) assert.Error(t, router.RunListener(listener))
} }
func TestWithHttptestWithAutoSelectedPort(t *testing.T) { func TestWithHttptestWithAutoSelectedPort(t *testing.T) {
@ -396,14 +379,7 @@ func TestConcurrentHandleContext(t *testing.T) {
wg.Add(iterations) wg.Add(iterations)
for i := 0; i < iterations; i++ { for i := 0; i < iterations; i++ {
go func() { go func() {
req, err := http.NewRequest(http.MethodGet, "/", nil) testGetRequestHandler(t, router, "/")
assert.NoError(t, err)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, "it worked", w.Body.String(), "resp body should match")
assert.Equal(t, 200, w.Code, "should get a 200")
wg.Done() wg.Done()
}() }()
} }
@ -425,6 +401,17 @@ func TestConcurrentHandleContext(t *testing.T) {
// testRequest(t, "http://localhost:8033/example") // testRequest(t, "http://localhost:8033/example")
// } // }
func testGetRequestHandler(t *testing.T, h http.Handler, url string) {
req, err := http.NewRequest(http.MethodGet, url, nil)
assert.NoError(t, err)
w := httptest.NewRecorder()
h.ServeHTTP(w, req)
assert.Equal(t, "it worked", w.Body.String(), "resp body should match")
assert.Equal(t, 200, w.Code, "should get a 200")
}
func TestTreeRunDynamicRouting(t *testing.T) { func TestTreeRunDynamicRouting(t *testing.T) {
router := New() router := New()
router.GET("/aa/*xx", func(c *Context) { c.String(http.StatusOK, "/aa/*xx") }) router.GET("/aa/*xx", func(c *Context) { c.String(http.StatusOK, "/aa/*xx") })
@ -574,28 +561,3 @@ func TestTreeRunDynamicRouting(t *testing.T) {
func isWindows() bool { func isWindows() bool {
return runtime.GOOS == "windows" return runtime.GOOS == "windows"
} }
func TestEscapedColon(t *testing.T) {
router := New()
f := func(u string) {
router.GET(u, func(c *Context) { c.String(http.StatusOK, u) })
}
f("/r/r\\:r")
f("/r/r:r")
f("/r/r/:r")
f("/r/r/\\:r")
f("/r/r/r\\:r")
assert.Panics(t, func() {
f("\\foo:")
})
router.updateRouteTrees()
ts := httptest.NewServer(router)
defer ts.Close()
testRequest(t, ts.URL+"/r/r123", "", "/r/r:r")
testRequest(t, ts.URL+"/r/r:r", "", "/r/r\\:r")
testRequest(t, ts.URL+"/r/r/r123", "", "/r/r/:r")
testRequest(t, ts.URL+"/r/r/:r", "", "/r/r/\\:r")
testRequest(t, ts.URL+"/r/r/r:r", "", "/r/r/r\\:r")
}

View File

@ -14,13 +14,11 @@ import (
"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"
"github.com/stretchr/testify/require"
"golang.org/x/net/http2" "golang.org/x/net/http2"
) )
@ -73,7 +71,7 @@ func TestLoadHTMLGlobDebugMode(t *testing.T) {
) )
defer ts.Close() defer ts.Close()
res, err := http.Get(ts.URL + "/test") res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
@ -131,7 +129,7 @@ func TestLoadHTMLGlobTestMode(t *testing.T) {
) )
defer ts.Close() defer ts.Close()
res, err := http.Get(ts.URL + "/test") res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
@ -151,7 +149,7 @@ func TestLoadHTMLGlobReleaseMode(t *testing.T) {
) )
defer ts.Close() defer ts.Close()
res, err := http.Get(ts.URL + "/test") res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
@ -178,7 +176,7 @@ func TestLoadHTMLGlobUsingTLS(t *testing.T) {
}, },
} }
client := &http.Client{Transport: tr} client := &http.Client{Transport: tr}
res, err := client.Get(ts.URL + "/test") res, err := client.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
@ -198,7 +196,7 @@ func TestLoadHTMLGlobFromFuncMap(t *testing.T) {
) )
defer ts.Close() defer ts.Close()
res, err := http.Get(ts.URL + "/raw") res, err := http.Get(fmt.Sprintf("%s/raw", ts.URL))
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
@ -229,7 +227,7 @@ func TestLoadHTMLFilesTestMode(t *testing.T) {
) )
defer ts.Close() defer ts.Close()
res, err := http.Get(ts.URL + "/test") res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
@ -249,7 +247,7 @@ func TestLoadHTMLFilesDebugMode(t *testing.T) {
) )
defer ts.Close() defer ts.Close()
res, err := http.Get(ts.URL + "/test") res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
@ -269,7 +267,7 @@ func TestLoadHTMLFilesReleaseMode(t *testing.T) {
) )
defer ts.Close() defer ts.Close()
res, err := http.Get(ts.URL + "/test") res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
@ -296,7 +294,7 @@ func TestLoadHTMLFilesUsingTLS(t *testing.T) {
}, },
} }
client := &http.Client{Transport: tr} client := &http.Client{Transport: tr}
res, err := client.Get(ts.URL + "/test") res, err := client.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
@ -316,7 +314,7 @@ func TestLoadHTMLFilesFuncMap(t *testing.T) {
) )
defer ts.Close() defer ts.Close()
res, err := http.Get(ts.URL + "/raw") res, err := http.Get(fmt.Sprintf("%s/raw", ts.URL))
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
@ -327,31 +325,31 @@ func TestLoadHTMLFilesFuncMap(t *testing.T) {
func TestAddRoute(t *testing.T) { func TestAddRoute(t *testing.T) {
router := New() router := New()
router.addRoute(http.MethodGet, "/", HandlersChain{func(_ *Context) {}}) router.addRoute("GET", "/", HandlersChain{func(_ *Context) {}})
assert.Len(t, router.trees, 1) assert.Len(t, router.trees, 1)
assert.NotNil(t, router.trees.get(http.MethodGet)) assert.NotNil(t, router.trees.get("GET"))
assert.Nil(t, router.trees.get(http.MethodPost)) assert.Nil(t, router.trees.get("POST"))
router.addRoute(http.MethodPost, "/", HandlersChain{func(_ *Context) {}}) router.addRoute("POST", "/", HandlersChain{func(_ *Context) {}})
assert.Len(t, router.trees, 2) assert.Len(t, router.trees, 2)
assert.NotNil(t, router.trees.get(http.MethodGet)) assert.NotNil(t, router.trees.get("GET"))
assert.NotNil(t, router.trees.get(http.MethodPost)) assert.NotNil(t, router.trees.get("POST"))
router.addRoute(http.MethodPost, "/post", HandlersChain{func(_ *Context) {}}) router.addRoute("POST", "/post", HandlersChain{func(_ *Context) {}})
assert.Len(t, router.trees, 2) assert.Len(t, router.trees, 2)
} }
func TestAddRouteFails(t *testing.T) { func TestAddRouteFails(t *testing.T) {
router := New() router := New()
assert.Panics(t, func() { router.addRoute("", "/", HandlersChain{func(_ *Context) {}}) }) assert.Panics(t, func() { router.addRoute("", "/", HandlersChain{func(_ *Context) {}}) })
assert.Panics(t, func() { router.addRoute(http.MethodGet, "a", HandlersChain{func(_ *Context) {}}) }) assert.Panics(t, func() { router.addRoute("GET", "a", HandlersChain{func(_ *Context) {}}) })
assert.Panics(t, func() { router.addRoute(http.MethodGet, "/", HandlersChain{}) }) assert.Panics(t, func() { router.addRoute("GET", "/", HandlersChain{}) })
router.addRoute(http.MethodPost, "/post", HandlersChain{func(_ *Context) {}}) router.addRoute("POST", "/post", HandlersChain{func(_ *Context) {}})
assert.Panics(t, func() { assert.Panics(t, func() {
router.addRoute(http.MethodPost, "/post", HandlersChain{func(_ *Context) {}}) router.addRoute("POST", "/post", HandlersChain{func(_ *Context) {}})
}) })
} }
@ -493,27 +491,27 @@ func TestListOfRoutes(t *testing.T) {
assert.Len(t, list, 7) assert.Len(t, list, 7)
assertRoutePresent(t, list, RouteInfo{ assertRoutePresent(t, list, RouteInfo{
Method: http.MethodGet, Method: "GET",
Path: "/favicon.ico", Path: "/favicon.ico",
Handler: "^(.*/vendor/)?github.com/gin-gonic/gin.handlerTest1$", Handler: "^(.*/vendor/)?github.com/gin-gonic/gin.handlerTest1$",
}) })
assertRoutePresent(t, list, RouteInfo{ assertRoutePresent(t, list, RouteInfo{
Method: http.MethodGet, Method: "GET",
Path: "/", Path: "/",
Handler: "^(.*/vendor/)?github.com/gin-gonic/gin.handlerTest1$", Handler: "^(.*/vendor/)?github.com/gin-gonic/gin.handlerTest1$",
}) })
assertRoutePresent(t, list, RouteInfo{ assertRoutePresent(t, list, RouteInfo{
Method: http.MethodGet, Method: "GET",
Path: "/users/", Path: "/users/",
Handler: "^(.*/vendor/)?github.com/gin-gonic/gin.handlerTest2$", Handler: "^(.*/vendor/)?github.com/gin-gonic/gin.handlerTest2$",
}) })
assertRoutePresent(t, list, RouteInfo{ assertRoutePresent(t, list, RouteInfo{
Method: http.MethodGet, Method: "GET",
Path: "/users/:id", Path: "/users/:id",
Handler: "^(.*/vendor/)?github.com/gin-gonic/gin.handlerTest1$", Handler: "^(.*/vendor/)?github.com/gin-gonic/gin.handlerTest1$",
}) })
assertRoutePresent(t, list, RouteInfo{ assertRoutePresent(t, list, RouteInfo{
Method: http.MethodPost, Method: "POST",
Path: "/users/:id", Path: "/users/:id",
Handler: "^(.*/vendor/)?github.com/gin-gonic/gin.handlerTest2$", Handler: "^(.*/vendor/)?github.com/gin-gonic/gin.handlerTest2$",
}) })
@ -531,7 +529,7 @@ func TestEngineHandleContext(t *testing.T) {
} }
assert.NotPanics(t, func() { assert.NotPanics(t, func() {
w := PerformRequest(r, http.MethodGet, "/") w := PerformRequest(r, "GET", "/")
assert.Equal(t, 301, w.Code) assert.Equal(t, 301, w.Code)
}) })
} }
@ -548,10 +546,10 @@ func TestEngineHandleContextManyReEntries(t *testing.T) {
r.GET("/:count", func(c *Context) { r.GET("/:count", func(c *Context) {
countStr := c.Param("count") countStr := c.Param("count")
count, err := strconv.Atoi(countStr) count, err := strconv.Atoi(countStr)
require.NoError(t, err) assert.NoError(t, err)
n, err := c.Writer.Write([]byte(".")) n, err := c.Writer.Write([]byte("."))
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, 1, n) assert.Equal(t, 1, n)
switch { switch {
@ -564,7 +562,7 @@ func TestEngineHandleContextManyReEntries(t *testing.T) {
}) })
assert.NotPanics(t, func() { assert.NotPanics(t, func() {
w := PerformRequest(r, http.MethodGet, "/"+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())
}) })
@ -573,44 +571,6 @@ func TestEngineHandleContextManyReEntries(t *testing.T) {
assert.Equal(t, int64(expectValue), middlewareCounter) assert.Equal(t, int64(expectValue), middlewareCounter)
} }
func TestEngineHandleContextPreventsMiddlewareReEntry(t *testing.T) {
// given
var handlerCounterV1, handlerCounterV2, middlewareCounterV1 int64
r := New()
v1 := r.Group("/v1")
{
v1.Use(func(c *Context) {
atomic.AddInt64(&middlewareCounterV1, 1)
})
v1.GET("/test", func(c *Context) {
atomic.AddInt64(&handlerCounterV1, 1)
c.Status(http.StatusOK)
})
}
v2 := r.Group("/v2")
{
v2.GET("/test", func(c *Context) {
c.Request.URL.Path = "/v1/test"
r.HandleContext(c)
}, func(c *Context) {
atomic.AddInt64(&handlerCounterV2, 1)
})
}
// when
responseV1 := PerformRequest(r, "GET", "/v1/test")
responseV2 := PerformRequest(r, "GET", "/v2/test")
// then
assert.Equal(t, 200, responseV1.Code)
assert.Equal(t, 200, responseV2.Code)
assert.Equal(t, int64(2), handlerCounterV1)
assert.Equal(t, int64(2), middlewareCounterV1)
assert.Equal(t, int64(1), handlerCounterV2)
}
func TestPrepareTrustedCIRDsWith(t *testing.T) { func TestPrepareTrustedCIRDsWith(t *testing.T) {
r := New() r := New()
@ -619,7 +579,7 @@ func TestPrepareTrustedCIRDsWith(t *testing.T) {
expectedTrustedCIDRs := []*net.IPNet{parseCIDR("0.0.0.0/0")} expectedTrustedCIDRs := []*net.IPNet{parseCIDR("0.0.0.0/0")}
err := r.SetTrustedProxies([]string{"0.0.0.0/0"}) err := r.SetTrustedProxies([]string{"0.0.0.0/0"})
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs) assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs)
} }
@ -627,7 +587,7 @@ func TestPrepareTrustedCIRDsWith(t *testing.T) {
{ {
err := r.SetTrustedProxies([]string{"192.168.1.33/33"}) err := r.SetTrustedProxies([]string{"192.168.1.33/33"})
require.Error(t, err) assert.Error(t, err)
} }
// valid ipv4 address // valid ipv4 address
@ -636,7 +596,7 @@ func TestPrepareTrustedCIRDsWith(t *testing.T) {
err := r.SetTrustedProxies([]string{"192.168.1.33"}) err := r.SetTrustedProxies([]string{"192.168.1.33"})
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs) assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs)
} }
@ -644,7 +604,7 @@ func TestPrepareTrustedCIRDsWith(t *testing.T) {
{ {
err := r.SetTrustedProxies([]string{"192.168.1.256"}) err := r.SetTrustedProxies([]string{"192.168.1.256"})
require.Error(t, err) assert.Error(t, err)
} }
// valid ipv6 address // valid ipv6 address
@ -652,7 +612,7 @@ func TestPrepareTrustedCIRDsWith(t *testing.T) {
expectedTrustedCIDRs := []*net.IPNet{parseCIDR("2002:0000:0000:1234:abcd:ffff:c0a8:0101/128")} 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"}) err := r.SetTrustedProxies([]string{"2002:0000:0000:1234:abcd:ffff:c0a8:0101"})
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs) assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs)
} }
@ -660,7 +620,7 @@ func TestPrepareTrustedCIRDsWith(t *testing.T) {
{ {
err := r.SetTrustedProxies([]string{"gggg:0000:0000:1234:abcd:ffff:c0a8:0101"}) err := r.SetTrustedProxies([]string{"gggg:0000:0000:1234:abcd:ffff:c0a8:0101"})
require.Error(t, err) assert.Error(t, err)
} }
// valid ipv6 cidr // valid ipv6 cidr
@ -668,7 +628,7 @@ func TestPrepareTrustedCIRDsWith(t *testing.T) {
expectedTrustedCIDRs := []*net.IPNet{parseCIDR("::/0")} expectedTrustedCIDRs := []*net.IPNet{parseCIDR("::/0")}
err := r.SetTrustedProxies([]string{"::/0"}) err := r.SetTrustedProxies([]string{"::/0"})
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs) assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs)
} }
@ -676,7 +636,7 @@ func TestPrepareTrustedCIRDsWith(t *testing.T) {
{ {
err := r.SetTrustedProxies([]string{"gggg:0000:0000:1234:abcd:ffff:c0a8:0101/129"}) err := r.SetTrustedProxies([]string{"gggg:0000:0000:1234:abcd:ffff:c0a8:0101/129"})
require.Error(t, err) assert.Error(t, err)
} }
// valid combination // valid combination
@ -692,7 +652,7 @@ func TestPrepareTrustedCIRDsWith(t *testing.T) {
"172.16.0.1", "172.16.0.1",
}) })
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs) assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs)
} }
@ -704,7 +664,7 @@ func TestPrepareTrustedCIRDsWith(t *testing.T) {
"172.16.0.256", "172.16.0.256",
}) })
require.Error(t, err) assert.Error(t, err)
} }
// nil value // nil value
@ -712,7 +672,7 @@ func TestPrepareTrustedCIRDsWith(t *testing.T) {
err := r.SetTrustedProxies(nil) err := r.SetTrustedProxies(nil)
assert.Nil(t, r.trustedCIDRs) assert.Nil(t, r.trustedCIDRs)
require.NoError(t, err) assert.Nil(t, err)
} }
} }
@ -736,71 +696,3 @@ 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: http.MethodGet, Handler: "github.com/gin-gonic/gin.handlerTest1"})
assertRoutePresent(t, routes, RouteInfo{Path: "/test2", Method: http.MethodGet, 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: http.MethodGet, Handler: "github.com/gin-gonic/gin.handlerTest1"})
assertRoutePresent(t, routes, RouteInfo{Path: "/test2", Method: http.MethodGet, 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(http.MethodGet, "/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())
}
// Test the fix for https://github.com/gin-gonic/gin/issues/4002
func TestMethodNotAllowedNoRoute(t *testing.T) {
g := New()
g.HandleMethodNotAllowed = true
req := httptest.NewRequest(http.MethodGet, "/", nil)
resp := httptest.NewRecorder()
assert.NotPanics(t, func() { g.ServeHTTP(resp, req) })
assert.Equal(t, http.StatusNotFound, resp.Code)
}

View File

@ -10,12 +10,10 @@ import (
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"os" "os"
"strconv"
"strings" "strings"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
type route struct { type route struct {
@ -297,9 +295,9 @@ func TestShouldBindUri(t *testing.T) {
} }
router.Handle(http.MethodGet, "/rest/:name/:id", func(c *Context) { router.Handle(http.MethodGet, "/rest/:name/:id", func(c *Context) {
var person Person var person Person
require.NoError(t, c.ShouldBindUri(&person)) assert.NoError(t, c.ShouldBindUri(&person))
assert.NotEqual(t, "", person.Name) assert.True(t, person.Name != "")
assert.NotEqual(t, "", person.ID) assert.True(t, person.ID != "")
c.String(http.StatusOK, "ShouldBindUri test OK") c.String(http.StatusOK, "ShouldBindUri test OK")
}) })
@ -319,9 +317,9 @@ func TestBindUri(t *testing.T) {
} }
router.Handle(http.MethodGet, "/rest/:name/:id", func(c *Context) { router.Handle(http.MethodGet, "/rest/:name/:id", func(c *Context) {
var person Person var person Person
require.NoError(t, c.BindUri(&person)) assert.NoError(t, c.BindUri(&person))
assert.NotEqual(t, "", person.Name) assert.True(t, person.Name != "")
assert.NotEqual(t, "", person.ID) assert.True(t, person.ID != "")
c.String(http.StatusOK, "BindUri test OK") c.String(http.StatusOK, "BindUri test OK")
}) })
@ -340,7 +338,7 @@ func TestBindUriError(t *testing.T) {
} }
router.Handle(http.MethodGet, "/new/rest/:num", func(c *Context) { router.Handle(http.MethodGet, "/new/rest/:num", func(c *Context) {
var m Member var m Member
require.Error(t, c.BindUri(&m)) assert.Error(t, c.BindUri(&m))
}) })
path1, _ := exampleFromPath("/new/rest/:num") path1, _ := exampleFromPath("/new/rest/:num")
@ -412,7 +410,7 @@ func exampleFromPath(path string) (string, Params) {
} }
if start >= 0 { if start >= 0 {
if c == '/' { if c == '/' {
value := strconv.Itoa(rand.Intn(100000)) value := fmt.Sprint(rand.Intn(100000))
params = append(params, Param{ params = append(params, Param{
Key: path[start:i], Key: path[start:i],
Value: value, Value: value,
@ -426,7 +424,7 @@ func exampleFromPath(path string) (string, Params) {
} }
} }
if start >= 0 { if start >= 0 {
value := strconv.Itoa(rand.Intn(100000)) value := fmt.Sprint(rand.Intn(100000))
params = append(params, Param{ params = append(params, Param{
Key: path[start:], Key: path[start:],
Value: value, Value: value,

46
go.mod
View File

@ -1,46 +1,36 @@
module github.com/gin-gonic/gin module github.com/gin-gonic/gin
go 1.23.0 go 1.20
require ( require (
github.com/bytedance/sonic v1.13.1 github.com/bytedance/sonic v1.9.1
github.com/gin-contrib/sse v0.1.0 github.com/gin-contrib/sse v0.1.0
github.com/go-playground/validator/v10 v10.22.1 github.com/go-playground/validator/v10 v10.14.0
github.com/goccy/go-json v0.10.2 github.com/goccy/go-json v0.10.2
github.com/json-iterator/go v1.1.12 github.com/json-iterator/go v1.1.12
github.com/mattn/go-isatty v0.0.20 github.com/mattn/go-isatty v0.0.19
github.com/pelletier/go-toml/v2 v2.2.2 github.com/pelletier/go-toml/v2 v2.0.8
github.com/quic-go/quic-go v0.48.2 github.com/stretchr/testify v1.8.3
github.com/stretchr/testify v1.9.0 github.com/ugorji/go/codec v1.2.11
github.com/ugorji/go/codec v1.2.12 golang.org/x/net v0.10.0
golang.org/x/net v0.37.0 google.golang.org/protobuf v1.30.0
google.golang.org/protobuf v1.34.1
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
require ( require (
github.com/bytedance/sonic/loader v0.2.4 // indirect github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect github.com/leodido/go-urn v1.2.4 // indirect
github.com/klauspost/cpuid/v2 v2.0.9 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/onsi/ginkgo/v2 v2.9.5 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
go.uber.org/mock v0.4.0 // indirect golang.org/x/arch v0.3.0 // indirect
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect golang.org/x/crypto v0.9.0 // indirect
golang.org/x/crypto v0.36.0 // indirect golang.org/x/sys v0.8.0 // indirect
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect golang.org/x/text v0.9.0 // indirect
golang.org/x/mod v0.17.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.23.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
) )

122
go.sum
View File

@ -1,114 +1,90 @@
github.com/bytedance/sonic v1.13.1 h1:Jyd5CIvdFnkOWuKXr+wm4Nyk2h0yAFsr8ucJgEasO3g= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.13.1/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= github.com/bytedance/sonic v1.9.0 h1:iwLYBds8bYtzwOX7kmdYwtS+aY2GgekVoIs2/IxR0tM=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/bytedance/sonic v1.9.0/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
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/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 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/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 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA= github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= 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/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 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/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/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/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q= github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
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/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KWE=
github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs=
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/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 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.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/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU= golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
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/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View 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))
}

View File

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

View File

@ -47,15 +47,8 @@ type LoggerConfig struct {
// SkipPaths is an 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
@ -90,8 +83,6 @@ 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:
@ -248,11 +239,8 @@ func LoggerWithConfig(conf LoggerConfig) HandlerFunc {
// Process request // Process request
c.Next() c.Next()
// Log only when it is not being skipped // Log only when path is not being skipped
if _, ok := skip[path]; ok || (conf.Skip != nil && conf.Skip(c)) { if _, ok := skip[path]; !ok {
return
}
param := LogFormatterParams{ param := LogFormatterParams{
Request: c.Request, Request: c.Request,
isTerm: isTerm, isTerm: isTerm,
@ -279,3 +267,4 @@ func LoggerWithConfig(conf LoggerConfig) HandlerFunc {
fmt.Fprint(out, formatter(param)) fmt.Fprint(out, formatter(param))
} }
} }
}

View File

@ -31,9 +31,9 @@ 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, http.MethodGet, "/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(), http.MethodGet) assert.Contains(t, buffer.String(), "GET")
assert.Contains(t, buffer.String(), "/example") assert.Contains(t, buffer.String(), "/example")
assert.Contains(t, buffer.String(), "a=100") assert.Contains(t, buffer.String(), "a=100")
@ -41,21 +41,21 @@ 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, http.MethodPost, "/example") PerformRequest(router, "POST", "/example")
assert.Contains(t, buffer.String(), "200") assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), http.MethodPost) assert.Contains(t, buffer.String(), "POST")
assert.Contains(t, buffer.String(), "/example") assert.Contains(t, buffer.String(), "/example")
buffer.Reset() buffer.Reset()
PerformRequest(router, http.MethodPut, "/example") PerformRequest(router, "PUT", "/example")
assert.Contains(t, buffer.String(), "200") assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), http.MethodPut) assert.Contains(t, buffer.String(), "PUT")
assert.Contains(t, buffer.String(), "/example") assert.Contains(t, buffer.String(), "/example")
buffer.Reset() buffer.Reset()
PerformRequest(router, http.MethodDelete, "/example") PerformRequest(router, "DELETE", "/example")
assert.Contains(t, buffer.String(), "200") assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), http.MethodDelete) assert.Contains(t, buffer.String(), "DELETE")
assert.Contains(t, buffer.String(), "/example") assert.Contains(t, buffer.String(), "/example")
buffer.Reset() buffer.Reset()
@ -77,9 +77,9 @@ func TestLogger(t *testing.T) {
assert.Contains(t, buffer.String(), "/example") assert.Contains(t, buffer.String(), "/example")
buffer.Reset() buffer.Reset()
PerformRequest(router, http.MethodGet, "/notfound") PerformRequest(router, "GET", "/notfound")
assert.Contains(t, buffer.String(), "404") assert.Contains(t, buffer.String(), "404")
assert.Contains(t, buffer.String(), http.MethodGet) assert.Contains(t, buffer.String(), "GET")
assert.Contains(t, buffer.String(), "/notfound") assert.Contains(t, buffer.String(), "/notfound")
} }
@ -95,9 +95,9 @@ 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, http.MethodGet, "/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(), http.MethodGet) assert.Contains(t, buffer.String(), "GET")
assert.Contains(t, buffer.String(), "/example") assert.Contains(t, buffer.String(), "/example")
assert.Contains(t, buffer.String(), "a=100") assert.Contains(t, buffer.String(), "a=100")
@ -105,21 +105,21 @@ 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, http.MethodPost, "/example") PerformRequest(router, "POST", "/example")
assert.Contains(t, buffer.String(), "200") assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), http.MethodPost) assert.Contains(t, buffer.String(), "POST")
assert.Contains(t, buffer.String(), "/example") assert.Contains(t, buffer.String(), "/example")
buffer.Reset() buffer.Reset()
PerformRequest(router, http.MethodPut, "/example") PerformRequest(router, "PUT", "/example")
assert.Contains(t, buffer.String(), "200") assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), http.MethodPut) assert.Contains(t, buffer.String(), "PUT")
assert.Contains(t, buffer.String(), "/example") assert.Contains(t, buffer.String(), "/example")
buffer.Reset() buffer.Reset()
PerformRequest(router, http.MethodDelete, "/example") PerformRequest(router, "DELETE", "/example")
assert.Contains(t, buffer.String(), "200") assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), http.MethodDelete) assert.Contains(t, buffer.String(), "DELETE")
assert.Contains(t, buffer.String(), "/example") assert.Contains(t, buffer.String(), "/example")
buffer.Reset() buffer.Reset()
@ -141,9 +141,9 @@ func TestLoggerWithConfig(t *testing.T) {
assert.Contains(t, buffer.String(), "/example") assert.Contains(t, buffer.String(), "/example")
buffer.Reset() buffer.Reset()
PerformRequest(router, http.MethodGet, "/notfound") PerformRequest(router, "GET", "/notfound")
assert.Contains(t, buffer.String(), "404") assert.Contains(t, buffer.String(), "404")
assert.Contains(t, buffer.String(), http.MethodGet) assert.Contains(t, buffer.String(), "GET")
assert.Contains(t, buffer.String(), "/notfound") assert.Contains(t, buffer.String(), "/notfound")
} }
@ -169,12 +169,12 @@ func TestLoggerWithFormatter(t *testing.T) {
) )
})) }))
router.GET("/example", func(c *Context) {}) router.GET("/example", func(c *Context) {})
PerformRequest(router, http.MethodGet, "/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]")
assert.Contains(t, buffer.String(), "200") assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), http.MethodGet) assert.Contains(t, buffer.String(), "GET")
assert.Contains(t, buffer.String(), "/example") assert.Contains(t, buffer.String(), "/example")
assert.Contains(t, buffer.String(), "a=100") assert.Contains(t, buffer.String(), "a=100")
} }
@ -210,12 +210,12 @@ func TestLoggerWithConfigFormatting(t *testing.T) {
gotKeys = c.Keys gotKeys = c.Keys
time.Sleep(time.Millisecond) time.Sleep(time.Millisecond)
}) })
PerformRequest(router, http.MethodGet, "/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]")
assert.Contains(t, buffer.String(), "200") assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), http.MethodGet) assert.Contains(t, buffer.String(), "GET")
assert.Contains(t, buffer.String(), "/example") assert.Contains(t, buffer.String(), "/example")
assert.Contains(t, buffer.String(), "a=100") assert.Contains(t, buffer.String(), "a=100")
@ -225,7 +225,7 @@ func TestLoggerWithConfigFormatting(t *testing.T) {
assert.Equal(t, 200, gotParam.StatusCode) assert.Equal(t, 200, gotParam.StatusCode)
assert.NotEmpty(t, gotParam.Latency) assert.NotEmpty(t, gotParam.Latency)
assert.Equal(t, "20.20.20.20", gotParam.ClientIP) assert.Equal(t, "20.20.20.20", gotParam.ClientIP)
assert.Equal(t, http.MethodGet, gotParam.Method) assert.Equal(t, "GET", gotParam.Method)
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)
@ -239,7 +239,7 @@ func TestDefaultLogFormatter(t *testing.T) {
StatusCode: 200, StatusCode: 200,
Latency: time.Second * 5, Latency: time.Second * 5,
ClientIP: "20.20.20.20", ClientIP: "20.20.20.20",
Method: http.MethodGet, Method: "GET",
Path: "/", Path: "/",
ErrorMessage: "", ErrorMessage: "",
isTerm: false, isTerm: false,
@ -250,7 +250,7 @@ func TestDefaultLogFormatter(t *testing.T) {
StatusCode: 200, StatusCode: 200,
Latency: time.Second * 5, Latency: time.Second * 5,
ClientIP: "20.20.20.20", ClientIP: "20.20.20.20",
Method: http.MethodGet, Method: "GET",
Path: "/", Path: "/",
ErrorMessage: "", ErrorMessage: "",
isTerm: true, isTerm: true,
@ -260,7 +260,7 @@ func TestDefaultLogFormatter(t *testing.T) {
StatusCode: 200, StatusCode: 200,
Latency: time.Millisecond * 9876543210, Latency: time.Millisecond * 9876543210,
ClientIP: "20.20.20.20", ClientIP: "20.20.20.20",
Method: http.MethodGet, Method: "GET",
Path: "/", Path: "/",
ErrorMessage: "", ErrorMessage: "",
isTerm: true, isTerm: true,
@ -271,7 +271,7 @@ func TestDefaultLogFormatter(t *testing.T) {
StatusCode: 200, StatusCode: 200,
Latency: time.Millisecond * 9876543210, Latency: time.Millisecond * 9876543210,
ClientIP: "20.20.20.20", ClientIP: "20.20.20.20",
Method: http.MethodGet, Method: "GET",
Path: "/", Path: "/",
ErrorMessage: "", ErrorMessage: "",
isTerm: false, isTerm: false,
@ -292,10 +292,10 @@ func TestColorForMethod(t *testing.T) {
return p.MethodColor() return p.MethodColor()
} }
assert.Equal(t, blue, colorForMethod(http.MethodGet), "get should be blue") assert.Equal(t, blue, colorForMethod("GET"), "get should be blue")
assert.Equal(t, cyan, colorForMethod(http.MethodPost), "post should be cyan") assert.Equal(t, cyan, colorForMethod("POST"), "post should be cyan")
assert.Equal(t, yellow, colorForMethod(http.MethodPut), "put should be yellow") assert.Equal(t, yellow, colorForMethod("PUT"), "put should be yellow")
assert.Equal(t, red, colorForMethod(http.MethodDelete), "delete should be red") assert.Equal(t, red, colorForMethod("DELETE"), "delete should be red")
assert.Equal(t, green, colorForMethod("PATCH"), "patch should be green") assert.Equal(t, green, colorForMethod("PATCH"), "patch should be green")
assert.Equal(t, magenta, colorForMethod("HEAD"), "head should be magenta") assert.Equal(t, magenta, colorForMethod("HEAD"), "head should be magenta")
assert.Equal(t, white, colorForMethod("OPTIONS"), "options should be white") assert.Equal(t, white, colorForMethod("OPTIONS"), "options should be white")
@ -310,7 +310,6 @@ func TestColorForStatus(t *testing.T) {
return p.StatusCodeColor() return p.StatusCodeColor()
} }
assert.Equal(t, white, colorForStatus(http.StatusContinue), "1xx should be white")
assert.Equal(t, green, colorForStatus(http.StatusOK), "2xx should be green") assert.Equal(t, green, colorForStatus(http.StatusOK), "2xx should be green")
assert.Equal(t, white, colorForStatus(http.StatusMovedPermanently), "3xx should be white") assert.Equal(t, white, colorForStatus(http.StatusMovedPermanently), "3xx should be white")
assert.Equal(t, yellow, colorForStatus(http.StatusNotFound), "4xx should be yellow") assert.Equal(t, yellow, colorForStatus(http.StatusNotFound), "4xx should be yellow")
@ -329,13 +328,13 @@ func TestIsOutputColor(t *testing.T) {
} }
consoleColorMode = autoColor consoleColorMode = autoColor
assert.True(t, p.IsOutputColor()) assert.Equal(t, true, p.IsOutputColor())
ForceConsoleColor() ForceConsoleColor()
assert.True(t, p.IsOutputColor()) assert.Equal(t, true, p.IsOutputColor())
DisableConsoleColor() DisableConsoleColor()
assert.False(t, p.IsOutputColor()) assert.Equal(t, false, p.IsOutputColor())
// test with isTerm flag false. // test with isTerm flag false.
p = LogFormatterParams{ p = LogFormatterParams{
@ -343,13 +342,13 @@ func TestIsOutputColor(t *testing.T) {
} }
consoleColorMode = autoColor consoleColorMode = autoColor
assert.False(t, p.IsOutputColor()) assert.Equal(t, false, p.IsOutputColor())
ForceConsoleColor() ForceConsoleColor()
assert.True(t, p.IsOutputColor()) assert.Equal(t, true, p.IsOutputColor())
DisableConsoleColor() DisableConsoleColor()
assert.False(t, p.IsOutputColor()) assert.Equal(t, false, p.IsOutputColor())
// reset console color mode. // reset console color mode.
consoleColorMode = autoColor consoleColorMode = autoColor
@ -369,15 +368,15 @@ func TestErrorLogger(t *testing.T) {
c.String(http.StatusInternalServerError, "hola!") c.String(http.StatusInternalServerError, "hola!")
}) })
w := PerformRequest(router, http.MethodGet, "/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, http.MethodGet, "/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, http.MethodGet, "/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())
} }
@ -389,11 +388,11 @@ func TestLoggerWithWriterSkippingPaths(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, http.MethodGet, "/logged") PerformRequest(router, "GET", "/logged")
assert.Contains(t, buffer.String(), "200") assert.Contains(t, buffer.String(), "200")
buffer.Reset() buffer.Reset()
PerformRequest(router, http.MethodGet, "/skipped") PerformRequest(router, "GET", "/skipped")
assert.Contains(t, buffer.String(), "") assert.Contains(t, buffer.String(), "")
} }
@ -407,31 +406,11 @@ 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, http.MethodGet, "/logged") PerformRequest(router, "GET", "/logged")
assert.Contains(t, buffer.String(), "200") assert.Contains(t, buffer.String(), "200")
buffer.Reset() buffer.Reset()
PerformRequest(router, http.MethodGet, "/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, http.MethodGet, "/logged")
assert.Contains(t, buffer.String(), "200")
buffer.Reset()
PerformRequest(router, http.MethodGet, "/skipped")
assert.Contains(t, buffer.String(), "") assert.Contains(t, buffer.String(), "")
} }

View File

@ -35,7 +35,7 @@ func TestMiddlewareGeneralCase(t *testing.T) {
signature += " XX " signature += " XX "
}) })
// RUN // RUN
w := PerformRequest(router, http.MethodGet, "/") 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, http.MethodGet, "/") 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, http.MethodGet, "/") w := PerformRequest(router, "GET", "/")
// TEST // TEST
assert.Equal(t, http.StatusMethodNotAllowed, w.Code) assert.Equal(t, http.StatusMethodNotAllowed, w.Code)
@ -149,7 +149,7 @@ func TestMiddlewareNoMethodDisabled(t *testing.T) {
}) })
// RUN // RUN
w := PerformRequest(router, http.MethodGet, "/") w := PerformRequest(router, "GET", "/")
// TEST // TEST
assert.Equal(t, http.StatusNotFound, w.Code) assert.Equal(t, http.StatusNotFound, w.Code)
@ -175,7 +175,7 @@ func TestMiddlewareAbort(t *testing.T) {
}) })
// RUN // RUN
w := PerformRequest(router, http.MethodGet, "/") w := PerformRequest(router, "GET", "/")
// TEST // TEST
assert.Equal(t, http.StatusUnauthorized, w.Code) assert.Equal(t, http.StatusUnauthorized, w.Code)
@ -196,7 +196,7 @@ func TestMiddlewareAbortHandlersChainAndNext(t *testing.T) {
c.Next() c.Next()
}) })
// RUN // RUN
w := PerformRequest(router, http.MethodGet, "/") w := PerformRequest(router, "GET", "/")
// TEST // TEST
assert.Equal(t, http.StatusGone, w.Code) assert.Equal(t, http.StatusGone, w.Code)
@ -219,7 +219,7 @@ func TestMiddlewareFailHandlersChain(t *testing.T) {
signature += "C" signature += "C"
}) })
// RUN // RUN
w := PerformRequest(router, http.MethodGet, "/") w := PerformRequest(router, "GET", "/")
// TEST // TEST
assert.Equal(t, http.StatusInternalServerError, w.Code) assert.Equal(t, http.StatusInternalServerError, w.Code)
@ -246,7 +246,7 @@ func TestMiddlewareWrite(t *testing.T) {
}) })
}) })
w := PerformRequest(router, http.MethodGet, "/") 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))

20
mode.go
View File

@ -8,7 +8,6 @@ import (
"flag" "flag"
"io" "io"
"os" "os"
"sync/atomic"
"github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/binding"
) )
@ -44,8 +43,10 @@ var DefaultWriter io.Writer = os.Stdout
// DefaultErrorWriter is the default io.Writer used by Gin to debug errors // DefaultErrorWriter is the default io.Writer used by Gin to debug errors
var DefaultErrorWriter io.Writer = os.Stderr var DefaultErrorWriter io.Writer = os.Stderr
var ginMode int32 = debugCode var (
var modeName atomic.Value ginMode = debugCode
modeName = DebugMode
)
func init() { func init() {
mode := os.Getenv(EnvGinMode) mode := os.Getenv(EnvGinMode)
@ -63,16 +64,17 @@ func SetMode(value string) {
} }
switch value { switch value {
case DebugMode, "": case DebugMode:
atomic.StoreInt32(&ginMode, debugCode) ginMode = debugCode
case ReleaseMode: case ReleaseMode:
atomic.StoreInt32(&ginMode, releaseCode) ginMode = releaseCode
case TestMode: case TestMode:
atomic.StoreInt32(&ginMode, testCode) ginMode = testCode
default: default:
panic("gin mode unknown: " + value + " (available mode: debug release test)") panic("gin mode unknown: " + value + " (available mode: debug release test)")
} }
modeName.Store(value)
modeName = value
} }
// DisableBindValidation closes the default validator. // DisableBindValidation closes the default validator.
@ -94,5 +96,5 @@ func EnableJsonDecoderDisallowUnknownFields() {
// Mode returns current gin mode. // Mode returns current gin mode.
func Mode() string { func Mode() string {
return modeName.Load().(string) return modeName
} }

View File

@ -5,8 +5,8 @@
package gin package gin
import ( import (
"flag"
"os" "os"
"sync/atomic"
"testing" "testing"
"github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/binding"
@ -18,24 +18,31 @@ func init() {
} }
func TestSetMode(t *testing.T) { func TestSetMode(t *testing.T) {
assert.Equal(t, int32(testCode), atomic.LoadInt32(&ginMode)) assert.Equal(t, testCode, ginMode)
assert.Equal(t, TestMode, Mode()) assert.Equal(t, TestMode, Mode())
os.Unsetenv(EnvGinMode) os.Unsetenv(EnvGinMode)
SetMode("") SetMode("")
assert.Equal(t, int32(testCode), atomic.LoadInt32(&ginMode)) assert.Equal(t, testCode, ginMode)
assert.Equal(t, TestMode, Mode()) assert.Equal(t, TestMode, Mode())
tmp := flag.CommandLine
flag.CommandLine = flag.NewFlagSet("", flag.ContinueOnError)
SetMode("")
assert.Equal(t, debugCode, ginMode)
assert.Equal(t, DebugMode, Mode())
flag.CommandLine = tmp
SetMode(DebugMode) SetMode(DebugMode)
assert.Equal(t, int32(debugCode), atomic.LoadInt32(&ginMode)) assert.Equal(t, debugCode, ginMode)
assert.Equal(t, DebugMode, Mode()) assert.Equal(t, DebugMode, Mode())
SetMode(ReleaseMode) SetMode(ReleaseMode)
assert.Equal(t, int32(releaseCode), atomic.LoadInt32(&ginMode)) assert.Equal(t, releaseCode, ginMode)
assert.Equal(t, ReleaseMode, Mode()) assert.Equal(t, ReleaseMode, Mode())
SetMode(TestMode) SetMode(TestMode)
assert.Equal(t, int32(testCode), atomic.LoadInt32(&ginMode)) assert.Equal(t, testCode, ginMode)
assert.Equal(t, TestMode, Mode()) assert.Equal(t, TestMode, Mode())
assert.Panics(t, func() { SetMode("unknown") }) assert.Panics(t, func() { SetMode("unknown") })

View File

@ -6,7 +6,6 @@
package gin package gin
import ( import (
"runtime"
"strings" "strings"
"testing" "testing"
@ -81,13 +80,9 @@ func TestPathCleanMallocs(t *testing.T) {
t.Skip("skipping malloc count in short mode") t.Skip("skipping malloc count in short mode")
} }
if runtime.GOMAXPROCS(0) > 1 {
t.Skip("skipping malloc count; GOMAXPROCS>1")
}
for _, test := range cleanTests { for _, test := range cleanTests {
allocs := testing.AllocsPerRun(100, func() { cleanPath(test.result) }) allocs := testing.AllocsPerRun(100, func() { cleanPath(test.result) })
assert.InDelta(t, 0, allocs, 0.01) assert.EqualValues(t, allocs, 0)
} }
} }

View File

@ -5,6 +5,7 @@
package gin package gin
import ( import (
"fmt"
"net" "net"
"net/http" "net/http"
"os" "os"
@ -25,14 +26,14 @@ func TestPanicClean(t *testing.T) {
panic("Oupps, Houston, we have a problem") panic("Oupps, Houston, we have a problem")
}) })
// RUN // RUN
w := PerformRequest(router, http.MethodGet, "/recovery", w := PerformRequest(router, "GET", "/recovery",
header{ header{
Key: "Host", Key: "Host",
Value: "www.google.com", Value: "www.google.com",
}, },
header{ header{
Key: "Authorization", Key: "Authorization",
Value: "Bearer " + password, Value: fmt.Sprintf("Bearer %s", password),
}, },
header{ header{
Key: "Content-Type", Key: "Content-Type",
@ -55,7 +56,7 @@ func TestPanicInHandler(t *testing.T) {
panic("Oupps, Houston, we have a problem") panic("Oupps, Houston, we have a problem")
}) })
// RUN // RUN
w := PerformRequest(router, http.MethodGet, "/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")
@ -66,7 +67,7 @@ func TestPanicInHandler(t *testing.T) {
// Debug mode prints the request // Debug mode prints the request
SetMode(DebugMode) SetMode(DebugMode)
// RUN // RUN
w = PerformRequest(router, http.MethodGet, "/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")
@ -83,7 +84,7 @@ func TestPanicWithAbort(t *testing.T) {
panic("Oupps, Houston, we have a problem") panic("Oupps, Houston, we have a problem")
}) })
// RUN // RUN
w := PerformRequest(router, http.MethodGet, "/recovery") w := PerformRequest(router, "GET", "/recovery")
// TEST // TEST
assert.Equal(t, http.StatusBadRequest, w.Code) assert.Equal(t, http.StatusBadRequest, w.Code)
} }
@ -134,7 +135,7 @@ func TestPanicWithBrokenPipe(t *testing.T) {
panic(e) panic(e)
}) })
// RUN // RUN
w := PerformRequest(router, http.MethodGet, "/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)
@ -155,7 +156,7 @@ func TestCustomRecoveryWithWriter(t *testing.T) {
panic("Oupps, Houston, we have a problem") panic("Oupps, Houston, we have a problem")
}) })
// RUN // RUN
w := PerformRequest(router, http.MethodGet, "/recovery") w := PerformRequest(router, "GET", "/recovery")
// TEST // TEST
assert.Equal(t, http.StatusBadRequest, w.Code) assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "panic recovered") assert.Contains(t, buffer.String(), "panic recovered")
@ -166,7 +167,7 @@ func TestCustomRecoveryWithWriter(t *testing.T) {
// Debug mode prints the request // Debug mode prints the request
SetMode(DebugMode) SetMode(DebugMode)
// RUN // RUN
w = PerformRequest(router, http.MethodGet, "/recovery") w = PerformRequest(router, "GET", "/recovery")
// TEST // TEST
assert.Equal(t, http.StatusBadRequest, w.Code) assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "GET /recovery") assert.Contains(t, buffer.String(), "GET /recovery")
@ -190,7 +191,7 @@ func TestCustomRecovery(t *testing.T) {
panic("Oupps, Houston, we have a problem") panic("Oupps, Houston, we have a problem")
}) })
// RUN // RUN
w := PerformRequest(router, http.MethodGet, "/recovery") w := PerformRequest(router, "GET", "/recovery")
// TEST // TEST
assert.Equal(t, http.StatusBadRequest, w.Code) assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "panic recovered") assert.Contains(t, buffer.String(), "panic recovered")
@ -201,7 +202,7 @@ func TestCustomRecovery(t *testing.T) {
// Debug mode prints the request // Debug mode prints the request
SetMode(DebugMode) SetMode(DebugMode)
// RUN // RUN
w = PerformRequest(router, http.MethodGet, "/recovery") w = PerformRequest(router, "GET", "/recovery")
// TEST // TEST
assert.Equal(t, http.StatusBadRequest, w.Code) assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "GET /recovery") assert.Contains(t, buffer.String(), "GET /recovery")
@ -225,7 +226,7 @@ func TestRecoveryWithWriterWithCustomRecovery(t *testing.T) {
panic("Oupps, Houston, we have a problem") panic("Oupps, Houston, we have a problem")
}) })
// RUN // RUN
w := PerformRequest(router, http.MethodGet, "/recovery") w := PerformRequest(router, "GET", "/recovery")
// TEST // TEST
assert.Equal(t, http.StatusBadRequest, w.Code) assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "panic recovered") assert.Contains(t, buffer.String(), "panic recovered")
@ -236,7 +237,7 @@ func TestRecoveryWithWriterWithCustomRecovery(t *testing.T) {
// Debug mode prints the request // Debug mode prints the request
SetMode(DebugMode) SetMode(DebugMode)
// RUN // RUN
w = PerformRequest(router, http.MethodGet, "/recovery") w = PerformRequest(router, "GET", "/recovery")
// TEST // TEST
assert.Equal(t, http.StatusBadRequest, w.Code) assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "GET /recovery") assert.Contains(t, buffer.String(), "GET /recovery")

View File

@ -15,22 +15,22 @@ type Render interface {
} }
var ( var (
_ Render = (*JSON)(nil) _ Render = JSON{}
_ Render = (*IndentedJSON)(nil) _ Render = IndentedJSON{}
_ Render = (*SecureJSON)(nil) _ Render = SecureJSON{}
_ Render = (*JsonpJSON)(nil) _ Render = JsonpJSON{}
_ Render = (*XML)(nil) _ Render = XML{}
_ Render = (*String)(nil) _ Render = String{}
_ Render = (*Redirect)(nil) _ Render = Redirect{}
_ Render = (*Data)(nil) _ Render = Data{}
_ Render = (*HTML)(nil) _ Render = HTML{}
_ HTMLRender = (*HTMLDebug)(nil) _ HTMLRender = HTMLDebug{}
_ HTMLRender = (*HTMLProduction)(nil) _ HTMLRender = HTMLProduction{}
_ Render = (*YAML)(nil) _ Render = YAML{}
_ Render = (*Reader)(nil) _ Render = Reader{}
_ Render = (*AsciiJSON)(nil) _ Render = AsciiJSON{}
_ Render = (*ProtoBuf)(nil) _ Render = ProtoBuf{}
_ Render = (*TOML)(nil) _ Render = TOML{}
) )
func writeContentType(w http.ResponseWriter, value []string) { func writeContentType(w http.ResponseWriter, value []string) {

View File

@ -12,7 +12,6 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ugorji/go/codec" "github.com/ugorji/go/codec"
) )
@ -30,7 +29,7 @@ func TestRenderMsgPack(t *testing.T) {
err := (MsgPack{data}).Render(w) err := (MsgPack{data}).Render(w)
require.NoError(t, err) assert.NoError(t, err)
h := new(codec.MsgpackHandle) h := new(codec.MsgpackHandle)
assert.NotNil(t, h) assert.NotNil(t, h)
@ -38,7 +37,7 @@ func TestRenderMsgPack(t *testing.T) {
assert.NotNil(t, buf) assert.NotNil(t, buf)
err = codec.NewEncoder(buf, h).Encode(data) err = codec.NewEncoder(buf, h).Encode(data)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, w.Body.String(), buf.String()) assert.Equal(t, w.Body.String(), buf.String())
assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type"))
} }

View File

@ -18,7 +18,6 @@ import (
"github.com/gin-gonic/gin/internal/json" "github.com/gin-gonic/gin/internal/json"
testdata "github.com/gin-gonic/gin/testdata/protoexample" testdata "github.com/gin-gonic/gin/testdata/protoexample"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
) )
@ -37,7 +36,7 @@ func TestRenderJSON(t *testing.T) {
err := (JSON{data}).Render(w) err := (JSON{data}).Render(w)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"\\u003cb\\u003e\"}", w.Body.String()) assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"\\u003cb\\u003e\"}", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
} }
@ -47,7 +46,7 @@ func TestRenderJSONError(t *testing.T) {
data := make(chan int) data := make(chan int)
// json: unsupported type: chan int // json: unsupported type: chan int
require.Error(t, (JSON{data}).Render(w)) assert.Error(t, (JSON{data}).Render(w))
} }
func TestRenderIndentedJSON(t *testing.T) { func TestRenderIndentedJSON(t *testing.T) {
@ -59,7 +58,7 @@ func TestRenderIndentedJSON(t *testing.T) {
err := (IndentedJSON{data}).Render(w) err := (IndentedJSON{data}).Render(w)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "{\n \"bar\": \"foo\",\n \"foo\": \"bar\"\n}", w.Body.String()) assert.Equal(t, "{\n \"bar\": \"foo\",\n \"foo\": \"bar\"\n}", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
} }
@ -70,7 +69,7 @@ func TestRenderIndentedJSONPanics(t *testing.T) {
// json: unsupported type: chan int // json: unsupported type: chan int
err := (IndentedJSON{data}).Render(w) err := (IndentedJSON{data}).Render(w)
require.Error(t, err) assert.Error(t, err)
} }
func TestRenderSecureJSON(t *testing.T) { func TestRenderSecureJSON(t *testing.T) {
@ -84,7 +83,7 @@ func TestRenderSecureJSON(t *testing.T) {
err1 := (SecureJSON{"while(1);", data}).Render(w1) err1 := (SecureJSON{"while(1);", data}).Render(w1)
require.NoError(t, err1) assert.NoError(t, err1)
assert.Equal(t, "{\"foo\":\"bar\"}", w1.Body.String()) assert.Equal(t, "{\"foo\":\"bar\"}", w1.Body.String())
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"))
@ -96,7 +95,7 @@ func TestRenderSecureJSON(t *testing.T) {
}} }}
err2 := (SecureJSON{"while(1);", datas}).Render(w2) err2 := (SecureJSON{"while(1);", datas}).Render(w2)
require.NoError(t, err2) assert.NoError(t, err2)
assert.Equal(t, "while(1);[{\"foo\":\"bar\"},{\"bar\":\"foo\"}]", w2.Body.String()) assert.Equal(t, "while(1);[{\"foo\":\"bar\"},{\"bar\":\"foo\"}]", w2.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w2.Header().Get("Content-Type")) assert.Equal(t, "application/json; charset=utf-8", w2.Header().Get("Content-Type"))
} }
@ -107,7 +106,7 @@ func TestRenderSecureJSONFail(t *testing.T) {
// json: unsupported type: chan int // json: unsupported type: chan int
err := (SecureJSON{"while(1);", data}).Render(w) err := (SecureJSON{"while(1);", data}).Render(w)
require.Error(t, err) assert.Error(t, err)
} }
func TestRenderJsonpJSON(t *testing.T) { func TestRenderJsonpJSON(t *testing.T) {
@ -121,7 +120,7 @@ func TestRenderJsonpJSON(t *testing.T) {
err1 := (JsonpJSON{"x", data}).Render(w1) err1 := (JsonpJSON{"x", data}).Render(w1)
require.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"))
@ -133,7 +132,7 @@ func TestRenderJsonpJSON(t *testing.T) {
}} }}
err2 := (JsonpJSON{"x", datas}).Render(w2) err2 := (JsonpJSON{"x", datas}).Render(w2)
require.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"))
} }
@ -192,7 +191,7 @@ func TestRenderJsonpJSONError2(t *testing.T) {
assert.Equal(t, "application/javascript; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "application/javascript; charset=utf-8", w.Header().Get("Content-Type"))
e := (JsonpJSON{"", data}).Render(w) e := (JsonpJSON{"", data}).Render(w)
require.NoError(t, e) assert.NoError(t, e)
assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String()) assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String())
assert.Equal(t, "application/javascript; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "application/javascript; charset=utf-8", w.Header().Get("Content-Type"))
@ -204,7 +203,7 @@ func TestRenderJsonpJSONFail(t *testing.T) {
// json: unsupported type: chan int // json: unsupported type: chan int
err := (JsonpJSON{"x", data}).Render(w) err := (JsonpJSON{"x", data}).Render(w)
require.Error(t, err) assert.Error(t, err)
} }
func TestRenderAsciiJSON(t *testing.T) { func TestRenderAsciiJSON(t *testing.T) {
@ -216,7 +215,7 @@ func TestRenderAsciiJSON(t *testing.T) {
err := (AsciiJSON{data1}).Render(w1) err := (AsciiJSON{data1}).Render(w1)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "{\"lang\":\"GO\\u8bed\\u8a00\",\"tag\":\"\\u003cbr\\u003e\"}", w1.Body.String()) assert.Equal(t, "{\"lang\":\"GO\\u8bed\\u8a00\",\"tag\":\"\\u003cbr\\u003e\"}", w1.Body.String())
assert.Equal(t, "application/json", w1.Header().Get("Content-Type")) assert.Equal(t, "application/json", w1.Header().Get("Content-Type"))
@ -224,7 +223,7 @@ func TestRenderAsciiJSON(t *testing.T) {
data2 := 3.1415926 data2 := 3.1415926
err = (AsciiJSON{data2}).Render(w2) err = (AsciiJSON{data2}).Render(w2)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "3.1415926", w2.Body.String()) assert.Equal(t, "3.1415926", w2.Body.String())
} }
@ -233,7 +232,7 @@ func TestRenderAsciiJSONFail(t *testing.T) {
data := make(chan int) data := make(chan int)
// json: unsupported type: chan int // json: unsupported type: chan int
require.Error(t, (AsciiJSON{data}).Render(w)) assert.Error(t, (AsciiJSON{data}).Render(w))
} }
func TestRenderPureJSON(t *testing.T) { func TestRenderPureJSON(t *testing.T) {
@ -243,7 +242,7 @@ func TestRenderPureJSON(t *testing.T) {
"html": "<b>", "html": "<b>",
} }
err := (PureJSON{data}).Render(w) err := (PureJSON{data}).Render(w)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"<b>\"}\n", w.Body.String()) assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"<b>\"}\n", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
} }
@ -281,12 +280,12 @@ b:
d: [3, 4] d: [3, 4]
` `
(YAML{data}).WriteContentType(w) (YAML{data}).WriteContentType(w)
assert.Equal(t, "application/yaml; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "application/x-yaml; charset=utf-8", w.Header().Get("Content-Type"))
err := (YAML{data}).Render(w) err := (YAML{data}).Render(w)
require.NoError(t, err) assert.NoError(t, err)
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, "|4-\n a : Easy!\n b:\n \tc: 2\n \td: [3, 4]\n \t\n", w.Body.String())
assert.Equal(t, "application/yaml; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "application/x-yaml; charset=utf-8", w.Header().Get("Content-Type"))
} }
type fail struct{} type fail struct{}
@ -299,7 +298,7 @@ func (ft *fail) MarshalYAML() (any, error) {
func TestRenderYAMLFail(t *testing.T) { func TestRenderYAMLFail(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
err := (YAML{&fail{}}).Render(w) err := (YAML{&fail{}}).Render(w)
require.Error(t, err) assert.Error(t, err)
} }
func TestRenderTOML(t *testing.T) { func TestRenderTOML(t *testing.T) {
@ -312,7 +311,7 @@ func TestRenderTOML(t *testing.T) {
assert.Equal(t, "application/toml; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "application/toml; charset=utf-8", w.Header().Get("Content-Type"))
err := (TOML{data}).Render(w) err := (TOML{data}).Render(w)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "foo = 'bar'\nhtml = '<b>'\n", w.Body.String()) assert.Equal(t, "foo = 'bar'\nhtml = '<b>'\n", w.Body.String())
assert.Equal(t, "application/toml; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "application/toml; charset=utf-8", w.Header().Get("Content-Type"))
} }
@ -320,7 +319,7 @@ func TestRenderTOML(t *testing.T) {
func TestRenderTOMLFail(t *testing.T) { func TestRenderTOMLFail(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
err := (TOML{net.IPv4bcast}).Render(w) err := (TOML{net.IPv4bcast}).Render(w)
require.Error(t, err) assert.Error(t, err)
} }
// test Protobuf rendering // test Protobuf rendering
@ -335,12 +334,12 @@ func TestRenderProtoBuf(t *testing.T) {
(ProtoBuf{data}).WriteContentType(w) (ProtoBuf{data}).WriteContentType(w)
protoData, err := proto.Marshal(data) protoData, err := proto.Marshal(data)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "application/x-protobuf", w.Header().Get("Content-Type")) assert.Equal(t, "application/x-protobuf", w.Header().Get("Content-Type"))
err = (ProtoBuf{data}).Render(w) err = (ProtoBuf{data}).Render(w)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, string(protoData), w.Body.String()) assert.Equal(t, string(protoData), w.Body.String())
assert.Equal(t, "application/x-protobuf", w.Header().Get("Content-Type")) assert.Equal(t, "application/x-protobuf", w.Header().Get("Content-Type"))
} }
@ -349,7 +348,7 @@ func TestRenderProtoBufFail(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
data := &testdata.Test{} data := &testdata.Test{}
err := (ProtoBuf{data}).Render(w) err := (ProtoBuf{data}).Render(w)
require.Error(t, err) assert.Error(t, err)
} }
func TestRenderXML(t *testing.T) { func TestRenderXML(t *testing.T) {
@ -363,14 +362,14 @@ func TestRenderXML(t *testing.T) {
err := (XML{data}).Render(w) err := (XML{data}).Render(w)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "<map><foo>bar</foo></map>", w.Body.String()) assert.Equal(t, "<map><foo>bar</foo></map>", w.Body.String())
assert.Equal(t, "application/xml; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "application/xml; charset=utf-8", w.Header().Get("Content-Type"))
} }
func TestRenderRedirect(t *testing.T) { func TestRenderRedirect(t *testing.T) {
req, err := http.NewRequest(http.MethodGet, "/test-redirect", nil) req, err := http.NewRequest("GET", "/test-redirect", nil)
require.NoError(t, err) assert.NoError(t, err)
data1 := Redirect{ data1 := Redirect{
Code: http.StatusMovedPermanently, Code: http.StatusMovedPermanently,
@ -380,7 +379,7 @@ func TestRenderRedirect(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
err = data1.Render(w) err = data1.Render(w)
require.NoError(t, err) assert.NoError(t, err)
data2 := Redirect{ data2 := Redirect{
Code: http.StatusOK, Code: http.StatusOK,
@ -391,7 +390,7 @@ func TestRenderRedirect(t *testing.T) {
w = httptest.NewRecorder() w = httptest.NewRecorder()
assert.PanicsWithValue(t, "Cannot redirect with status code 200", func() { assert.PanicsWithValue(t, "Cannot redirect with status code 200", func() {
err := data2.Render(w) err := data2.Render(w)
require.NoError(t, err) assert.NoError(t, err)
}) })
data3 := Redirect{ data3 := Redirect{
@ -402,7 +401,7 @@ func TestRenderRedirect(t *testing.T) {
w = httptest.NewRecorder() w = httptest.NewRecorder()
err = data3.Render(w) err = data3.Render(w)
require.NoError(t, err) assert.NoError(t, err)
// only improve coverage // only improve coverage
data2.WriteContentType(w) data2.WriteContentType(w)
@ -417,7 +416,7 @@ func TestRenderData(t *testing.T) {
Data: data, Data: data,
}).Render(w) }).Render(w)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "#!PNG some raw data", w.Body.String()) assert.Equal(t, "#!PNG some raw data", w.Body.String())
assert.Equal(t, "image/png", w.Header().Get("Content-Type")) assert.Equal(t, "image/png", w.Header().Get("Content-Type"))
} }
@ -436,7 +435,7 @@ func TestRenderString(t *testing.T) {
Data: []any{"manu", 2}, Data: []any{"manu", 2},
}).Render(w) }).Render(w)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "hola manu 2", w.Body.String()) assert.Equal(t, "hola manu 2", w.Body.String())
assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
} }
@ -449,7 +448,7 @@ func TestRenderStringLenZero(t *testing.T) {
Data: []any{}, Data: []any{},
}).Render(w) }).Render(w)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "hola %s %d", w.Body.String()) assert.Equal(t, "hola %s %d", w.Body.String())
assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
} }
@ -465,7 +464,7 @@ func TestRenderHTMLTemplate(t *testing.T) {
err := instance.Render(w) err := instance.Render(w)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "Hello alexandernyquist", w.Body.String()) assert.Equal(t, "Hello alexandernyquist", w.Body.String())
assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type"))
} }
@ -481,7 +480,7 @@ func TestRenderHTMLTemplateEmptyName(t *testing.T) {
err := instance.Render(w) err := instance.Render(w)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "Hello alexandernyquist", w.Body.String()) assert.Equal(t, "Hello alexandernyquist", w.Body.String())
assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type"))
} }
@ -500,7 +499,7 @@ func TestRenderHTMLDebugFiles(t *testing.T) {
err := instance.Render(w) err := instance.Render(w)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "<h1>Hello thinkerou</h1>", w.Body.String()) assert.Equal(t, "<h1>Hello thinkerou</h1>", w.Body.String())
assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type"))
} }
@ -519,7 +518,7 @@ func TestRenderHTMLDebugGlob(t *testing.T) {
err := instance.Render(w) err := instance.Render(w)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "<h1>Hello thinkerou</h1>", w.Body.String()) assert.Equal(t, "<h1>Hello thinkerou</h1>", w.Body.String())
assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type"))
} }
@ -549,7 +548,7 @@ func TestRenderReader(t *testing.T) {
Headers: headers, Headers: headers,
}).Render(w) }).Render(w)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, body, w.Body.String()) assert.Equal(t, body, w.Body.String())
assert.Equal(t, "image/png", w.Header().Get("Content-Type")) assert.Equal(t, "image/png", w.Header().Get("Content-Type"))
assert.Equal(t, strconv.Itoa(len(body)), w.Header().Get("Content-Length")) assert.Equal(t, strconv.Itoa(len(body)), w.Header().Get("Content-Length"))
@ -572,23 +571,10 @@ func TestRenderReaderNoContentLength(t *testing.T) {
Headers: headers, Headers: headers,
}).Render(w) }).Render(w)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, body, w.Body.String()) assert.Equal(t, body, w.Body.String())
assert.Equal(t, "image/png", w.Header().Get("Content-Type")) assert.Equal(t, "image/png", w.Header().Get("Content-Type"))
assert.NotContains(t, "Content-Length", w.Header()) assert.NotContains(t, "Content-Length", w.Header())
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 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)
require.Error(t, err)
assert.Equal(t, `write "my-prefix:" error`, err.Error())
}

View File

@ -15,7 +15,7 @@ type YAML struct {
Data any Data any
} }
var yamlContentType = []string{"application/yaml; charset=utf-8"} var yamlContentType = []string{"application/x-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 {

View File

@ -10,7 +10,6 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
// TODO // TODO
@ -96,13 +95,13 @@ func TestResponseWriterWrite(t *testing.T) {
assert.Equal(t, http.StatusOK, w.Status()) assert.Equal(t, http.StatusOK, w.Status())
assert.Equal(t, http.StatusOK, testWriter.Code) assert.Equal(t, http.StatusOK, testWriter.Code)
assert.Equal(t, "hola", testWriter.Body.String()) assert.Equal(t, "hola", testWriter.Body.String())
require.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", testWriter.Body.String()) assert.Equal(t, "hola adios", testWriter.Body.String())
require.NoError(t, err) assert.NoError(t, err)
} }
func TestResponseWriterHijack(t *testing.T) { func TestResponseWriterHijack(t *testing.T) {
@ -113,7 +112,7 @@ func TestResponseWriterHijack(t *testing.T) {
assert.Panics(t, func() { assert.Panics(t, func() {
_, _, err := w.Hijack() _, _, err := w.Hijack()
require.NoError(t, err) assert.NoError(t, err)
}) })
assert.True(t, w.Written()) assert.True(t, w.Written())
@ -136,7 +135,7 @@ func TestResponseWriterFlush(t *testing.T) {
// should return 500 // should return 500
resp, err := http.Get(testServer.URL) resp, err := http.Get(testServer.URL)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, http.StatusInternalServerError, resp.StatusCode) assert.Equal(t, http.StatusInternalServerError, resp.StatusCode)
} }
@ -157,33 +156,3 @@ func TestResponseWriterStatusCode(t *testing.T) {
// status must be 200 although we tried to change it // status must be 200 although we tried to change it
assert.Equal(t, http.StatusOK, w.Status()) 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")
}

View File

@ -218,7 +218,7 @@ 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)
} }

View File

@ -13,7 +13,6 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
type header struct { type header struct {
@ -181,58 +180,58 @@ func TestRouteRedirectTrailingSlash(t *testing.T) {
w = PerformRequest(router, http.MethodGet, "/path2", header{Key: "X-Forwarded-Prefix", Value: "/api"}) w = PerformRequest(router, http.MethodGet, "/path2", header{Key: "X-Forwarded-Prefix", Value: "/api"})
assert.Equal(t, "/api/path2/", w.Header().Get("Location")) assert.Equal(t, "/api/path2/", w.Header().Get("Location"))
assert.Equal(t, http.StatusMovedPermanently, w.Code) assert.Equal(t, 301, w.Code)
w = PerformRequest(router, http.MethodGet, "/path2/", header{Key: "X-Forwarded-Prefix", Value: "/api/"}) w = PerformRequest(router, http.MethodGet, "/path2/", header{Key: "X-Forwarded-Prefix", Value: "/api/"})
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, 200, w.Code)
w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "../../api#?"}) w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "../../api#?"})
assert.Equal(t, "/api/path", w.Header().Get("Location")) assert.Equal(t, "/api/path", w.Header().Get("Location"))
assert.Equal(t, http.StatusMovedPermanently, w.Code) assert.Equal(t, 301, w.Code)
w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "../../api"}) w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "../../api"})
assert.Equal(t, "/api/path", w.Header().Get("Location")) assert.Equal(t, "/api/path", w.Header().Get("Location"))
assert.Equal(t, http.StatusMovedPermanently, w.Code) assert.Equal(t, 301, w.Code)
w = PerformRequest(router, http.MethodGet, "/path2", header{Key: "X-Forwarded-Prefix", Value: "../../api"}) w = PerformRequest(router, http.MethodGet, "/path2", header{Key: "X-Forwarded-Prefix", Value: "../../api"})
assert.Equal(t, "/api/path2/", w.Header().Get("Location")) assert.Equal(t, "/api/path2/", w.Header().Get("Location"))
assert.Equal(t, http.StatusMovedPermanently, w.Code) assert.Equal(t, 301, w.Code)
w = PerformRequest(router, http.MethodGet, "/path2", header{Key: "X-Forwarded-Prefix", Value: "/../../api"}) w = PerformRequest(router, http.MethodGet, "/path2", header{Key: "X-Forwarded-Prefix", Value: "/../../api"})
assert.Equal(t, "/api/path2/", w.Header().Get("Location")) assert.Equal(t, "/api/path2/", w.Header().Get("Location"))
assert.Equal(t, http.StatusMovedPermanently, w.Code) assert.Equal(t, 301, w.Code)
w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "api/../../"}) w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "api/../../"})
assert.Equal(t, "//path", w.Header().Get("Location")) assert.Equal(t, "//path", w.Header().Get("Location"))
assert.Equal(t, http.StatusMovedPermanently, w.Code) assert.Equal(t, 301, w.Code)
w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "api/../../../"}) w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "api/../../../"})
assert.Equal(t, "/path", w.Header().Get("Location")) assert.Equal(t, "/path", w.Header().Get("Location"))
assert.Equal(t, http.StatusMovedPermanently, w.Code) assert.Equal(t, 301, w.Code)
w = PerformRequest(router, http.MethodGet, "/path2", header{Key: "X-Forwarded-Prefix", Value: "../../gin-gonic.com"}) w = PerformRequest(router, http.MethodGet, "/path2", header{Key: "X-Forwarded-Prefix", Value: "../../gin-gonic.com"})
assert.Equal(t, "/gin-goniccom/path2/", w.Header().Get("Location")) assert.Equal(t, "/gin-goniccom/path2/", w.Header().Get("Location"))
assert.Equal(t, http.StatusMovedPermanently, w.Code) assert.Equal(t, 301, w.Code)
w = PerformRequest(router, http.MethodGet, "/path2", header{Key: "X-Forwarded-Prefix", Value: "/../../gin-gonic.com"}) w = PerformRequest(router, http.MethodGet, "/path2", header{Key: "X-Forwarded-Prefix", Value: "/../../gin-gonic.com"})
assert.Equal(t, "/gin-goniccom/path2/", w.Header().Get("Location")) assert.Equal(t, "/gin-goniccom/path2/", w.Header().Get("Location"))
assert.Equal(t, http.StatusMovedPermanently, w.Code) assert.Equal(t, 301, w.Code)
w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "https://gin-gonic.com/#"}) w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "https://gin-gonic.com/#"})
assert.Equal(t, "https/gin-goniccom/https/gin-goniccom/path", w.Header().Get("Location")) assert.Equal(t, "https/gin-goniccom/https/gin-goniccom/path", w.Header().Get("Location"))
assert.Equal(t, http.StatusMovedPermanently, w.Code) assert.Equal(t, 301, w.Code)
w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "#api"}) w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "#api"})
assert.Equal(t, "api/api/path", w.Header().Get("Location")) assert.Equal(t, "api/api/path", w.Header().Get("Location"))
assert.Equal(t, http.StatusMovedPermanently, w.Code) assert.Equal(t, 301, w.Code)
w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "/nor-mal/#?a=1"}) w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "/nor-mal/#?a=1"})
assert.Equal(t, "/nor-mal/a1/path", w.Header().Get("Location")) assert.Equal(t, "/nor-mal/a1/path", w.Header().Get("Location"))
assert.Equal(t, http.StatusMovedPermanently, w.Code) assert.Equal(t, 301, w.Code)
w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "/nor-mal/%2e%2e/"}) w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "/nor-mal/%2e%2e/"})
assert.Equal(t, "/nor-mal/2e2e/path", w.Header().Get("Location")) assert.Equal(t, "/nor-mal/2e2e/path", w.Header().Get("Location"))
assert.Equal(t, http.StatusMovedPermanently, w.Code) assert.Equal(t, 301, w.Code)
router.RedirectTrailingSlash = false router.RedirectTrailingSlash = false
@ -338,45 +337,6 @@ func TestRouteParamsByNameWithExtraSlash(t *testing.T) {
assert.Equal(t, "/is/super/great", wild) assert.Equal(t, "/is/super/great", wild)
} }
// TestRouteParamsNotEmpty tests that context parameters will be set
// even if a route with params/wildcards is registered after the context
// initialisation (which happened in a previous requests).
func TestRouteParamsNotEmpty(t *testing.T) {
name := ""
lastName := ""
wild := ""
router := New()
w := PerformRequest(router, http.MethodGet, "/test/john/smith/is/super/great")
assert.Equal(t, http.StatusNotFound, w.Code)
router.GET("/test/:name/:last_name/*wild", func(c *Context) {
name = c.Params.ByName("name")
lastName = c.Params.ByName("last_name")
var ok bool
wild, ok = c.Params.Get("wild")
assert.True(t, ok)
assert.Equal(t, name, c.Param("name"))
assert.Equal(t, lastName, c.Param("last_name"))
assert.Empty(t, c.Param("wtf"))
assert.Empty(t, c.Params.ByName("wtf"))
wtf, ok := c.Params.Get("wtf")
assert.Empty(t, wtf)
assert.False(t, ok)
})
w = PerformRequest(router, http.MethodGet, "/test/john/smith/is/super/great")
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "john", name)
assert.Equal(t, "smith", lastName)
assert.Equal(t, "/is/super/great", wild)
}
// TestHandleStaticFile - ensure the static file handles properly // TestHandleStaticFile - ensure the static file handles properly
func TestRouteStaticFile(t *testing.T) { func TestRouteStaticFile(t *testing.T) {
// SETUP file // SETUP file
@ -387,7 +347,7 @@ func TestRouteStaticFile(t *testing.T) {
} }
defer os.Remove(f.Name()) defer os.Remove(f.Name())
_, err = f.WriteString("Gin Web Framework") _, err = f.WriteString("Gin Web Framework")
require.NoError(t, err) assert.NoError(t, err)
f.Close() f.Close()
dir, filename := filepath.Split(f.Name()) dir, filename := filepath.Split(f.Name())
@ -422,7 +382,7 @@ func TestRouteStaticFileFS(t *testing.T) {
} }
defer os.Remove(f.Name()) defer os.Remove(f.Name())
_, err = f.WriteString("Gin Web Framework") _, err = f.WriteString("Gin Web Framework")
require.NoError(t, err) assert.NoError(t, err)
f.Close() f.Close()
dir, filename := filepath.Split(f.Name()) dir, filename := filepath.Split(f.Name())
@ -485,7 +445,7 @@ func TestRouterMiddlewareAndStatic(t *testing.T) {
// Content-Type='text/plain; charset=utf-8' when go version <= 1.16, // Content-Type='text/plain; charset=utf-8' when go version <= 1.16,
// else, Content-Type='text/x-go; charset=utf-8' // else, Content-Type='text/x-go; charset=utf-8'
assert.NotEqual(t, "", w.Header().Get("Content-Type")) assert.NotEqual(t, "", w.Header().Get("Content-Type"))
assert.NotEqual(t, "Mon, 02 Jan 2006 15:04:05 MST", w.Header().Get("Last-Modified")) assert.NotEqual(t, w.Header().Get("Last-Modified"), "Mon, 02 Jan 2006 15:04:05 MST")
assert.Equal(t, "Mon, 02 Jan 2006 15:04:05 MST", w.Header().Get("Expires")) assert.Equal(t, "Mon, 02 Jan 2006 15:04:05 MST", w.Header().Get("Expires"))
assert.Equal(t, "Gin Framework", w.Header().Get("x-GIN")) assert.Equal(t, "Gin Framework", w.Header().Get("x-GIN"))
} }
@ -515,18 +475,6 @@ func TestRouteNotAllowedEnabled2(t *testing.T) {
assert.Equal(t, http.StatusMethodNotAllowed, w.Code) assert.Equal(t, http.StatusMethodNotAllowed, w.Code)
} }
func TestRouteNotAllowedEnabled3(t *testing.T) {
router := New()
router.HandleMethodNotAllowed = true
router.GET("/path", func(c *Context) {})
router.POST("/path", func(c *Context) {})
w := PerformRequest(router, http.MethodPut, "/path")
assert.Equal(t, http.StatusMethodNotAllowed, w.Code)
allowed := w.Header().Get("Allow")
assert.Contains(t, allowed, http.MethodGet)
assert.Contains(t, allowed, http.MethodPost)
}
func TestRouteNotAllowedDisabled(t *testing.T) { func TestRouteNotAllowedDisabled(t *testing.T) {
router := New() router := New()
router.HandleMethodNotAllowed = false router.HandleMethodNotAllowed = false
@ -557,10 +505,10 @@ func TestRouterNotFoundWithRemoveExtraSlash(t *testing.T) {
{"/nope", http.StatusNotFound, ""}, // NotFound {"/nope", http.StatusNotFound, ""}, // NotFound
} }
for _, tr := range testRoutes { for _, tr := range testRoutes {
w := PerformRequest(router, http.MethodGet, tr.route) w := PerformRequest(router, "GET", tr.route)
assert.Equal(t, tr.code, w.Code) assert.Equal(t, tr.code, w.Code)
if w.Code != http.StatusNotFound { if w.Code != http.StatusNotFound {
assert.Equal(t, tr.location, w.Header().Get("Location")) assert.Equal(t, tr.location, fmt.Sprint(w.Header().Get("Location")))
} }
} }
} }
@ -590,7 +538,7 @@ func TestRouterNotFound(t *testing.T) {
w := PerformRequest(router, http.MethodGet, tr.route) w := PerformRequest(router, http.MethodGet, tr.route)
assert.Equal(t, tr.code, w.Code) assert.Equal(t, tr.code, w.Code)
if w.Code != http.StatusNotFound { if w.Code != http.StatusNotFound {
assert.Equal(t, tr.location, w.Header().Get("Location")) assert.Equal(t, tr.location, fmt.Sprint(w.Header().Get("Location")))
} }
} }
@ -620,11 +568,11 @@ func TestRouterNotFound(t *testing.T) {
router = New() router = New()
router.NoRoute(func(c *Context) { router.NoRoute(func(c *Context) {
if c.Request.RequestURI == "/login" { if c.Request.RequestURI == "/login" {
c.String(http.StatusOK, "login") c.String(200, "login")
} }
}) })
router.GET("/logout", func(c *Context) { router.GET("/logout", func(c *Context) {
c.String(http.StatusOK, "logout") c.String(200, "logout")
}) })
w = PerformRequest(router, http.MethodGet, "/login") w = PerformRequest(router, http.MethodGet, "/login")
assert.Equal(t, "login", w.Body.String()) assert.Equal(t, "login", w.Body.String())
@ -636,7 +584,7 @@ func TestRouterStaticFSNotFound(t *testing.T) {
router := New() router := New()
router.StaticFS("/", http.FileSystem(http.Dir("/thisreallydoesntexist/"))) router.StaticFS("/", http.FileSystem(http.Dir("/thisreallydoesntexist/")))
router.NoRoute(func(c *Context) { router.NoRoute(func(c *Context) {
c.String(http.StatusNotFound, "non existent") c.String(404, "non existent")
}) })
w := PerformRequest(router, http.MethodGet, "/nonexistent") w := PerformRequest(router, http.MethodGet, "/nonexistent")
@ -719,12 +667,12 @@ func TestRouteRawPathNoUnescape(t *testing.T) {
func TestRouteServeErrorWithWriteHeader(t *testing.T) { func TestRouteServeErrorWithWriteHeader(t *testing.T) {
route := New() route := New()
route.Use(func(c *Context) { route.Use(func(c *Context) {
c.Status(http.StatusMisdirectedRequest) c.Status(421)
c.Next() c.Next()
}) })
w := PerformRequest(route, http.MethodGet, "/NotFound") w := PerformRequest(route, http.MethodGet, "/NotFound")
assert.Equal(t, http.StatusMisdirectedRequest, w.Code) assert.Equal(t, 421, w.Code)
assert.Equal(t, 0, w.Body.Len()) assert.Equal(t, 0, w.Body.Len())
} }
@ -786,6 +734,6 @@ func TestEngineHandleMethodNotAllowedCornerCase(t *testing.T) {
v1.GET("/orgs/:id", handlerTest1) v1.GET("/orgs/:id", handlerTest1)
v1.DELETE("/orgs/:id", handlerTest1) v1.DELETE("/orgs/:id", handlerTest1)
w := PerformRequest(r, http.MethodGet, "/base/v1/user/groups") w := PerformRequest(r, "GET", "/base/v1/user/groups")
assert.Equal(t, http.StatusNotFound, w.Code) assert.Equal(t, http.StatusNotFound, w.Code)
} }

72
tree.go
View File

@ -65,10 +65,17 @@ func (trees methodTrees) get(method string) *node {
return nil return nil
} }
func min(a, b int) int {
if a <= b {
return a
}
return b
}
func longestCommonPrefix(a, b string) int { func longestCommonPrefix(a, b string) int {
i := 0 i := 0
max_ := min(len(a), len(b)) max := min(len(a), len(b))
for i < max_ && a[i] == b[i] { for i < max && a[i] == b[i] {
i++ i++
} }
return i return i
@ -198,7 +205,7 @@ walk:
} }
// Check if a child with the next path byte exists // Check if a child with the next path byte exists
for i, max_ := 0, len(n.indices); i < max_; i++ { for i, max := 0, len(n.indices); i < max; i++ {
if c == n.indices[i] { if c == n.indices[i] {
parentFullPathIndex += len(n.path) parentFullPathIndex += len(n.path)
i = n.incrementChildPrio(i) i = n.incrementChildPrio(i)
@ -262,19 +269,7 @@ walk:
// Returns -1 as index, if no wildcard was found. // Returns -1 as index, if no wildcard was found.
func findWildcard(path string) (wildcard string, i int, valid bool) { func findWildcard(path string) (wildcard string, i int, valid bool) {
// Find start // Find start
escapeColon := false
for start, c := range []byte(path) { for start, c := range []byte(path) {
if escapeColon {
escapeColon = false
if c == ':' {
continue
}
panic("invalid escape string in path '" + path + "'")
}
if c == '\\' {
escapeColon = true
continue
}
// A wildcard starts with ':' (param) or '*' (catch-all) // A wildcard starts with ':' (param) or '*' (catch-all)
if c != ':' && c != '*' { if c != ':' && c != '*' {
continue continue
@ -356,10 +351,7 @@ func (n *node) insertChild(path string, fullPath string, handlers HandlersChain)
} }
if len(n.path) > 0 && n.path[len(n.path)-1] == '/' { if len(n.path) > 0 && n.path[len(n.path)-1] == '/' {
pathSeg := "" pathSeg := strings.SplitN(n.children[0].path, "/", 2)[0]
if len(n.children) != 0 {
pathSeg = strings.SplitN(n.children[0].path, "/", 2)[0]
}
panic("catch-all wildcard '" + path + panic("catch-all wildcard '" + path +
"' in new path '" + fullPath + "' in new path '" + fullPath +
"' conflicts with existing path segment '" + pathSeg + "' conflicts with existing path segment '" + pathSeg +
@ -369,7 +361,7 @@ func (n *node) insertChild(path string, fullPath string, handlers HandlersChain)
// currently fixed width 1 for '/' // currently fixed width 1 for '/'
i-- i--
if i < 0 || path[i] != '/' { if path[i] != '/' {
panic("no / before catch-all in path '" + fullPath + "'") panic("no / before catch-all in path '" + fullPath + "'")
} }
@ -486,7 +478,7 @@ walk: // Outer loop for walking the tree
// We can recommend to redirect to the same URL without a // We can recommend to redirect to the same URL without a
// trailing slash if a leaf exists for that path. // trailing slash if a leaf exists for that path.
value.tsr = path == "/" && n.handlers != nil value.tsr = path == "/" && n.handlers != nil
return value return
} }
// Handle wildcard child, which is always at the end of the array // Handle wildcard child, which is always at the end of the array
@ -505,14 +497,7 @@ walk: // Outer loop for walking the tree
} }
// Save param value // Save param value
if params != nil { if params != nil && cap(*params) > 0 {
// Preallocate capacity if necessary
if cap(*params) < int(globalParamsCount) {
newParams := make(Params, len(*params), globalParamsCount)
copy(newParams, *params)
*params = newParams
}
if value.params == nil { if value.params == nil {
value.params = params value.params = params
} }
@ -541,12 +526,12 @@ walk: // Outer loop for walking the tree
// ... but we can't // ... but we can't
value.tsr = len(path) == end+1 value.tsr = len(path) == end+1
return value return
} }
if value.handlers = n.handlers; value.handlers != nil { if value.handlers = n.handlers; value.handlers != nil {
value.fullPath = n.fullPath value.fullPath = n.fullPath
return value return
} }
if len(n.children) == 1 { if len(n.children) == 1 {
// No handle found. Check if a handle for this path + a // No handle found. Check if a handle for this path + a
@ -554,18 +539,11 @@ walk: // Outer loop for walking the tree
n = n.children[0] n = n.children[0]
value.tsr = (n.path == "/" && n.handlers != nil) || (n.path == "" && n.indices == "/") value.tsr = (n.path == "/" && n.handlers != nil) || (n.path == "" && n.indices == "/")
} }
return value return
case catchAll: case catchAll:
// Save param value // Save param value
if params != nil { if params != nil {
// Preallocate capacity if necessary
if cap(*params) < int(globalParamsCount) {
newParams := make(Params, len(*params), globalParamsCount)
copy(newParams, *params)
*params = newParams
}
if value.params == nil { if value.params == nil {
value.params = params value.params = params
} }
@ -586,7 +564,7 @@ walk: // Outer loop for walking the tree
value.handlers = n.handlers value.handlers = n.handlers
value.fullPath = n.fullPath value.fullPath = n.fullPath
return value return
default: default:
panic("invalid node type") panic("invalid node type")
@ -617,7 +595,7 @@ walk: // Outer loop for walking the tree
// Check if this node has a handle registered. // Check if this node has a handle registered.
if value.handlers = n.handlers; value.handlers != nil { if value.handlers = n.handlers; value.handlers != nil {
value.fullPath = n.fullPath value.fullPath = n.fullPath
return value return
} }
// If there is no handle for this route, but this route has a // If there is no handle for this route, but this route has a
@ -625,12 +603,12 @@ walk: // Outer loop for walking the tree
// additional trailing slash // additional trailing slash
if path == "/" && n.wildChild && n.nType != root { if path == "/" && n.wildChild && n.nType != root {
value.tsr = true value.tsr = true
return value return
} }
if path == "/" && n.nType == static { if path == "/" && n.nType == static {
value.tsr = true value.tsr = true
return value return
} }
// No handle found. Check if a handle for this path + a // No handle found. Check if a handle for this path + a
@ -640,11 +618,11 @@ walk: // Outer loop for walking the tree
n = n.children[i] n = n.children[i]
value.tsr = (len(n.path) == 1 && n.handlers != nil) || value.tsr = (len(n.path) == 1 && n.handlers != nil) ||
(n.nType == catchAll && n.children[0].handlers != nil) (n.nType == catchAll && n.children[0].handlers != nil)
return value return
} }
} }
return value return
} }
// Nothing found. We can recommend to redirect to the same URL with an // Nothing found. We can recommend to redirect to the same URL with an
@ -670,7 +648,7 @@ walk: // Outer loop for walking the tree
} }
} }
return value return
} }
} }
@ -775,7 +753,7 @@ walk: // Outer loop for walking the tree
// Runes are up to 4 byte long, // Runes are up to 4 byte long,
// -4 would definitely be another rune. // -4 would definitely be another rune.
var off int var off int
for max_ := min(npLen, 3); off < max_; off++ { for max := min(npLen, 3); off < max; off++ {
if i := npLen - off; utf8.RuneStart(oldPath[i]) { if i := npLen - off; utf8.RuneStart(oldPath[i]) {
// read rune from cached path // read rune from cached path
rv, _ = utf8.DecodeRuneInString(oldPath[i:]) rv, _ = utf8.DecodeRuneInString(oldPath[i:])

View File

@ -192,7 +192,6 @@ func TestTreeWildcard(t *testing.T) {
"/get/abc/123abg/:param", "/get/abc/123abg/:param",
"/get/abc/123abf/:param", "/get/abc/123abf/:param",
"/get/abc/123abfff/:param", "/get/abc/123abfff/:param",
"/get/abc/escaped_colon/test\\:param",
} }
for _, route := range routes { for _, route := range routes {
tree.addRoute(route, fakeHandler(route)) tree.addRoute(route, fakeHandler(route))
@ -316,7 +315,6 @@ func TestTreeWildcard(t *testing.T) {
{"/get/abc/123abg/test", false, "/get/abc/123abg/:param", Params{Param{Key: "param", Value: "test"}}}, {"/get/abc/123abg/test", false, "/get/abc/123abg/:param", Params{Param{Key: "param", Value: "test"}}},
{"/get/abc/123abf/testss", false, "/get/abc/123abf/:param", Params{Param{Key: "param", Value: "testss"}}}, {"/get/abc/123abf/testss", false, "/get/abc/123abf/:param", Params{Param{Key: "param", Value: "testss"}}},
{"/get/abc/123abfff/te", false, "/get/abc/123abfff/:param", Params{Param{Key: "param", Value: "te"}}}, {"/get/abc/123abfff/te", false, "/get/abc/123abfff/:param", Params{Param{Key: "param", Value: "te"}}},
{"/get/abc/escaped_colon/test\\:param", false, "/get/abc/escaped_colon/test\\:param", nil},
}) })
checkPriorities(t, tree) checkPriorities(t, tree)
@ -419,11 +417,6 @@ func TestTreeWildcardConflict(t *testing.T) {
{"/user_:name", false}, {"/user_:name", false},
{"/id:id", false}, {"/id:id", false},
{"/id/:id", false}, {"/id/:id", false},
{"/static/*file", false},
{"/static/", true},
{"/escape/test\\:d1", false},
{"/escape/test\\:d2", false},
{"/escape/test:param", false},
} }
testRoutes(t, routes) testRoutes(t, routes)
} }
@ -900,9 +893,9 @@ func TestTreeInvalidNodeType(t *testing.T) {
func TestTreeInvalidParamsType(t *testing.T) { func TestTreeInvalidParamsType(t *testing.T) {
tree := &node{} tree := &node{}
// add a child with wildcard tree.wildChild = true
route := "/:path" tree.children = append(tree.children, &node{})
tree.addRoute(route, fakeHandler(route)) tree.children[0].nType = 2
// set invalid Params type // set invalid Params type
params := make(Params, 0) params := make(Params, 0)
@ -911,34 +904,6 @@ func TestTreeInvalidParamsType(t *testing.T) {
tree.getValue("/test", &params, getSkippedNodes(), false) tree.getValue("/test", &params, getSkippedNodes(), false)
} }
func TestTreeExpandParamsCapacity(t *testing.T) {
data := []struct {
path string
}{
{"/:path"},
{"/*path"},
}
for _, item := range data {
tree := &node{}
tree.addRoute(item.path, fakeHandler(item.path))
params := make(Params, 0)
value := tree.getValue("/test", &params, getSkippedNodes(), false)
if value.params == nil {
t.Errorf("Expected %s params to be set, but they weren't", item.path)
continue
}
if len(*value.params) != 1 {
t.Errorf("Wrong number of %s params: got %d, want %d",
item.path, len(*value.params), 1)
continue
}
}
}
func TestTreeWildcardConflictEx(t *testing.T) { func TestTreeWildcardConflictEx(t *testing.T) {
conflicts := [...]struct { conflicts := [...]struct {
route string route string
@ -976,45 +941,3 @@ func TestTreeWildcardConflictEx(t *testing.T) {
} }
} }
} }
func TestTreeInvalidEscape(t *testing.T) {
routes := map[string]bool{
"/r1/r": true,
"/r2/:r": true,
"/r3/\\:r": true,
}
tree := &node{}
for route, valid := range routes {
recv := catchPanic(func() {
tree.addRoute(route, fakeHandler(route))
})
if recv == nil != valid {
t.Fatalf("%s should be %t but got %v", route, valid, recv)
}
}
}
func TestWildcardInvalidSlash(t *testing.T) {
const panicMsgPrefix = "no / before catch-all in path"
routes := map[string]bool{
"/foo/bar": true,
"/foo/x*zy": false,
"/foo/b*r": false,
}
for route, valid := range routes {
tree := &node{}
recv := catchPanic(func() {
tree.addRoute(route, nil)
})
if recv == nil != valid {
t.Fatalf("%s should be %t but got %v", route, valid, recv)
}
if rs, ok := recv.(string); recv != nil && (!ok || !strings.HasPrefix(rs, panicMsgPrefix)) {
t.Fatalf(`"Expected panic "%s" for route '%s', got "%v"`, panicMsgPrefix, route, recv)
}
}
}

View File

@ -29,7 +29,7 @@ type testStruct struct {
} }
func (t *testStruct) ServeHTTP(w http.ResponseWriter, req *http.Request) { func (t *testStruct) ServeHTTP(w http.ResponseWriter, req *http.Request) {
assert.Equal(t.T, http.MethodPost, req.Method) assert.Equal(t.T, "POST", req.Method)
assert.Equal(t.T, "/path", req.URL.Path) assert.Equal(t.T, "/path", req.URL.Path)
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, "hello") fmt.Fprint(w, "hello")
@ -39,17 +39,17 @@ func TestWrap(t *testing.T) {
router := New() router := New()
router.POST("/path", WrapH(&testStruct{t})) router.POST("/path", WrapH(&testStruct{t}))
router.GET("/path2", WrapF(func(w http.ResponseWriter, req *http.Request) { router.GET("/path2", WrapF(func(w http.ResponseWriter, req *http.Request) {
assert.Equal(t, http.MethodGet, req.Method) assert.Equal(t, "GET", req.Method)
assert.Equal(t, "/path2", req.URL.Path) assert.Equal(t, "/path2", req.URL.Path)
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
fmt.Fprint(w, "hola!") fmt.Fprint(w, "hola!")
})) }))
w := PerformRequest(router, http.MethodPost, "/path") w := PerformRequest(router, "POST", "/path")
assert.Equal(t, http.StatusInternalServerError, w.Code) assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.Equal(t, "hello", w.Body.String()) assert.Equal(t, "hello", w.Body.String())
w = PerformRequest(router, http.MethodGet, "/path2") w = PerformRequest(router, "GET", "/path2")
assert.Equal(t, http.StatusBadRequest, w.Code) assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Equal(t, "hola!", w.Body.String()) assert.Equal(t, "hola!", w.Body.String())
} }
@ -119,13 +119,13 @@ func TestBindMiddleware(t *testing.T) {
called = true called = true
value = c.MustGet(BindKey).(*bindTestStruct) value = c.MustGet(BindKey).(*bindTestStruct)
}) })
PerformRequest(router, http.MethodGet, "/?foo=hola&bar=10") PerformRequest(router, "GET", "/?foo=hola&bar=10")
assert.True(t, called) assert.True(t, called)
assert.Equal(t, "hola", value.Foo) assert.Equal(t, "hola", value.Foo)
assert.Equal(t, 10, value.Bar) assert.Equal(t, 10, value.Bar)
called = false called = false
PerformRequest(router, http.MethodGet, "/?foo=hola&bar=1") PerformRequest(router, "GET", "/?foo=hola&bar=1")
assert.False(t, called) assert.False(t, called)
assert.Panics(t, func() { assert.Panics(t, func() {
@ -145,6 +145,6 @@ func TestMarshalXMLforH(t *testing.T) {
} }
func TestIsASCII(t *testing.T) { func TestIsASCII(t *testing.T) {
assert.True(t, isASCII("test")) assert.Equal(t, isASCII("test"), true)
assert.False(t, isASCII("🧡💛💚💙💜")) assert.Equal(t, isASCII("🧡💛💚💙💜"), false)
} }

View File

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