Skip to content

Commit f8d4083

Browse files
committed
(feat) pointers to structs
1 parent c65dd57 commit f8d4083

File tree

4 files changed

+133
-53
lines changed

4 files changed

+133
-53
lines changed

binder.go

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,17 @@ import (
1212
"regexp"
1313
)
1414

15-
var ArrayMatcherRegexp = regexp.MustCompile(`\[([0-9]+)\]`)
16-
var MapMatcherRegexp = regexp.MustCompile(`\[([a-zA-Z0-9\-\_\.]+)\]`)
17-
var ArrayNotationRegexp = regexp.MustCompile(`\[([a-zA-Z0-9\-\_\.]+)\]`)
18-
var PathMatcherRegexp = regexp.MustCompile(`\{([^}]+)\}`)
19-
var DefaultBodySize = int64(32 << 20) // 32 MB
20-
var DefaultHeaderTagName = "header"
21-
var DefaultFormTagName = "form"
22-
var DefaultQueryTagName = "query"
23-
var DefaultParamTagName = "param"
24-
var MaxArraySize = 1000 // max size of array
15+
var ArrayMatcherRegexp = regexp.MustCompile(`\[([0-9]+)\]`) // matches [0] to use in indexed arrays
16+
var MapMatcherRegexp = regexp.MustCompile(`\[([a-zA-Z0-9\-\_\.]+)\]`) // matches [key] to use in maps and deep objects
17+
var ArrayNotationRegexp = regexp.MustCompile(`\[([a-zA-Z0-9\-\_\.]+)\]`) // matches [id] to use in deep objects
18+
var PathMatcherRegexp = regexp.MustCompile(`\{([^}]+)\}`) // matches {id} to use in path parameters
19+
var DefaultDeepObjectSeparator = "." // default separator for deep fields
20+
var DefaultBodySize = int64(32 << 20) // 32 MB
21+
var DefaultHeaderTagName = "header" // default tag name for header
22+
var DefaultFormTagName = "form" // default tag name for form
23+
var DefaultQueryTagName = "query" // default tag name for query
24+
var DefaultParamTagName = "param" // default tag name for param
25+
var MaxArraySize = 1000 // max size of array
2526

2627
// JSONSerializer is the interface that encodes and decodes JSON to and from interfaces.
2728
type JSONSerializer interface {

default_binder.go

Lines changed: 70 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ type DefaultBinder struct {
1717
ArrayMatcher *regexp.Regexp
1818
MapMatcher *regexp.Regexp
1919
ArrayNotationMatcher *regexp.Regexp
20+
DeepObjectSeparator string
2021
MaxBodySize int64
2122
MaxArraySize int
2223
HeaderTagName string
@@ -40,6 +41,7 @@ func NewBinder() *DefaultBinder {
4041
FormTagName: DefaultFormTagName,
4142
QueryTagName: DefaultQueryTagName,
4243
ParamTagName: DefaultParamTagName,
44+
DeepObjectSeparator: DefaultDeepObjectSeparator,
4345
BindOrder: []BindFunc{},
4446
}
4547

@@ -205,14 +207,20 @@ func (b *DefaultBinder) bindData(destination interface{}, data map[string][]stri
205207
}
206208

207209
// !struct
208-
if typ.Kind() != reflect.Struct {
210+
if typ.Kind() != reflect.Struct && typ.Kind() != reflect.Ptr {
209211
if tag == b.ParamTagName || tag == b.QueryTagName || tag == b.HeaderTagName {
210212
// incompatible type, data is probably to be found in the body
211213
return nil
212214
}
213215
return errors.New("binding element must be a struct")
214216
}
215217

218+
// deference struct
219+
if typ.Kind() == reflect.Ptr {
220+
typ = typ.Elem()
221+
val = val.Elem()
222+
}
223+
216224
for i := 0; i < typ.NumField(); i++ { // iterate over all destination fields
217225
typeField := typ.Field(i)
218226
structField := val.Field(i)
@@ -226,7 +234,6 @@ func (b *DefaultBinder) bindData(destination interface{}, data map[string][]stri
226234
}
227235
structFieldKind := structField.Kind()
228236
inputFieldName := typeField.Tag.Get(tag)
229-
// valueKind := structField.Type().Kind()
230237

231238
if typeField.Anonymous && structFieldKind == reflect.Struct && inputFieldName != "" {
232239
// if anonymous struct with query/param/form tags, report an error
@@ -258,25 +265,25 @@ func (b *DefaultBinder) bindData(destination interface{}, data map[string][]stri
258265
//if the field is a struct, we need to recursively bind data to it
259266
if structFieldKind == reflect.Struct {
260267
// the data now is only the data that is relevant to the current struct
261-
structData := trimData(inputFieldName, data, b.ArrayNotationMatcher)
262-
structFiles := trimFileFields(inputFieldName, dataFiles, b.ArrayNotationMatcher)
268+
structData := trimData(inputFieldName, data, b.ArrayNotationMatcher, b.DeepObjectSeparator)
269+
structFiles := trimFileFields(inputFieldName, dataFiles, b.ArrayNotationMatcher, b.DeepObjectSeparator)
263270
if err := b.bindData(structField.Addr().Interface(), structData, tag, structFiles); err != nil {
264271
return err
265272
}
266273
continue
267274
} else if structFieldKind == reflect.Map {
268275
// the data now is only the data that is relevant to the current field
269-
mapData := trimData(inputFieldName, data, b.MapMatcher)
270-
mapFiles := trimFileFields(inputFieldName, dataFiles, b.MapMatcher)
276+
mapData := trimData(inputFieldName, data, b.MapMatcher, b.DeepObjectSeparator)
277+
mapFiles := trimFileFields(inputFieldName, dataFiles, b.MapMatcher, b.DeepObjectSeparator)
271278
if err := b.bindData(structField.Addr().Interface(), mapData, tag, mapFiles); err != nil {
272279
return err
273280
}
274281
// continue
275282
} else if structFieldKind == reflect.Slice {
276283
// the data now is only the data that is relevant to the current field
277284

278-
sliceData := trimData(inputFieldName, data, b.ArrayMatcher)
279-
sliceFiles := trimFileFields(inputFieldName, dataFiles, b.ArrayMatcher)
285+
sliceData := trimData(inputFieldName, data, b.ArrayMatcher, b.DeepObjectSeparator)
286+
sliceFiles := trimFileFields(inputFieldName, dataFiles, b.ArrayMatcher, b.DeepObjectSeparator)
280287
if err := handleArrayValues(structField, structFieldKind, sliceData, sliceFiles, inputFieldName, b.MaxArraySize); err != nil {
281288
return err
282289
}
@@ -299,6 +306,61 @@ func (b *DefaultBinder) bindData(destination interface{}, data map[string][]stri
299306
}
300307

301308
if !exists {
309+
310+
if structFieldKind == reflect.Ptr { // if the field is a pointer, we need to check if it is a struct
311+
312+
elem := typeField.Type.Elem() // get the type of the pointer
313+
valueKind := elem.Kind()
314+
if valueKind == reflect.Struct {
315+
structData := trimData(inputFieldName, data, b.ArrayNotationMatcher, b.DeepObjectSeparator)
316+
structFiles := trimFileFields(inputFieldName, dataFiles, b.ArrayNotationMatcher, b.DeepObjectSeparator)
317+
318+
if len(structData) == 0 && len(structFiles) == 0 { // no data for this field
319+
continue
320+
}
321+
322+
if structField.IsNil() {
323+
structField.Set(reflect.New(structField.Type().Elem()))
324+
}
325+
326+
// fmt.Println("structFiles", structFiles)
327+
if err := b.bindData(structField.Addr().Interface(), structData, tag, structFiles); err != nil {
328+
return err
329+
}
330+
continue
331+
} else if valueKind == reflect.Slice {
332+
// the data now is only the data that is relevant to the current field
333+
sliceData := trimData(inputFieldName, data, b.ArrayMatcher, b.DeepObjectSeparator)
334+
sliceFiles := trimFileFields(inputFieldName, dataFiles, b.ArrayMatcher, b.DeepObjectSeparator)
335+
336+
if len(sliceData) == 0 && len(sliceFiles) == 0 { // no data for this field
337+
continue
338+
}
339+
340+
if structField.IsNil() {
341+
structField.Set(reflect.New(structField.Type().Elem()))
342+
}
343+
344+
if err := handleArrayValues(structField, structFieldKind, sliceData, sliceFiles, inputFieldName, b.MaxArraySize); err != nil {
345+
return err
346+
}
347+
} else if valueKind == reflect.Map {
348+
// the data now is only the data that is relevant to the current field
349+
mapData := trimData(inputFieldName, data, b.MapMatcher, b.DeepObjectSeparator)
350+
mapFiles := trimFileFields(inputFieldName, dataFiles, b.MapMatcher, b.DeepObjectSeparator)
351+
352+
if len(mapData) == 0 && len(mapFiles) == 0 { // no data for this field
353+
continue
354+
}
355+
if structField.IsNil() {
356+
structField.Set(reflect.New(structField.Type().Elem()))
357+
}
358+
359+
if err := b.bindData(structField.Interface(), mapData, tag, mapFiles); err != nil {
360+
return err
361+
}
362+
}
363+
}
302364
continue
303365
}
304366

example/main.go

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,49 +5,57 @@ import (
55
"log"
66
"mime/multipart"
77
"net/http"
8-
"time"
98

109
"github.com/gobigbang/binder"
1110
)
1211

12+
type DeepStruct struct {
13+
DeepValue string `json:"value" xml:"value" form:"value" query:"value"`
14+
}
15+
1316
type InnerStruct struct {
14-
Test string `json:"test" xml:"test" form:"test" query:"test"`
15-
File *multipart.FileHeader `json:"file" xml:"file" form:"file"`
16-
Age int `json:"age" xml:"age" form:"age"`
17-
Map map[string]interface{} `json:"map" xml:"map" form:"map"`
17+
Test string `json:"test" xml:"test" form:"test" query:"test"`
18+
File *multipart.FileHeader `json:"file" xml:"file" form:"file"`
19+
Age int `json:"age" xml:"age" form:"age"`
20+
Map map[string]interface{} `json:"map" xml:"map" form:"map"`
21+
DeepStruct *DeepStruct `json:"deep_struct" xml:"deep_struct" form:"deep" query:"deep"`
1822
}
1923

2024
type TestStruct struct {
21-
Name string `json:"name" xml:"name" form:"name" query:"name" path:"name"`
22-
HeaderValue string `json:"header_value" xml:"header_value" form:"header_value" query:"header_value" header:"X-Header-Value"`
23-
Age int `json:"age" xml:"age" form:"age" query:"age"`
24-
FloatNumber *float64 `json:"float_number" xml:"float_number" form:"float_number" query:"float_number"`
25-
Email string `json:"email" xml:"email" form:"email" query:"email"`
26-
File *multipart.FileHeader `json:"file" xml:"file" form:"file"`
27-
Inner InnerStruct `json:"inner" xml:"inner" form:"inner" query:"inner"`
28-
Elements []int `json:"elements" xml:"elements" form:"elements[]" query:"elements[]"`
29-
Map map[string]interface{} `json:"map" xml:"map" form:"map"`
25+
Name string `json:"name" xml:"name" form:"name" query:"name" path:"name"`
26+
HeaderValue string `json:"header_value" xml:"header_value" form:"header_value" query:"header_value" header:"X-Header-Value"`
27+
Age int `json:"age" xml:"age" form:"age" query:"age"`
28+
FloatNumber *float64 `json:"float_number" xml:"float_number" form:"float_number" query:"float_number"`
29+
IntNumber *int `json:"int_number" xml:"int_number" form:"int_number" query:"int_number"`
30+
Int64Number *int64 `json:"int64_number" xml:"int64_number" form:"int64_number" query:"int64_number"`
31+
BoolValue *bool `json:"bool_value" xml:"bool_value" form:"bool_value" query:"bool_value"`
32+
Email string `json:"email" xml:"email" form:"email" query:"email"`
33+
StringArray *[]string `json:"string_array" xml:"string_array" form:"string_array" query:"string_array"`
34+
File *multipart.FileHeader `json:"file" xml:"file" form:"file"`
35+
Inner *InnerStruct `json:"inner" xml:"inner" form:"inner" query:"inner"`
36+
Elements []int `json:"elements" xml:"elements" form:"elements[]" query:"elements[]"`
37+
Map *map[string]interface{} `json:"map" xml:"map" form:"map"`
3038
}
3139

3240
func handler() http.HandlerFunc {
3341
return func(w http.ResponseWriter, r *http.Request) {
3442
data := &TestStruct{}
35-
if err := binder.BindHttp(r, data); err != nil {
43+
if err := binder.BindHttpBody(r, data); err != nil {
3644
http.Error(w, err.Error(), http.StatusBadRequest)
3745
return
3846
}
3947

40-
if data.File != nil {
41-
file, err := data.File.Open()
42-
if err != nil {
43-
http.Error(w, err.Error(), http.StatusInternalServerError)
44-
return
45-
}
48+
// if data.File != nil {
49+
// file, err := data.File.Open()
50+
// if err != nil {
51+
// http.Error(w, err.Error(), http.StatusInternalServerError)
52+
// return
53+
// }
4654

47-
// serve the file
48-
http.ServeContent(w, r, data.File.Filename, time.Now(), file)
49-
return
50-
}
55+
// // serve the file
56+
// http.ServeContent(w, r, data.File.Filename, time.Now(), file)
57+
// return
58+
// }
5159
// data := make(map[string]interface{})
5260

5361
// if err := b.Bind(r, &data); err != nil {

utils.go

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,45 +12,54 @@ import (
1212
)
1313

1414
// getPrefixedFieldNames returns a map of field names that are prefixed with the given prefix.
15-
func getPrefixedFieldNames(prefix string, keys []string, matcher *regexp.Regexp) map[string]string {
15+
func getPrefixedFieldNames(prefix string, keys []string, matcher *regexp.Regexp, deepSeparator string) map[string]string {
1616
result := map[string]string{}
1717
for _, k := range keys {
1818
if strings.HasPrefix(k, prefix) {
19-
if strings.HasPrefix(k, prefix+".") {
20-
result[k] = strings.TrimPrefix(k, prefix+".") // dot notation
21-
} else if matches := matcher.FindStringSubmatch(k); len(matches) > 0 {
22-
if len(matches) <= 1 {
19+
if strings.HasPrefix(k, prefix+deepSeparator) {
20+
result[k] = strings.TrimPrefix(k, prefix+deepSeparator) // dot notation
21+
} else if matches := matcher.FindAllStringSubmatch(k, -1); len(matches) > 0 {
22+
if len(matches) == 0 {
2323
continue
2424
}
25-
result[k] = matches[1] // array notation
25+
finalValue := []string{}
26+
// convert all the matches to dot notation (it should be faster than using check for each match)
27+
for _, match := range matches {
28+
val := match[1]
29+
if val == "" {
30+
break
31+
}
32+
finalValue = append(finalValue, val)
33+
}
34+
result[k] = strings.Join(finalValue, deepSeparator)
2635
}
2736
}
2837
}
2938
return result
3039
}
3140

3241
// trimData trims the data map to only include keys that start with the given prefix.
33-
func trimData(prefix string, data map[string][]string, matcher *regexp.Regexp) map[string][]string {
42+
func trimData(prefix string, data map[string][]string, matcher *regexp.Regexp, deepSeparator string) map[string][]string {
3443
result := map[string][]string{}
3544
keys := []string{}
3645
for key := range data {
3746
keys = append(keys, key)
3847
}
39-
fieldNames := getPrefixedFieldNames(prefix, keys, matcher)
48+
fieldNames := getPrefixedFieldNames(prefix, keys, matcher, deepSeparator)
4049
for k, v := range fieldNames {
4150
result[v] = data[k]
4251
}
4352
return result
4453
}
4554

4655
// trimFileFields trims the files map to only include keys that start with the given prefix.
47-
func trimFileFields(prefix string, files map[string][]*multipart.FileHeader, matcher *regexp.Regexp) map[string][]*multipart.FileHeader {
56+
func trimFileFields(prefix string, files map[string][]*multipart.FileHeader, matcher *regexp.Regexp, deepSeparator string) map[string][]*multipart.FileHeader {
4857
result := map[string][]*multipart.FileHeader{}
4958
keys := []string{}
5059
for key := range files {
5160
keys = append(keys, key)
5261
}
53-
fieldNames := getPrefixedFieldNames(prefix, keys, matcher)
62+
fieldNames := getPrefixedFieldNames(prefix, keys, matcher, deepSeparator)
5463
for k, v := range fieldNames {
5564
result[v] = files[k]
5665
}

0 commit comments

Comments
 (0)