Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(param): parse embedded struct #38

Merged
merged 4 commits into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 145 additions & 56 deletions http/param/param.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,41 +9,29 @@ import (
"strings"
)

// TagResolver is a function that decides from a field type what key of http parameter should be searched.
// Second return value should return whether the key should be searched in http parameter at all.
type TagResolver func(fieldTag reflect.StructTag) (string, bool)
const (
defaultTagName = "param"
queryTagValuePrefix = "query"
pathTagValuePrefix = "path"
)

// FixedTagNameParamTagResolver returns a TagResolver, that matches struct params by specific tag.
// Example: FixedTagNameParamTagResolver("mytag") matches a field tagged with `mytag:"param_name"`
func FixedTagNameParamTagResolver(tagName string) TagResolver {
return func(fieldTag reflect.StructTag) (string, bool) {
taggedParamName := fieldTag.Get(tagName)
return taggedParamName, taggedParamName != ""
}
}
// TagResolver is a function that decides from a field tag what parameter should be searched.
// Second return value should return whether the parameter should be searched at all.
type TagResolver func(fieldTag reflect.StructTag) (string, bool)

// TagWithModifierTagResolver returns a TagResolver, that matches struct params by specific tag and
// by a value before a '=' separator.
// Example: FixedTagNameParamTagResolver("mytag", "mymodifier") matches a field tagged with `mytag:"mymodifier=param_name"`
func TagWithModifierTagResolver(tagName string, tagModifier string) TagResolver {
// TagNameResolver returns a TagResolver that returns the value of tag with tagName, and whether the tag exists at all.
// It can be used to replace Parser.ParamTagResolver to change what tag name the Parser reacts to.
func TagNameResolver(tagName string) TagResolver {
return func(fieldTag reflect.StructTag) (string, bool) {
tagValue := fieldTag.Get(tagName)
if tagValue == "" {
return "", false
}
splits := strings.Split(tagValue, "=")
//nolint:gomnd // 2 not really that magic number - one value before '=', one after
if len(splits) != 2 {
return "", false
}
if splits[0] == tagModifier {
return splits[1], true
}
return "", false
return tagValue, true
}
}

// PathParamFunc is a function that returns value of specified http path parameter
// PathParamFunc is a function that returns value of specified http path parameter.
type PathParamFunc func(r *http.Request, key string) string

// Parser can Parse query and path parameters from http.Request into a struct.
Expand All @@ -53,18 +41,16 @@ type PathParamFunc func(r *http.Request, key string) string
// PathParamFunc is for getting path parameter from http.Request, as each http router handles it in different way (if at all).
// For example for chi, use WithPathParamFunc(chi.URLParam) to be able to use tags for path parameters.
type Parser struct {
QueryParamTagResolver TagResolver
PathParamTagResolver TagResolver
PathParamFunc PathParamFunc
ParamTagResolver TagResolver
PathParamFunc PathParamFunc
}

// DefaultParser returns query and path parameter Parser with intended struct tags
// `param:"query=param_name"` for query parameters and `param:"path=param_name"` for path parameters
func DefaultParser() Parser {
return Parser{
QueryParamTagResolver: TagWithModifierTagResolver("param", "query"),
PathParamTagResolver: TagWithModifierTagResolver("param", "path"),
PathParamFunc: nil, // keep nil, as there is no sensible default of how to get value of path parameter
ParamTagResolver: TagNameResolver(defaultTagName),
PathParamFunc: nil, // keep nil, as there is no sensible default of how to get value of path parameter
}
}

Expand All @@ -75,7 +61,8 @@ func (p Parser) WithPathParamFunc(f PathParamFunc) Parser {
return p
}

// Parse accepts the request and a pointer to struct that is tagged with appropriate tags set in Parser.
// Parse accepts the request and a pointer to struct with its fields tagged with appropriate tags set in Parser.
// Such tagged fields must be in top level struct, or in exported struct embedded in top-level struct.
// All such tagged fields are assigned the respective parameter from the actual request.
//
// Fields are assigned their zero value if the field was tagged but request did not contain such parameter.
Expand All @@ -100,48 +87,120 @@ func (p Parser) Parse(r *http.Request, dest any) error {
return fmt.Errorf("can only parse into struct, but got %s", v.Type().Name())
}

for i := 0; i < v.NumField(); i++ {
typeField := v.Type().Field(i)
if !typeField.IsExported() {
continue
var fieldIndexPaths []taggedFieldIndexPath
p.findTaggedIndexPaths(v.Type(), []int{}, &fieldIndexPaths)

for i := range fieldIndexPaths {
// Zero the value, even if it would not be set by following path or query parameter.
// This will cause potential partial result from previous parser (e.g. json.Unmarshal) to be discarded on
// fields that are tagged for path or query parameter.
err := zeroPath(v, &fieldIndexPaths[i])
if err != nil {
return err
}
valueField := v.Field(i)
err := p.parseParam(r, typeField, valueField)
}

for _, path := range fieldIndexPaths {
err := p.parseParam(r, path)
if err != nil {
return err
}
}
return nil
}

func (p Parser) parseParam(r *http.Request, typeField reflect.StructField, v reflect.Value) error {
tag := typeField.Tag
pathParamName, okPath := p.PathParamTagResolver(tag)
queryParamName, okQuery := p.QueryParamTagResolver(tag)
if !okPath && !okQuery {
// do nothing if tagged neither for query nor param
return nil
type paramType int

const (
paramTypeQuery = iota
Fazt01 marked this conversation as resolved.
Show resolved Hide resolved
paramTypePath
)

type taggedFieldIndexPath struct {
paramType paramType
paramName string
indexPath []int
destValue reflect.Value
}

func (p Parser) findTaggedIndexPaths(typ reflect.Type, currentNestingIndexPath []int, resultPaths *[]taggedFieldIndexPath) {
Fazt01 marked this conversation as resolved.
Show resolved Hide resolved
for i := 0; i < typ.NumField(); i++ {
typeField := typ.Field(i)
if typeField.Anonymous {
t := typeField.Type
if t.Kind() == reflect.Pointer {
t = t.Elem()
}
if t.Kind() == reflect.Struct {
p.findTaggedIndexPaths(t, append(currentNestingIndexPath, i), resultPaths)
}
}
if !typeField.IsExported() {
continue
}
tag := typeField.Tag
pathParamName, okPath := p.resolvePath(tag)
queryParamName, okQuery := p.resolveQuery(tag)
if okPath {
newPath := make([]int, 0, len(currentNestingIndexPath)+1)
newPath = append(newPath, currentNestingIndexPath...)
newPath = append(newPath, i)
*resultPaths = append(*resultPaths, taggedFieldIndexPath{
paramType: paramTypePath,
paramName: pathParamName,
indexPath: newPath,
})
}
if okQuery {
newPath := make([]int, 0, len(currentNestingIndexPath)+1)
newPath = append(newPath, currentNestingIndexPath...)
newPath = append(newPath, i)
*resultPaths = append(*resultPaths, taggedFieldIndexPath{
paramType: paramTypeQuery,
paramName: queryParamName,
indexPath: newPath,
})
}
}
}

// Zero the value, even if it would not be set by following path or query parameter.
// This will cause potential partial result from previous parser (e.g. json.Unmarshal) to be discarded on
// fields that are tagged for path or query parameter.
v.Set(reflect.Zero(typeField.Type))
func zeroPath(v reflect.Value, path *taggedFieldIndexPath) error {
for n, i := range path.indexPath {
if v.Kind() == reflect.Pointer {
v = v.Elem()
}
if v.Kind() != reflect.Struct {
return fmt.Errorf("expected to nest into struct, but got %s", v.Type().Name())
}
typeField := v.Type().Field(i)
v = v.Field(i)

if okPath {
err := p.parsePathParam(r, pathParamName, v)
if err != nil {
return err
if n == len(path.indexPath)-1 {
v.Set(reflect.Zero(typeField.Type))
path.destValue = v
} else if v.Kind() == reflect.Pointer && v.IsNil() {
if !v.CanSet() {
return fmt.Errorf("cannot set embedded pointer to unexported struct: %v", v.Type().Elem())
}
v.Set(reflect.New(v.Type().Elem()))
}
}
return nil
}

if okQuery {
err := p.parseQueryParam(r, queryParamName, v)
func (p Parser) parseParam(r *http.Request, path taggedFieldIndexPath) error {
switch path.paramType {
case paramTypePath:
err := p.parsePathParam(r, path.paramName, path.destValue)
if err != nil {
return err
}
case paramTypeQuery:
err := p.parseQueryParam(r, path.paramName, path.destValue)
if err != nil {
return err
}
}

return nil
}

Expand Down Expand Up @@ -246,3 +305,33 @@ func unmarshalPrimitiveValue(text string, dest reflect.Value) error {
}
return nil
}

// resolveTagValueWithModifier returns a parameter value in tag value containing a prefix "tagModifier=".
// Example: resolveTagValueWithModifier("query=param_name", "query") returns "param_name", true.
func (p Parser) resolveTagValueWithModifier(tagValue string, tagModifier string) (string, bool) {
splits := strings.Split(tagValue, "=")
//nolint:gomnd // 2 not really that magic number - one value before '=', one after
if len(splits) != 2 {
return "", false
}
if splits[0] == tagModifier {
return splits[1], true
}
return "", false
}

func (p Parser) resolveTagWithModifier(fieldTag reflect.StructTag, tagModifier string) (string, bool) {
tagValue, ok := p.ParamTagResolver(fieldTag)
if !ok {
return "", false
}
return p.resolveTagValueWithModifier(tagValue, tagModifier)
}

func (p Parser) resolvePath(fieldTag reflect.StructTag) (string, bool) {
return p.resolveTagWithModifier(fieldTag, pathTagValuePrefix)
}

func (p Parser) resolveQuery(fieldTag reflect.StructTag) (string, bool) {
return p.resolveTagWithModifier(fieldTag, queryTagValuePrefix)
}
Loading