Skip to content

Commit 9a39ea1

Browse files
committed
feat: new http binding
1 parent 5c11ae1 commit 9a39ea1

18 files changed

+2882
-0
lines changed

http/binding/binding.go

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
/*
2+
* Copyright 2025 CloudWeGo Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package binding
18+
19+
import (
20+
"encoding/json"
21+
"errors"
22+
"fmt"
23+
"reflect"
24+
"sync"
25+
)
26+
27+
// Decoder decodes request data into a struct.
28+
// The Decode method binds values from RequestContext into the provided value v,
29+
// which must be a pointer to a struct.
30+
type Decoder interface {
31+
Decode(req RequestContext, v any) (bool, error)
32+
}
33+
34+
type decodeCtx struct {
35+
RequestContext
36+
GetResult
37+
}
38+
39+
// decodeCtxPool is a memory pool for decodeCtx instances to reduce allocations
40+
var decodeCtxPool = sync.Pool{
41+
New: func() interface{} {
42+
return &decodeCtx{
43+
GetResult: GetResult{
44+
vv: make([][]byte, 0, 4),
45+
},
46+
}
47+
},
48+
}
49+
50+
// getDecodeCtx retrieves a decodeCtx from the pool (internal use only).
51+
func getDecodeCtx(req RequestContext) *decodeCtx {
52+
ctx := decodeCtxPool.Get().(*decodeCtx)
53+
ctx.RequestContext = req
54+
return ctx
55+
}
56+
57+
// releaseDecodeCtx returns a decodeCtx to the pool after resetting it (internal use only).
58+
func releaseDecodeCtx(ctx *decodeCtx) {
59+
if ctx == nil {
60+
return
61+
}
62+
ctx.GetResult.Reset()
63+
ctx.RequestContext = nil
64+
decodeCtxPool.Put(ctx)
65+
}
66+
67+
type fieldDecoder interface {
68+
Decode(ctx *decodeCtx, rv reflect.Value) (bool, error)
69+
GetFieldName() string
70+
}
71+
72+
type DecodeConfig struct {
73+
// JSONUnmarshalFunc is the function used for JSON unmarshaling
74+
// If nil, will use encoding/json.Unmarshal as default
75+
JSONUnmarshalFunc func(data []byte, v interface{}) error
76+
77+
// Tags specifies the tags to use for decoding in order of preference.
78+
// If not set (nil or empty), the default tags are used: path, form, query, cookie, header
79+
// If set (e.g., []string{"form", "query"}), only the specified tags are used in the given order.
80+
Tags []string
81+
}
82+
83+
func (c *DecodeConfig) getJSONUnmarshal() func(data []byte, v interface{}) error {
84+
if c.JSONUnmarshalFunc != nil {
85+
return c.JSONUnmarshalFunc
86+
}
87+
// Default to encoding/json
88+
return json.Unmarshal
89+
}
90+
91+
// NewDecoder creates a new Decoder for the given struct type.
92+
// The rt parameter must be a pointer to struct type (e.g., reflect.TypeOf((*MyStruct)(nil))).
93+
// The config parameter specifies decoding behavior (tags, JSON unmarshaler, etc.).
94+
// If config is nil, default configuration is used.
95+
//
96+
// Supported struct tags (in default priority order):
97+
// - path: binds from path parameters
98+
// - form: binds from POST form data, falls back to query parameters
99+
// - query: binds from URL query parameters
100+
// - cookie: binds from HTTP cookies
101+
// - header: binds from HTTP headers
102+
//
103+
// Returns an error if rt is not a pointer to struct type.
104+
func NewDecoder(rt reflect.Type, config *DecodeConfig) (Decoder, error) {
105+
if rt.Kind() != reflect.Pointer {
106+
return nil, errors.New("not pointer type")
107+
}
108+
rt = rt.Elem()
109+
if rt.Kind() != reflect.Struct {
110+
return nil, fmt.Errorf("unsupported %s type binding", rt)
111+
}
112+
if config == nil {
113+
config = &DecodeConfig{}
114+
}
115+
return newStructDecoder(rt, config)
116+
}
117+
118+
func getFieldDecoder(fi *fieldInfo) (fieldDecoder, error) {
119+
ft := fi.fieldType
120+
121+
fp := reflect.PointerTo(ft)
122+
// Priority: UnmarshalParam (custom) > TextUnmarshaler (standard) > base types
123+
if fp.Implements(paramUnmarshalerType) {
124+
return newUnmarshalParamDecoder(fi), nil
125+
}
126+
if fp.Implements(textUnmarshalerType) {
127+
return newTextUnmarshalerDecoder(fi), nil
128+
}
129+
130+
switch ft.Kind() {
131+
case reflect.Slice, reflect.Array:
132+
elemType := dereferenceType(ft.Elem())
133+
// Check if it's a file slice
134+
if elemType == fileBindingType {
135+
return newFileTypeSliceDecoder(fi), nil
136+
}
137+
138+
ep := reflect.PointerTo(elemType)
139+
// Check if element type implements UnmarshalParam
140+
if ep.Implements(paramUnmarshalerType) {
141+
return newUnmarshalParamSliceDecoder(fi), nil
142+
}
143+
// Check if element type implements TextUnmarshaler
144+
if ep.Implements(textUnmarshalerType) {
145+
return newTextUnmarshalerSliceDecoder(fi), nil
146+
}
147+
return newSliceDecoder(fi), nil
148+
149+
case reflect.Struct:
150+
if ft == fileBindingType {
151+
return newFileTypeDecoder(fi), nil
152+
}
153+
}
154+
return newBaseDecoder(fi), nil
155+
}
156+
157+
type textUnmarshaler interface {
158+
UnmarshalText(text []byte) error
159+
}
160+
161+
var textUnmarshalerType = reflect.TypeOf((*textUnmarshaler)(nil)).Elem()
162+
163+
type paramUnmarshaler interface {
164+
UnmarshalParam(param string) error
165+
}
166+
167+
var paramUnmarshalerType = reflect.TypeOf((*paramUnmarshaler)(nil)).Elem()

http/binding/binding_base.go

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
/*
2+
* Copyright 2025 CloudWeGo Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package binding
18+
19+
import (
20+
"fmt"
21+
"reflect"
22+
"strconv"
23+
)
24+
25+
type baseDecoder struct {
26+
*fieldInfo
27+
28+
// needcopy indicates if string values need to be copied.
29+
// For string target fields, we may need to copy the byte slice to create a proper string.
30+
// When data comes from string sources (headers, query params), we can use b2s() for zero-copy.
31+
// When data comes from other sources, string(v) creates a copy which is safe for string fields.
32+
needcopy bool
33+
decodeValue func(rv reflect.Value, s string) error
34+
}
35+
36+
func newBaseDecoder(fi *fieldInfo) fieldDecoder {
37+
dec := &baseDecoder{fieldInfo: fi}
38+
fn := getBaseDecodeByKind(fi.fieldKind)
39+
if fn == nil {
40+
// Use method that has access to jsonUnmarshal
41+
fn = dec.decodeJSONValue
42+
}
43+
// Set needcopy flag for string fields that may require copying
44+
dec.needcopy = fi.fieldKind == reflect.String
45+
dec.decodeValue = fn
46+
return dec
47+
}
48+
49+
func (d *baseDecoder) Decode(ctx *decodeCtx, rv reflect.Value) (bool, error) {
50+
_, v := d.FetchBindValue(ctx)
51+
if v == nil {
52+
return false, nil
53+
}
54+
55+
f := d.FieldSetter(rv)
56+
rv = f.Value()
57+
58+
// Optimize string conversion based on data source:
59+
// - If data comes from string sources (headers, query params), use b2s() for zero-copy conversion
60+
// - If decoding to string field from non-string source, use string(v) to create a safe copy
61+
s := b2s(v)
62+
if d.needcopy && !ctx.IsStr() {
63+
s = string(v)
64+
}
65+
if err := d.decodeValue(rv, s); err != nil {
66+
f.Reset()
67+
return false, fmt.Errorf("unable to decode '%s' as %s: %w", s, d.fieldType.String(), err)
68+
}
69+
return true, nil
70+
}
71+
72+
// use slice for better performance,
73+
var type2decoder = [...]func(rv reflect.Value, s string) error{
74+
reflect.Bool: decodeBool,
75+
reflect.Uint: decodeUint,
76+
reflect.Uint8: decodeUint8,
77+
reflect.Uint16: decodeUint16,
78+
reflect.Uint32: decodeUint32,
79+
reflect.Uint64: decodeUint64,
80+
reflect.Int: decodeInt,
81+
reflect.Int8: decodeInt8,
82+
reflect.Int16: decodeInt16,
83+
reflect.Int32: decodeInt32,
84+
reflect.Int64: decodeInt64,
85+
reflect.String: decodeString,
86+
reflect.Float32: decodeFloat32,
87+
reflect.Float64: decodeFloat64,
88+
}
89+
90+
func getBaseDecodeByKind(k reflect.Kind) (ret func(rv reflect.Value, s string) error) {
91+
if int(k) >= len(type2decoder) {
92+
return nil
93+
}
94+
return type2decoder[k]
95+
}
96+
97+
// decodeJSONValue is a method on baseDecoder that uses the configured JSON unmarshal function
98+
func (d *baseDecoder) decodeJSONValue(rv reflect.Value, s string) error {
99+
return d.jsonUnmarshal(s2b(s), rv.Addr().Interface())
100+
}
101+
102+
func decodeBool(rv reflect.Value, s string) error {
103+
val, err := strconv.ParseBool(s)
104+
if err == nil {
105+
*(*bool)(rvUnsafePointer(&rv)) = val
106+
}
107+
return err
108+
109+
}
110+
111+
func decodeUint(rv reflect.Value, s string) error {
112+
val, err := strconv.ParseUint(s, 10, 0)
113+
if err == nil {
114+
*(*uint)(rvUnsafePointer(&rv)) = uint(val)
115+
}
116+
return err
117+
118+
}
119+
120+
func decodeUint8(rv reflect.Value, s string) error {
121+
val, err := strconv.ParseUint(s, 10, 8)
122+
if err == nil {
123+
*(*uint8)(rvUnsafePointer(&rv)) = uint8(val)
124+
}
125+
return err
126+
}
127+
128+
func decodeUint16(rv reflect.Value, s string) error {
129+
val, err := strconv.ParseUint(s, 10, 16)
130+
if err == nil {
131+
*(*uint16)(rvUnsafePointer(&rv)) = uint16(val)
132+
}
133+
return err
134+
}
135+
136+
func decodeUint32(rv reflect.Value, s string) error {
137+
val, err := strconv.ParseUint(s, 10, 32)
138+
if err == nil {
139+
*(*uint32)(rvUnsafePointer(&rv)) = uint32(val)
140+
}
141+
return err
142+
143+
}
144+
145+
func decodeUint64(rv reflect.Value, s string) error {
146+
val, err := strconv.ParseUint(s, 10, 64)
147+
if err == nil {
148+
*(*uint64)(rvUnsafePointer(&rv)) = val
149+
}
150+
return err
151+
152+
}
153+
154+
func decodeInt(rv reflect.Value, s string) error {
155+
val, err := strconv.Atoi(s)
156+
if err == nil {
157+
*(*int)(rvUnsafePointer(&rv)) = val
158+
}
159+
return err
160+
161+
}
162+
163+
func decodeInt8(rv reflect.Value, s string) error {
164+
val, err := strconv.ParseInt(s, 10, 8)
165+
if err == nil {
166+
*(*int8)(rvUnsafePointer(&rv)) = int8(val)
167+
}
168+
return err
169+
170+
}
171+
172+
func decodeInt16(rv reflect.Value, s string) error {
173+
val, err := strconv.ParseInt(s, 10, 16)
174+
if err == nil {
175+
*(*int16)(rvUnsafePointer(&rv)) = int16(val)
176+
}
177+
return err
178+
}
179+
180+
func decodeInt32(rv reflect.Value, s string) error {
181+
val, err := strconv.ParseInt(s, 10, 32)
182+
if err == nil {
183+
*(*int32)(rvUnsafePointer(&rv)) = int32(val)
184+
}
185+
return err
186+
}
187+
188+
func decodeInt64(rv reflect.Value, s string) error {
189+
val, err := strconv.ParseInt(s, 10, 64)
190+
if err == nil {
191+
*(*int64)(rvUnsafePointer(&rv)) = val
192+
}
193+
return err
194+
}
195+
196+
func decodeString(rv reflect.Value, s string) error {
197+
*(*string)(rvUnsafePointer(&rv)) = s
198+
return nil
199+
}
200+
201+
func decodeFloat32(rv reflect.Value, s string) error {
202+
val, err := strconv.ParseFloat(s, 32)
203+
if err == nil {
204+
*(*float32)(rvUnsafePointer(&rv)) = float32(val)
205+
}
206+
return err
207+
}
208+
209+
func decodeFloat64(rv reflect.Value, s string) error {
210+
val, err := strconv.ParseFloat(s, 64)
211+
if err == nil {
212+
*(*float64)(rvUnsafePointer(&rv)) = val
213+
}
214+
return err
215+
}

0 commit comments

Comments
 (0)