mirror of
https://github.com/gin-gonic/gin.git
synced 2026-06-13 09:18:15 +08:00
Compare commits
3 Commits
f339817a0a
...
62d9866697
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62d9866697 | ||
|
|
d7776de7d4 | ||
|
|
bf3ab6608c |
@ -23,6 +23,7 @@ const (
|
||||
MIMEYAML = "application/x-yaml"
|
||||
MIMEYAML2 = "application/yaml"
|
||||
MIMETOML = "application/toml"
|
||||
MIMEBSON = "application/bson"
|
||||
)
|
||||
|
||||
// Binding describes the interface which needs to be implemented for binding the
|
||||
@ -86,6 +87,7 @@ var (
|
||||
Header Binding = headerBinding{}
|
||||
Plain BindingBody = plainBinding{}
|
||||
TOML BindingBody = tomlBinding{}
|
||||
BSON BindingBody = bsonBinding{}
|
||||
)
|
||||
|
||||
// Default returns the appropriate Binding instance based on the HTTP method
|
||||
@ -110,6 +112,8 @@ func Default(method, contentType string) Binding {
|
||||
return TOML
|
||||
case MIMEMultipartPOSTForm:
|
||||
return FormMultipart
|
||||
case MIMEBSON:
|
||||
return BSON
|
||||
default: // case MIMEPOSTForm:
|
||||
return Form
|
||||
}
|
||||
|
||||
@ -21,6 +21,7 @@ const (
|
||||
MIMEYAML = "application/x-yaml"
|
||||
MIMEYAML2 = "application/yaml"
|
||||
MIMETOML = "application/toml"
|
||||
MIMEBSON = "application/bson"
|
||||
)
|
||||
|
||||
// Binding describes the interface which needs to be implemented for binding the
|
||||
@ -82,6 +83,7 @@ var (
|
||||
Header = headerBinding{}
|
||||
TOML = tomlBinding{}
|
||||
Plain = plainBinding{}
|
||||
BSON BindingBody = bsonBinding{}
|
||||
)
|
||||
|
||||
// Default returns the appropriate Binding instance based on the HTTP method
|
||||
@ -104,6 +106,8 @@ func Default(method, contentType string) Binding {
|
||||
return FormMultipart
|
||||
case MIMETOML:
|
||||
return TOML
|
||||
case MIMEBSON:
|
||||
return BSON
|
||||
default: // case MIMEPOSTForm:
|
||||
return Form
|
||||
}
|
||||
|
||||
@ -21,6 +21,7 @@ import (
|
||||
"github.com/gin-gonic/gin/testdata/protoexample"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
@ -172,6 +173,9 @@ func TestBindingDefault(t *testing.T) {
|
||||
|
||||
assert.Equal(t, TOML, Default(http.MethodPost, MIMETOML))
|
||||
assert.Equal(t, TOML, Default(http.MethodPut, MIMETOML))
|
||||
|
||||
assert.Equal(t, BSON, Default(http.MethodPost, MIMEBSON))
|
||||
assert.Equal(t, BSON, Default(http.MethodPut, MIMEBSON))
|
||||
}
|
||||
|
||||
func TestBindingJSONNilBody(t *testing.T) {
|
||||
@ -731,6 +735,18 @@ func TestBindingProtoBufFail(t *testing.T) {
|
||||
string(data), string(data[1:]))
|
||||
}
|
||||
|
||||
func TestBindingBSON(t *testing.T) {
|
||||
var obj FooStruct
|
||||
obj.Foo = "bar"
|
||||
data, _ := bson.Marshal(&obj)
|
||||
testBodyBinding(t,
|
||||
BSON, "bson",
|
||||
"/", "/",
|
||||
string(data),
|
||||
// note: for badbody, we remove first byte to make it invalid
|
||||
string(data[1:]))
|
||||
}
|
||||
|
||||
func TestValidationFails(t *testing.T) {
|
||||
var obj FooStruct
|
||||
req := requestWithBody(http.MethodPost, "/", `{"bar": "foo"}`)
|
||||
|
||||
30
binding/bson.go
Normal file
30
binding/bson.go
Normal file
@ -0,0 +1,30 @@
|
||||
// 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 binding
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
)
|
||||
|
||||
type bsonBinding struct{}
|
||||
|
||||
func (bsonBinding) Name() string {
|
||||
return "bson"
|
||||
}
|
||||
|
||||
func (b bsonBinding) Bind(req *http.Request, obj any) error {
|
||||
buf, err := io.ReadAll(req.Body)
|
||||
if err == nil {
|
||||
err = b.BindBody(buf, obj)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (bsonBinding) BindBody(body []byte, obj any) error {
|
||||
return bson.Unmarshal(body, obj)
|
||||
}
|
||||
105
context.go
105
context.go
@ -10,7 +10,6 @@ import (
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"maps"
|
||||
"math"
|
||||
"mime/multipart"
|
||||
"net"
|
||||
@ -40,6 +39,7 @@ const (
|
||||
MIMEYAML2 = binding.MIMEYAML2
|
||||
MIMETOML = binding.MIMETOML
|
||||
MIMEPROTOBUF = binding.MIMEPROTOBUF
|
||||
MIMEBSON = binding.MIMEBSON
|
||||
)
|
||||
|
||||
// BodyBytesKey indicates a default body bytes key.
|
||||
@ -55,6 +55,42 @@ const ContextRequestKey ContextKeyType = 0
|
||||
// abortIndex represents a typical value used in abort functions.
|
||||
const abortIndex int8 = math.MaxInt8 >> 1
|
||||
|
||||
// ContextKeys is a thread-safe key-value store wrapper around sync.Map
|
||||
// that provides compatibility with existing map[any]any API expectations
|
||||
type ContextKeys struct {
|
||||
m sync.Map
|
||||
}
|
||||
|
||||
// Store stores a value in the context keys
|
||||
func (ck *ContextKeys) Store(key, value any) {
|
||||
ck.m.Store(key, value)
|
||||
}
|
||||
|
||||
// Load retrieves a value from the context keys
|
||||
func (ck *ContextKeys) Load(key any) (value any, exists bool) {
|
||||
return ck.m.Load(key)
|
||||
}
|
||||
|
||||
// Delete removes a value from the context keys
|
||||
func (ck *ContextKeys) Delete(key any) {
|
||||
ck.m.Delete(key)
|
||||
}
|
||||
|
||||
// Range iterates over all key-value pairs in the context keys
|
||||
func (ck *ContextKeys) Range(f func(key, value any) bool) {
|
||||
ck.m.Range(f)
|
||||
}
|
||||
|
||||
// IsEmpty returns true if the context keys contain no values
|
||||
func (ck *ContextKeys) IsEmpty() bool {
|
||||
empty := true
|
||||
ck.m.Range(func(key, value any) bool {
|
||||
empty = false
|
||||
return false // Stop iteration on first item
|
||||
})
|
||||
return empty
|
||||
}
|
||||
|
||||
// Context is the most important part of gin. It allows us to pass variables between middleware,
|
||||
// manage the flow, validate the JSON of a request and render a JSON response for example.
|
||||
type Context struct {
|
||||
@ -71,11 +107,9 @@ type Context struct {
|
||||
params *Params
|
||||
skippedNodes *[]skippedNode
|
||||
|
||||
// This mutex protects Keys map.
|
||||
mu sync.RWMutex
|
||||
|
||||
// Keys is a key/value pair exclusively for the context of each request.
|
||||
Keys map[any]any
|
||||
// Using ContextKeys wrapper around sync.Map for better concurrent performance.
|
||||
Keys *ContextKeys
|
||||
|
||||
// Errors is a list of errors attached to all the handlers/middlewares who used this context.
|
||||
Errors errorMsgs
|
||||
@ -106,7 +140,7 @@ func (c *Context) reset() {
|
||||
c.index = -1
|
||||
|
||||
c.fullPath = ""
|
||||
c.Keys = nil
|
||||
c.Keys = nil // Reset to nil for backward compatibility
|
||||
c.Errors = c.Errors[:0]
|
||||
c.Accepted = nil
|
||||
c.queryCache = nil
|
||||
@ -131,10 +165,14 @@ func (c *Context) Copy() *Context {
|
||||
cp.handlers = nil
|
||||
cp.fullPath = c.fullPath
|
||||
|
||||
cKeys := c.Keys
|
||||
c.mu.RLock()
|
||||
cp.Keys = maps.Clone(cKeys)
|
||||
c.mu.RUnlock()
|
||||
// Copy ContextKeys contents if they exist
|
||||
if c.Keys != nil {
|
||||
cp.Keys = &ContextKeys{}
|
||||
c.Keys.Range(func(key, value any) bool {
|
||||
cp.Keys.Store(key, value)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
cParams := c.Params
|
||||
cp.Params = make([]Param, len(cParams))
|
||||
@ -271,24 +309,22 @@ 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.
|
||||
// Uses ContextKeys wrapper around sync.Map for better concurrent performance.
|
||||
func (c *Context) Set(key any, value any) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.Keys == nil {
|
||||
c.Keys = make(map[any]any)
|
||||
c.Keys = &ContextKeys{}
|
||||
}
|
||||
|
||||
c.Keys[key] = value
|
||||
c.Keys.Store(key, value)
|
||||
}
|
||||
|
||||
// Get returns the value for the given key, ie: (value, true).
|
||||
// If the value does not exist it returns (nil, false)
|
||||
// Uses ContextKeys wrapper around sync.Map for better concurrent performance.
|
||||
func (c *Context) Get(key any) (value any, exists bool) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
value, exists = c.Keys[key]
|
||||
return
|
||||
if c.Keys == nil {
|
||||
return nil, false
|
||||
}
|
||||
return c.Keys.Load(key)
|
||||
}
|
||||
|
||||
// MustGet returns the value for the given key if it exists, otherwise it panics.
|
||||
@ -478,14 +514,27 @@ func (c *Context) GetStringMapStringSlice(key any) map[string][]string {
|
||||
|
||||
// Delete deletes the key from the Context's Key map, if it exists.
|
||||
// This operation is safe to be used by concurrent go-routines
|
||||
// Uses ContextKeys wrapper around sync.Map for better concurrent performance.
|
||||
func (c *Context) Delete(key any) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.Keys != nil {
|
||||
delete(c.Keys, key)
|
||||
c.Keys.Delete(key)
|
||||
}
|
||||
}
|
||||
|
||||
// GetKeysAsMap returns a copy of the context keys as a regular map[any]any.
|
||||
// This is useful for compatibility with existing APIs that expect regular maps.
|
||||
// Note: This creates a snapshot of the keys at the time of calling.
|
||||
func (c *Context) GetKeysAsMap() map[any]any {
|
||||
result := make(map[any]any)
|
||||
if c.Keys != nil {
|
||||
c.Keys.Range(func(key, value any) bool {
|
||||
result[key] = value
|
||||
return true
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/************************************/
|
||||
/************ INPUT DATA ************/
|
||||
/************************************/
|
||||
@ -1237,6 +1286,11 @@ func (c *Context) ProtoBuf(code int, obj any) {
|
||||
c.Render(code, render.ProtoBuf{Data: obj})
|
||||
}
|
||||
|
||||
// BSON serializes the given struct as BSON into the response body.
|
||||
func (c *Context) BSON(code int, obj any) {
|
||||
c.Render(code, render.BSON{Data: obj})
|
||||
}
|
||||
|
||||
// String writes the given string into the response body.
|
||||
func (c *Context) String(code int, format string, values ...any) {
|
||||
c.Render(code, render.String{Format: format, Data: values})
|
||||
@ -1344,6 +1398,7 @@ type Negotiate struct {
|
||||
Data any
|
||||
TOMLData any
|
||||
PROTOBUFData any
|
||||
BSONData any
|
||||
}
|
||||
|
||||
// Negotiate calls different Render according to acceptable Accept format.
|
||||
@ -1373,6 +1428,10 @@ func (c *Context) Negotiate(code int, config Negotiate) {
|
||||
data := chooseData(config.PROTOBUFData, config.Data)
|
||||
c.ProtoBuf(code, data)
|
||||
|
||||
case binding.MIMEBSON:
|
||||
data := chooseData(config.BSONData, config.Data)
|
||||
c.BSON(code, data)
|
||||
|
||||
default:
|
||||
c.AbortWithError(http.StatusNotAcceptable, errors.New("the accepted formats are not offered by the server")) //nolint: errcheck
|
||||
}
|
||||
|
||||
@ -32,6 +32,7 @@ import (
|
||||
testdata "github.com/gin-gonic/gin/testdata/protoexample"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
@ -683,7 +684,9 @@ func TestContextCopy(t *testing.T) {
|
||||
assert.Equal(t, cp.engine, c.engine)
|
||||
assert.Equal(t, cp.Params, c.Params)
|
||||
cp.Set("foo", "notBar")
|
||||
assert.NotEqual(t, cp.Keys["foo"], c.Keys["foo"])
|
||||
cpFooValue, _ := cp.Get("foo")
|
||||
cFooValue, _ := c.Get("foo")
|
||||
assert.NotEqual(t, cpFooValue, cFooValue)
|
||||
assert.Equal(t, cp.fullPath, c.fullPath)
|
||||
}
|
||||
|
||||
@ -1701,6 +1704,23 @@ func TestContextNegotiationWithPROTOBUF(t *testing.T) {
|
||||
assert.Equal(t, "application/x-protobuf", w.Header().Get("Content-Type"))
|
||||
}
|
||||
|
||||
func TestContextNegotiationWithBSON(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := CreateTestContext(w)
|
||||
c.Request, _ = http.NewRequest(http.MethodPost, "", nil)
|
||||
|
||||
c.Negotiate(http.StatusOK, Negotiate{
|
||||
Offered: []string{MIMEBSON, MIMEXML, MIMEJSON, MIMEYAML, MIMEYAML2},
|
||||
Data: H{"foo": "bar"},
|
||||
})
|
||||
|
||||
bData, _ := bson.Marshal(H{"foo": "bar"})
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, string(bData), w.Body.String())
|
||||
assert.Equal(t, "application/bson", w.Header().Get("Content-Type"))
|
||||
}
|
||||
|
||||
func TestContextNegotiationNotSupport(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := CreateTestContext(w)
|
||||
|
||||
13
go.mod
13
go.mod
@ -2,11 +2,13 @@ module github.com/gin-gonic/gin
|
||||
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.24.7
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.14.2
|
||||
github.com/gin-contrib/sse v1.1.0
|
||||
github.com/go-playground/validator/v10 v10.28.0
|
||||
github.com/goccy/go-json v0.10.2
|
||||
github.com/goccy/go-json v0.10.5
|
||||
github.com/goccy/go-yaml v1.19.1
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/mattn/go-isatty v0.0.20
|
||||
@ -15,10 +17,13 @@ require (
|
||||
github.com/quic-go/quic-go v0.57.1
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/ugorji/go/codec v1.3.1
|
||||
go.mongodb.org/mongo-driver v1.17.7
|
||||
golang.org/x/net v0.47.0
|
||||
google.golang.org/protobuf v1.36.10
|
||||
)
|
||||
|
||||
require gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
|
||||
require (
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/bytedance/sonic/loader v0.4.0 // indirect
|
||||
@ -30,13 +35,13 @@ require (
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/kr/text v0.2.0 // 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/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
golang.org/x/arch v0.20.0 // indirect
|
||||
go.uber.org/mock v0.6.0 // indirect
|
||||
golang.org/x/arch v0.22.0 // indirect
|
||||
golang.org/x/crypto v0.45.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
17
go.sum
17
go.sum
@ -22,8 +22,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
|
||||
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.19.1 h1:3rG3+v8pkhRqoQ/88NYNMHYVGYztCOCIZ7UQhu7H+NE=
|
||||
github.com/goccy/go-yaml v1.19.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
@ -41,8 +41,9 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
@ -70,10 +71,12 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
|
||||
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
|
||||
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||
go.mongodb.org/mongo-driver v1.17.7 h1:a9w+U3Vt67eYzcfq3k/OAv284/uUUkL0uP75VE5rCOU=
|
||||
go.mongodb.org/mongo-driver v1.17.7/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
|
||||
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
|
||||
@ -284,7 +284,7 @@ func LoggerWithConfig(conf LoggerConfig) HandlerFunc {
|
||||
param := LogFormatterParams{
|
||||
Request: c.Request,
|
||||
isTerm: isTerm,
|
||||
Keys: c.Keys,
|
||||
Keys: c.GetKeysAsMap(),
|
||||
}
|
||||
|
||||
// Stop timer
|
||||
|
||||
@ -207,7 +207,7 @@ func TestLoggerWithConfigFormatting(t *testing.T) {
|
||||
router.GET("/example", func(c *Context) {
|
||||
// set dummy ClientIP
|
||||
c.Request.Header.Set("X-Forwarded-For", "20.20.20.20")
|
||||
gotKeys = c.Keys
|
||||
gotKeys = c.GetKeysAsMap()
|
||||
time.Sleep(time.Millisecond)
|
||||
})
|
||||
PerformRequest(router, http.MethodGet, "/example?a=100")
|
||||
|
||||
34
render/bson.go
Normal file
34
render/bson.go
Normal file
@ -0,0 +1,34 @@
|
||||
// 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 render
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
)
|
||||
|
||||
// BSON contains the given interface object.
|
||||
type BSON struct {
|
||||
Data any
|
||||
}
|
||||
|
||||
var bsonContentType = []string{"application/bson"}
|
||||
|
||||
// Render (BSON) marshals the given interface object and writes data with custom ContentType.
|
||||
func (r BSON) Render(w http.ResponseWriter) error {
|
||||
r.WriteContentType(w)
|
||||
|
||||
bytes, err := bson.Marshal(&r.Data)
|
||||
if err == nil {
|
||||
_, err = w.Write(bytes)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// WriteContentType (BSONBuf) writes BSONBuf ContentType.
|
||||
func (r BSON) WriteContentType(w http.ResponseWriter) {
|
||||
writeContentType(w, bsonContentType)
|
||||
}
|
||||
@ -19,6 +19,7 @@ import (
|
||||
testdata "github.com/gin-gonic/gin/testdata/protoexample"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
@ -359,6 +360,31 @@ func TestRenderProtoBufFail(t *testing.T) {
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestRenderBSON(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
reps := []int64{int64(1), int64(2)}
|
||||
type mystruct struct {
|
||||
Label string
|
||||
Reps []int64
|
||||
}
|
||||
|
||||
data := &mystruct{
|
||||
Label: "test",
|
||||
Reps: reps,
|
||||
}
|
||||
|
||||
(BSON{data}).WriteContentType(w)
|
||||
bsonData, err := bson.Marshal(data)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "application/bson", w.Header().Get("Content-Type"))
|
||||
|
||||
err = (BSON{data}).Render(w)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, bsonData, w.Body.Bytes())
|
||||
assert.Equal(t, "application/bson", w.Header().Get("Content-Type"))
|
||||
}
|
||||
|
||||
func TestRenderXML(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
data := xmlmap{
|
||||
|
||||
74
tree.go
74
tree.go
@ -9,6 +9,7 @@ import (
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
"unsafe"
|
||||
|
||||
"github.com/gin-gonic/gin/internal/bytesconv"
|
||||
)
|
||||
@ -59,11 +60,30 @@ func (trees methodTrees) get(method string) *node {
|
||||
}
|
||||
|
||||
func longestCommonPrefix(a, b string) int {
|
||||
// Use unsafe operations for better performance in this hot path
|
||||
aBytes := ([]byte)(a)
|
||||
bBytes := ([]byte)(b)
|
||||
|
||||
minLen := min(len(aBytes), len(bBytes))
|
||||
|
||||
// Use word-sized comparison for better performance on 64-bit systems
|
||||
// Compare 8 bytes at a time when possible
|
||||
wordSize := 8
|
||||
i := 0
|
||||
max_ := min(len(a), len(b))
|
||||
for i < max_ && a[i] == b[i] {
|
||||
|
||||
// Word-by-word comparison for better performance
|
||||
for i+wordSize <= minLen {
|
||||
if *(*uint64)(unsafe.Pointer(&aBytes[i])) != *(*uint64)(unsafe.Pointer(&bBytes[i])) {
|
||||
break
|
||||
}
|
||||
i += wordSize
|
||||
}
|
||||
|
||||
// Byte-by-byte comparison for the remainder
|
||||
for i < minLen && aBytes[i] == bBytes[i] {
|
||||
i++
|
||||
}
|
||||
|
||||
return i
|
||||
}
|
||||
|
||||
@ -421,13 +441,18 @@ func (n *node) getValue(path string, params *Params, skippedNodes *[]skippedNode
|
||||
walk: // Outer loop for walking the tree
|
||||
for {
|
||||
prefix := n.path
|
||||
if len(path) > len(prefix) {
|
||||
if path[:len(prefix)] == prefix {
|
||||
path = path[len(prefix):]
|
||||
prefixLen := len(prefix)
|
||||
if len(path) > prefixLen {
|
||||
// Use bytes comparison for better performance
|
||||
pathBytes := ([]byte)(path)
|
||||
if string(pathBytes[:prefixLen]) == prefix {
|
||||
path = path[prefixLen:]
|
||||
|
||||
// Try all the non-wildcard children first by matching the indices
|
||||
idxc := path[0]
|
||||
for i, c := range []byte(n.indices) {
|
||||
pathBytes = ([]byte)(path) // Update pathBytes after path change
|
||||
idxc := pathBytes[0]
|
||||
indicesBytes := ([]byte)(n.indices)
|
||||
for i, c := range indicesBytes {
|
||||
if c == idxc {
|
||||
// strings.HasPrefix(n.children[len(n.children)-1].path, ":") == n.wildChild
|
||||
if n.wildChild {
|
||||
@ -460,7 +485,11 @@ walk: // Outer loop for walking the tree
|
||||
for length := len(*skippedNodes); length > 0; length-- {
|
||||
skippedNode := (*skippedNodes)[length-1]
|
||||
*skippedNodes = (*skippedNodes)[:length-1]
|
||||
if strings.HasSuffix(skippedNode.path, path) {
|
||||
// Use more efficient suffix check
|
||||
skippedPathBytes := ([]byte)(skippedNode.path)
|
||||
pathBytes := ([]byte)(path)
|
||||
if len(skippedPathBytes) >= len(pathBytes) &&
|
||||
string(skippedPathBytes[len(skippedPathBytes)-len(pathBytes):]) == path {
|
||||
path = skippedNode.path
|
||||
n = skippedNode.node
|
||||
if value.params != nil {
|
||||
@ -489,8 +518,10 @@ walk: // Outer loop for walking the tree
|
||||
// tree_test.go line: 204
|
||||
|
||||
// Find param end (either '/' or path end)
|
||||
// Use bytes operations for better performance
|
||||
pathBytes := ([]byte)(path)
|
||||
end := 0
|
||||
for end < len(path) && path[end] != '/' {
|
||||
for end < len(pathBytes) && pathBytes[end] != '/' {
|
||||
end++
|
||||
}
|
||||
|
||||
@ -509,14 +540,17 @@ walk: // Outer loop for walking the tree
|
||||
// Expand slice within preallocated capacity
|
||||
i := len(*value.params)
|
||||
*value.params = (*value.params)[:i+1]
|
||||
|
||||
// Use bytes slicing to avoid string allocation
|
||||
val := path[:end]
|
||||
if unescape {
|
||||
if unescape && end > 0 {
|
||||
// Only unescape if there are actually characters to unescape
|
||||
if v, err := url.QueryUnescape(val); err == nil {
|
||||
val = v
|
||||
}
|
||||
}
|
||||
(*value.params)[i] = Param{
|
||||
Key: n.path[1:],
|
||||
Key: n.path[1:], // Skip the ':' character
|
||||
Value: val,
|
||||
}
|
||||
}
|
||||
@ -562,14 +596,16 @@ walk: // Outer loop for walking the tree
|
||||
// Expand slice within preallocated capacity
|
||||
i := len(*value.params)
|
||||
*value.params = (*value.params)[:i+1]
|
||||
|
||||
val := path
|
||||
if unescape {
|
||||
if unescape && len(path) > 0 {
|
||||
// Only attempt unescape if path is not empty
|
||||
if v, err := url.QueryUnescape(path); err == nil {
|
||||
val = v
|
||||
}
|
||||
}
|
||||
(*value.params)[i] = Param{
|
||||
Key: n.path[2:],
|
||||
Key: n.path[2:], // Skip the '*'
|
||||
Value: val,
|
||||
}
|
||||
}
|
||||
@ -591,7 +627,11 @@ walk: // Outer loop for walking the tree
|
||||
for length := len(*skippedNodes); length > 0; length-- {
|
||||
skippedNode := (*skippedNodes)[length-1]
|
||||
*skippedNodes = (*skippedNodes)[:length-1]
|
||||
if strings.HasSuffix(skippedNode.path, path) {
|
||||
// Use more efficient suffix check
|
||||
skippedPathBytes := ([]byte)(skippedNode.path)
|
||||
pathBytes := ([]byte)(path)
|
||||
if len(skippedPathBytes) >= len(pathBytes) &&
|
||||
string(skippedPathBytes[len(skippedPathBytes)-len(pathBytes):]) == path {
|
||||
path = skippedNode.path
|
||||
n = skippedNode.node
|
||||
if value.params != nil {
|
||||
@ -648,7 +688,11 @@ walk: // Outer loop for walking the tree
|
||||
for length := len(*skippedNodes); length > 0; length-- {
|
||||
skippedNode := (*skippedNodes)[length-1]
|
||||
*skippedNodes = (*skippedNodes)[:length-1]
|
||||
if strings.HasSuffix(skippedNode.path, path) {
|
||||
// Use more efficient suffix check
|
||||
skippedPathBytes := ([]byte)(skippedNode.path)
|
||||
pathBytes := ([]byte)(path)
|
||||
if len(skippedPathBytes) >= len(pathBytes) &&
|
||||
string(skippedPathBytes[len(skippedPathBytes)-len(pathBytes):]) == path {
|
||||
path = skippedNode.path
|
||||
n = skippedNode.node
|
||||
if value.params != nil {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user