Skip to content

Commit 6059f50

Browse files
authored
feat(param): parse embedded struct (#38)
* refactor(param): simplify tag resolver - remove tag value prefix customization * feat(param): parse into embedded structs * chore: changelog * chore: review changes
1 parent f9d8e66 commit 6059f50

File tree

3 files changed

+245
-92
lines changed

3 files changed

+245
-92
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ How to release a new version:
44
- Manually release new version.
55

66
## [Unreleased]
7+
### Added
8+
- package `http/param`: can parse into embedded structs.
9+
10+
### Removed
11+
- package `http/param`: can no longer change the tag value prefix the parser reacts to (e.g. from `param:"query=q"` to `param:"myPrefix=q"`)
712

813
## [0.7.1] - 2024-07-11
914
### Changed

http/param/param.go

Lines changed: 144 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
4735
type 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.
5543
type 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
6350
func 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

Comments
 (0)