Repository: cloudwego/hertz
Pull request #1172 (2024-08-16 11:52:25)

feat: Add method PostFormArray to app.RequestContext.
#### What type of PR is this?
feat
<!--
Add one of the following kinds:

build: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
ci: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)
docs: Documentation only changes
feat: A new feature
optimize: A new optimization
fix: A bug fix
perf: A code change that improves performance
refactor: A code change that neither fixes a bug nor adds a feature
style: Changes that do not affect the meaning of the code (white space, formatting, missing semi-colons, etc)
test: Adding missing tests or correcting existing tests
chore: Changes to the build process or auxiliary tools and libraries such as documentation generation
-->

#### Check the PR title.
<!--
The description of the title will be attached in Release Notes, 
so please describe it from user-oriented, what this PR does / why we need it.
Please check your PR title with the below requirements:
-->
- [X] This PR title match the format: \<type\>(optional scope): \<description\>
- [X] The description of this PR title is user-oriented and clear enough for others to understand.
- [X] Attach the PR updating the user documentation if the current PR requires user awareness at the usage level. [User docs repo](https://github.com/cloudwego/cloudwego.github.io)


#### (Optional) Translate the PR title into Chinese.

为 `app.RequestContext` 添加 `PostFormArray` 方法

#### (Optional) More detailed description for this PR(en: English/zh: Chinese).
<!--
Provide more detailed info for review(e.g., it's recommended to provide perf data if this is a perf type PR).
-->
en:
[feat(hertz)](https://github.com/cloudwego/hertz/pull/1172)
zh(optional):

#### (Optional) Which issue(s) this PR fixes:
<!--
Automatically closes linked issue when PR is merged.
Eg: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
-->

#### (Optional) The PR that updates user documentation:
<!--
If the current PR requires user awareness at the usage level, please submit a PR to update user docs. [User docs repo](https://github.com/cloudwego/cloudwego.github.io)
-->

CLAassistant: [![CLA assistant check](https://cla-assistant.io/pull/badge/not_signed)](https://cla-assistant.io/cloudwego/hertz?pullRequest=1172) <br/>Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our [Contributor License Agreement](https://cla-assistant.io/cloudwego/hertz?pullRequest=1172) before we can accept your contribution.<br/><sub>You have signed the CLA already but the status is still pending? Let us [recheck](https://cla-assistant.io/check/cloudwego/hertz?pullRequest=1172) it.</sub>

Patch:
diff --git a/pkg/app/context.go b/pkg/app/context.go
index 369ece7..1d925b7 100644
--- a/pkg/app/context.go
+++ b/pkg/app/context.go
@@ -694,6 +694,17 @@ func (ctx *RequestContext) multipartFormValue(key string) (string, bool) {
 	return "", false
 }
 
+func (ctx *RequestContext) multipartFormValueArray(key string) ([]string, bool) {
+	mf, err := ctx.MultipartForm()
+	if err == nil && mf.Value != nil {
+		vv := mf.Value[key]
+		if len(vv) > 0 {
+			return vv, true
+		}
+	}
+	return nil, false
+}
+
 func (ctx *RequestContext) RequestBodyStream() io.Reader {
 	return ctx.Request.BodyStream()
 }
@@ -1366,6 +1377,13 @@ func (ctx *RequestContext) PostForm(key string) string {
 	return value
 }
 
+// PostFormArray returns the specified key from a POST urlencoded form or multipart form
+// when it exists, otherwise it returns an empty array `([])`.
+func (ctx *RequestContext) PostFormArray(key string) []string {
+	values, _ := ctx.GetPostFormArray(key)
+	return values
+}
+
 // DefaultPostForm returns the specified key from a POST urlencoded form or multipart form
 // when it exists, otherwise it returns the specified defaultValue string.
 //
@@ -1393,6 +1411,28 @@ func (ctx *RequestContext) GetPostForm(key string) (string, bool) {
 	return ctx.multipartFormValue(key)
 }
 
+// GetPostFormArray is like PostFormArray(key). It returns the specified key from a POST urlencoded
+// form or multipart form when it exists `([]string, true)` (even when the value is an empty string),
+// otherwise it returns ([]string(nil), false).
+//
+// For example, during a PATCH request to update the item's tags:
+//
+//	    tag=tag1 tag=tag2 tag=tag3  -->  (["tag1", "tag2", "tag3"], true) := GetPostFormArray("tags") // set tags to ["tag1", "tag2", "tag3"]
+//		   tags=                  -->  (nil, true) := GetPostFormArray("tags") // set tags to nil
+//	                            -->  (nil, false) := GetPostFormArray("tags") // do nothing with tags
+func (ctx *RequestContext) GetPostFormArray(key string) ([]string, bool) {
+	vs := ctx.PostArgs().PeekAll(key)
+	values := make([]string, len(vs))
+	for i, v := range vs {
+		values[i] = string(v)
+	}
+	if len(values) == 0 {
+		return ctx.multipartFormValueArray(key)
+	} else {
+		return values, true
+	}
+}
+
 // bodyAllowedForStatus is a copy of http.bodyAllowedForStatus non-exported function.
 func bodyAllowedForStatus(status int) bool {
 	switch {


Test patch:
diff --git a/pkg/app/context_test.go b/pkg/app/context_test.go
index 1317a06..28e54ce 100644
--- a/pkg/app/context_test.go
+++ b/pkg/app/context_test.go
@@ -367,6 +367,40 @@ hello=world`)
 	}
 }
 
+func TestPostFormArray(t *testing.T) {
+	t.Parallel()
+
+	ctx := makeCtxByReqString(t, `POST /upload HTTP/1.1
+Host: localhost:10000
+Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryJwfATyF8tmxSJnLg
+Content-Length: 521
+
+------WebKitFormBoundaryJwfATyF8tmxSJnLg
+Content-Disposition: form-data; name="tag"
+
+red
+------WebKitFormBoundaryJwfATyF8tmxSJnLg
+Content-Disposition: form-data; name="tag"
+
+green
+------WebKitFormBoundaryJwfATyF8tmxSJnLg
+Content-Disposition: form-data; name="tag"
+
+blue
+------WebKitFormBoundaryJwfATyF8tmxSJnLg--
+`)
+	assert.DeepEqual(t, []string{"red", "green", "blue"}, ctx.PostFormArray("tag"))
+
+	ctx = makeCtxByReqString(t, `POST /upload HTTP/1.1
+Host: localhost:10000
+Content-Type: application/x-www-form-urlencoded; charset=UTF-8
+Content-Length: 26
+
+tag=red&tag=green&tag=blue
+`)
+	assert.DeepEqual(t, []string{"red", "green", "blue"}, ctx.PostFormArray("tag"))
+}
+
 func TestDefaultPostForm(t *testing.T) {
 	ctx := makeCtxByReqString(t, `POST /upload HTTP/1.1
 Host: localhost:10000
@@ -930,6 +964,14 @@ func TestGetPostForm(t *testing.T) {
 	assert.DeepEqual(t, true, exists)
 }
 
+func TestGetPostFormArray(t *testing.T) {
+	c := NewContext(0)
+	c.Request.Header.SetContentTypeBytes([]byte(consts.MIMEApplicationHTMLForm))
+	c.Request.SetBodyString("a=1&b=2&b=3")
+	v, _ := c.GetPostFormArray("b")
+	assert.DeepEqual(t, []string{"2", "3"}, v)
+}
+
 func TestRemoteAddr(t *testing.T) {
 	c := NewContext(0)
 	c.Request.SetRequestURI("http://aaa.com?a=1&b=")