@@ -9,41 +9,29 @@ import (
99 "strings"
1010)
1111
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+ )
1517
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 )
2421
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 {
2925 return func (fieldTag reflect.StructTag ) (string , bool ) {
3026 tagValue := fieldTag .Get (tagName )
3127 if tagValue == "" {
3228 return "" , false
3329 }
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
4331 }
4432}
4533
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.
4735type PathParamFunc func (r * http.Request , key string ) string
4836
4937// 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
5341// PathParamFunc is for getting path parameter from http.Request, as each http router handles it in different way (if at all).
5442// For example for chi, use WithPathParamFunc(chi.URLParam) to be able to use tags for path parameters.
5543type Parser struct {
56- QueryParamTagResolver TagResolver
57- PathParamTagResolver TagResolver
58- PathParamFunc PathParamFunc
44+ ParamTagResolver TagResolver
45+ PathParamFunc PathParamFunc
5946}
6047
6148// DefaultParser returns query and path parameter Parser with intended struct tags
6249// `param:"query=param_name"` for query parameters and `param:"path=param_name"` for path parameters
6350func DefaultParser () Parser {
6451 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
6854 }
6955}
7056
@@ -75,7 +61,8 @@ func (p Parser) WithPathParamFunc(f PathParamFunc) Parser {
7561 return p
7662}
7763
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.
7966// All such tagged fields are assigned the respective parameter from the actual request.
8067//
8168// 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 {
10087 return fmt .Errorf ("can only parse into struct, but got %s" , v .Type ().Name ())
10188 }
10289
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
10799 }
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 )
110104 if err != nil {
111105 return err
112106 }
113107 }
114108 return nil
115109}
116110
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+ }
124163 }
164+ return paths
165+ }
125166
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 )
130176
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 ()))
135185 }
136186 }
187+ return nil
188+ }
137189
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 )
140199 if err != nil {
141200 return err
142201 }
143202 }
144-
145203 return nil
146204}
147205
@@ -246,3 +304,33 @@ func unmarshalPrimitiveValue(text string, dest reflect.Value) error {
246304 }
247305 return nil
248306}
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