@@ -9,41 +9,29 @@ import (
9
9
"strings"
10
10
)
11
11
12
- // TagResolver is a function that decides from a field type what key of http parameter should be searched.
13
- // Second return value should return whether the key should be searched in http parameter at all.
14
- type TagResolver func (fieldTag reflect.StructTag ) (string , bool )
12
+ const (
13
+ defaultTagName = "param"
14
+ queryTagValuePrefix = "query"
15
+ pathTagValuePrefix = "path"
16
+ )
15
17
16
- // FixedTagNameParamTagResolver returns a TagResolver, that matches struct params by specific tag.
17
- // Example: FixedTagNameParamTagResolver("mytag") matches a field tagged with `mytag:"param_name"`
18
- func FixedTagNameParamTagResolver (tagName string ) TagResolver {
19
- return func (fieldTag reflect.StructTag ) (string , bool ) {
20
- taggedParamName := fieldTag .Get (tagName )
21
- return taggedParamName , taggedParamName != ""
22
- }
23
- }
18
+ // TagResolver is a function that decides from a field tag what parameter should be searched.
19
+ // Second return value should return whether the parameter should be searched at all.
20
+ type TagResolver func (fieldTag reflect.StructTag ) (string , bool )
24
21
25
- // TagWithModifierTagResolver returns a TagResolver, that matches struct params by specific tag and
26
- // by a value before a '=' separator.
27
- // Example: FixedTagNameParamTagResolver("mytag", "mymodifier") matches a field tagged with `mytag:"mymodifier=param_name"`
28
- func TagWithModifierTagResolver (tagName string , tagModifier string ) TagResolver {
22
+ // TagNameResolver returns a TagResolver that returns the value of tag with tagName, and whether the tag exists at all.
23
+ // It can be used to replace Parser.ParamTagResolver to change what tag name the Parser reacts to.
24
+ func TagNameResolver (tagName string ) TagResolver {
29
25
return func (fieldTag reflect.StructTag ) (string , bool ) {
30
26
tagValue := fieldTag .Get (tagName )
31
27
if tagValue == "" {
32
28
return "" , false
33
29
}
34
- splits := strings .Split (tagValue , "=" )
35
- //nolint:gomnd // 2 not really that magic number - one value before '=', one after
36
- if len (splits ) != 2 {
37
- return "" , false
38
- }
39
- if splits [0 ] == tagModifier {
40
- return splits [1 ], true
41
- }
42
- return "" , false
30
+ return tagValue , true
43
31
}
44
32
}
45
33
46
- // PathParamFunc is a function that returns value of specified http path parameter
34
+ // PathParamFunc is a function that returns value of specified http path parameter.
47
35
type PathParamFunc func (r * http.Request , key string ) string
48
36
49
37
// Parser can Parse query and path parameters from http.Request into a struct.
@@ -53,18 +41,16 @@ type PathParamFunc func(r *http.Request, key string) string
53
41
// PathParamFunc is for getting path parameter from http.Request, as each http router handles it in different way (if at all).
54
42
// For example for chi, use WithPathParamFunc(chi.URLParam) to be able to use tags for path parameters.
55
43
type Parser struct {
56
- QueryParamTagResolver TagResolver
57
- PathParamTagResolver TagResolver
58
- PathParamFunc PathParamFunc
44
+ ParamTagResolver TagResolver
45
+ PathParamFunc PathParamFunc
59
46
}
60
47
61
48
// DefaultParser returns query and path parameter Parser with intended struct tags
62
49
// `param:"query=param_name"` for query parameters and `param:"path=param_name"` for path parameters
63
50
func DefaultParser () Parser {
64
51
return Parser {
65
- QueryParamTagResolver : TagWithModifierTagResolver ("param" , "query" ),
66
- PathParamTagResolver : TagWithModifierTagResolver ("param" , "path" ),
67
- PathParamFunc : nil , // keep nil, as there is no sensible default of how to get value of path parameter
52
+ ParamTagResolver : TagNameResolver (defaultTagName ),
53
+ PathParamFunc : nil , // keep nil, as there is no sensible default of how to get value of path parameter
68
54
}
69
55
}
70
56
@@ -75,7 +61,8 @@ func (p Parser) WithPathParamFunc(f PathParamFunc) Parser {
75
61
return p
76
62
}
77
63
78
- // Parse accepts the request and a pointer to struct that is tagged with appropriate tags set in Parser.
64
+ // Parse accepts the request and a pointer to struct with its fields tagged with appropriate tags set in Parser.
65
+ // Such tagged fields must be in top level struct, or in exported struct embedded in top-level struct.
79
66
// All such tagged fields are assigned the respective parameter from the actual request.
80
67
//
81
68
// Fields are assigned their zero value if the field was tagged but request did not contain such parameter.
@@ -100,48 +87,119 @@ func (p Parser) Parse(r *http.Request, dest any) error {
100
87
return fmt .Errorf ("can only parse into struct, but got %s" , v .Type ().Name ())
101
88
}
102
89
103
- for i := 0 ; i < v .NumField (); i ++ {
104
- typeField := v .Type ().Field (i )
105
- if ! typeField .IsExported () {
106
- continue
90
+ fieldIndexPaths := p .findTaggedIndexPaths (v .Type (), []int {}, []taggedFieldIndexPath {})
91
+
92
+ for i := range fieldIndexPaths {
93
+ // Zero the value, even if it would not be set by following path or query parameter.
94
+ // This will cause potential partial result from previous parser (e.g. json.Unmarshal) to be discarded on
95
+ // fields that are tagged for path or query parameter.
96
+ err := zeroPath (v , & fieldIndexPaths [i ])
97
+ if err != nil {
98
+ return err
107
99
}
108
- valueField := v .Field (i )
109
- err := p .parseParam (r , typeField , valueField )
100
+ }
101
+
102
+ for _ , path := range fieldIndexPaths {
103
+ err := p .parseParam (r , path )
110
104
if err != nil {
111
105
return err
112
106
}
113
107
}
114
108
return nil
115
109
}
116
110
117
- func (p Parser ) parseParam (r * http.Request , typeField reflect.StructField , v reflect.Value ) error {
118
- tag := typeField .Tag
119
- pathParamName , okPath := p .PathParamTagResolver (tag )
120
- queryParamName , okQuery := p .QueryParamTagResolver (tag )
121
- if ! okPath && ! okQuery {
122
- // do nothing if tagged neither for query nor param
123
- return nil
111
+ type paramType int
112
+
113
+ const (
114
+ paramTypeQuery paramType = iota
115
+ paramTypePath
116
+ )
117
+
118
+ type taggedFieldIndexPath struct {
119
+ paramType paramType
120
+ paramName string
121
+ indexPath []int
122
+ destValue reflect.Value
123
+ }
124
+
125
+ func (p Parser ) findTaggedIndexPaths (typ reflect.Type , currentNestingIndexPath []int , paths []taggedFieldIndexPath ) []taggedFieldIndexPath {
126
+ for i := 0 ; i < typ .NumField (); i ++ {
127
+ typeField := typ .Field (i )
128
+ if typeField .Anonymous {
129
+ t := typeField .Type
130
+ if t .Kind () == reflect .Pointer {
131
+ t = t .Elem ()
132
+ }
133
+ if t .Kind () == reflect .Struct {
134
+ paths = p .findTaggedIndexPaths (t , append (currentNestingIndexPath , i ), paths )
135
+ }
136
+ }
137
+ if ! typeField .IsExported () {
138
+ continue
139
+ }
140
+ tag := typeField .Tag
141
+ pathParamName , okPath := p .resolvePath (tag )
142
+ queryParamName , okQuery := p .resolveQuery (tag )
143
+ if okPath {
144
+ newPath := make ([]int , 0 , len (currentNestingIndexPath )+ 1 )
145
+ newPath = append (newPath , currentNestingIndexPath ... )
146
+ newPath = append (newPath , i )
147
+ paths = append (paths , taggedFieldIndexPath {
148
+ paramType : paramTypePath ,
149
+ paramName : pathParamName ,
150
+ indexPath : newPath ,
151
+ })
152
+ }
153
+ if okQuery {
154
+ newPath := make ([]int , 0 , len (currentNestingIndexPath )+ 1 )
155
+ newPath = append (newPath , currentNestingIndexPath ... )
156
+ newPath = append (newPath , i )
157
+ paths = append (paths , taggedFieldIndexPath {
158
+ paramType : paramTypeQuery ,
159
+ paramName : queryParamName ,
160
+ indexPath : newPath ,
161
+ })
162
+ }
124
163
}
164
+ return paths
165
+ }
125
166
126
- // Zero the value, even if it would not be set by following path or query parameter.
127
- // This will cause potential partial result from previous parser (e.g. json.Unmarshal) to be discarded on
128
- // fields that are tagged for path or query parameter.
129
- v .Set (reflect .Zero (typeField .Type ))
167
+ func zeroPath (v reflect.Value , path * taggedFieldIndexPath ) error {
168
+ for n , i := range path .indexPath {
169
+ if v .Kind () == reflect .Pointer {
170
+ v = v .Elem ()
171
+ }
172
+ // findTaggedIndexPaths prepared a path.indexPath in such a way, that respective field is always
173
+ // pointer to struct or struct -> should be always able to .Field() here
174
+ typeField := v .Type ().Field (i )
175
+ v = v .Field (i )
130
176
131
- if okPath {
132
- err := p .parsePathParam (r , pathParamName , v )
133
- if err != nil {
134
- return err
177
+ if n == len (path .indexPath )- 1 {
178
+ v .Set (reflect .Zero (typeField .Type ))
179
+ path .destValue = v
180
+ } else if v .Kind () == reflect .Pointer && v .IsNil () {
181
+ if ! v .CanSet () {
182
+ return fmt .Errorf ("cannot set embedded pointer to unexported struct: %v" , v .Type ().Elem ())
183
+ }
184
+ v .Set (reflect .New (v .Type ().Elem ()))
135
185
}
136
186
}
187
+ return nil
188
+ }
137
189
138
- if okQuery {
139
- err := p .parseQueryParam (r , queryParamName , v )
190
+ func (p Parser ) parseParam (r * http.Request , path taggedFieldIndexPath ) error {
191
+ switch path .paramType {
192
+ case paramTypePath :
193
+ err := p .parsePathParam (r , path .paramName , path .destValue )
194
+ if err != nil {
195
+ return err
196
+ }
197
+ case paramTypeQuery :
198
+ err := p .parseQueryParam (r , path .paramName , path .destValue )
140
199
if err != nil {
141
200
return err
142
201
}
143
202
}
144
-
145
203
return nil
146
204
}
147
205
@@ -246,3 +304,33 @@ func unmarshalPrimitiveValue(text string, dest reflect.Value) error {
246
304
}
247
305
return nil
248
306
}
307
+
308
+ // resolveTagValueWithModifier returns a parameter value in tag value containing a prefix "tagModifier=".
309
+ // Example: resolveTagValueWithModifier("query=param_name", "query") returns "param_name", true.
310
+ func (p Parser ) resolveTagValueWithModifier (tagValue string , tagModifier string ) (string , bool ) {
311
+ splits := strings .Split (tagValue , "=" )
312
+ //nolint:gomnd // 2 not really that magic number - one value before '=', one after
313
+ if len (splits ) != 2 {
314
+ return "" , false
315
+ }
316
+ if splits [0 ] == tagModifier {
317
+ return splits [1 ], true
318
+ }
319
+ return "" , false
320
+ }
321
+
322
+ func (p Parser ) resolveTagWithModifier (fieldTag reflect.StructTag , tagModifier string ) (string , bool ) {
323
+ tagValue , ok := p .ParamTagResolver (fieldTag )
324
+ if ! ok {
325
+ return "" , false
326
+ }
327
+ return p .resolveTagValueWithModifier (tagValue , tagModifier )
328
+ }
329
+
330
+ func (p Parser ) resolvePath (fieldTag reflect.StructTag ) (string , bool ) {
331
+ return p .resolveTagWithModifier (fieldTag , pathTagValuePrefix )
332
+ }
333
+
334
+ func (p Parser ) resolveQuery (fieldTag reflect.StructTag ) (string , bool ) {
335
+ return p .resolveTagWithModifier (fieldTag , queryTagValuePrefix )
336
+ }
0 commit comments