mirror of
https://github.com/gin-gonic/gin.git
synced 2026-07-09 04:02:00 +08:00
Compare commits
49 Commits
7f80bd32c4
...
8eb54b845e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8eb54b845e | ||
|
|
688a429d19 | ||
|
|
0a864884de | ||
|
|
dd33ff7938 | ||
|
|
77d70e5858 | ||
|
|
a9c5b36578 | ||
|
|
e30123ad73 | ||
|
|
3c12d2a80e | ||
|
|
61c2b1c28f | ||
|
|
41d8591eb1 | ||
|
|
848e1cdd0d | ||
|
|
c8af82af15 | ||
|
|
40725d85ba | ||
|
|
c4287b1300 | ||
|
|
8fb3136664 | ||
|
|
674522db91 | ||
|
|
8f7c340577 | ||
|
|
d00e6a5695 | ||
|
|
19f5a13fb4 | ||
|
|
fb09c825e8 | ||
|
|
3d8e288c64 | ||
|
|
2e2bd1f408 | ||
|
|
da67cc1b98 | ||
|
|
ef68fa032c | ||
|
|
b38c59de7f | ||
|
|
cf32d2dcf8 | ||
|
|
4714c2a9a3 | ||
|
|
7a1b655074 | ||
|
|
67c9d4ee11 | ||
|
|
bb82473103 | ||
|
|
255af882db | ||
|
|
71496abe68 | ||
|
|
0eb99493c2 | ||
|
|
afa0c31d97 | ||
|
|
56fccc39ec | ||
|
|
3319038418 | ||
|
|
49e9137c68 | ||
|
|
1b53a47790 | ||
|
|
3afff295a2 | ||
|
|
8763f33c65 | ||
|
|
e737e3e267 | ||
|
|
4ccfa7c275 | ||
|
|
90cf460269 | ||
|
|
ebe5e2a6bf | ||
|
|
733ee094fc | ||
|
|
a4baac6e5e | ||
|
|
1eb827240e | ||
|
|
3b28645dc9 | ||
|
|
c3c8620a7f |
18
.github/workflows/gin.yml
vendored
18
.github/workflows/gin.yml
vendored
@ -24,18 +24,24 @@ jobs:
|
||||
with:
|
||||
go-version: "^1"
|
||||
- name: Setup golangci-lint
|
||||
uses: golangci/golangci-lint-action@v6
|
||||
uses: golangci/golangci-lint-action@v8
|
||||
with:
|
||||
version: v1.61.0
|
||||
version: v2.1.6
|
||||
args: --verbose
|
||||
test:
|
||||
needs: lint
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
go: ["1.22", "1.23"]
|
||||
go: ["1.23", "1.24"]
|
||||
test-tags:
|
||||
["", "-tags nomsgpack", '-tags "sonic avx"', "-tags go_json", "-race"]
|
||||
[
|
||||
"",
|
||||
"-tags nomsgpack",
|
||||
'--ldflags="-checklinkname=0" -tags sonic',
|
||||
"-tags go_json",
|
||||
"-race",
|
||||
]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
go-build: ~/.cache/go-build
|
||||
@ -75,7 +81,3 @@ jobs:
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
flags: ${{ matrix.os }},go-${{ matrix.go }},${{ matrix.test-tags }}
|
||||
|
||||
- name: Format
|
||||
if: matrix.go-version == '1.22.x'
|
||||
run: diff -u <(echo -n) <(gofmt -d .)
|
||||
|
||||
126
.golangci.yml
126
.golangci.yml
@ -1,16 +1,11 @@
|
||||
run:
|
||||
timeout: 5m
|
||||
version: "2"
|
||||
linters:
|
||||
enable:
|
||||
- asciicheck
|
||||
- copyloopvar
|
||||
- dogsled
|
||||
- durationcheck
|
||||
- errcheck
|
||||
- errorlint
|
||||
- exportloopref
|
||||
- gci
|
||||
- gofmt
|
||||
- goimports
|
||||
- gosec
|
||||
- misspell
|
||||
- nakedret
|
||||
@ -21,52 +16,71 @@ linters:
|
||||
- testifylint
|
||||
- usestdlibvars
|
||||
- wastedassign
|
||||
|
||||
linters-settings:
|
||||
gosec:
|
||||
# To select a subset of rules to run.
|
||||
# Available rules: https://github.com/securego/gosec#available-rules
|
||||
# Default: [] - means include all rules
|
||||
includes:
|
||||
- G102
|
||||
- G106
|
||||
- G108
|
||||
- G109
|
||||
- G111
|
||||
- G112
|
||||
- G201
|
||||
- G203
|
||||
perfsprint:
|
||||
err-error: true
|
||||
errorf: true
|
||||
fiximports: true
|
||||
int-conversion: true
|
||||
sprintf1: true
|
||||
strconcat: true
|
||||
testifylint:
|
||||
enable-all: true
|
||||
|
||||
issues:
|
||||
exclude-rules:
|
||||
- linters:
|
||||
- structcheck
|
||||
- unused
|
||||
text: "`data` is unused"
|
||||
- linters:
|
||||
- staticcheck
|
||||
text: "SA1019:"
|
||||
- linters:
|
||||
- revive
|
||||
text: "var-naming:"
|
||||
- linters:
|
||||
- revive
|
||||
text: "exported:"
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- gosec # security is not make sense in tests
|
||||
- linters:
|
||||
- revive
|
||||
path: _test\.go
|
||||
- path: gin.go
|
||||
linters:
|
||||
- gci
|
||||
settings:
|
||||
gosec:
|
||||
includes:
|
||||
- G102
|
||||
- G106
|
||||
- G108
|
||||
- G109
|
||||
- G111
|
||||
- G112
|
||||
- G201
|
||||
- G203
|
||||
perfsprint:
|
||||
int-conversion: true
|
||||
err-error: true
|
||||
errorf: true
|
||||
sprintf1: true
|
||||
strconcat: true
|
||||
testifylint:
|
||||
enable-all: true
|
||||
exclusions:
|
||||
generated: lax
|
||||
presets:
|
||||
- comments
|
||||
- common-false-positives
|
||||
- legacy
|
||||
- std-error-handling
|
||||
rules:
|
||||
- linters:
|
||||
- structcheck
|
||||
- unused
|
||||
text: '`data` is unused'
|
||||
- linters:
|
||||
- staticcheck
|
||||
text: 'SA1019:'
|
||||
- linters:
|
||||
- revive
|
||||
text: 'var-naming:'
|
||||
- linters:
|
||||
- revive
|
||||
text: 'exported:'
|
||||
- linters:
|
||||
- gosec
|
||||
path: _test\.go
|
||||
- linters:
|
||||
- revive
|
||||
path: _test\.go
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
formatters:
|
||||
enable:
|
||||
- gci
|
||||
- gofmt
|
||||
- gofumpt
|
||||
- goimports
|
||||
settings:
|
||||
gofmt:
|
||||
rewrite-rules:
|
||||
- pattern: 'interface{}'
|
||||
replacement: 'any'
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
- gin.go
|
||||
|
||||
@ -488,7 +488,7 @@
|
||||
- [FIX] Refactor render
|
||||
- [FIX] Reworked tests
|
||||
- [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)
|
||||
|
||||
## Gin 1.1.4
|
||||
|
||||
@ -30,7 +30,7 @@ If you need performance and good productivity, you will love Gin.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Gin requires [Go](https://go.dev/) version [1.22](https://go.dev/doc/devel/release#go1.22.0) or above.
|
||||
Gin requires [Go](https://go.dev/) version [1.23](https://go.dev/doc/devel/release#go1.23.0) or above.
|
||||
|
||||
### Getting Gin
|
||||
|
||||
@ -73,7 +73,7 @@ func main() {
|
||||
To run the code, use the `go run` command, like:
|
||||
|
||||
```sh
|
||||
$ go run example.go
|
||||
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!
|
||||
@ -94,7 +94,7 @@ See the [API documentation on go.dev](https://pkg.go.dev/github.com/gin-gonic/gi
|
||||
|
||||
The documentation is also available on [gin-gonic.com](https://gin-gonic.com) in several languages:
|
||||
|
||||
- [English](https://gin-gonic.com/docs/)
|
||||
- [English](https://gin-gonic.com/en/docs/)
|
||||
- [简体中文](https://gin-gonic.com/zh-cn/docs/)
|
||||
- [繁體中文](https://gin-gonic.com/zh-tw/docs/)
|
||||
- [日本語](https://gin-gonic.com/ja/docs/)
|
||||
@ -103,6 +103,7 @@ The documentation is also available on [gin-gonic.com](https://gin-gonic.com) in
|
||||
- [Turkish](https://gin-gonic.com/tr/docs/)
|
||||
- [Persian](https://gin-gonic.com/fa/docs/)
|
||||
- [Português](https://gin-gonic.com/pt/docs/)
|
||||
- [Russian](https://gin-gonic.com/ru/docs/)
|
||||
|
||||
### Articles
|
||||
|
||||
@ -152,7 +153,7 @@ Gin uses a custom version of [HttpRouter](https://github.com/julienschmidt/httpr
|
||||
|
||||
## Middleware
|
||||
|
||||
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) and [gin-gonic/contrib](https://github.com/gin-gonic/contrib).
|
||||
|
||||
## Uses
|
||||
|
||||
|
||||
@ -51,8 +51,6 @@ type FooBarFileStruct struct {
|
||||
type FooBarFileFailStruct struct {
|
||||
FooBarStruct
|
||||
File *multipart.FileHeader `invalid_name:"file" binding:"required"`
|
||||
// for unexport test
|
||||
data *multipart.FileHeader `form:"data" binding:"required"`
|
||||
}
|
||||
|
||||
type FooDefaultBarStruct struct {
|
||||
@ -69,15 +67,19 @@ type FooStructDisallowUnknownFields struct {
|
||||
}
|
||||
|
||||
type FooBarStructForTimeType struct {
|
||||
TimeFoo time.Time `form:"time_foo" time_format:"2006-01-02" time_utc:"1" time_location:"Asia/Chongqing"`
|
||||
TimeBar time.Time `form:"time_bar" time_format:"2006-01-02" time_utc:"1"`
|
||||
CreateTime time.Time `form:"createTime" time_format:"unixNano"`
|
||||
UnixTime time.Time `form:"unixTime" time_format:"unix"`
|
||||
TimeFoo time.Time `form:"time_foo" time_format:"2006-01-02" time_utc:"1" time_location:"Asia/Chongqing"`
|
||||
TimeBar time.Time `form:"time_bar" time_format:"2006-01-02" time_utc:"1"`
|
||||
CreateTime time.Time `form:"createTime" time_format:"unixNano"`
|
||||
UnixTime time.Time `form:"unixTime" time_format:"unix"`
|
||||
UnixMilliTime time.Time `form:"unixMilliTime" time_format:"unixmilli"`
|
||||
UnixMicroTime time.Time `form:"unixMicroTime" time_format:"uNiXmiCrO"`
|
||||
}
|
||||
|
||||
type FooStructForTimeTypeNotUnixFormat struct {
|
||||
CreateTime time.Time `form:"createTime" time_format:"unixNano"`
|
||||
UnixTime time.Time `form:"unixTime" time_format:"unix"`
|
||||
CreateTime time.Time `form:"createTime" time_format:"unixNano"`
|
||||
UnixTime time.Time `form:"unixTime" time_format:"unix"`
|
||||
UnixMilliTime time.Time `form:"unixMilliTime" time_format:"unixMilli"`
|
||||
UnixMicroTime time.Time `form:"unixMicroTime" time_format:"unixMicro"`
|
||||
}
|
||||
|
||||
type FooStructForTimeTypeNotFormat struct {
|
||||
@ -265,10 +267,10 @@ func TestBindingFormDefaultValue2(t *testing.T) {
|
||||
func TestBindingFormForTime(t *testing.T) {
|
||||
testFormBindingForTime(t, http.MethodPost,
|
||||
"/", "/",
|
||||
"time_foo=2017-11-15&time_bar=&createTime=1562400033000000123&unixTime=1562400033", "bar2=foo")
|
||||
"time_foo=2017-11-15&time_bar=&createTime=1562400033000000123&unixTime=1562400033&unixMilliTime=1562400033001&unixMicroTime=1562400033000012", "bar2=foo")
|
||||
testFormBindingForTimeNotUnixFormat(t, http.MethodPost,
|
||||
"/", "/",
|
||||
"time_foo=2017-11-15&createTime=bad&unixTime=bad", "bar2=foo")
|
||||
"time_foo=2017-11-15&createTime=bad&unixTime=bad&unixMilliTime=bad&unixMicroTime=bad", "bar2=foo")
|
||||
testFormBindingForTimeNotFormat(t, http.MethodPost,
|
||||
"/", "/",
|
||||
"time_foo=2017-11-15", "bar2=foo")
|
||||
@ -282,11 +284,11 @@ func TestBindingFormForTime(t *testing.T) {
|
||||
|
||||
func TestBindingFormForTime2(t *testing.T) {
|
||||
testFormBindingForTime(t, http.MethodGet,
|
||||
"/?time_foo=2017-11-15&time_bar=&createTime=1562400033000000123&unixTime=1562400033", "/?bar2=foo",
|
||||
"/?time_foo=2017-11-15&time_bar=&createTime=1562400033000000123&unixTime=1562400033&unixMilliTime=1562400033001&unixMicroTime=1562400033000012", "/?bar2=foo",
|
||||
"", "")
|
||||
testFormBindingForTimeNotUnixFormat(t, http.MethodPost,
|
||||
"/", "/",
|
||||
"time_foo=2017-11-15&createTime=bad&unixTime=bad", "bar2=foo")
|
||||
"time_foo=2017-11-15&createTime=bad&unixTime=bad&unixMilliTime=bad&unixMicroTime=bad", "bar2=foo")
|
||||
testFormBindingForTimeNotFormat(t, http.MethodGet,
|
||||
"/?time_foo=2017-11-15", "/?bar2=foo",
|
||||
"", "")
|
||||
@ -952,6 +954,8 @@ func testFormBindingForTime(t *testing.T, method, path, badPath, body, badBody s
|
||||
assert.Equal(t, "UTC", obj.TimeBar.Location().String())
|
||||
assert.Equal(t, int64(1562400033000000123), obj.CreateTime.UnixNano())
|
||||
assert.Equal(t, int64(1562400033), obj.UnixTime.Unix())
|
||||
assert.Equal(t, int64(1562400033001), obj.UnixMilliTime.UnixMilli())
|
||||
assert.Equal(t, int64(1562400033000012), obj.UnixMicroTime.UnixMicro())
|
||||
|
||||
obj = FooBarStructForTimeType{}
|
||||
req = requestWithBody(method, badPath, badBody)
|
||||
@ -1057,7 +1061,7 @@ func testFormBindingInvalidName(t *testing.T, method, path, badPath, body, badBo
|
||||
}
|
||||
err := b.Bind(req, &obj)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "", obj.TestName)
|
||||
assert.Empty(t, obj.TestName)
|
||||
|
||||
obj = InvalidNameType{}
|
||||
req = requestWithBody(method, badPath, badBody)
|
||||
@ -1312,7 +1316,7 @@ func testBodyBindingFail(t *testing.T, b Binding, name, path, badPath, body, bad
|
||||
req := requestWithBody(http.MethodPost, path, body)
|
||||
err := b.Bind(req, &obj)
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, "", obj.Foo)
|
||||
assert.Empty(t, obj.Foo)
|
||||
|
||||
obj = FooStruct{}
|
||||
req = requestWithBody(http.MethodPost, badPath, badBody)
|
||||
|
||||
@ -18,14 +18,16 @@ func TestSliceValidationError(t *testing.T) {
|
||||
{"has nil elements", SliceValidationError{errors.New("test error"), nil}, "[0]: test error"},
|
||||
{"has zero elements", SliceValidationError{}, ""},
|
||||
{"has one element", SliceValidationError{errors.New("test one error")}, "[0]: test one error"},
|
||||
{"has two elements",
|
||||
{
|
||||
"has two elements",
|
||||
SliceValidationError{
|
||||
errors.New("first error"),
|
||||
errors.New("second error"),
|
||||
},
|
||||
"[0]: first error\n[1]: second error",
|
||||
},
|
||||
{"has many elements",
|
||||
{
|
||||
"has many elements",
|
||||
SliceValidationError{
|
||||
errors.New("first error"),
|
||||
errors.New("second error"),
|
||||
|
||||
@ -11,9 +11,11 @@ import (
|
||||
|
||||
const defaultMemory = 32 << 20
|
||||
|
||||
type formBinding struct{}
|
||||
type formPostBinding struct{}
|
||||
type formMultipartBinding struct{}
|
||||
type (
|
||||
formBinding struct{}
|
||||
formPostBinding struct{}
|
||||
formMultipartBinding struct{}
|
||||
)
|
||||
|
||||
func (formBinding) Name() string {
|
||||
return "form"
|
||||
|
||||
@ -13,8 +13,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin/codec/json"
|
||||
"github.com/gin-gonic/gin/internal/bytesconv"
|
||||
"github.com/gin-gonic/gin/internal/json"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -333,9 +333,9 @@ func setWithProperType(val string, value reflect.Value, field reflect.StructFiel
|
||||
case multipart.FileHeader:
|
||||
return nil
|
||||
}
|
||||
return json.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
|
||||
return json.API.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
|
||||
case reflect.Map:
|
||||
return json.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
|
||||
return json.API.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
|
||||
case reflect.Ptr:
|
||||
if !value.Elem().IsValid() {
|
||||
value.Set(reflect.New(value.Type().Elem()))
|
||||
@ -398,18 +398,24 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val
|
||||
}
|
||||
|
||||
switch tf := strings.ToLower(timeFormat); tf {
|
||||
case "unix", "unixnano":
|
||||
case "unix", "unixmilli", "unixmicro", "unixnano":
|
||||
tv, err := strconv.ParseInt(val, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d := time.Duration(1)
|
||||
if tf == "unixnano" {
|
||||
d = time.Second
|
||||
var t time.Time
|
||||
switch tf {
|
||||
case "unix":
|
||||
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))
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -509,9 +509,9 @@ func TestMappingCustomStructTypeWithFormTag(t *testing.T) {
|
||||
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)
|
||||
assert.Equal(t, "file", s.FileData.Protocol)
|
||||
assert.Equal(t, "/foo", s.FileData.Path)
|
||||
assert.Equal(t, "happiness", s.FileData.Name)
|
||||
}
|
||||
|
||||
func TestMappingCustomStructTypeWithURITag(t *testing.T) {
|
||||
@ -521,9 +521,9 @@ func TestMappingCustomStructTypeWithURITag(t *testing.T) {
|
||||
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)
|
||||
assert.Equal(t, "file", s.FileData.Protocol)
|
||||
assert.Equal(t, "/foo", s.FileData.Path)
|
||||
assert.Equal(t, "happiness", s.FileData.Name)
|
||||
}
|
||||
|
||||
func TestMappingCustomPointerStructTypeWithFormTag(t *testing.T) {
|
||||
@ -533,9 +533,9 @@ func TestMappingCustomPointerStructTypeWithFormTag(t *testing.T) {
|
||||
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)
|
||||
assert.Equal(t, "file", s.FileData.Protocol)
|
||||
assert.Equal(t, "/foo", s.FileData.Path)
|
||||
assert.Equal(t, "happiness", s.FileData.Name)
|
||||
}
|
||||
|
||||
func TestMappingCustomPointerStructTypeWithURITag(t *testing.T) {
|
||||
@ -545,9 +545,9 @@ func TestMappingCustomPointerStructTypeWithURITag(t *testing.T) {
|
||||
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)
|
||||
assert.Equal(t, "file", s.FileData.Protocol)
|
||||
assert.Equal(t, "/foo", s.FileData.Path)
|
||||
assert.Equal(t, "happiness", s.FileData.Name)
|
||||
}
|
||||
|
||||
type customPath []string
|
||||
@ -570,8 +570,8 @@ func TestMappingCustomSliceUri(t *testing.T) {
|
||||
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])
|
||||
assert.Equal(t, "bar", s.FileData[0])
|
||||
assert.Equal(t, "foo", s.FileData[1])
|
||||
}
|
||||
|
||||
func TestMappingCustomSliceForm(t *testing.T) {
|
||||
@ -581,8 +581,8 @@ func TestMappingCustomSliceForm(t *testing.T) {
|
||||
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])
|
||||
assert.Equal(t, "bar", s.FileData[0])
|
||||
assert.Equal(t, "foo", s.FileData[1])
|
||||
}
|
||||
|
||||
type objectID [12]byte
|
||||
@ -621,7 +621,7 @@ func TestMappingCustomArrayUri(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
expected, _ := convertTo(val)
|
||||
assert.EqualValues(t, expected, s.FileData)
|
||||
assert.Equal(t, expected, s.FileData)
|
||||
}
|
||||
|
||||
func TestMappingCustomArrayForm(t *testing.T) {
|
||||
@ -633,5 +633,5 @@ func TestMappingCustomArrayForm(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
expected, _ := convertTo(val)
|
||||
assert.EqualValues(t, expected, s.FileData)
|
||||
assert.Equal(t, expected, s.FileData)
|
||||
}
|
||||
|
||||
@ -17,7 +17,6 @@ func (headerBinding) Name() string {
|
||||
}
|
||||
|
||||
func (headerBinding) Bind(req *http.Request, obj any) error {
|
||||
|
||||
if err := mapHeader(obj, req.Header); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin/internal/json"
|
||||
"github.com/gin-gonic/gin/codec/json"
|
||||
)
|
||||
|
||||
// EnableDecoderUseNumber is used to call the UseNumber method on the JSON
|
||||
@ -42,7 +42,7 @@ func (jsonBinding) BindBody(body []byte, obj any) error {
|
||||
}
|
||||
|
||||
func decodeJSON(r io.Reader, obj any) error {
|
||||
decoder := json.NewDecoder(r)
|
||||
decoder := json.API.NewDecoder(r)
|
||||
if EnableDecoderUseNumber {
|
||||
decoder.UseNumber()
|
||||
}
|
||||
|
||||
@ -5,8 +5,16 @@
|
||||
package binding
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/gin-gonic/gin/codec/json"
|
||||
"github.com/gin-gonic/gin/render"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/modern-go/reflect2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@ -28,3 +36,181 @@ func TestJSONBindingBindBodyMap(t *testing.T) {
|
||||
assert.Equal(t, "FOO", s["foo"])
|
||||
assert.Equal(t, "world", s["hello"])
|
||||
}
|
||||
|
||||
func TestCustomJsonCodec(t *testing.T) {
|
||||
// Restore json encoding configuration after testing
|
||||
oldMarshal := json.API
|
||||
defer func() {
|
||||
json.API = oldMarshal
|
||||
}()
|
||||
// Custom json api
|
||||
json.API = customJsonApi{}
|
||||
|
||||
// test decode json
|
||||
obj := customReq{}
|
||||
err := jsonBinding{}.BindBody([]byte(`{"time_empty":null,"time_struct": "2001-12-05 10:01:02.345","time_nil":null,"time_pointer":"2002-12-05 10:01:02.345"}`), &obj)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, zeroTime, obj.TimeEmpty)
|
||||
assert.Equal(t, time.Date(2001, 12, 5, 10, 1, 2, 345000000, time.Local), obj.TimeStruct)
|
||||
assert.Nil(t, obj.TimeNil)
|
||||
assert.Equal(t, time.Date(2002, 12, 5, 10, 1, 2, 345000000, time.Local), *obj.TimePointer)
|
||||
// test encode json
|
||||
w := httptest.NewRecorder()
|
||||
err2 := (render.PureJSON{Data: obj}).Render(w)
|
||||
require.NoError(t, err2)
|
||||
assert.JSONEq(t, "{\"time_empty\":null,\"time_struct\":\"2001-12-05 10:01:02.345\",\"time_nil\":null,\"time_pointer\":\"2002-12-05 10:01:02.345\"}\n", w.Body.String())
|
||||
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
}
|
||||
|
||||
type customReq struct {
|
||||
TimeEmpty time.Time `json:"time_empty"`
|
||||
TimeStruct time.Time `json:"time_struct"`
|
||||
TimeNil *time.Time `json:"time_nil"`
|
||||
TimePointer *time.Time `json:"time_pointer"`
|
||||
}
|
||||
|
||||
var customConfig = jsoniter.Config{
|
||||
EscapeHTML: true,
|
||||
SortMapKeys: true,
|
||||
ValidateJsonRawMessage: true,
|
||||
}.Froze()
|
||||
|
||||
func init() {
|
||||
customConfig.RegisterExtension(&TimeEx{})
|
||||
customConfig.RegisterExtension(&TimePointerEx{})
|
||||
}
|
||||
|
||||
type customJsonApi struct{}
|
||||
|
||||
func (j customJsonApi) Marshal(v any) ([]byte, error) {
|
||||
return customConfig.Marshal(v)
|
||||
}
|
||||
|
||||
func (j customJsonApi) Unmarshal(data []byte, v any) error {
|
||||
return customConfig.Unmarshal(data, v)
|
||||
}
|
||||
|
||||
func (j customJsonApi) MarshalIndent(v any, prefix, indent string) ([]byte, error) {
|
||||
return customConfig.MarshalIndent(v, prefix, indent)
|
||||
}
|
||||
|
||||
func (j customJsonApi) NewEncoder(writer io.Writer) json.Encoder {
|
||||
return customConfig.NewEncoder(writer)
|
||||
}
|
||||
|
||||
func (j customJsonApi) NewDecoder(reader io.Reader) json.Decoder {
|
||||
return customConfig.NewDecoder(reader)
|
||||
}
|
||||
|
||||
// region Time Extension
|
||||
|
||||
var (
|
||||
zeroTime = time.Time{}
|
||||
timeType = reflect2.TypeOfPtr((*time.Time)(nil)).Elem()
|
||||
defaultTimeCodec = &timeCodec{}
|
||||
)
|
||||
|
||||
type TimeEx struct {
|
||||
jsoniter.DummyExtension
|
||||
}
|
||||
|
||||
func (te *TimeEx) CreateDecoder(typ reflect2.Type) jsoniter.ValDecoder {
|
||||
if typ == timeType {
|
||||
return defaultTimeCodec
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (te *TimeEx) CreateEncoder(typ reflect2.Type) jsoniter.ValEncoder {
|
||||
if typ == timeType {
|
||||
return defaultTimeCodec
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type timeCodec struct{}
|
||||
|
||||
func (tc timeCodec) IsEmpty(ptr unsafe.Pointer) bool {
|
||||
t := *((*time.Time)(ptr))
|
||||
return t.Equal(zeroTime)
|
||||
}
|
||||
|
||||
func (tc timeCodec) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) {
|
||||
t := *((*time.Time)(ptr))
|
||||
if t.Equal(zeroTime) {
|
||||
stream.WriteNil()
|
||||
return
|
||||
}
|
||||
stream.WriteString(t.In(time.Local).Format("2006-01-02 15:04:05.000"))
|
||||
}
|
||||
|
||||
func (tc timeCodec) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
|
||||
ts := iter.ReadString()
|
||||
if len(ts) == 0 {
|
||||
*((*time.Time)(ptr)) = zeroTime
|
||||
return
|
||||
}
|
||||
t, err := time.ParseInLocation("2006-01-02 15:04:05.000", ts, time.Local)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
*((*time.Time)(ptr)) = t
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region *Time Extension
|
||||
|
||||
var (
|
||||
timePointerType = reflect2.TypeOfPtr((**time.Time)(nil)).Elem()
|
||||
defaultTimePointerCodec = &timePointerCodec{}
|
||||
)
|
||||
|
||||
type TimePointerEx struct {
|
||||
jsoniter.DummyExtension
|
||||
}
|
||||
|
||||
func (tpe *TimePointerEx) CreateDecoder(typ reflect2.Type) jsoniter.ValDecoder {
|
||||
if typ == timePointerType {
|
||||
return defaultTimePointerCodec
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tpe *TimePointerEx) CreateEncoder(typ reflect2.Type) jsoniter.ValEncoder {
|
||||
if typ == timePointerType {
|
||||
return defaultTimePointerCodec
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type timePointerCodec struct{}
|
||||
|
||||
func (tpc timePointerCodec) IsEmpty(ptr unsafe.Pointer) bool {
|
||||
t := *((**time.Time)(ptr))
|
||||
return t == nil || (*t).Equal(zeroTime)
|
||||
}
|
||||
|
||||
func (tpc timePointerCodec) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) {
|
||||
t := *((**time.Time)(ptr))
|
||||
if t == nil || (*t).Equal(zeroTime) {
|
||||
stream.WriteNil()
|
||||
return
|
||||
}
|
||||
stream.WriteString(t.In(time.Local).Format("2006-01-02 15:04:05.000"))
|
||||
}
|
||||
|
||||
func (tpc timePointerCodec) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
|
||||
ts := iter.ReadString()
|
||||
if len(ts) == 0 {
|
||||
*((**time.Time)(ptr)) = nil
|
||||
return
|
||||
}
|
||||
t, err := time.ParseInLocation("2006-01-02 15:04:05.000", ts, time.Local)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
*((**time.Time)(ptr)) = &t
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
@ -15,7 +15,7 @@ func (plainBinding) Name() string {
|
||||
return "plain"
|
||||
}
|
||||
|
||||
func (plainBinding) Bind(req *http.Request, obj interface{}) error {
|
||||
func (plainBinding) Bind(req *http.Request, obj any) error {
|
||||
all, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@ -34,7 +34,7 @@ func (protobufBinding) BindBody(body []byte, obj any) error {
|
||||
if err := proto.Unmarshal(body, msg); err != nil {
|
||||
return err
|
||||
}
|
||||
// Here it's same to return validate(obj), but util now we can't add
|
||||
// Here it's same to return validate(obj), but until now we can't add
|
||||
// `binding:""` to the struct which automatically generate by gen-proto
|
||||
return nil
|
||||
// return validate(obj)
|
||||
|
||||
@ -31,5 +31,5 @@ func decodeToml(r io.Reader, obj any) error {
|
||||
if err := decoder.Decode(obj); err != nil {
|
||||
return err
|
||||
}
|
||||
return decoder.Decode(obj)
|
||||
return validate(obj)
|
||||
}
|
||||
|
||||
@ -158,16 +158,16 @@ type structNoValidationPointer struct {
|
||||
}
|
||||
|
||||
func TestValidateNoValidationPointers(t *testing.T) {
|
||||
//origin := createNoValidation_values()
|
||||
//test := createNoValidation_values()
|
||||
// origin := createNoValidation_values()
|
||||
// test := createNoValidation_values()
|
||||
empty := structNoValidationPointer{}
|
||||
|
||||
//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))
|
||||
require.NoError(t, validate(&empty))
|
||||
|
||||
//assert.Equal(t, origin, test)
|
||||
// assert.Equal(t, origin, test)
|
||||
}
|
||||
|
||||
type Object map[string]any
|
||||
@ -198,7 +198,7 @@ type structModifyValidation struct {
|
||||
}
|
||||
|
||||
func toZero(sl validator.StructLevel) {
|
||||
var s *structModifyValidation = sl.Top().Interface().(*structModifyValidation)
|
||||
s := sl.Top().Interface().(*structModifyValidation)
|
||||
s.Integer = 0
|
||||
}
|
||||
|
||||
@ -249,5 +249,5 @@ func TestValidatorEngine(t *testing.T) {
|
||||
// Check that we got back non-nil errs
|
||||
require.Error(t, errs)
|
||||
// Check that the error matches expectation
|
||||
require.Error(t, errs, "", "", "notone")
|
||||
require.Error(t, errs, "notone")
|
||||
}
|
||||
|
||||
@ -24,6 +24,7 @@ func (xmlBinding) Bind(req *http.Request, obj any) error {
|
||||
func (xmlBinding) BindBody(body []byte, obj any) error {
|
||||
return decodeXML(bytes.NewReader(body), obj)
|
||||
}
|
||||
|
||||
func decodeXML(r io.Reader, obj any) error {
|
||||
decoder := xml.NewDecoder(r)
|
||||
if err := decoder.Decode(obj); err != nil {
|
||||
|
||||
57
codec/json/api.go
Normal file
57
codec/json/api.go
Normal file
@ -0,0 +1,57 @@
|
||||
// Copyright 2025 Gin Core Team. All rights reserved.
|
||||
// Use of this source code is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package json
|
||||
|
||||
import "io"
|
||||
|
||||
// API the json codec in use.
|
||||
var API Core
|
||||
|
||||
// Core the api for json codec.
|
||||
type Core interface {
|
||||
Marshal(v any) ([]byte, error)
|
||||
Unmarshal(data []byte, v any) error
|
||||
MarshalIndent(v any, prefix, indent string) ([]byte, error)
|
||||
NewEncoder(writer io.Writer) Encoder
|
||||
NewDecoder(reader io.Reader) Decoder
|
||||
}
|
||||
|
||||
// Encoder an interface writes JSON values to an output stream.
|
||||
type Encoder interface {
|
||||
// SetEscapeHTML specifies whether problematic HTML characters
|
||||
// should be escaped inside JSON quoted strings.
|
||||
// The default behavior is to escape &, <, and > to \u0026, \u003c, and \u003e
|
||||
// to avoid certain safety problems that can arise when embedding JSON in HTML.
|
||||
//
|
||||
// In non-HTML settings where the escaping interferes with the readability
|
||||
// of the output, SetEscapeHTML(false) disables this behavior.
|
||||
SetEscapeHTML(on bool)
|
||||
|
||||
// Encode writes the JSON encoding of v to the stream,
|
||||
// followed by a newline character.
|
||||
//
|
||||
// See the documentation for Marshal for details about the
|
||||
// conversion of Go values to JSON.
|
||||
Encode(v any) error
|
||||
}
|
||||
|
||||
// Decoder an interface reads and decodes JSON values from an input stream.
|
||||
type Decoder interface {
|
||||
// UseNumber causes the Decoder to unmarshal a number into an any as a
|
||||
// Number instead of as a float64.
|
||||
UseNumber()
|
||||
|
||||
// DisallowUnknownFields causes the Decoder to return an error when the destination
|
||||
// is a struct and the input contains object keys which do not match any
|
||||
// non-ignored, exported fields in the destination.
|
||||
DisallowUnknownFields()
|
||||
|
||||
// Decode reads the next JSON-encoded value from its
|
||||
// input and stores it in the value pointed to by v.
|
||||
//
|
||||
// See the documentation for Unmarshal for details about
|
||||
// the conversion of JSON into a Go value.
|
||||
Decode(v any) error
|
||||
}
|
||||
42
codec/json/go_json.go
Normal file
42
codec/json/go_json.go
Normal file
@ -0,0 +1,42 @@
|
||||
// Copyright 2025 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 go_json
|
||||
|
||||
package json
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/goccy/go-json"
|
||||
)
|
||||
|
||||
// Package indicates what library is being used for JSON encoding.
|
||||
const Package = "github.com/goccy/go-json"
|
||||
|
||||
func init() {
|
||||
API = gojsonApi{}
|
||||
}
|
||||
|
||||
type gojsonApi struct{}
|
||||
|
||||
func (j gojsonApi) Marshal(v any) ([]byte, error) {
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
func (j gojsonApi) Unmarshal(data []byte, v any) error {
|
||||
return json.Unmarshal(data, v)
|
||||
}
|
||||
|
||||
func (j gojsonApi) MarshalIndent(v any, prefix, indent string) ([]byte, error) {
|
||||
return json.MarshalIndent(v, prefix, indent)
|
||||
}
|
||||
|
||||
func (j gojsonApi) NewEncoder(writer io.Writer) Encoder {
|
||||
return json.NewEncoder(writer)
|
||||
}
|
||||
|
||||
func (j gojsonApi) NewDecoder(reader io.Reader) Decoder {
|
||||
return json.NewDecoder(reader)
|
||||
}
|
||||
41
codec/json/json.go
Normal file
41
codec/json/json.go
Normal file
@ -0,0 +1,41 @@
|
||||
// Copyright 2025 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 !jsoniter && !go_json && !(sonic && (linux || windows || darwin))
|
||||
|
||||
package json
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
)
|
||||
|
||||
// Package indicates what library is being used for JSON encoding.
|
||||
const Package = "encoding/json"
|
||||
|
||||
func init() {
|
||||
API = jsonApi{}
|
||||
}
|
||||
|
||||
type jsonApi struct{}
|
||||
|
||||
func (j jsonApi) Marshal(v any) ([]byte, error) {
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
func (j jsonApi) Unmarshal(data []byte, v any) error {
|
||||
return json.Unmarshal(data, v)
|
||||
}
|
||||
|
||||
func (j jsonApi) MarshalIndent(v any, prefix, indent string) ([]byte, error) {
|
||||
return json.MarshalIndent(v, prefix, indent)
|
||||
}
|
||||
|
||||
func (j jsonApi) NewEncoder(writer io.Writer) Encoder {
|
||||
return json.NewEncoder(writer)
|
||||
}
|
||||
|
||||
func (j jsonApi) NewDecoder(reader io.Reader) Decoder {
|
||||
return json.NewDecoder(reader)
|
||||
}
|
||||
44
codec/json/jsoniter.go
Normal file
44
codec/json/jsoniter.go
Normal file
@ -0,0 +1,44 @@
|
||||
// Copyright 2025 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 jsoniter
|
||||
|
||||
package json
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
// Package indicates what library is being used for JSON encoding.
|
||||
const Package = "github.com/json-iterator/go"
|
||||
|
||||
func init() {
|
||||
API = jsoniterApi{}
|
||||
}
|
||||
|
||||
var json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
|
||||
type jsoniterApi struct{}
|
||||
|
||||
func (j jsoniterApi) Marshal(v any) ([]byte, error) {
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
func (j jsoniterApi) Unmarshal(data []byte, v any) error {
|
||||
return json.Unmarshal(data, v)
|
||||
}
|
||||
|
||||
func (j jsoniterApi) MarshalIndent(v any, prefix, indent string) ([]byte, error) {
|
||||
return json.MarshalIndent(v, prefix, indent)
|
||||
}
|
||||
|
||||
func (j jsoniterApi) NewEncoder(writer io.Writer) Encoder {
|
||||
return json.NewEncoder(writer)
|
||||
}
|
||||
|
||||
func (j jsoniterApi) NewDecoder(reader io.Reader) Decoder {
|
||||
return json.NewDecoder(reader)
|
||||
}
|
||||
44
codec/json/sonic.go
Normal file
44
codec/json/sonic.go
Normal file
@ -0,0 +1,44 @@
|
||||
// Copyright 2025 Gin Core Team. All rights reserved.
|
||||
// Use of this source code is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build sonic && (linux || windows || darwin)
|
||||
|
||||
package json
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
)
|
||||
|
||||
// Package indicates what library is being used for JSON encoding.
|
||||
const Package = "github.com/bytedance/sonic"
|
||||
|
||||
func init() {
|
||||
API = sonicApi{}
|
||||
}
|
||||
|
||||
var json = sonic.ConfigStd
|
||||
|
||||
type sonicApi struct{}
|
||||
|
||||
func (j sonicApi) Marshal(v any) ([]byte, error) {
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
func (j sonicApi) Unmarshal(data []byte, v any) error {
|
||||
return json.Unmarshal(data, v)
|
||||
}
|
||||
|
||||
func (j sonicApi) MarshalIndent(v any, prefix, indent string) ([]byte, error) {
|
||||
return json.MarshalIndent(v, prefix, indent)
|
||||
}
|
||||
|
||||
func (j sonicApi) NewEncoder(writer io.Writer) Encoder {
|
||||
return json.NewEncoder(writer)
|
||||
}
|
||||
|
||||
func (j sonicApi) NewDecoder(reader io.Reader) Decoder {
|
||||
return json.NewDecoder(reader)
|
||||
}
|
||||
123
context.go
123
context.go
@ -6,6 +6,7 @@ package gin
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
@ -72,7 +73,7 @@ type Context struct {
|
||||
mu sync.RWMutex
|
||||
|
||||
// Keys is a key/value pair exclusively for the context of each request.
|
||||
Keys map[string]any
|
||||
Keys map[any]any
|
||||
|
||||
// Errors is a list of errors attached to all the handlers/middlewares who used this context.
|
||||
Errors errorMsgs
|
||||
@ -129,7 +130,7 @@ func (c *Context) Copy() *Context {
|
||||
cp.fullPath = c.fullPath
|
||||
|
||||
cKeys := c.Keys
|
||||
cp.Keys = make(map[string]any, len(cKeys))
|
||||
cp.Keys = make(map[any]any, len(cKeys))
|
||||
c.mu.RLock()
|
||||
for k, v := range cKeys {
|
||||
cp.Keys[k] = v
|
||||
@ -264,11 +265,11 @@ func (c *Context) Error(err error) *Error {
|
||||
|
||||
// Set is used to store a new key/value pair exclusively for this context.
|
||||
// It also lazy initializes c.Keys if it was not used previously.
|
||||
func (c *Context) Set(key string, value any) {
|
||||
func (c *Context) Set(key any, value any) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.Keys == nil {
|
||||
c.Keys = make(map[string]any)
|
||||
c.Keys = make(map[any]any)
|
||||
}
|
||||
|
||||
c.Keys[key] = value
|
||||
@ -276,7 +277,7 @@ func (c *Context) Set(key string, value any) {
|
||||
|
||||
// Get returns the value for the given key, ie: (value, true).
|
||||
// If the value does not exist it returns (nil, false)
|
||||
func (c *Context) Get(key string) (value any, exists bool) {
|
||||
func (c *Context) Get(key any) (value any, exists bool) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
value, exists = c.Keys[key]
|
||||
@ -284,14 +285,14 @@ func (c *Context) Get(key string) (value any, exists bool) {
|
||||
}
|
||||
|
||||
// MustGet returns the value for the given key if it exists, otherwise it panics.
|
||||
func (c *Context) MustGet(key string) any {
|
||||
func (c *Context) MustGet(key any) any {
|
||||
if value, exists := c.Get(key); exists {
|
||||
return value
|
||||
}
|
||||
panic("Key \"" + key + "\" does not exist")
|
||||
panic(fmt.Sprintf("key %v does not exist", key))
|
||||
}
|
||||
|
||||
func getTyped[T any](c *Context, key string) (res T) {
|
||||
func getTyped[T any](c *Context, key any) (res T) {
|
||||
if val, ok := c.Get(key); ok && val != nil {
|
||||
res, _ = val.(T)
|
||||
}
|
||||
@ -299,162 +300,162 @@ 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) {
|
||||
func (c *Context) GetString(key any) (s string) {
|
||||
return getTyped[string](c, key)
|
||||
}
|
||||
|
||||
// GetBool returns the value associated with the key as a boolean.
|
||||
func (c *Context) GetBool(key string) (b bool) {
|
||||
func (c *Context) GetBool(key any) (b bool) {
|
||||
return getTyped[bool](c, key)
|
||||
}
|
||||
|
||||
// GetInt returns the value associated with the key as an integer.
|
||||
func (c *Context) GetInt(key string) (i int) {
|
||||
func (c *Context) GetInt(key any) (i int) {
|
||||
return getTyped[int](c, key)
|
||||
}
|
||||
|
||||
// GetInt8 returns the value associated with the key as an integer 8.
|
||||
func (c *Context) GetInt8(key string) (i8 int8) {
|
||||
func (c *Context) GetInt8(key any) (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) {
|
||||
func (c *Context) GetInt16(key any) (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) {
|
||||
func (c *Context) GetInt32(key any) (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 any) (i64 int64) {
|
||||
return getTyped[int64](c, key)
|
||||
}
|
||||
|
||||
// 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 any) (ui uint) {
|
||||
return getTyped[uint](c, key)
|
||||
}
|
||||
|
||||
// GetUint8 returns the value associated with the key as an unsigned integer 8.
|
||||
func (c *Context) GetUint8(key string) (ui8 uint8) {
|
||||
func (c *Context) GetUint8(key any) (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) {
|
||||
func (c *Context) GetUint16(key any) (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) {
|
||||
func (c *Context) GetUint32(key any) (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 any) (ui64 uint64) {
|
||||
return getTyped[uint64](c, key)
|
||||
}
|
||||
|
||||
// GetFloat32 returns the value associated with the key as a float32.
|
||||
func (c *Context) GetFloat32(key string) (f32 float32) {
|
||||
func (c *Context) GetFloat32(key any) (f32 float32) {
|
||||
return getTyped[float32](c, key)
|
||||
}
|
||||
|
||||
// GetFloat64 returns the value associated with the key as a float64.
|
||||
func (c *Context) GetFloat64(key string) (f64 float64) {
|
||||
func (c *Context) GetFloat64(key any) (f64 float64) {
|
||||
return getTyped[float64](c, key)
|
||||
}
|
||||
|
||||
// GetTime returns the value associated with the key as time.
|
||||
func (c *Context) GetTime(key string) (t time.Time) {
|
||||
func (c *Context) GetTime(key any) (t time.Time) {
|
||||
return getTyped[time.Time](c, key)
|
||||
}
|
||||
|
||||
// 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 any) (d time.Duration) {
|
||||
return getTyped[time.Duration](c, key)
|
||||
}
|
||||
|
||||
// GetIntSlice returns the value associated with the key as a slice of integers.
|
||||
func (c *Context) GetIntSlice(key string) (is []int) {
|
||||
func (c *Context) GetIntSlice(key any) (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) {
|
||||
func (c *Context) GetInt8Slice(key any) (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) {
|
||||
func (c *Context) GetInt16Slice(key any) (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) {
|
||||
func (c *Context) GetInt32Slice(key any) (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) {
|
||||
func (c *Context) GetInt64Slice(key any) (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) {
|
||||
func (c *Context) GetUintSlice(key any) (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) {
|
||||
func (c *Context) GetUint8Slice(key any) (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) {
|
||||
func (c *Context) GetUint16Slice(key any) (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) {
|
||||
func (c *Context) GetUint32Slice(key any) (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) {
|
||||
func (c *Context) GetUint64Slice(key any) (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) {
|
||||
func (c *Context) GetFloat32Slice(key any) (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) {
|
||||
func (c *Context) GetFloat64Slice(key any) (f64s []float64) {
|
||||
return getTyped[[]float64](c, key)
|
||||
}
|
||||
|
||||
// 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 any) (ss []string) {
|
||||
return getTyped[[]string](c, key)
|
||||
}
|
||||
|
||||
// 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 any) (sm map[string]any) {
|
||||
return getTyped[map[string]any](c, key)
|
||||
}
|
||||
|
||||
// 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 any) (sms map[string]string) {
|
||||
return getTyped[map[string]string](c, key)
|
||||
}
|
||||
|
||||
// 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 any) (smss map[string][]string) {
|
||||
return getTyped[map[string][]string](c, key)
|
||||
}
|
||||
|
||||
@ -684,15 +685,15 @@ func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string, perm
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
if len(perm) <= 0 {
|
||||
perm = append(perm, 0o750)
|
||||
var mode os.FileMode = 0o750
|
||||
if len(perm) > 0 {
|
||||
mode = perm[0]
|
||||
}
|
||||
|
||||
if err = os.MkdirAll(filepath.Dir(dst), perm[0]); err != nil {
|
||||
dir := filepath.Dir(dst)
|
||||
if err = os.MkdirAll(dir, mode); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = os.Chmod(filepath.Dir(dst), perm[0]); err != nil {
|
||||
if err = os.Chmod(dir, mode); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -769,8 +770,19 @@ func (c *Context) BindUri(obj any) error {
|
||||
// It will abort the request with HTTP 400 if any error occurs.
|
||||
// See the binding package.
|
||||
func (c *Context) MustBindWith(obj any, b binding.Binding) error {
|
||||
if err := c.ShouldBindWith(obj, b); err != nil {
|
||||
c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) //nolint: errcheck
|
||||
err := c.ShouldBindWith(obj, b)
|
||||
if err != nil {
|
||||
var maxBytesErr *http.MaxBytesError
|
||||
|
||||
// Note: When using sonic or go-json as JSON encoder, they do not propagate the http.MaxBytesError error
|
||||
// https://github.com/goccy/go-json/issues/485
|
||||
// https://github.com/bytedance/sonic/issues/800
|
||||
switch {
|
||||
case errors.As(err, &maxBytesErr):
|
||||
c.AbortWithError(http.StatusRequestEntityTooLarge, err).SetType(ErrorTypeBind) //nolint: errcheck
|
||||
default:
|
||||
c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) //nolint: errcheck
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@ -889,7 +901,7 @@ func (c *Context) ShouldBindBodyWithPlain(obj any) error {
|
||||
|
||||
// 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.
|
||||
// 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,
|
||||
// the remote IP (coming from Request.RemoteAddr) is returned.
|
||||
func (c *Context) ClientIP() string {
|
||||
@ -1027,6 +1039,19 @@ func (c *Context) SetCookie(name, value string, maxAge int, path, domain string,
|
||||
})
|
||||
}
|
||||
|
||||
// SetCookieData adds a Set-Cookie header to the ResponseWriter's headers.
|
||||
// It accepts a pointer to http.Cookie structure for more flexibility in setting cookie attributes.
|
||||
// The provided cookie must have a valid Name. Invalid cookies may be silently dropped.
|
||||
func (c *Context) SetCookieData(cookie *http.Cookie) {
|
||||
if cookie.Path == "" {
|
||||
cookie.Path = "/"
|
||||
}
|
||||
if cookie.SameSite == http.SameSiteDefaultMode {
|
||||
cookie.SameSite = c.sameSite
|
||||
}
|
||||
http.SetCookie(c.Writer, cookie)
|
||||
}
|
||||
|
||||
// Cookie returns the named cookie provided in the request or
|
||||
// ErrNoCookie if not found. And return the named cookie is unescaped.
|
||||
// If multiple cookies match the given name, only one cookie will
|
||||
|
||||
291
context_test.go
291
context_test.go
@ -28,6 +28,7 @@ import (
|
||||
|
||||
"github.com/gin-contrib/sse"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
"github.com/gin-gonic/gin/codec/json"
|
||||
testdata "github.com/gin-gonic/gin/testdata/protoexample"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@ -173,6 +174,9 @@ func TestSaveUploadedFileWithPermission(t *testing.T) {
|
||||
assert.Equal(t, "permission_test", f.Filename)
|
||||
var mode fs.FileMode = 0o755
|
||||
require.NoError(t, c.SaveUploadedFile(f, "permission_test", mode))
|
||||
t.Cleanup(func() {
|
||||
assert.NoError(t, os.Remove("permission_test"))
|
||||
})
|
||||
info, err := os.Stat(filepath.Dir("permission_test"))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, info.Mode().Perm(), mode)
|
||||
@ -253,7 +257,46 @@ func TestContextSetGet(t *testing.T) {
|
||||
assert.False(t, err)
|
||||
|
||||
assert.Equal(t, "bar", c.MustGet("foo"))
|
||||
assert.Panics(t, func() { c.MustGet("no_exist") })
|
||||
assert.Panicsf(t, func() {
|
||||
c.MustGet("no_exist")
|
||||
}, "key no_exist does not exist")
|
||||
}
|
||||
|
||||
func TestContextSetGetAnyKey(t *testing.T) {
|
||||
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||
|
||||
type key struct{}
|
||||
|
||||
tests := []struct {
|
||||
key any
|
||||
}{
|
||||
{1},
|
||||
{int32(1)},
|
||||
{int64(1)},
|
||||
{uint(1)},
|
||||
{float32(1)},
|
||||
{key{}},
|
||||
{&key{}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(reflect.TypeOf(tt.key).String(), func(t *testing.T) {
|
||||
c.Set(tt.key, 1)
|
||||
value, ok := c.Get(tt.key)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, 1, value)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextSetGetPanicsWhenKeyNotComparable(t *testing.T) {
|
||||
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||
|
||||
assert.Panics(t, func() {
|
||||
c.Set([]int{1}, 1)
|
||||
c.Set(func() {}, 1)
|
||||
c.Set(make(chan int), 1)
|
||||
})
|
||||
}
|
||||
|
||||
func TestContextSetGetValues(t *testing.T) {
|
||||
@ -671,7 +714,7 @@ func TestContextDefaultQueryOnEmptyRequest(t *testing.T) {
|
||||
|
||||
func TestContextQueryAndPostForm(t *testing.T) {
|
||||
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||
body := bytes.NewBufferString("foo=bar&page=11&both=&foo=second")
|
||||
body := strings.NewReader("foo=bar&page=11&both=&foo=second")
|
||||
c.Request, _ = http.NewRequest(http.MethodPost,
|
||||
"/?both=GET&id=main&id=omit&array[]=first&array[]=second&ids[a]=hi&ids[b]=3.14", body)
|
||||
c.Request.Header.Add("Content-Type", MIMEPOSTForm)
|
||||
@ -885,10 +928,10 @@ func TestContextGetCookie(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestContextBodyAllowedForStatus(t *testing.T) {
|
||||
assert.False(t, false, bodyAllowedForStatus(http.StatusProcessing))
|
||||
assert.False(t, false, bodyAllowedForStatus(http.StatusNoContent))
|
||||
assert.False(t, false, bodyAllowedForStatus(http.StatusNotModified))
|
||||
assert.True(t, true, bodyAllowedForStatus(http.StatusInternalServerError))
|
||||
assert.False(t, bodyAllowedForStatus(http.StatusProcessing))
|
||||
assert.False(t, bodyAllowedForStatus(http.StatusNoContent))
|
||||
assert.False(t, bodyAllowedForStatus(http.StatusNotModified))
|
||||
assert.True(t, bodyAllowedForStatus(http.StatusInternalServerError))
|
||||
}
|
||||
|
||||
type TestRender struct{}
|
||||
@ -918,7 +961,7 @@ func TestContextRenderJSON(t *testing.T) {
|
||||
c.JSON(http.StatusCreated, H{"foo": "bar", "html": "<b>"})
|
||||
|
||||
assert.Equal(t, http.StatusCreated, w.Code)
|
||||
assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"\\u003cb\\u003e\"}", w.Body.String())
|
||||
assert.JSONEq(t, "{\"foo\":\"bar\",\"html\":\"\\u003cb\\u003e\"}", w.Body.String())
|
||||
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
}
|
||||
|
||||
@ -946,7 +989,7 @@ func TestContextRenderJSONPWithoutCallback(t *testing.T) {
|
||||
c.JSONP(http.StatusCreated, H{"foo": "bar"})
|
||||
|
||||
assert.Equal(t, http.StatusCreated, w.Code)
|
||||
assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String())
|
||||
assert.JSONEq(t, `{"foo":"bar"}`, w.Body.String())
|
||||
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
}
|
||||
|
||||
@ -972,7 +1015,7 @@ func TestContextRenderAPIJSON(t *testing.T) {
|
||||
c.JSON(http.StatusCreated, H{"foo": "bar"})
|
||||
|
||||
assert.Equal(t, http.StatusCreated, w.Code)
|
||||
assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String())
|
||||
assert.JSONEq(t, `{"foo":"bar"}`, w.Body.String())
|
||||
assert.Equal(t, "application/vnd.api+json", w.Header().Get("Content-Type"))
|
||||
}
|
||||
|
||||
@ -998,7 +1041,7 @@ func TestContextRenderIndentedJSON(t *testing.T) {
|
||||
c.IndentedJSON(http.StatusCreated, H{"foo": "bar", "bar": "foo", "nested": H{"foo": "bar"}})
|
||||
|
||||
assert.Equal(t, http.StatusCreated, w.Code)
|
||||
assert.Equal(t, "{\n \"bar\": \"foo\",\n \"foo\": \"bar\",\n \"nested\": {\n \"foo\": \"bar\"\n }\n}", w.Body.String())
|
||||
assert.JSONEq(t, "{\n \"bar\": \"foo\",\n \"foo\": \"bar\",\n \"nested\": {\n \"foo\": \"bar\"\n }\n}", w.Body.String())
|
||||
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
}
|
||||
|
||||
@ -1059,7 +1102,7 @@ func TestContextRenderPureJSON(t *testing.T) {
|
||||
c, _ := CreateTestContext(w)
|
||||
c.PureJSON(http.StatusCreated, H{"foo": "bar", "html": "<b>"})
|
||||
assert.Equal(t, http.StatusCreated, w.Code)
|
||||
assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"<b>\"}\n", w.Body.String())
|
||||
assert.JSONEq(t, "{\"foo\":\"bar\",\"html\":\"<b>\"}\n", w.Body.String())
|
||||
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
}
|
||||
|
||||
@ -1142,7 +1185,7 @@ func TestContextRenderNoContentXML(t *testing.T) {
|
||||
assert.Equal(t, "application/xml; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
}
|
||||
|
||||
// TestContextString tests that the response is returned
|
||||
// TestContextRenderString tests that the response is returned
|
||||
// with Content-Type set to text/plain
|
||||
func TestContextRenderString(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
@ -1167,7 +1210,7 @@ func TestContextRenderNoContentString(t *testing.T) {
|
||||
assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
}
|
||||
|
||||
// TestContextString tests that the response is returned
|
||||
// TestContextRenderHTMLString tests that the response is returned
|
||||
// with Content-Type set to text/html
|
||||
func TestContextRenderHTMLString(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
@ -1233,7 +1276,7 @@ func TestContextRenderSSE(t *testing.T) {
|
||||
"bar": "foo",
|
||||
})
|
||||
|
||||
assert.Equal(t, strings.Replace(w.Body.String(), " ", "", -1), strings.Replace("event:float\ndata:1.5\n\nid:123\ndata:text\n\nevent:chat\ndata:{\"bar\":\"foo\",\"foo\":\"bar\"}\n\n", " ", "", -1))
|
||||
assert.Equal(t, strings.ReplaceAll(w.Body.String(), " ", ""), strings.ReplaceAll("event:float\ndata:1.5\n\nid:123\ndata:text\n\nevent:chat\ndata:{\"bar\":\"foo\",\"foo\":\"bar\"}\n\n", " ", ""))
|
||||
}
|
||||
|
||||
func TestContextRenderFile(t *testing.T) {
|
||||
@ -1247,7 +1290,7 @@ func TestContextRenderFile(t *testing.T) {
|
||||
assert.Contains(t, w.Body.String(), "func New(opts ...OptionFunc) *Engine {")
|
||||
// Content-Type='text/plain; charset=utf-8' when go version <= 1.16,
|
||||
// else, Content-Type='text/x-go; charset=utf-8'
|
||||
assert.NotEqual(t, "", w.Header().Get("Content-Type"))
|
||||
assert.NotEmpty(t, w.Header().Get("Content-Type"))
|
||||
}
|
||||
|
||||
func TestContextRenderFileFromFS(t *testing.T) {
|
||||
@ -1261,7 +1304,7 @@ func TestContextRenderFileFromFS(t *testing.T) {
|
||||
assert.Contains(t, w.Body.String(), "func New(opts ...OptionFunc) *Engine {")
|
||||
// Content-Type='text/plain; charset=utf-8' when go version <= 1.16,
|
||||
// else, Content-Type='text/x-go; charset=utf-8'
|
||||
assert.NotEqual(t, "", w.Header().Get("Content-Type"))
|
||||
assert.NotEmpty(t, w.Header().Get("Content-Type"))
|
||||
assert.Equal(t, "/some/path", c.Request.URL.Path)
|
||||
}
|
||||
|
||||
@ -1432,7 +1475,7 @@ func TestContextNegotiationWithJSON(t *testing.T) {
|
||||
})
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String())
|
||||
assert.JSONEq(t, `{"foo":"bar"}`, w.Body.String())
|
||||
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
}
|
||||
|
||||
@ -1518,7 +1561,7 @@ func TestContextNegotiationFormat(t *testing.T) {
|
||||
c.Request, _ = http.NewRequest(http.MethodPost, "", nil)
|
||||
|
||||
assert.Panics(t, func() { c.NegotiateFormat() })
|
||||
assert.Equal(t, MIMEJSON, c.NegotiateFormat(MIMEJSON, MIMEXML))
|
||||
assert.Equal(t, MIMEJSON, c.NegotiateFormat(MIMEJSON, MIMEXML)) //nolint:testifylint
|
||||
assert.Equal(t, MIMEHTML, c.NegotiateFormat(MIMEHTML, MIMEJSON))
|
||||
}
|
||||
|
||||
@ -1540,7 +1583,7 @@ func TestContextNegotiationFormatWithWildcardAccept(t *testing.T) {
|
||||
assert.Equal(t, "*/*", c.NegotiateFormat("*/*"))
|
||||
assert.Equal(t, "text/*", c.NegotiateFormat("text/*"))
|
||||
assert.Equal(t, "application/*", c.NegotiateFormat("application/*"))
|
||||
assert.Equal(t, MIMEJSON, c.NegotiateFormat(MIMEJSON))
|
||||
assert.Equal(t, MIMEJSON, c.NegotiateFormat(MIMEJSON)) //nolint:testifylint
|
||||
assert.Equal(t, MIMEXML, c.NegotiateFormat(MIMEXML))
|
||||
assert.Equal(t, MIMEHTML, c.NegotiateFormat(MIMEHTML))
|
||||
|
||||
@ -1550,9 +1593,9 @@ func TestContextNegotiationFormatWithWildcardAccept(t *testing.T) {
|
||||
|
||||
assert.Equal(t, "*/*", c.NegotiateFormat("*/*"))
|
||||
assert.Equal(t, "text/*", c.NegotiateFormat("text/*"))
|
||||
assert.Equal(t, "", c.NegotiateFormat("application/*"))
|
||||
assert.Equal(t, "", c.NegotiateFormat(MIMEJSON))
|
||||
assert.Equal(t, "", c.NegotiateFormat(MIMEXML))
|
||||
assert.Empty(t, c.NegotiateFormat("application/*"))
|
||||
assert.Empty(t, c.NegotiateFormat(MIMEJSON))
|
||||
assert.Empty(t, c.NegotiateFormat(MIMEXML))
|
||||
assert.Equal(t, MIMEHTML, c.NegotiateFormat(MIMEHTML))
|
||||
}
|
||||
|
||||
@ -1564,9 +1607,9 @@ func TestContextNegotiationFormatCustom(t *testing.T) {
|
||||
c.Accepted = nil
|
||||
c.SetAccepted(MIMEJSON, MIMEXML)
|
||||
|
||||
assert.Equal(t, MIMEJSON, c.NegotiateFormat(MIMEJSON, MIMEXML))
|
||||
assert.Equal(t, MIMEJSON, c.NegotiateFormat(MIMEJSON, MIMEXML)) //nolint:testifylint
|
||||
assert.Equal(t, MIMEXML, c.NegotiateFormat(MIMEXML, MIMEHTML))
|
||||
assert.Equal(t, MIMEJSON, c.NegotiateFormat(MIMEJSON))
|
||||
assert.Equal(t, MIMEJSON, c.NegotiateFormat(MIMEJSON)) //nolint:testifylint
|
||||
}
|
||||
|
||||
func TestContextNegotiationFormat2(t *testing.T) {
|
||||
@ -1574,7 +1617,7 @@ func TestContextNegotiationFormat2(t *testing.T) {
|
||||
c.Request, _ = http.NewRequest(http.MethodPost, "/", nil)
|
||||
c.Request.Header.Add("Accept", "image/tiff-fx")
|
||||
|
||||
assert.Equal(t, "", c.NegotiateFormat("image/tiff"))
|
||||
assert.Empty(t, c.NegotiateFormat("image/tiff"))
|
||||
}
|
||||
|
||||
func TestContextIsAborted(t *testing.T) {
|
||||
@ -1634,7 +1677,7 @@ func TestContextAbortWithStatusJSON(t *testing.T) {
|
||||
_, err := buf.ReadFrom(w.Body)
|
||||
require.NoError(t, err)
|
||||
jsonStringBody := buf.String()
|
||||
assert.Equal(t, "{\"foo\":\"fooValue\",\"bar\":\"barValue\"}", jsonStringBody)
|
||||
assert.JSONEq(t, "{\"foo\":\"fooValue\",\"bar\":\"barValue\"}", jsonStringBody)
|
||||
}
|
||||
|
||||
func TestContextError(t *testing.T) {
|
||||
@ -1848,9 +1891,41 @@ func TestContextContentType(t *testing.T) {
|
||||
assert.Equal(t, "application/json", c.ContentType())
|
||||
}
|
||||
|
||||
func TestContextBindRequestTooLarge(t *testing.T) {
|
||||
// When using sonic or go-json as JSON encoder, they do not propagate the http.MaxBytesError error
|
||||
// The response will fail with a generic 400 instead of 413
|
||||
// https://github.com/goccy/go-json/issues/485
|
||||
// https://github.com/bytedance/sonic/issues/800
|
||||
var expectedCode int
|
||||
switch json.Package {
|
||||
case "github.com/goccy/go-json", "github.com/bytedance/sonic":
|
||||
expectedCode = http.StatusBadRequest
|
||||
default:
|
||||
expectedCode = http.StatusRequestEntityTooLarge
|
||||
}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := CreateTestContext(w)
|
||||
|
||||
c.Request, _ = http.NewRequest(http.MethodPost, "/", strings.NewReader(`{"foo":"bar", "bar":"foo"}`))
|
||||
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 10)
|
||||
|
||||
var obj struct {
|
||||
Foo string `json:"foo"`
|
||||
Bar string `json:"bar"`
|
||||
}
|
||||
require.Error(t, c.BindJSON(&obj))
|
||||
c.Writer.WriteHeaderNow()
|
||||
|
||||
assert.Empty(t, obj.Bar)
|
||||
assert.Empty(t, obj.Foo)
|
||||
assert.Equal(t, expectedCode, w.Code)
|
||||
assert.True(t, c.IsAborted())
|
||||
}
|
||||
|
||||
func TestContextAutoBindJSON(t *testing.T) {
|
||||
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||
c.Request, _ = http.NewRequest(http.MethodPost, "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}"))
|
||||
c.Request, _ = http.NewRequest(http.MethodPost, "/", strings.NewReader(`{"foo":"bar", "bar":"foo"}`))
|
||||
c.Request.Header.Add("Content-Type", MIMEJSON)
|
||||
|
||||
var obj struct {
|
||||
@ -1867,7 +1942,7 @@ func TestContextBindWithJSON(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := CreateTestContext(w)
|
||||
|
||||
c.Request, _ = http.NewRequest(http.MethodPost, "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}"))
|
||||
c.Request, _ = http.NewRequest(http.MethodPost, "/", strings.NewReader(`{"foo":"bar", "bar":"foo"}`))
|
||||
c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type
|
||||
|
||||
var obj struct {
|
||||
@ -1884,7 +1959,7 @@ func TestContextBindWithXML(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := CreateTestContext(w)
|
||||
|
||||
c.Request, _ = http.NewRequest(http.MethodPost, "/", bytes.NewBufferString(`<?xml version="1.0" encoding="UTF-8"?>
|
||||
c.Request, _ = http.NewRequest(http.MethodPost, "/", strings.NewReader(`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<root>
|
||||
<foo>FOO</foo>
|
||||
<bar>BAR</bar>
|
||||
@ -1951,7 +2026,7 @@ func TestContextBindWithQuery(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := CreateTestContext(w)
|
||||
|
||||
c.Request, _ = http.NewRequest(http.MethodPost, "/?foo=bar&bar=foo", bytes.NewBufferString("foo=unused"))
|
||||
c.Request, _ = http.NewRequest(http.MethodPost, "/?foo=bar&bar=foo", strings.NewReader("foo=unused"))
|
||||
|
||||
var obj struct {
|
||||
Foo string `form:"foo"`
|
||||
@ -1967,7 +2042,7 @@ func TestContextBindWithYAML(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := CreateTestContext(w)
|
||||
|
||||
c.Request, _ = http.NewRequest(http.MethodPost, "/", bytes.NewBufferString("foo: bar\nbar: foo"))
|
||||
c.Request, _ = http.NewRequest(http.MethodPost, "/", strings.NewReader("foo: bar\nbar: foo"))
|
||||
c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type
|
||||
|
||||
var obj struct {
|
||||
@ -1984,7 +2059,7 @@ func TestContextBindWithTOML(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := CreateTestContext(w)
|
||||
|
||||
c.Request, _ = http.NewRequest(http.MethodPost, "/", bytes.NewBufferString("foo = 'bar'\nbar = 'foo'"))
|
||||
c.Request, _ = http.NewRequest(http.MethodPost, "/", strings.NewReader("foo = 'bar'\nbar = 'foo'"))
|
||||
c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type
|
||||
|
||||
var obj struct {
|
||||
@ -2001,7 +2076,7 @@ func TestContextBadAutoBind(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := CreateTestContext(w)
|
||||
|
||||
c.Request, _ = http.NewRequest(http.MethodPost, "http://example.com", bytes.NewBufferString("\"foo\":\"bar\", \"bar\":\"foo\"}"))
|
||||
c.Request, _ = http.NewRequest(http.MethodPost, "http://example.com", strings.NewReader("\"foo\":\"bar\", \"bar\":\"foo\"}"))
|
||||
c.Request.Header.Add("Content-Type", MIMEJSON)
|
||||
var obj struct {
|
||||
Foo string `json:"foo"`
|
||||
@ -2020,7 +2095,7 @@ func TestContextBadAutoBind(t *testing.T) {
|
||||
|
||||
func TestContextAutoShouldBindJSON(t *testing.T) {
|
||||
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||
c.Request, _ = http.NewRequest(http.MethodPost, "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}"))
|
||||
c.Request, _ = http.NewRequest(http.MethodPost, "/", strings.NewReader(`{"foo":"bar", "bar":"foo"}`))
|
||||
c.Request.Header.Add("Content-Type", MIMEJSON)
|
||||
|
||||
var obj struct {
|
||||
@ -2037,7 +2112,7 @@ func TestContextShouldBindWithJSON(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := CreateTestContext(w)
|
||||
|
||||
c.Request, _ = http.NewRequest(http.MethodPost, "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}"))
|
||||
c.Request, _ = http.NewRequest(http.MethodPost, "/", strings.NewReader(`{"foo":"bar", "bar":"foo"}`))
|
||||
c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type
|
||||
|
||||
var obj struct {
|
||||
@ -2054,7 +2129,7 @@ func TestContextShouldBindWithXML(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := CreateTestContext(w)
|
||||
|
||||
c.Request, _ = http.NewRequest(http.MethodPost, "/", bytes.NewBufferString(`<?xml version="1.0" encoding="UTF-8"?>
|
||||
c.Request, _ = http.NewRequest(http.MethodPost, "/", strings.NewReader(`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<root>
|
||||
<foo>FOO</foo>
|
||||
<bar>BAR</bar>
|
||||
@ -2121,7 +2196,7 @@ func TestContextShouldBindWithQuery(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := CreateTestContext(w)
|
||||
|
||||
c.Request, _ = http.NewRequest(http.MethodPost, "/?foo=bar&bar=foo&Foo=bar1&Bar=foo1", bytes.NewBufferString("foo=unused"))
|
||||
c.Request, _ = http.NewRequest(http.MethodPost, "/?foo=bar&bar=foo&Foo=bar1&Bar=foo1", strings.NewReader("foo=unused"))
|
||||
|
||||
var obj struct {
|
||||
Foo string `form:"foo"`
|
||||
@ -2141,7 +2216,7 @@ func TestContextShouldBindWithYAML(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := CreateTestContext(w)
|
||||
|
||||
c.Request, _ = http.NewRequest(http.MethodPost, "/", bytes.NewBufferString("foo: bar\nbar: foo"))
|
||||
c.Request, _ = http.NewRequest(http.MethodPost, "/", strings.NewReader("foo: bar\nbar: foo"))
|
||||
c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type
|
||||
|
||||
var obj struct {
|
||||
@ -2158,7 +2233,7 @@ func TestContextShouldBindWithTOML(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := CreateTestContext(w)
|
||||
|
||||
c.Request, _ = http.NewRequest(http.MethodPost, "/", bytes.NewBufferString("foo='bar'\nbar= 'foo'"))
|
||||
c.Request, _ = http.NewRequest(http.MethodPost, "/", strings.NewReader("foo='bar'\nbar= 'foo'"))
|
||||
c.Request.Header.Add("Content-Type", MIMETOML) // set fake content-type
|
||||
|
||||
var obj struct {
|
||||
@ -2175,7 +2250,7 @@ func TestContextBadAutoShouldBind(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := CreateTestContext(w)
|
||||
|
||||
c.Request, _ = http.NewRequest(http.MethodPost, "http://example.com", bytes.NewBufferString("\"foo\":\"bar\", \"bar\":\"foo\"}"))
|
||||
c.Request, _ = http.NewRequest(http.MethodPost, "http://example.com", strings.NewReader(`"foo":"bar", "bar":"foo"}`))
|
||||
c.Request.Header.Add("Content-Type", MIMEJSON)
|
||||
var obj struct {
|
||||
Foo string `json:"foo"`
|
||||
@ -2239,7 +2314,7 @@ func TestContextShouldBindBodyWith(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := CreateTestContext(w)
|
||||
c.Request, _ = http.NewRequest(
|
||||
http.MethodPost, "http://example.com", bytes.NewBufferString(tt.bodyA),
|
||||
http.MethodPost, "http://example.com", strings.NewReader(tt.bodyA),
|
||||
)
|
||||
// When it binds to typeA and typeB, it finds the body is
|
||||
// not typeB but typeA.
|
||||
@ -2257,7 +2332,7 @@ func TestContextShouldBindBodyWith(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := CreateTestContext(w)
|
||||
c.Request, _ = http.NewRequest(
|
||||
http.MethodPost, "http://example.com", bytes.NewBufferString(tt.bodyB),
|
||||
http.MethodPost, "http://example.com", strings.NewReader(tt.bodyB),
|
||||
)
|
||||
objA := typeA{}
|
||||
require.Error(t, c.ShouldBindBodyWith(&objA, tt.bindingA))
|
||||
@ -2603,7 +2678,7 @@ func TestContextShouldBindBodyWithPlain(t *testing.T) {
|
||||
|
||||
func TestContextGolangContext(t *testing.T) {
|
||||
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||
c.Request, _ = http.NewRequest(http.MethodPost, "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}"))
|
||||
c.Request, _ = http.NewRequest(http.MethodPost, "/", strings.NewReader(`{"foo":"bar", "bar":"foo"}`))
|
||||
require.NoError(t, c.Err())
|
||||
assert.Nil(t, c.Done())
|
||||
ti, ok := c.Deadline()
|
||||
@ -2651,7 +2726,7 @@ func TestGetRequestHeaderValue(t *testing.T) {
|
||||
|
||||
func TestContextGetRawData(t *testing.T) {
|
||||
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||
body := bytes.NewBufferString("Fetch binary post data")
|
||||
body := strings.NewReader("Fetch binary post data")
|
||||
c.Request, _ = http.NewRequest(http.MethodPost, "/", body)
|
||||
c.Request.Header.Add("Content-Type", MIMEPOSTForm)
|
||||
|
||||
@ -3076,7 +3151,7 @@ func TestInterceptedHeader(t *testing.T) {
|
||||
// Compared to this time, this is when the response headers will be flushed
|
||||
// As response is flushed on c.String, the Header cannot be set by the first
|
||||
// middleware. Assert this
|
||||
assert.Equal(t, "", w.Result().Header.Get("X-Test"))
|
||||
assert.Empty(t, w.Result().Header.Get("X-Test"))
|
||||
assert.Equal(t, "present", w.Result().Header.Get("X-Test-2"))
|
||||
}
|
||||
|
||||
@ -3123,3 +3198,129 @@ func TestContextNext(t *testing.T) {
|
||||
assert.True(t, exists)
|
||||
assert.Equal(t, "value3", value)
|
||||
}
|
||||
|
||||
func TestContextSetCookieData(t *testing.T) {
|
||||
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||
c.SetSameSite(http.SameSiteLaxMode)
|
||||
var setCookie string
|
||||
|
||||
// Basic cookie settings
|
||||
cookie := &http.Cookie{
|
||||
Name: "user",
|
||||
Value: "gin",
|
||||
MaxAge: 1,
|
||||
Path: "/",
|
||||
Domain: "localhost",
|
||||
Secure: true,
|
||||
HttpOnly: true,
|
||||
}
|
||||
c.SetCookieData(cookie)
|
||||
setCookie = c.Writer.Header().Get("Set-Cookie")
|
||||
assert.Contains(t, setCookie, "user=gin")
|
||||
assert.Contains(t, setCookie, "Path=/")
|
||||
assert.Contains(t, setCookie, "Domain=localhost")
|
||||
assert.Contains(t, setCookie, "Max-Age=1")
|
||||
assert.Contains(t, setCookie, "HttpOnly")
|
||||
assert.Contains(t, setCookie, "Secure")
|
||||
// SameSite=Lax might be omitted in Go 1.23+ as it's the default
|
||||
// assert.Contains(t, setCookie, "SameSite=Lax")
|
||||
|
||||
// Test that when Path is empty, "/" is automatically set
|
||||
cookie = &http.Cookie{
|
||||
Name: "user",
|
||||
Value: "gin",
|
||||
MaxAge: 1,
|
||||
Path: "",
|
||||
Domain: "localhost",
|
||||
Secure: true,
|
||||
HttpOnly: true,
|
||||
}
|
||||
c.SetCookieData(cookie)
|
||||
setCookie = c.Writer.Header().Get("Set-Cookie")
|
||||
assert.Contains(t, setCookie, "user=gin")
|
||||
assert.Contains(t, setCookie, "Path=/")
|
||||
assert.Contains(t, setCookie, "Domain=localhost")
|
||||
assert.Contains(t, setCookie, "Max-Age=1")
|
||||
assert.Contains(t, setCookie, "HttpOnly")
|
||||
assert.Contains(t, setCookie, "Secure")
|
||||
// SameSite=Lax might be omitted in Go 1.23+ as it's the default
|
||||
// assert.Contains(t, setCookie, "SameSite=Lax")
|
||||
|
||||
// Test additional cookie attributes (Expires)
|
||||
expireTime := time.Now().Add(24 * time.Hour)
|
||||
cookie = &http.Cookie{
|
||||
Name: "user",
|
||||
Value: "gin",
|
||||
Path: "/",
|
||||
Domain: "localhost",
|
||||
Expires: expireTime,
|
||||
Secure: true,
|
||||
HttpOnly: true,
|
||||
}
|
||||
c.SetCookieData(cookie)
|
||||
|
||||
// Since the Expires value varies by time, partially verify with Contains
|
||||
setCookie = c.Writer.Header().Get("Set-Cookie")
|
||||
assert.Contains(t, setCookie, "user=gin")
|
||||
assert.Contains(t, setCookie, "Path=/")
|
||||
assert.Contains(t, setCookie, "Domain=localhost")
|
||||
assert.Contains(t, setCookie, "HttpOnly")
|
||||
assert.Contains(t, setCookie, "Secure")
|
||||
// SameSite=Lax might be omitted in Go 1.23+ as it's the default
|
||||
// assert.Contains(t, setCookie, "SameSite=Lax")
|
||||
|
||||
// Test for Partitioned attribute (Go 1.18+)
|
||||
cookie = &http.Cookie{
|
||||
Name: "user",
|
||||
Value: "gin",
|
||||
Path: "/",
|
||||
Domain: "localhost",
|
||||
Secure: true,
|
||||
HttpOnly: true,
|
||||
Partitioned: true,
|
||||
}
|
||||
c.SetCookieData(cookie)
|
||||
setCookie = c.Writer.Header().Get("Set-Cookie")
|
||||
assert.Contains(t, setCookie, "user=gin")
|
||||
assert.Contains(t, setCookie, "Path=/")
|
||||
assert.Contains(t, setCookie, "Domain=localhost")
|
||||
assert.Contains(t, setCookie, "HttpOnly")
|
||||
assert.Contains(t, setCookie, "Secure")
|
||||
// SameSite=Lax might be omitted in Go 1.23+ as it's the default
|
||||
// assert.Contains(t, setCookie, "SameSite=Lax")
|
||||
// Not testing for Partitioned attribute as it may not be supported in all Go versions
|
||||
|
||||
// Test that SameSiteStrictMode is explicitly included in the header
|
||||
t.Run("SameSite=Strict is included", func(t *testing.T) {
|
||||
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||
cookie := &http.Cookie{
|
||||
Name: "user",
|
||||
Value: "gin",
|
||||
Path: "/",
|
||||
Domain: "localhost",
|
||||
Secure: true,
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
}
|
||||
c.SetCookieData(cookie)
|
||||
setCookie := c.Writer.Header().Get("Set-Cookie")
|
||||
assert.Contains(t, setCookie, "SameSite=Strict")
|
||||
})
|
||||
|
||||
// Test that SameSiteNoneMode is explicitly included in the header
|
||||
t.Run("SameSite=None is included", func(t *testing.T) {
|
||||
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||
cookie := &http.Cookie{
|
||||
Name: "user",
|
||||
Value: "gin",
|
||||
Path: "/",
|
||||
Domain: "localhost",
|
||||
Secure: true,
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteNoneMode,
|
||||
}
|
||||
c.SetCookieData(cookie)
|
||||
setCookie := c.Writer.Header().Get("Set-Cookie")
|
||||
assert.Contains(t, setCookie, "SameSite=None")
|
||||
})
|
||||
}
|
||||
|
||||
4
debug.go
4
debug.go
@ -25,7 +25,7 @@ func IsDebugging() bool {
|
||||
var DebugPrintRouteFunc func(httpMethod, absolutePath, handlerName string, nuHandlers int)
|
||||
|
||||
// DebugPrintFunc indicates debug log output format.
|
||||
var DebugPrintFunc func(format string, values ...interface{})
|
||||
var DebugPrintFunc func(format string, values ...any)
|
||||
|
||||
func debugPrintRoute(httpMethod, absolutePath string, handlers HandlersChain) {
|
||||
if IsDebugging() {
|
||||
@ -78,7 +78,7 @@ func getMinVer(v string) (uint64, error) {
|
||||
|
||||
func debugPrintWARNINGDefault() {
|
||||
if v, e := getMinVer(runtime.Version()); e == nil && v < ginSupportMinGoVer {
|
||||
debugPrint(`[WARNING] Now Gin requires Go 1.22+.
|
||||
debugPrint(`[WARNING] Now Gin requires Go 1.23+.
|
||||
|
||||
`)
|
||||
}
|
||||
|
||||
@ -106,7 +106,7 @@ func TestDebugPrintWARNINGDefault(t *testing.T) {
|
||||
})
|
||||
m, e := getMinVer(runtime.Version())
|
||||
if e == nil && m < ginSupportMinGoVer {
|
||||
assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.22+.\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.23+.\n\n[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
|
||||
} else {
|
||||
assert.Equal(t, "[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
|
||||
}
|
||||
|
||||
154
docs/doc.md
154
docs/doc.md
@ -63,6 +63,7 @@
|
||||
- [http2 server push](#http2-server-push)
|
||||
- [Define format for the log of routes](#define-format-for-the-log-of-routes)
|
||||
- [Set and get a cookie](#set-and-get-a-cookie)
|
||||
- [Custom json codec at runtime](#custom-json-codec-at-runtime)
|
||||
- [Don't trust all proxies](#dont-trust-all-proxies)
|
||||
- [Testing](#testing)
|
||||
|
||||
@ -70,7 +71,7 @@
|
||||
|
||||
### Build with json replacement
|
||||
|
||||
Gin uses `encoding/json` as default json package but you can change it by build from other tags.
|
||||
Gin uses `encoding/json` as the default JSON package but you can change it by building from other tags.
|
||||
|
||||
[jsoniter](https://github.com/json-iterator/go)
|
||||
|
||||
@ -84,10 +85,10 @@ go build -tags=jsoniter .
|
||||
go build -tags=go_json .
|
||||
```
|
||||
|
||||
[sonic](https://github.com/bytedance/sonic) (you have to ensure that your cpu support avx instruction.)
|
||||
[sonic](https://github.com/bytedance/sonic)
|
||||
|
||||
```sh
|
||||
$ go build -tags="sonic avx" .
|
||||
$ go build -tags=sonic .
|
||||
```
|
||||
|
||||
### Build without `MsgPack` rendering feature
|
||||
@ -120,7 +121,7 @@ func main() {
|
||||
router.HEAD("/someHead", head)
|
||||
router.OPTIONS("/someOptions", options)
|
||||
|
||||
// By default it serves on :8080 unless a
|
||||
// By default, it serves on :8080 unless a
|
||||
// PORT environment variable was defined.
|
||||
router.Run()
|
||||
// router.Run(":3000") for a hard coded port
|
||||
@ -172,7 +173,7 @@ func main() {
|
||||
router := gin.Default()
|
||||
|
||||
// Query string parameters are parsed using the existing underlying request object.
|
||||
// The request responds to an url matching: /welcome?firstname=Jane&lastname=Doe
|
||||
// The request responds to a URL matching: /welcome?firstname=Jane&lastname=Doe
|
||||
router.GET("/welcome", func(c *gin.Context) {
|
||||
firstname := c.DefaultQuery("firstname", "Guest")
|
||||
lastname := c.Query("lastname") // shortcut for c.Request.URL.Query().Get("lastname")
|
||||
@ -300,7 +301,7 @@ curl -X POST http://localhost:8080/upload \
|
||||
|
||||
#### Multiple files
|
||||
|
||||
See the detail [example code](https://github.com/gin-gonic/examples/tree/master/upload-file/multiple).
|
||||
See the detailed [example code](https://github.com/gin-gonic/examples/tree/master/upload-file/multiple).
|
||||
|
||||
```go
|
||||
func main() {
|
||||
@ -704,7 +705,7 @@ $ curl -v -X POST \
|
||||
{"error":"Key: 'Login.Password' Error:Field validation for 'Password' failed on the 'required' tag"}
|
||||
```
|
||||
|
||||
Skip validate: when running the above example using the above the `curl` command, it returns error. Because the example use `binding:"required"` for `Password`. If use `binding:"-"` for `Password`, then it will not return error when running the above example again.
|
||||
Skip-validation: Running the example above using the `curl` command returns an error. This is because the example uses `binding:"required"` for `Password`. If instead, you use `binding:"-"` for `Password`, then it will not return an error when you run the example again.
|
||||
|
||||
### Custom Validators
|
||||
|
||||
@ -832,6 +833,8 @@ type Person struct {
|
||||
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"`
|
||||
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() {
|
||||
@ -851,6 +854,8 @@ func startPage(c *gin.Context) {
|
||||
log.Println(person.Birthday)
|
||||
log.Println(person.CreateTime)
|
||||
log.Println(person.UnixTime)
|
||||
log.Println(person.UnixMilliTime)
|
||||
log.Println(person.UnixMicroTime)
|
||||
}
|
||||
|
||||
c.String(http.StatusOK, "Success")
|
||||
@ -860,7 +865,7 @@ func startPage(c *gin.Context) {
|
||||
Test it with:
|
||||
|
||||
```sh
|
||||
curl -X GET "localhost:8085/testing?name=appleboy&address=xyz&birthday=1992-03-15&createTime=1562400033000000123&unixTime=1562400033"
|
||||
curl -X GET "localhost:8085/testing?name=appleboy&address=xyz&birthday=1992-03-15&createTime=1562400033000000123&unixTime=1562400033&unixMilliTime=1562400033001&unixMicroTime=1562400033000012"
|
||||
```
|
||||
|
||||
|
||||
@ -868,7 +873,7 @@ curl -X GET "localhost:8085/testing?name=appleboy&address=xyz&birthday=1992-03-1
|
||||
|
||||
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:
|
||||
|
||||
```
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
@ -1183,7 +1188,7 @@ func main() {
|
||||
})
|
||||
|
||||
r.GET("/moreJSON", func(c *gin.Context) {
|
||||
// You also can use a struct
|
||||
// You can also use a struct
|
||||
var msg struct {
|
||||
Name string `json:"user"`
|
||||
Message string
|
||||
@ -1389,13 +1394,19 @@ func main() {
|
||||
|
||||
### HTML rendering
|
||||
|
||||
Using LoadHTMLGlob() or LoadHTMLFiles()
|
||||
Using LoadHTMLGlob() or LoadHTMLFiles() or LoadHTMLFS()
|
||||
|
||||
```go
|
||||
//go:embed templates/*
|
||||
var templates embed.FS
|
||||
|
||||
func main() {
|
||||
router := gin.Default()
|
||||
router.LoadHTMLGlob("templates/*")
|
||||
//router.LoadHTMLFiles("templates/template1.html", "templates/template2.html")
|
||||
//router.LoadHTMLFS(http.Dir("templates"), "template1.html", "template2.html")
|
||||
//or
|
||||
//router.LoadHTMLFS(http.FS(templates), "templates/template1.html", "templates/template2.html")
|
||||
router.GET("/index", func(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, "index.tmpl", gin.H{
|
||||
"title": "Main website",
|
||||
@ -1486,7 +1497,7 @@ You may use custom delims
|
||||
|
||||
#### Custom Template Funcs
|
||||
|
||||
See the detail [example code](https://github.com/gin-gonic/examples/tree/master/template).
|
||||
See the detailed [example code](https://github.com/gin-gonic/examples/tree/master/template).
|
||||
|
||||
main.go
|
||||
|
||||
@ -1538,7 +1549,7 @@ Date: 2017/07/01
|
||||
|
||||
### Multitemplate
|
||||
|
||||
Gin allow by default use only one html.Template. Check [a multitemplate render](https://github.com/gin-contrib/multitemplate) for using features like go 1.6 `block template`.
|
||||
Gin allows only one html.Template by default. Check [a multitemplate render](https://github.com/gin-contrib/multitemplate) for using features like go 1.6 `block template`.
|
||||
|
||||
### Redirects
|
||||
|
||||
@ -2087,7 +2098,7 @@ type formB struct {
|
||||
func SomeHandler(c *gin.Context) {
|
||||
objA := formA{}
|
||||
objB := formB{}
|
||||
// This c.ShouldBind consumes c.Request.Body and it cannot be reused.
|
||||
// Calling c.ShouldBind consumes c.Request.Body and it cannot be reused.
|
||||
if errA := c.ShouldBind(&objA); errA == nil {
|
||||
c.String(http.StatusOK, `the body should be formA`)
|
||||
// Always an error is occurred by this because c.Request.Body is EOF now.
|
||||
@ -2294,12 +2305,64 @@ func main() {
|
||||
router := gin.Default()
|
||||
|
||||
router.GET("/cookie", func(c *gin.Context) {
|
||||
cookie, err := c.Cookie("gin_cookie")
|
||||
|
||||
if err != nil {
|
||||
cookie = "NotSet"
|
||||
// Using http.Cookie struct for more control
|
||||
c.SetCookieData(&http.Cookie{
|
||||
Name: "gin_cookie",
|
||||
Value: "test",
|
||||
Path: "/",
|
||||
Domain: "localhost",
|
||||
MaxAge: 3600,
|
||||
Secure: false,
|
||||
HttpOnly: true,
|
||||
// Additional fields available in http.Cookie
|
||||
Expires: time.Now().Add(24 * time.Hour),
|
||||
// Partitioned: true, // Available in newer Go versions
|
||||
})
|
||||
}
|
||||
|
||||
fmt.Printf("Cookie value: %s \n", cookie)
|
||||
})
|
||||
|
||||
router.Run()
|
||||
}
|
||||
```
|
||||
|
||||
You can also use the `SetCookieData` method, which accepts a `*http.Cookie` directly for more flexibility:
|
||||
|
||||
```go
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
router := gin.Default()
|
||||
|
||||
router.GET("/cookie", func(c *gin.Context) {
|
||||
cookie, err := c.Cookie("gin_cookie")
|
||||
|
||||
if err != nil {
|
||||
cookie = "NotSet"
|
||||
c.SetCookie("gin_cookie", "test", 3600, "/", "localhost", false, true)
|
||||
// Using http.Cookie struct for more control
|
||||
c.SetCookieData(&http.Cookie{
|
||||
Name: "gin_cookie",
|
||||
Value: "test",
|
||||
Path: "/",
|
||||
Domain: "localhost",
|
||||
MaxAge: 3600,
|
||||
Secure: false,
|
||||
HttpOnly: true,
|
||||
// Additional fields available in http.Cookie
|
||||
Expires: time.Now().Add(24 * time.Hour),
|
||||
// Partitioned: true, // Available in newer Go versions
|
||||
})
|
||||
}
|
||||
|
||||
fmt.Printf("Cookie value: %s \n", cookie)
|
||||
@ -2309,6 +2372,65 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
### Custom json codec at runtime
|
||||
|
||||
Gin support custom json serialization and deserialization logic without using compile tags.
|
||||
|
||||
1. Define a custom struct implements the `json.Core` interface.
|
||||
|
||||
2. Before your engine starts, assign values to `json.API` using the custom struct.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/codec/json"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
var customConfig = jsoniter.Config{
|
||||
EscapeHTML: true,
|
||||
SortMapKeys: true,
|
||||
ValidateJsonRawMessage: true,
|
||||
}.Froze()
|
||||
|
||||
// implement api.JsonApi
|
||||
type customJsonApi struct {
|
||||
}
|
||||
|
||||
func (j customJsonApi) Marshal(v any) ([]byte, error) {
|
||||
return customConfig.Marshal(v)
|
||||
}
|
||||
|
||||
func (j customJsonApi) Unmarshal(data []byte, v any) error {
|
||||
return customConfig.Unmarshal(data, v)
|
||||
}
|
||||
|
||||
func (j customJsonApi) MarshalIndent(v any, prefix, indent string) ([]byte, error) {
|
||||
return customConfig.MarshalIndent(v, prefix, indent)
|
||||
}
|
||||
|
||||
func (j customJsonApi) NewEncoder(writer io.Writer) json.Encoder {
|
||||
return customConfig.NewEncoder(writer)
|
||||
}
|
||||
|
||||
func (j customJsonApi) NewDecoder(reader io.Reader) json.Decoder {
|
||||
return customConfig.NewDecoder(reader)
|
||||
}
|
||||
|
||||
func main() {
|
||||
//Replace the default json api
|
||||
json.API = customJsonApi{}
|
||||
|
||||
//Start your gin engine
|
||||
router := gin.Default()
|
||||
router.Run(":8080")
|
||||
}
|
||||
```
|
||||
|
||||
## Don't trust all proxies
|
||||
|
||||
Gin lets you specify which headers to hold the real client IP (if any),
|
||||
@ -2320,7 +2442,7 @@ or network CIDRs from where clients which their request headers related to clien
|
||||
IP can be trusted. They can be IPv4 addresses, IPv4 CIDRs, IPv6 addresses or
|
||||
IPv6 CIDRs.
|
||||
|
||||
**Attention:** Gin trust all proxies by default if you don't specify a trusted
|
||||
**Attention:** Gin trusts all proxies by default if you don't specify a trusted
|
||||
proxy using the function above, **this is NOT safe**. At the same time, if you don't
|
||||
use any proxy, you can disable this feature by using `Engine.SetTrustedProxies(nil)`,
|
||||
then `Context.ClientIP()` will return the remote address directly to avoid some
|
||||
|
||||
@ -9,7 +9,7 @@ import (
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin/internal/json"
|
||||
"github.com/gin-gonic/gin/codec/json"
|
||||
)
|
||||
|
||||
// ErrorType is an unsigned 64-bit error code as defined in the gin spec.
|
||||
@ -77,7 +77,7 @@ func (msg *Error) JSON() any {
|
||||
|
||||
// MarshalJSON implements the json.Marshaller interface.
|
||||
func (msg *Error) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(msg.JSON())
|
||||
return json.API.Marshal(msg.JSON())
|
||||
}
|
||||
|
||||
// Error implements the error interface.
|
||||
@ -91,7 +91,7 @@ func (msg *Error) IsType(flags ErrorType) bool {
|
||||
}
|
||||
|
||||
// Unwrap returns the wrapped error, to allow interoperability with errors.Is(), errors.As() and errors.Unwrap()
|
||||
func (msg *Error) Unwrap() error {
|
||||
func (msg Error) Unwrap() error {
|
||||
return msg.Err
|
||||
}
|
||||
|
||||
@ -157,7 +157,7 @@ func (a errorMsgs) JSON() any {
|
||||
|
||||
// MarshalJSON implements the json.Marshaller interface.
|
||||
func (a errorMsgs) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(a.JSON())
|
||||
return json.API.Marshal(a.JSON())
|
||||
}
|
||||
|
||||
func (a errorMsgs) String() string {
|
||||
|
||||
@ -9,7 +9,7 @@ import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin/internal/json"
|
||||
"github.com/gin-gonic/gin/codec/json"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@ -33,8 +33,8 @@ func TestError(t *testing.T) {
|
||||
"meta": "some data",
|
||||
}, err.JSON())
|
||||
|
||||
jsonBytes, _ := json.Marshal(err)
|
||||
assert.Equal(t, "{\"error\":\"test error\",\"meta\":\"some data\"}", string(jsonBytes))
|
||||
jsonBytes, _ := json.API.Marshal(err)
|
||||
assert.JSONEq(t, "{\"error\":\"test error\",\"meta\":\"some data\"}", string(jsonBytes))
|
||||
|
||||
err.SetMeta(H{ //nolint: errcheck
|
||||
"status": "200",
|
||||
@ -92,14 +92,14 @@ Error #03: third
|
||||
H{"error": "second", "meta": "some data"},
|
||||
H{"error": "third", "status": "400"},
|
||||
}, errs.JSON())
|
||||
jsonBytes, _ := json.Marshal(errs)
|
||||
assert.Equal(t, "[{\"error\":\"first\"},{\"error\":\"second\",\"meta\":\"some data\"},{\"error\":\"third\",\"status\":\"400\"}]", string(jsonBytes))
|
||||
jsonBytes, _ := json.API.Marshal(errs)
|
||||
assert.JSONEq(t, "[{\"error\":\"first\"},{\"error\":\"second\",\"meta\":\"some data\"},{\"error\":\"third\",\"status\":\"400\"}]", string(jsonBytes))
|
||||
errs = errorMsgs{
|
||||
{Err: errors.New("first"), Type: ErrorTypePrivate},
|
||||
}
|
||||
assert.Equal(t, H{"error": "first"}, errs.JSON())
|
||||
jsonBytes, _ = json.Marshal(errs)
|
||||
assert.Equal(t, "{\"error\":\"first\"}", string(jsonBytes))
|
||||
jsonBytes, _ = json.API.Marshal(errs)
|
||||
assert.JSONEq(t, "{\"error\":\"first\"}", string(jsonBytes))
|
||||
|
||||
errs = errorMsgs{}
|
||||
assert.Nil(t, errs.Last())
|
||||
@ -126,4 +126,15 @@ func TestErrorUnwrap(t *testing.T) {
|
||||
require.ErrorIs(t, err, innerErr)
|
||||
var testErr TestErr
|
||||
require.ErrorAs(t, err, &testErr)
|
||||
|
||||
// Test non-pointer usage of gin.Error
|
||||
errNonPointer := Error{
|
||||
Err: innerErr,
|
||||
Type: ErrorTypeAny,
|
||||
}
|
||||
wrappedErr := fmt.Errorf("wrapped: %w", errNonPointer)
|
||||
// Check that 'errors.Is()' and 'errors.As()' behave as expected for non-pointer usage
|
||||
require.ErrorIs(t, wrappedErr, innerErr)
|
||||
var testErrNonPointer TestErr
|
||||
require.ErrorAs(t, wrappedErr, &testErrNonPointer)
|
||||
}
|
||||
|
||||
1
fs.go
1
fs.go
@ -17,7 +17,6 @@ type OnlyFilesFS struct {
|
||||
// Open passes `Open` to the upstream implementation without `Readdir` functionality.
|
||||
func (o OnlyFilesFS) Open(name string) (http.File, error) {
|
||||
f, err := o.FileSystem.Open(name)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
23
gin.go
23
gin.go
@ -16,8 +16,8 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/gin-gonic/gin/internal/bytesconv"
|
||||
filesystem "github.com/gin-gonic/gin/internal/fs"
|
||||
"github.com/gin-gonic/gin/render"
|
||||
|
||||
"github.com/quic-go/quic-go/http3"
|
||||
"golang.org/x/net/http2"
|
||||
"golang.org/x/net/http2/h2c"
|
||||
@ -215,7 +215,7 @@ func New(opts ...OptionFunc) *Engine {
|
||||
trustedProxies: []string{"0.0.0.0/0", "::/0"},
|
||||
trustedCIDRs: defaultTrustedCIDRs,
|
||||
}
|
||||
engine.RouterGroup.engine = engine
|
||||
engine.engine = engine
|
||||
engine.pool.New = func() any {
|
||||
return engine.allocateContext(engine.maxParams)
|
||||
}
|
||||
@ -285,6 +285,19 @@ func (engine *Engine) LoadHTMLFiles(files ...string) {
|
||||
engine.SetHTMLTemplate(templ)
|
||||
}
|
||||
|
||||
// LoadHTMLFS loads an http.FileSystem and a slice of patterns
|
||||
// and associates the result with HTML renderer.
|
||||
func (engine *Engine) LoadHTMLFS(fs http.FileSystem, patterns ...string) {
|
||||
if IsDebugging() {
|
||||
engine.HTMLRender = render.HTMLDebug{FileSystem: fs, Patterns: patterns, FuncMap: engine.FuncMap, Delims: engine.delims}
|
||||
return
|
||||
}
|
||||
|
||||
templ := template.Must(template.New("").Delims(engine.delims.Left, engine.delims.Right).Funcs(engine.FuncMap).ParseFS(
|
||||
filesystem.FileSystem{FileSystem: fs}, patterns...))
|
||||
engine.SetHTMLTemplate(templ)
|
||||
}
|
||||
|
||||
// SetHTMLTemplate associate a template with HTML renderer.
|
||||
func (engine *Engine) SetHTMLTemplate(templ *template.Template) {
|
||||
if len(engine.trees) > 0 {
|
||||
@ -321,7 +334,7 @@ func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
|
||||
return engine
|
||||
}
|
||||
|
||||
// With returns a Engine with the configuration set in the OptionFunc.
|
||||
// With returns an Engine with the configuration set in the OptionFunc.
|
||||
func (engine *Engine) With(opts ...OptionFunc) *Engine {
|
||||
for _, opt := range opts {
|
||||
opt(engine)
|
||||
@ -363,7 +376,7 @@ func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
|
||||
}
|
||||
|
||||
// Routes returns a slice of registered routes, including some useful information, such as:
|
||||
// the http method, path and the handler name.
|
||||
// the http method, path, and the handler name.
|
||||
func (engine *Engine) Routes() (routes RoutesInfo) {
|
||||
for _, tree := range engine.trees {
|
||||
routes = iterate("", tree.method, routes, tree.root)
|
||||
@ -637,10 +650,12 @@ func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
// Disclaimer: You can loop yourself to deal with this, use wisely.
|
||||
func (engine *Engine) HandleContext(c *Context) {
|
||||
oldIndexValue := c.index
|
||||
oldHandlers := c.handlers
|
||||
c.reset()
|
||||
engine.handleHTTPRequest(c)
|
||||
|
||||
c.index = oldIndexValue
|
||||
c.handlers = oldHandlers
|
||||
}
|
||||
|
||||
func (engine *Engine) handleHTTPRequest(c *Context) {
|
||||
|
||||
13
ginS/gins.go
13
ginS/gins.go
@ -12,8 +12,10 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
var once sync.Once
|
||||
var internalEngine *gin.Engine
|
||||
var (
|
||||
once sync.Once
|
||||
internalEngine *gin.Engine
|
||||
)
|
||||
|
||||
func engine() *gin.Engine {
|
||||
once.Do(func() {
|
||||
@ -32,6 +34,11 @@ func LoadHTMLFiles(files ...string) {
|
||||
engine().LoadHTMLFiles(files...)
|
||||
}
|
||||
|
||||
// LoadHTMLFS is a wrapper for Engine.LoadHTMLFS.
|
||||
func LoadHTMLFS(fs http.FileSystem, patterns ...string) {
|
||||
engine().LoadHTMLFS(fs, patterns...)
|
||||
}
|
||||
|
||||
// SetHTMLTemplate is a wrapper for Engine.SetHTMLTemplate.
|
||||
func SetHTMLTemplate(templ *template.Template) {
|
||||
engine().SetHTMLTemplate(templ)
|
||||
@ -154,7 +161,7 @@ func RunUnix(file string) (err error) {
|
||||
|
||||
// RunFd attaches the router to a http.Server and starts listening and serving HTTP requests
|
||||
// through the specified file descriptor.
|
||||
// Note: the method will block the calling goroutine indefinitely unless on error happens.
|
||||
// Note: the method will block the calling goroutine indefinitely unless an error happens.
|
||||
func RunFd(fd int) (err error) {
|
||||
return engine().RunFd(fd)
|
||||
}
|
||||
|
||||
@ -28,7 +28,6 @@ import (
|
||||
// params[1]=response status (custom compare status) default:"200 OK"
|
||||
// params[2]=response body (custom compare content) default:"it worked"
|
||||
func testRequest(t *testing.T, params ...string) {
|
||||
|
||||
if len(params) == 0 {
|
||||
t.Fatal("url cannot be empty")
|
||||
}
|
||||
@ -47,12 +46,12 @@ func testRequest(t *testing.T, params ...string) {
|
||||
body, ioerr := io.ReadAll(resp.Body)
|
||||
require.NoError(t, ioerr)
|
||||
|
||||
var responseStatus = "200 OK"
|
||||
responseStatus := "200 OK"
|
||||
if len(params) > 1 && params[1] != "" {
|
||||
responseStatus = params[1]
|
||||
}
|
||||
|
||||
var responseBody = "it worked"
|
||||
responseBody := "it worked"
|
||||
if len(params) > 2 && params[2] != "" {
|
||||
responseBody = params[2]
|
||||
}
|
||||
@ -170,7 +169,7 @@ func TestRunTLS(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPusher(t *testing.T) {
|
||||
var html = template.Must(template.New("https").Parse(`
|
||||
html := template.Must(template.New("https").Parse(`
|
||||
<html>
|
||||
<head>
|
||||
<title>Https Test</title>
|
||||
|
||||
153
gin_test.go
153
gin_test.go
@ -46,7 +46,7 @@ func setupHTMLFiles(t *testing.T, mode string, tls bool, loadMethod func(*Engine
|
||||
})
|
||||
router.GET("/raw", func(c *Context) {
|
||||
c.HTML(http.StatusOK, "raw.tmpl", map[string]any{
|
||||
"now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC),
|
||||
"now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC), //nolint:gofumpt
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -325,6 +325,115 @@ func TestLoadHTMLFilesFuncMap(t *testing.T) {
|
||||
assert.Equal(t, "Date: 2017/07/01", string(resp))
|
||||
}
|
||||
|
||||
var tmplFS = http.Dir("testdata/template")
|
||||
|
||||
func TestLoadHTMLFSTestMode(t *testing.T) {
|
||||
ts := setupHTMLFiles(
|
||||
t,
|
||||
TestMode,
|
||||
false,
|
||||
func(router *Engine) {
|
||||
router.LoadHTMLFS(tmplFS, "hello.tmpl", "raw.tmpl")
|
||||
},
|
||||
)
|
||||
defer ts.Close()
|
||||
|
||||
res, err := http.Get(ts.URL + "/test")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
resp, _ := io.ReadAll(res.Body)
|
||||
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
|
||||
}
|
||||
|
||||
func TestLoadHTMLFSDebugMode(t *testing.T) {
|
||||
ts := setupHTMLFiles(
|
||||
t,
|
||||
DebugMode,
|
||||
false,
|
||||
func(router *Engine) {
|
||||
router.LoadHTMLFS(tmplFS, "hello.tmpl", "raw.tmpl")
|
||||
},
|
||||
)
|
||||
defer ts.Close()
|
||||
|
||||
res, err := http.Get(ts.URL + "/test")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
resp, _ := io.ReadAll(res.Body)
|
||||
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
|
||||
}
|
||||
|
||||
func TestLoadHTMLFSReleaseMode(t *testing.T) {
|
||||
ts := setupHTMLFiles(
|
||||
t,
|
||||
ReleaseMode,
|
||||
false,
|
||||
func(router *Engine) {
|
||||
router.LoadHTMLFS(tmplFS, "hello.tmpl", "raw.tmpl")
|
||||
},
|
||||
)
|
||||
defer ts.Close()
|
||||
|
||||
res, err := http.Get(ts.URL + "/test")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
resp, _ := io.ReadAll(res.Body)
|
||||
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
|
||||
}
|
||||
|
||||
func TestLoadHTMLFSUsingTLS(t *testing.T) {
|
||||
ts := setupHTMLFiles(
|
||||
t,
|
||||
TestMode,
|
||||
true,
|
||||
func(router *Engine) {
|
||||
router.LoadHTMLFS(tmplFS, "hello.tmpl", "raw.tmpl")
|
||||
},
|
||||
)
|
||||
defer ts.Close()
|
||||
|
||||
// Use InsecureSkipVerify for avoiding `x509: certificate signed by unknown authority` error
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
}
|
||||
client := &http.Client{Transport: tr}
|
||||
res, err := client.Get(ts.URL + "/test")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
resp, _ := io.ReadAll(res.Body)
|
||||
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
|
||||
}
|
||||
|
||||
func TestLoadHTMLFSFuncMap(t *testing.T) {
|
||||
ts := setupHTMLFiles(
|
||||
t,
|
||||
TestMode,
|
||||
false,
|
||||
func(router *Engine) {
|
||||
router.LoadHTMLFS(tmplFS, "hello.tmpl", "raw.tmpl")
|
||||
},
|
||||
)
|
||||
defer ts.Close()
|
||||
|
||||
res, err := http.Get(ts.URL + "/raw")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
resp, _ := io.ReadAll(res.Body)
|
||||
assert.Equal(t, "Date: 2017/07/01", string(resp))
|
||||
}
|
||||
|
||||
func TestAddRoute(t *testing.T) {
|
||||
router := New()
|
||||
router.addRoute(http.MethodGet, "/", HandlersChain{func(_ *Context) {}})
|
||||
@ -573,6 +682,44 @@ func TestEngineHandleContextManyReEntries(t *testing.T) {
|
||||
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) {
|
||||
r := New()
|
||||
|
||||
@ -700,7 +847,7 @@ func handlerTest1(c *Context) {}
|
||||
func handlerTest2(c *Context) {}
|
||||
|
||||
func TestNewOptionFunc(t *testing.T) {
|
||||
var fc = func(e *Engine) {
|
||||
fc := func(e *Engine) {
|
||||
e.GET("/test1", handlerTest1)
|
||||
e.GET("/test2", handlerTest2)
|
||||
|
||||
@ -736,7 +883,7 @@ func TestWithOptionFunc(t *testing.T) {
|
||||
type Birthday string
|
||||
|
||||
func (b *Birthday) UnmarshalParam(param string) error {
|
||||
*b = Birthday(strings.Replace(param, "-", "/", -1))
|
||||
*b = Birthday(strings.ReplaceAll(param, "-", "/"))
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@ -298,8 +298,8 @@ func TestShouldBindUri(t *testing.T) {
|
||||
router.Handle(http.MethodGet, "/rest/:name/:id", func(c *Context) {
|
||||
var person Person
|
||||
require.NoError(t, c.ShouldBindUri(&person))
|
||||
assert.NotEqual(t, "", person.Name)
|
||||
assert.NotEqual(t, "", person.ID)
|
||||
assert.NotEmpty(t, person.Name)
|
||||
assert.NotEmpty(t, person.ID)
|
||||
c.String(http.StatusOK, "ShouldBindUri test OK")
|
||||
})
|
||||
|
||||
@ -320,8 +320,8 @@ func TestBindUri(t *testing.T) {
|
||||
router.Handle(http.MethodGet, "/rest/:name/:id", func(c *Context) {
|
||||
var person Person
|
||||
require.NoError(t, c.BindUri(&person))
|
||||
assert.NotEqual(t, "", person.Name)
|
||||
assert.NotEqual(t, "", person.ID)
|
||||
assert.NotEmpty(t, person.Name)
|
||||
assert.NotEmpty(t, person.ID)
|
||||
c.String(http.StatusOK, "BindUri test OK")
|
||||
})
|
||||
|
||||
|
||||
41
go.mod
41
go.mod
@ -1,29 +1,29 @@
|
||||
module github.com/gin-gonic/gin
|
||||
|
||||
go 1.22
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.11.6
|
||||
github.com/gin-contrib/sse v0.1.0
|
||||
github.com/go-playground/validator/v10 v10.20.0
|
||||
github.com/bytedance/sonic v1.13.2
|
||||
github.com/gin-contrib/sse v1.1.0
|
||||
github.com/go-playground/validator/v10 v10.26.0
|
||||
github.com/goccy/go-json v0.10.2
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/mattn/go-isatty v0.0.20
|
||||
github.com/pelletier/go-toml/v2 v2.2.2
|
||||
github.com/quic-go/quic-go v0.48.2
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/modern-go/reflect2 v1.0.2
|
||||
github.com/pelletier/go-toml/v2 v2.2.4
|
||||
github.com/quic-go/quic-go v0.52.0
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/ugorji/go/codec v1.2.12
|
||||
golang.org/x/net v0.33.0
|
||||
google.golang.org/protobuf v1.34.1
|
||||
golang.org/x/net v0.41.0
|
||||
google.golang.org/protobuf v1.36.6
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
||||
github.com/cloudwego/base64x v0.1.5 // 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.8 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||
@ -31,17 +31,16 @@ require (
|
||||
github.com/klauspost/cpuid/v2 v2.0.9 // 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/onsi/ginkgo/v2 v2.9.5 // 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
|
||||
go.uber.org/mock v0.4.0 // indirect
|
||||
go.uber.org/mock v0.5.0 // indirect
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
|
||||
golang.org/x/crypto v0.31.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
|
||||
golang.org/x/mod v0.17.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
|
||||
golang.org/x/crypto v0.39.0 // indirect
|
||||
golang.org/x/mod v0.25.0 // indirect
|
||||
golang.org/x/sync v0.15.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/text v0.26.0 // indirect
|
||||
golang.org/x/tools v0.33.0 // indirect
|
||||
)
|
||||
|
||||
79
go.sum
79
go.sum
@ -1,21 +1,21 @@
|
||||
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
||||
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
|
||||
github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
|
||||
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||
github.com/cloudwego/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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
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.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||
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/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
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=
|
||||
@ -24,8 +24,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
|
||||
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
|
||||
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||
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=
|
||||
@ -55,61 +55,54 @@ github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
|
||||
github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
|
||||
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/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
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/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/quic-go/quic-go v0.52.0 h1:/SlHrCRElyaU6MaEPKqKr9z83sBg2v4FLLvWM+Z47pA=
|
||||
github.com/quic-go/quic-go v0.52.0/go.mod h1:MFlGGpcpJqRAfmYi6NC2cptDPSxRWTOGNuP4wqrWmzQ=
|
||||
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.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.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.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.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
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/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
|
||||
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
|
||||
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
||||
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
||||
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/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
||||
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||
golang.org/x/sync v0.15.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.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
||||
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
||||
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
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/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/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=
|
||||
|
||||
@ -6,14 +6,17 @@ package bytesconv
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
cRand "crypto/rand"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var testString = "Albert Einstein: Logic will get you from A to B. Imagination will take you everywhere."
|
||||
var testBytes = []byte(testString)
|
||||
var (
|
||||
testString = "Albert Einstein: Logic will get you from A to B. Imagination will take you everywhere."
|
||||
testBytes = []byte(testString)
|
||||
)
|
||||
|
||||
func rawBytesToStr(b []byte) string {
|
||||
return string(b)
|
||||
@ -28,7 +31,10 @@ func rawStrToBytes(s string) []byte {
|
||||
func TestBytesToString(t *testing.T) {
|
||||
data := make([]byte, 1024)
|
||||
for i := 0; i < 100; i++ {
|
||||
rand.Read(data)
|
||||
_, err := cRand.Read(data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if rawBytesToStr(data) != BytesToString(data) {
|
||||
t.Fatal("don't match")
|
||||
}
|
||||
@ -42,7 +48,7 @@ const (
|
||||
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
|
||||
)
|
||||
|
||||
var src = rand.NewSource(time.Now().UnixNano())
|
||||
var src = rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
|
||||
func RandStringBytesMaskImprSrcSB(n int) string {
|
||||
sb := strings.Builder{}
|
||||
|
||||
21
internal/fs/fs.go
Normal file
21
internal/fs/fs.go
Normal file
@ -0,0 +1,21 @@
|
||||
package fs
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// FileSystem implements an [fs.FS].
|
||||
type FileSystem struct {
|
||||
http.FileSystem
|
||||
}
|
||||
|
||||
// Open passes `Open` to the upstream implementation and return an [fs.File].
|
||||
func (o FileSystem) Open(name string) (fs.File, error) {
|
||||
f, err := o.FileSystem.Open(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return fs.File(f), nil
|
||||
}
|
||||
49
internal/fs/fs_test.go
Normal file
49
internal/fs/fs_test.go
Normal file
@ -0,0 +1,49 @@
|
||||
package fs
|
||||
|
||||
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 TestFileSystem_Open(t *testing.T) {
|
||||
var testFile *os.File
|
||||
mockFS := &mockFileSystem{
|
||||
open: func(name string) (http.File, error) {
|
||||
return testFile, nil
|
||||
},
|
||||
}
|
||||
fs := &FileSystem{mockFS}
|
||||
|
||||
file, err := fs.Open("foo")
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, testFile, file)
|
||||
}
|
||||
|
||||
func TestFileSystem_Open_err(t *testing.T) {
|
||||
testError := errors.New("mock")
|
||||
mockFS := &mockFileSystem{
|
||||
open: func(_ string) (http.File, error) {
|
||||
return nil, testError
|
||||
},
|
||||
}
|
||||
fs := &FileSystem{mockFS}
|
||||
|
||||
file, err := fs.Open("foo")
|
||||
|
||||
require.ErrorIs(t, err, testError)
|
||||
assert.Nil(t, file)
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
// Copyright 2017 Bo-Yi Wu. All rights reserved.
|
||||
// Use of this source code is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go_json
|
||||
|
||||
package json
|
||||
|
||||
import json "github.com/goccy/go-json"
|
||||
|
||||
var (
|
||||
// Marshal is exported by gin/json package.
|
||||
Marshal = json.Marshal
|
||||
// Unmarshal is exported by gin/json package.
|
||||
Unmarshal = json.Unmarshal
|
||||
// MarshalIndent is exported by gin/json package.
|
||||
MarshalIndent = json.MarshalIndent
|
||||
// NewDecoder is exported by gin/json package.
|
||||
NewDecoder = json.NewDecoder
|
||||
// NewEncoder is exported by gin/json package.
|
||||
NewEncoder = json.NewEncoder
|
||||
)
|
||||
@ -1,22 +0,0 @@
|
||||
// Copyright 2017 Bo-Yi Wu. All rights reserved.
|
||||
// Use of this source code is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !jsoniter && !go_json && !(sonic && avx && (linux || windows || darwin) && amd64)
|
||||
|
||||
package json
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
var (
|
||||
// Marshal is exported by gin/json package.
|
||||
Marshal = json.Marshal
|
||||
// Unmarshal is exported by gin/json package.
|
||||
Unmarshal = json.Unmarshal
|
||||
// MarshalIndent is exported by gin/json package.
|
||||
MarshalIndent = json.MarshalIndent
|
||||
// NewDecoder is exported by gin/json package.
|
||||
NewDecoder = json.NewDecoder
|
||||
// NewEncoder is exported by gin/json package.
|
||||
NewEncoder = json.NewEncoder
|
||||
)
|
||||
@ -1,23 +0,0 @@
|
||||
// Copyright 2017 Bo-Yi Wu. All rights reserved.
|
||||
// Use of this source code is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build jsoniter
|
||||
|
||||
package json
|
||||
|
||||
import jsoniter "github.com/json-iterator/go"
|
||||
|
||||
var (
|
||||
json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
// Marshal is exported by gin/json package.
|
||||
Marshal = json.Marshal
|
||||
// Unmarshal is exported by gin/json package.
|
||||
Unmarshal = json.Unmarshal
|
||||
// MarshalIndent is exported by gin/json package.
|
||||
MarshalIndent = json.MarshalIndent
|
||||
// NewDecoder is exported by gin/json package.
|
||||
NewDecoder = json.NewDecoder
|
||||
// NewEncoder is exported by gin/json package.
|
||||
NewEncoder = json.NewEncoder
|
||||
)
|
||||
@ -1,23 +0,0 @@
|
||||
// Copyright 2022 Gin Core Team. All rights reserved.
|
||||
// Use of this source code is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build sonic && avx && (linux || windows || darwin) && amd64
|
||||
|
||||
package json
|
||||
|
||||
import "github.com/bytedance/sonic"
|
||||
|
||||
var (
|
||||
json = sonic.ConfigStd
|
||||
// Marshal is exported by gin/json package.
|
||||
Marshal = json.Marshal
|
||||
// Unmarshal is exported by gin/json package.
|
||||
Unmarshal = json.Unmarshal
|
||||
// MarshalIndent is exported by gin/json package.
|
||||
MarshalIndent = json.MarshalIndent
|
||||
// NewDecoder is exported by gin/json package.
|
||||
NewDecoder = json.NewDecoder
|
||||
// NewEncoder is exported by gin/json package.
|
||||
NewEncoder = json.NewEncoder
|
||||
)
|
||||
40
logger.go
40
logger.go
@ -82,7 +82,7 @@ type LogFormatterParams struct {
|
||||
// BodySize is the size of the Response Body
|
||||
BodySize int
|
||||
// Keys are the keys set on the request's context.
|
||||
Keys map[string]any
|
||||
Keys map[any]any
|
||||
}
|
||||
|
||||
// StatusCodeColor is the ANSI color for appropriately logging http status code to a terminal.
|
||||
@ -103,6 +103,27 @@ func (p *LogFormatterParams) StatusCodeColor() string {
|
||||
}
|
||||
}
|
||||
|
||||
// LatencyColor is the ANSI color for latency
|
||||
func (p *LogFormatterParams) LatencyColor() string {
|
||||
latency := p.Latency
|
||||
switch {
|
||||
case latency < time.Millisecond*100:
|
||||
return white
|
||||
case latency < time.Millisecond*200:
|
||||
return green
|
||||
case latency < time.Millisecond*300:
|
||||
return cyan
|
||||
case latency < time.Millisecond*500:
|
||||
return blue
|
||||
case latency < time.Second:
|
||||
return yellow
|
||||
case latency < time.Second*2:
|
||||
return magenta
|
||||
default:
|
||||
return red
|
||||
}
|
||||
}
|
||||
|
||||
// MethodColor is the ANSI color for appropriately logging http method to a terminal.
|
||||
func (p *LogFormatterParams) MethodColor() string {
|
||||
method := p.Method
|
||||
@ -139,20 +160,27 @@ func (p *LogFormatterParams) IsOutputColor() bool {
|
||||
|
||||
// defaultLogFormatter is the default log format function Logger middleware uses.
|
||||
var defaultLogFormatter = func(param LogFormatterParams) string {
|
||||
var statusColor, methodColor, resetColor string
|
||||
var statusColor, methodColor, resetColor, latencyColor string
|
||||
if param.IsOutputColor() {
|
||||
statusColor = param.StatusCodeColor()
|
||||
methodColor = param.MethodColor()
|
||||
resetColor = param.ResetColor()
|
||||
latencyColor = param.LatencyColor()
|
||||
}
|
||||
|
||||
if param.Latency > time.Minute {
|
||||
param.Latency = param.Latency.Truncate(time.Second)
|
||||
switch {
|
||||
case param.Latency > time.Minute:
|
||||
param.Latency = param.Latency.Truncate(time.Second * 10)
|
||||
case param.Latency > time.Second:
|
||||
param.Latency = param.Latency.Truncate(time.Millisecond * 10)
|
||||
case param.Latency > time.Millisecond:
|
||||
param.Latency = param.Latency.Truncate(time.Microsecond * 10)
|
||||
}
|
||||
return fmt.Sprintf("[GIN] %v |%s %3d %s| %13v | %15s |%s %-7s %s %#v\n%s",
|
||||
|
||||
return fmt.Sprintf("[GIN] %v |%s %3d %s|%s %8v %s| %15s |%s %-7s %s %#v\n%s",
|
||||
param.TimeStamp.Format("2006/01/02 - 15:04:05"),
|
||||
statusColor, param.StatusCode, resetColor,
|
||||
param.Latency,
|
||||
latencyColor, param.Latency, resetColor,
|
||||
param.ClientIP,
|
||||
methodColor, param.Method, resetColor,
|
||||
param.Path,
|
||||
|
||||
@ -181,7 +181,7 @@ func TestLoggerWithFormatter(t *testing.T) {
|
||||
|
||||
func TestLoggerWithConfigFormatting(t *testing.T) {
|
||||
var gotParam LogFormatterParams
|
||||
var gotKeys map[string]any
|
||||
var gotKeys map[any]any
|
||||
buffer := new(strings.Builder)
|
||||
|
||||
router := New()
|
||||
@ -277,11 +277,11 @@ func TestDefaultLogFormatter(t *testing.T) {
|
||||
isTerm: false,
|
||||
}
|
||||
|
||||
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 | 200 | 5s | 20.20.20.20 | GET \"/\"\n", defaultLogFormatter(termFalseParam))
|
||||
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 | 200 | 2743h29m3s | 20.20.20.20 | GET \"/\"\n", defaultLogFormatter(termFalseLongDurationParam))
|
||||
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 | 200 | 5s | 20.20.20.20 | GET \"/\"\n", defaultLogFormatter(termFalseParam))
|
||||
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 | 200 | 2743h29m0s | 20.20.20.20 | GET \"/\"\n", defaultLogFormatter(termFalseLongDurationParam))
|
||||
|
||||
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 |\x1b[97;42m 200 \x1b[0m| 5s | 20.20.20.20 |\x1b[97;44m GET \x1b[0m \"/\"\n", defaultLogFormatter(termTrueParam))
|
||||
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 |\x1b[97;42m 200 \x1b[0m| 2743h29m3s | 20.20.20.20 |\x1b[97;44m GET \x1b[0m \"/\"\n", defaultLogFormatter(termTrueLongDurationParam))
|
||||
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 |\x1b[97;42m 200 \x1b[0m|\x1b[97;41m 5s \x1b[0m| 20.20.20.20 |\x1b[97;44m GET \x1b[0m \"/\"\n", defaultLogFormatter(termTrueParam))
|
||||
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 |\x1b[97;42m 200 \x1b[0m|\x1b[97;41m 2743h29m0s \x1b[0m| 20.20.20.20 |\x1b[97;44m GET \x1b[0m \"/\"\n", defaultLogFormatter(termTrueLongDurationParam))
|
||||
}
|
||||
|
||||
func TestColorForMethod(t *testing.T) {
|
||||
@ -317,6 +317,23 @@ func TestColorForStatus(t *testing.T) {
|
||||
assert.Equal(t, red, colorForStatus(2), "other things should be red")
|
||||
}
|
||||
|
||||
func TestColorForLatency(t *testing.T) {
|
||||
colorForLantency := func(latency time.Duration) string {
|
||||
p := LogFormatterParams{
|
||||
Latency: latency,
|
||||
}
|
||||
return p.LatencyColor()
|
||||
}
|
||||
|
||||
assert.Equal(t, white, colorForLantency(time.Duration(0)), "0 should be white")
|
||||
assert.Equal(t, white, colorForLantency(time.Millisecond*20), "20ms should be white")
|
||||
assert.Equal(t, green, colorForLantency(time.Millisecond*150), "150ms should be green")
|
||||
assert.Equal(t, cyan, colorForLantency(time.Millisecond*250), "250ms should be cyan")
|
||||
assert.Equal(t, yellow, colorForLantency(time.Millisecond*600), "600ms should be yellow")
|
||||
assert.Equal(t, magenta, colorForLantency(time.Millisecond*1500), "1.5s should be magenta")
|
||||
assert.Equal(t, red, colorForLantency(time.Second*3), "other things should be red")
|
||||
}
|
||||
|
||||
func TestResetColor(t *testing.T) {
|
||||
p := LogFormatterParams{}
|
||||
assert.Equal(t, string([]byte{27, 91, 48, 109}), p.ResetColor())
|
||||
@ -371,11 +388,11 @@ func TestErrorLogger(t *testing.T) {
|
||||
|
||||
w := PerformRequest(router, http.MethodGet, "/error")
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, "{\"error\":\"this is an error\"}", w.Body.String())
|
||||
assert.JSONEq(t, "{\"error\":\"this is an error\"}", w.Body.String())
|
||||
|
||||
w = PerformRequest(router, http.MethodGet, "/abort")
|
||||
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
||||
assert.Equal(t, "{\"error\":\"no authorized\"}", w.Body.String())
|
||||
assert.JSONEq(t, "{\"error\":\"no authorized\"}", w.Body.String())
|
||||
|
||||
w = PerformRequest(router, http.MethodGet, "/print")
|
||||
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||
|
||||
@ -203,7 +203,7 @@ func TestMiddlewareAbortHandlersChainAndNext(t *testing.T) {
|
||||
assert.Equal(t, "ACB", signature)
|
||||
}
|
||||
|
||||
// TestFailHandlersChain - ensure that Fail interrupt used middleware in fifo order as
|
||||
// TestMiddlewareFailHandlersChain - ensure that Fail interrupt used middleware in fifo order as
|
||||
// as well as Abort
|
||||
func TestMiddlewareFailHandlersChain(t *testing.T) {
|
||||
// SETUP
|
||||
@ -249,5 +249,5 @@ func TestMiddlewareWrite(t *testing.T) {
|
||||
w := PerformRequest(router, http.MethodGet, "/")
|
||||
|
||||
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.ReplaceAll("hola\n<map><foo>bar</foo></map>{\"foo\":\"bar\"}{\"foo\":\"bar\"}event:test\ndata:message\n\n", " ", ""), strings.ReplaceAll(w.Body.String(), " ", ""))
|
||||
}
|
||||
|
||||
6
mode.go
6
mode.go
@ -44,8 +44,10 @@ var DefaultWriter io.Writer = os.Stdout
|
||||
// DefaultErrorWriter is the default io.Writer used by Gin to debug errors
|
||||
var DefaultErrorWriter io.Writer = os.Stderr
|
||||
|
||||
var ginMode int32 = debugCode
|
||||
var modeName atomic.Value
|
||||
var (
|
||||
ginMode int32 = debugCode
|
||||
modeName atomic.Value
|
||||
)
|
||||
|
||||
func init() {
|
||||
mode := os.Getenv(EnvGinMode)
|
||||
|
||||
38
recovery.go
38
recovery.go
@ -19,12 +19,9 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
dunno = []byte("???")
|
||||
centerDot = []byte("·")
|
||||
dot = []byte(".")
|
||||
slash = []byte("/")
|
||||
)
|
||||
const dunno = "???"
|
||||
|
||||
var dunnoBytes = []byte(dunno)
|
||||
|
||||
// RecoveryFunc defines the function passable to CustomRecovery.
|
||||
type RecoveryFunc func(c *Context, err any)
|
||||
@ -73,12 +70,7 @@ func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
|
||||
stack := stack(3)
|
||||
httpRequest, _ := httputil.DumpRequest(c.Request, false)
|
||||
headers := strings.Split(string(httpRequest), "\r\n")
|
||||
for idx, header := range headers {
|
||||
current := strings.Split(header, ":")
|
||||
if current[0] == "Authorization" {
|
||||
headers[idx] = current[0] + ": *"
|
||||
}
|
||||
}
|
||||
maskAuthorization(headers)
|
||||
headersToStr := strings.Join(headers, "\r\n")
|
||||
if brokenPipe {
|
||||
logger.Printf("%s\n%s%s", err, headersToStr, reset)
|
||||
@ -134,22 +126,32 @@ func stack(skip int) []byte {
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// maskAuthorization replaces any "Authorization: <token>" header with "Authorization: *", hiding sensitive credentials.
|
||||
func maskAuthorization(headers []string) {
|
||||
for idx, header := range headers {
|
||||
key, _, _ := strings.Cut(header, ":")
|
||||
if strings.EqualFold(key, "Authorization") {
|
||||
headers[idx] = key + ": *"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// source returns a space-trimmed slice of the n'th line.
|
||||
func source(lines [][]byte, n int) []byte {
|
||||
n-- // in stack trace, lines are 1-indexed but our array is 0-indexed
|
||||
if n < 0 || n >= len(lines) {
|
||||
return dunno
|
||||
return dunnoBytes
|
||||
}
|
||||
return bytes.TrimSpace(lines[n])
|
||||
}
|
||||
|
||||
// function returns, if possible, the name of the function containing the PC.
|
||||
func function(pc uintptr) []byte {
|
||||
func function(pc uintptr) string {
|
||||
fn := runtime.FuncForPC(pc)
|
||||
if fn == nil {
|
||||
return dunno
|
||||
}
|
||||
name := []byte(fn.Name())
|
||||
name := fn.Name()
|
||||
// The name includes the path name to the package, which is unnecessary
|
||||
// since the file name is already included. Plus, it has center dots.
|
||||
// That is, we see
|
||||
@ -158,13 +160,13 @@ func function(pc uintptr) []byte {
|
||||
// *T.ptrmethod
|
||||
// Also the package path might contain dot (e.g. code.google.com/...),
|
||||
// so first eliminate the path prefix
|
||||
if lastSlash := bytes.LastIndex(name, slash); lastSlash >= 0 {
|
||||
if lastSlash := strings.LastIndexByte(name, '/'); lastSlash >= 0 {
|
||||
name = name[lastSlash+1:]
|
||||
}
|
||||
if period := bytes.Index(name, dot); period >= 0 {
|
||||
if period := strings.IndexByte(name, '.'); period >= 0 {
|
||||
name = name[period+1:]
|
||||
}
|
||||
name = bytes.ReplaceAll(name, centerDot, dot)
|
||||
name = strings.ReplaceAll(name, "·", ".")
|
||||
return name
|
||||
}
|
||||
|
||||
|
||||
@ -88,16 +88,34 @@ func TestPanicWithAbort(t *testing.T) {
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
}
|
||||
|
||||
func TestMaskAuthorization(t *testing.T) {
|
||||
secret := "Bearer aaaabbbbccccddddeeeeffff"
|
||||
headers := []string{
|
||||
"Host: www.example.com",
|
||||
"Authorization: " + secret,
|
||||
"User-Agent: curl/7.51.0",
|
||||
"Accept: */*",
|
||||
"Content-Type: application/json",
|
||||
"Content-Length: 1",
|
||||
}
|
||||
maskAuthorization(headers)
|
||||
|
||||
for _, h := range headers {
|
||||
assert.NotContains(t, h, secret)
|
||||
}
|
||||
assert.Contains(t, headers, "Authorization: *")
|
||||
}
|
||||
|
||||
func TestSource(t *testing.T) {
|
||||
bs := source(nil, 0)
|
||||
assert.Equal(t, dunno, bs)
|
||||
assert.Equal(t, dunnoBytes, bs)
|
||||
|
||||
in := [][]byte{
|
||||
[]byte("Hello world."),
|
||||
[]byte("Hi, gin.."),
|
||||
}
|
||||
bs = source(in, 10)
|
||||
assert.Equal(t, dunno, bs)
|
||||
assert.Equal(t, dunnoBytes, bs)
|
||||
|
||||
bs = source(in, 1)
|
||||
assert.Equal(t, []byte("Hello world."), bs)
|
||||
|
||||
@ -7,6 +7,8 @@ package render
|
||||
import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin/internal/fs"
|
||||
)
|
||||
|
||||
// Delims represents a set of Left and Right delimiters for HTML template rendering.
|
||||
@ -31,10 +33,12 @@ type HTMLProduction struct {
|
||||
|
||||
// HTMLDebug contains template delims and pattern and function with file list.
|
||||
type HTMLDebug struct {
|
||||
Files []string
|
||||
Glob string
|
||||
Delims Delims
|
||||
FuncMap template.FuncMap
|
||||
Files []string
|
||||
Glob string
|
||||
FileSystem http.FileSystem
|
||||
Patterns []string
|
||||
Delims Delims
|
||||
FuncMap template.FuncMap
|
||||
}
|
||||
|
||||
// HTML contains template reference and its name with given interface object.
|
||||
@ -63,6 +67,7 @@ func (r HTMLDebug) Instance(name string, data any) Render {
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
|
||||
func (r HTMLDebug) loadTemplate() *template.Template {
|
||||
if r.FuncMap == nil {
|
||||
r.FuncMap = template.FuncMap{}
|
||||
@ -73,7 +78,11 @@ func (r HTMLDebug) loadTemplate() *template.Template {
|
||||
if r.Glob != "" {
|
||||
return template.Must(template.New("").Delims(r.Delims.Left, r.Delims.Right).Funcs(r.FuncMap).ParseGlob(r.Glob))
|
||||
}
|
||||
panic("the HTML debug render was created without files or glob pattern")
|
||||
if r.FileSystem != nil && len(r.Patterns) > 0 {
|
||||
return template.Must(template.New("").Delims(r.Delims.Left, r.Delims.Right).Funcs(r.FuncMap).ParseFS(
|
||||
fs.FileSystem{FileSystem: r.FileSystem}, r.Patterns...))
|
||||
}
|
||||
panic("the HTML debug render was created without files or glob pattern or file system with patterns")
|
||||
}
|
||||
|
||||
// Render (HTML) executes template and writes its result with custom ContentType for response.
|
||||
|
||||
@ -9,9 +9,10 @@ import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"unicode"
|
||||
|
||||
"github.com/gin-gonic/gin/codec/json"
|
||||
"github.com/gin-gonic/gin/internal/bytesconv"
|
||||
"github.com/gin-gonic/gin/internal/json"
|
||||
)
|
||||
|
||||
// JSON contains the given interface object.
|
||||
@ -65,7 +66,7 @@ func (r JSON) WriteContentType(w http.ResponseWriter) {
|
||||
// WriteJSON marshals the given interface object and writes it with custom ContentType.
|
||||
func WriteJSON(w http.ResponseWriter, obj any) error {
|
||||
writeContentType(w, jsonContentType)
|
||||
jsonBytes, err := json.Marshal(obj)
|
||||
jsonBytes, err := json.API.Marshal(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -76,7 +77,7 @@ func WriteJSON(w http.ResponseWriter, obj any) error {
|
||||
// Render (IndentedJSON) marshals the given interface object and writes it with custom ContentType.
|
||||
func (r IndentedJSON) Render(w http.ResponseWriter) error {
|
||||
r.WriteContentType(w)
|
||||
jsonBytes, err := json.MarshalIndent(r.Data, "", " ")
|
||||
jsonBytes, err := json.API.MarshalIndent(r.Data, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -92,7 +93,7 @@ func (r IndentedJSON) WriteContentType(w http.ResponseWriter) {
|
||||
// Render (SecureJSON) marshals the given interface object and writes it with custom ContentType.
|
||||
func (r SecureJSON) Render(w http.ResponseWriter) error {
|
||||
r.WriteContentType(w)
|
||||
jsonBytes, err := json.Marshal(r.Data)
|
||||
jsonBytes, err := json.API.Marshal(r.Data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -115,7 +116,7 @@ func (r SecureJSON) WriteContentType(w http.ResponseWriter) {
|
||||
// Render (JsonpJSON) marshals the given interface object and writes it and its callback with custom ContentType.
|
||||
func (r JsonpJSON) Render(w http.ResponseWriter) (err error) {
|
||||
r.WriteContentType(w)
|
||||
ret, err := json.Marshal(r.Data)
|
||||
ret, err := json.API.Marshal(r.Data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -151,20 +152,23 @@ func (r JsonpJSON) WriteContentType(w http.ResponseWriter) {
|
||||
}
|
||||
|
||||
// Render (AsciiJSON) marshals the given interface object and writes it with custom ContentType.
|
||||
func (r AsciiJSON) Render(w http.ResponseWriter) (err error) {
|
||||
func (r AsciiJSON) Render(w http.ResponseWriter) error {
|
||||
r.WriteContentType(w)
|
||||
ret, err := json.Marshal(r.Data)
|
||||
ret, err := json.API.Marshal(r.Data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var buffer bytes.Buffer
|
||||
escapeBuf := make([]byte, 0, 6) // Preallocate 6 bytes for Unicode escape sequences
|
||||
|
||||
for _, r := range bytesconv.BytesToString(ret) {
|
||||
cvt := string(r)
|
||||
if r >= 128 {
|
||||
cvt = fmt.Sprintf("\\u%04x", int64(r))
|
||||
if r > unicode.MaxASCII {
|
||||
escapeBuf = fmt.Appendf(escapeBuf[:0], "\\u%04x", r) // Reuse escapeBuf
|
||||
buffer.Write(escapeBuf)
|
||||
} else {
|
||||
buffer.WriteByte(byte(r))
|
||||
}
|
||||
buffer.WriteString(cvt)
|
||||
}
|
||||
|
||||
_, err = w.Write(buffer.Bytes())
|
||||
@ -179,7 +183,7 @@ func (r AsciiJSON) WriteContentType(w http.ResponseWriter) {
|
||||
// Render (PureJSON) writes custom ContentType and encodes the given interface object.
|
||||
func (r PureJSON) Render(w http.ResponseWriter) error {
|
||||
r.WriteContentType(w)
|
||||
encoder := json.NewEncoder(w)
|
||||
encoder := json.API.NewEncoder(w)
|
||||
encoder.SetEscapeHTML(false)
|
||||
return encoder.Encode(r.Data)
|
||||
}
|
||||
|
||||
@ -15,7 +15,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin/internal/json"
|
||||
"github.com/gin-gonic/gin/codec/json"
|
||||
testdata "github.com/gin-gonic/gin/testdata/protoexample"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@ -38,7 +38,7 @@ func TestRenderJSON(t *testing.T) {
|
||||
err := (JSON{data}).Render(w)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"\\u003cb\\u003e\"}", w.Body.String())
|
||||
assert.JSONEq(t, "{\"foo\":\"bar\",\"html\":\"\\u003cb\\u003e\"}", w.Body.String())
|
||||
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
}
|
||||
|
||||
@ -60,7 +60,7 @@ func TestRenderIndentedJSON(t *testing.T) {
|
||||
err := (IndentedJSON{data}).Render(w)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "{\n \"bar\": \"foo\",\n \"foo\": \"bar\"\n}", w.Body.String())
|
||||
assert.JSONEq(t, "{\n \"bar\": \"foo\",\n \"foo\": \"bar\"\n}", w.Body.String())
|
||||
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
}
|
||||
|
||||
@ -85,7 +85,7 @@ func TestRenderSecureJSON(t *testing.T) {
|
||||
err1 := (SecureJSON{"while(1);", data}).Render(w1)
|
||||
|
||||
require.NoError(t, err1)
|
||||
assert.Equal(t, "{\"foo\":\"bar\"}", w1.Body.String())
|
||||
assert.JSONEq(t, "{\"foo\":\"bar\"}", w1.Body.String())
|
||||
assert.Equal(t, "application/json; charset=utf-8", w1.Header().Get("Content-Type"))
|
||||
|
||||
w2 := httptest.NewRecorder()
|
||||
@ -173,7 +173,7 @@ func TestRenderJsonpJSONError(t *testing.T) {
|
||||
err = jsonpJSON.Render(ew)
|
||||
assert.Equal(t, `write "`+`(`+`" error`, err.Error())
|
||||
|
||||
data, _ := json.Marshal(jsonpJSON.Data) // error was returned while writing data
|
||||
data, _ := json.API.Marshal(jsonpJSON.Data) // error was returned while writing data
|
||||
ew.bufString = string(data)
|
||||
err = jsonpJSON.Render(ew)
|
||||
assert.Equal(t, `write "`+string(data)+`" error`, err.Error())
|
||||
@ -194,7 +194,7 @@ func TestRenderJsonpJSONError2(t *testing.T) {
|
||||
e := (JsonpJSON{"", data}).Render(w)
|
||||
require.NoError(t, e)
|
||||
|
||||
assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String())
|
||||
assert.JSONEq(t, "{\"foo\":\"bar\"}", w.Body.String())
|
||||
assert.Equal(t, "application/javascript; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
}
|
||||
|
||||
@ -217,7 +217,7 @@ func TestRenderAsciiJSON(t *testing.T) {
|
||||
err := (AsciiJSON{data1}).Render(w1)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "{\"lang\":\"GO\\u8bed\\u8a00\",\"tag\":\"\\u003cbr\\u003e\"}", w1.Body.String())
|
||||
assert.JSONEq(t, "{\"lang\":\"GO\\u8bed\\u8a00\",\"tag\":\"\\u003cbr\\u003e\"}", w1.Body.String())
|
||||
assert.Equal(t, "application/json", w1.Header().Get("Content-Type"))
|
||||
|
||||
w2 := httptest.NewRecorder()
|
||||
@ -244,7 +244,7 @@ func TestRenderPureJSON(t *testing.T) {
|
||||
}
|
||||
err := (PureJSON{data}).Render(w)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"<b>\"}\n", w.Body.String())
|
||||
assert.JSONEq(t, "{\"foo\":\"bar\",\"html\":\"<b>\"}\n", w.Body.String())
|
||||
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
}
|
||||
|
||||
@ -489,10 +489,12 @@ func TestRenderHTMLTemplateEmptyName(t *testing.T) {
|
||||
func TestRenderHTMLDebugFiles(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
htmlRender := HTMLDebug{
|
||||
Files: []string{"../testdata/template/hello.tmpl"},
|
||||
Glob: "",
|
||||
Delims: Delims{Left: "{[{", Right: "}]}"},
|
||||
FuncMap: nil,
|
||||
Files: []string{"../testdata/template/hello.tmpl"},
|
||||
Glob: "",
|
||||
FileSystem: nil,
|
||||
Patterns: nil,
|
||||
Delims: Delims{Left: "{[{", Right: "}]}"},
|
||||
FuncMap: nil,
|
||||
}
|
||||
instance := htmlRender.Instance("hello.tmpl", map[string]any{
|
||||
"name": "thinkerou",
|
||||
@ -508,10 +510,33 @@ func TestRenderHTMLDebugFiles(t *testing.T) {
|
||||
func TestRenderHTMLDebugGlob(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
htmlRender := HTMLDebug{
|
||||
Files: nil,
|
||||
Glob: "../testdata/template/hello*",
|
||||
Delims: Delims{Left: "{[{", Right: "}]}"},
|
||||
FuncMap: nil,
|
||||
Files: nil,
|
||||
Glob: "../testdata/template/hello*",
|
||||
FileSystem: nil,
|
||||
Patterns: nil,
|
||||
Delims: Delims{Left: "{[{", Right: "}]}"},
|
||||
FuncMap: nil,
|
||||
}
|
||||
instance := htmlRender.Instance("hello.tmpl", map[string]any{
|
||||
"name": "thinkerou",
|
||||
})
|
||||
|
||||
err := instance.Render(w)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "<h1>Hello thinkerou</h1>", w.Body.String())
|
||||
assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
}
|
||||
|
||||
func TestRenderHTMLDebugFS(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
htmlRender := HTMLDebug{
|
||||
Files: nil,
|
||||
Glob: "",
|
||||
FileSystem: http.Dir("../testdata/template"),
|
||||
Patterns: []string{"hello.tmpl"},
|
||||
Delims: Delims{Left: "{[{", Right: "}]}"},
|
||||
FuncMap: nil,
|
||||
}
|
||||
instance := htmlRender.Instance("hello.tmpl", map[string]any{
|
||||
"name": "thinkerou",
|
||||
@ -526,10 +551,12 @@ func TestRenderHTMLDebugGlob(t *testing.T) {
|
||||
|
||||
func TestRenderHTMLDebugPanics(t *testing.T) {
|
||||
htmlRender := HTMLDebug{
|
||||
Files: nil,
|
||||
Glob: "",
|
||||
Delims: Delims{"{{", "}}"},
|
||||
FuncMap: nil,
|
||||
Files: nil,
|
||||
Glob: "",
|
||||
FileSystem: nil,
|
||||
Patterns: nil,
|
||||
Delims: Delims{"{{", "}}"},
|
||||
FuncMap: nil,
|
||||
}
|
||||
assert.Panics(t, func() { htmlRender.Instance("", nil) })
|
||||
}
|
||||
@ -581,7 +608,7 @@ func TestRenderReaderNoContentLength(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRenderWriteError(t *testing.T) {
|
||||
data := []interface{}{"value1", "value2"}
|
||||
data := []any{"value1", "value2"}
|
||||
prefix := "my-prefix:"
|
||||
r := SecureJSON{Data: data, Prefix: prefix}
|
||||
ew := &errorWriter{
|
||||
|
||||
@ -484,7 +484,7 @@ func TestRouterMiddlewareAndStatic(t *testing.T) {
|
||||
assert.Contains(t, w.Body.String(), "package gin")
|
||||
// Content-Type='text/plain; charset=utf-8' when go version <= 1.16,
|
||||
// else, Content-Type='text/x-go; charset=utf-8'
|
||||
assert.NotEqual(t, "", w.Header().Get("Content-Type"))
|
||||
assert.NotEmpty(t, w.Header().Get("Content-Type"))
|
||||
assert.NotEqual(t, "Mon, 02 Jan 2006 15:04:05 MST", w.Header().Get("Last-Modified"))
|
||||
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"))
|
||||
@ -764,7 +764,7 @@ func TestRouteContextHoldsFullPath(t *testing.T) {
|
||||
// Test not found
|
||||
router.Use(func(c *Context) {
|
||||
// For not found routes full path is empty
|
||||
assert.Equal(t, "", c.FullPath())
|
||||
assert.Empty(t, c.FullPath())
|
||||
})
|
||||
|
||||
w := PerformRequest(router, http.MethodGet, "/not-found")
|
||||
|
||||
4
tree.go
4
tree.go
@ -234,7 +234,7 @@ walk:
|
||||
// Wildcard conflict
|
||||
pathSeg := path
|
||||
if n.nType != catchAll {
|
||||
pathSeg = strings.SplitN(pathSeg, "/", 2)[0]
|
||||
pathSeg, _, _ = strings.Cut(pathSeg, "/")
|
||||
}
|
||||
prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path
|
||||
panic("'" + pathSeg +
|
||||
@ -358,7 +358,7 @@ func (n *node) insertChild(path string, fullPath string, handlers HandlersChain)
|
||||
if len(n.path) > 0 && n.path[len(n.path)-1] == '/' {
|
||||
pathSeg := ""
|
||||
if len(n.children) != 0 {
|
||||
pathSeg = strings.SplitN(n.children[0].path, "/", 2)[0]
|
||||
pathSeg, _, _ = strings.Cut(n.children[0].path, "/")
|
||||
}
|
||||
panic("catch-all wildcard '" + path +
|
||||
"' in new path '" + fullPath +
|
||||
|
||||
@ -481,7 +481,7 @@ func TestTreeDuplicatePath(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
//printChildren(tree, "")
|
||||
// printChildren(tree, "")
|
||||
|
||||
checkRequests(t, tree, testRequests{
|
||||
{"/", false, "/", nil},
|
||||
@ -532,7 +532,7 @@ func TestTreeCatchAllConflictRoot(t *testing.T) {
|
||||
|
||||
func TestTreeCatchMaxParams(t *testing.T) {
|
||||
tree := &node{}
|
||||
var route = "/cmd/*filepath"
|
||||
route := "/cmd/*filepath"
|
||||
tree.addRoute(route, fakeHandler(route))
|
||||
}
|
||||
|
||||
@ -692,7 +692,7 @@ func TestTreeRootTrailingSlashRedirect(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRedirectTrailingSlash(t *testing.T) {
|
||||
var data = []struct {
|
||||
data := []struct {
|
||||
path string
|
||||
}{
|
||||
{"/hello/:name"},
|
||||
|
||||
@ -94,7 +94,7 @@ func somefunction() {
|
||||
}
|
||||
|
||||
func TestJoinPaths(t *testing.T) {
|
||||
assert.Equal(t, "", joinPaths("", ""))
|
||||
assert.Empty(t, joinPaths("", ""))
|
||||
assert.Equal(t, "/", joinPaths("", "/"))
|
||||
assert.Equal(t, "/a", joinPaths("/a", ""))
|
||||
assert.Equal(t, "/a/", joinPaths("/a/", ""))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user