Skip to content

Commit 9958824

Browse files
committed
feat(http): add form parser
The form parser is useful for parsing application/x-www-form-urlencoded data. Signed-off-by: Marek Cermak <[email protected]>
1 parent c9f3c99 commit 9958824

File tree

3 files changed

+92
-0
lines changed

3 files changed

+92
-0
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ How to release a new version:
55

66
## [Unreleased]
77

8+
### Added
9+
- package `http/param`: can parse form data into embedded structs.
10+
811
## [0.8.0] - 2024-11-14
912
### Added
1013
- package `http/param`: can parse into embedded structs.

http/param/param.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const (
1313
defaultTagName = "param"
1414
queryTagValuePrefix = "query"
1515
pathTagValuePrefix = "path"
16+
formTagValuePrefix = "form"
1617
)
1718

1819
// TagResolver is a function that decides from a field tag what parameter should be searched.
@@ -34,6 +35,9 @@ func TagNameResolver(tagName string) TagResolver {
3435
// PathParamFunc is a function that returns value of specified http path parameter.
3536
type PathParamFunc func(r *http.Request, key string) string
3637

38+
// FormParamFunc is a function that returns value of specified form parameter.
39+
type FormParamFunc func(r *http.Request, key string) string
40+
3741
// Parser can Parse query and path parameters from http.Request into a struct.
3842
// Fields struct have to be tagged such that either QueryParamTagResolver or PathParamTagResolver returns
3943
// valid parameter name from the provided tag.
@@ -43,6 +47,7 @@ type PathParamFunc func(r *http.Request, key string) string
4347
type Parser struct {
4448
ParamTagResolver TagResolver
4549
PathParamFunc PathParamFunc
50+
FormParamFunc FormParamFunc
4651
}
4752

4853
// DefaultParser returns query and path parameter Parser with intended struct tags
@@ -51,6 +56,7 @@ func DefaultParser() Parser {
5156
return Parser{
5257
ParamTagResolver: TagNameResolver(defaultTagName),
5358
PathParamFunc: nil, // keep nil, as there is no sensible default of how to get value of path parameter
59+
FormParamFunc: nil, // keep nil, as there is no sensible default of how to get value of form parameter
5460
}
5561
}
5662

@@ -61,6 +67,13 @@ func (p Parser) WithPathParamFunc(f PathParamFunc) Parser {
6167
return p
6268
}
6369

70+
// WithFormParamFunc returns a copy of Parser with set function for getting form parameters from http.Request.
71+
// For more see Parser description.
72+
func (p Parser) WithFormParamFunc(f FormParamFunc) Parser {
73+
p.FormParamFunc = f
74+
return p
75+
}
76+
6477
// Parse accepts the request and a pointer to struct with its fields tagged with appropriate tags set in Parser.
6578
// Such tagged fields must be in top level struct, or in exported struct embedded in top-level struct.
6679
// All such tagged fields are assigned the respective parameter from the actual request.
@@ -113,6 +126,7 @@ type paramType int
113126
const (
114127
paramTypeQuery paramType = iota
115128
paramTypePath
129+
paramTypeForm
116130
)
117131

118132
type taggedFieldIndexPath struct {
@@ -139,6 +153,7 @@ func (p Parser) findTaggedIndexPaths(typ reflect.Type, currentNestingIndexPath [
139153
}
140154
tag := typeField.Tag
141155
pathParamName, okPath := p.resolvePath(tag)
156+
formParamName, okForm := p.resolveForm(tag)
142157
queryParamName, okQuery := p.resolveQuery(tag)
143158
if okPath {
144159
newPath := make([]int, 0, len(currentNestingIndexPath)+1)
@@ -150,6 +165,16 @@ func (p Parser) findTaggedIndexPaths(typ reflect.Type, currentNestingIndexPath [
150165
indexPath: newPath,
151166
})
152167
}
168+
if okForm {
169+
newPath := make([]int, 0, len(currentNestingIndexPath)+1)
170+
newPath = append(newPath, currentNestingIndexPath...)
171+
newPath = append(newPath, i)
172+
paths = append(paths, taggedFieldIndexPath{
173+
paramType: paramTypeForm,
174+
paramName: formParamName,
175+
indexPath: newPath,
176+
})
177+
}
153178
if okQuery {
154179
newPath := make([]int, 0, len(currentNestingIndexPath)+1)
155180
newPath = append(newPath, currentNestingIndexPath...)
@@ -194,6 +219,11 @@ func (p Parser) parseParam(r *http.Request, path taggedFieldIndexPath) error {
194219
if err != nil {
195220
return err
196221
}
222+
case paramTypeForm:
223+
err := p.parseFormParam(r, path.paramName, path.destValue)
224+
if err != nil {
225+
return err
226+
}
197227
case paramTypeQuery:
198228
err := p.parseQueryParam(r, path.paramName, path.destValue)
199229
if err != nil {
@@ -217,6 +247,22 @@ func (p Parser) parsePathParam(r *http.Request, paramName string, v reflect.Valu
217247
return nil
218248
}
219249

250+
func (p Parser) parseFormParam(r *http.Request, paramName string, v reflect.Value) error {
251+
if r.Method != http.MethodPost && r.Method != http.MethodPut && r.Method != http.MethodPatch {
252+
return fmt.Errorf("struct's field was tagged for parsing the form parameter (%s) but request method is not POST, PUT or PATCH", paramName)
253+
}
254+
if err := r.ParseForm(); err != nil {
255+
return fmt.Errorf("parsing form data: %w", err)
256+
}
257+
if values, ok := r.Form[paramName]; ok && len(values) > 0 {
258+
err := unmarshalValueOrSlice(values, v)
259+
if err != nil {
260+
return fmt.Errorf("unmarshaling form parameter %s: %w", paramName, err)
261+
}
262+
}
263+
return nil
264+
}
265+
220266
func (p Parser) parseQueryParam(r *http.Request, paramName string, v reflect.Value) error {
221267
query := r.URL.Query()
222268
if values, ok := query[paramName]; ok && len(values) > 0 {
@@ -331,6 +377,10 @@ func (p Parser) resolvePath(fieldTag reflect.StructTag) (string, bool) {
331377
return p.resolveTagWithModifier(fieldTag, pathTagValuePrefix)
332378
}
333379

380+
func (p Parser) resolveForm(fieldTag reflect.StructTag) (string, bool) {
381+
return p.resolveTagWithModifier(fieldTag, formTagValuePrefix)
382+
}
383+
334384
func (p Parser) resolveQuery(fieldTag reflect.StructTag) (string, bool) {
335385
return p.resolveTagWithModifier(fieldTag, queryTagValuePrefix)
336386
}

http/param/param_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,45 @@ func TestParser_Parse_PathParam_FuncNotDefinedError(t *testing.T) {
451451
assert.Error(t, err)
452452
}
453453

454+
type structWithFormParams struct {
455+
Subject string `param:"form=subject"`
456+
Amount *int `param:"form=amount"`
457+
Object *maybeShinyObject `param:"form=object"`
458+
Nothing string `param:"form=nothing"`
459+
}
460+
461+
func TestParser_Parse_FormParam(t *testing.T) {
462+
r := chi.NewRouter()
463+
p := DefaultParser().WithFormParamFunc(func(r *http.Request, key string) string {
464+
return r.FormValue(key)
465+
})
466+
result := structWithFormParams{
467+
Nothing: "should be replaced",
468+
}
469+
expected := structWithFormParams{
470+
Subject: "world",
471+
Amount: ptr(69),
472+
Object: &maybeShinyObject{
473+
IsShiny: true,
474+
Object: "apples",
475+
},
476+
Nothing: "",
477+
}
478+
var parseError error
479+
r.Post("/hello/objects", func(w http.ResponseWriter, r *http.Request) {
480+
parseError = p.Parse(r, &result)
481+
})
482+
483+
urlEncodedBodyContent := "subject=world&amount=69&object=shiny-apples&nothing="
484+
urlEncodedBody := strings.NewReader(urlEncodedBodyContent)
485+
req := httptest.NewRequest(http.MethodPost, "https://test.com/hello/objects", urlEncodedBody)
486+
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
487+
r.ServeHTTP(httptest.NewRecorder(), req)
488+
489+
assert.NoError(t, parseError)
490+
assert.Equal(t, expected, result)
491+
}
492+
454493
type otherFieldsStruct struct {
455494
Q string `param:"query=q"`
456495
Other string `json:"other"`

0 commit comments

Comments
 (0)