Repository: gin-gonic/gin
Pull request #2720 (2021-05-14 04:00:48)

feat(binding): support custom struct tag
#2719 



codecov[bot]: # [Codecov](https://codecov.io/gh/gin-gonic/gin/pull/2720?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic) Report
> Merging [#2720](https://codecov.io/gh/gin-gonic/gin/pull/2720?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic) (e8f867a) into [master](https://codecov.io/gh/gin-gonic/gin/commit/4fe5f3e4b4fe62c057ec22caee4908beeef5f59c?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic) (4fe5f3e) will **decrease** coverage by `0.04%`.
> The diff coverage is `0.00%`.

[![Impacted file tree graph](https://codecov.io/gh/gin-gonic/gin/pull/2720/graphs/tree.svg?width=650&height=150&src=pr&token=Qpqxhwery8&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic)](https://codecov.io/gh/gin-gonic/gin/pull/2720?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic)

```diff
@@            Coverage Diff             @@
##           master    #2720      +/-   ##
==========================================
- Coverage   98.67%   98.62%   -0.05%     
==========================================
  Files          41       41              
  Lines        2036     2037       +1     
==========================================
  Hits         2009     2009              
- Misses         15       16       +1     
  Partials       12       12              
```


| [Impacted Files](https://codecov.io/gh/gin-gonic/gin/pull/2720?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic) | Coverage Δ | |
|---|---|---|
| [binding/form\_mapping.go](https://codecov.io/gh/gin-gonic/gin/pull/2720/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic#diff-YmluZGluZy9mb3JtX21hcHBpbmcuZ28=) | `99.52% <0.00%> (-0.48%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/gin-gonic/gin/pull/2720?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/gin-gonic/gin/pull/2720?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic). Last update [4fe5f3e...e8f867a](https://codecov.io/gh/gin-gonic/gin/pull/2720?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic).


Patch:
diff --git a/README.md b/README.md
index ef37fd2..ef80117 100644
--- a/README.md
+++ b/README.md
@@ -2021,6 +2021,61 @@ enough to call binding at once.
 can be called by `c.ShouldBind()` multiple times without any damage to
 performance (See [#1341](https://github.com/gin-gonic/gin/pull/1341)).
 
+### Bind form-data request with custom struct and custom tag
+
+```go
+const (
+	customerTag = "url"
+	defaultMemory = 32 << 20
+)
+
+type customerBinding struct {}
+
+func (customerBinding) Name() string {
+	return "form"
+}
+
+func (customerBinding) Bind(req *http.Request, obj interface{}) error {
+	if err := req.ParseForm(); err != nil {
+		return err
+	}
+	if err := req.ParseMultipartForm(defaultMemory); err != nil {
+		if err != http.ErrNotMultipart {
+			return err
+		}
+	}
+	if err := binding.MapFormWithTag(obj, req.Form, customerTag); err != nil {
+		return err
+	}
+	return validate(obj)
+}
+
+func validate(obj interface{}) error {
+	if binding.Validator == nil {
+		return nil
+	}
+	return binding.Validator.ValidateStruct(obj)
+}
+
+// Now we can do this!!!
+// FormA is a external type that we can't modify it's tag
+type FormA struct {
+	FieldA string `url:"field_a"`
+}
+
+func ListHandler(s *Service) func(ctx *gin.Context) {
+	return func(ctx *gin.Context) {
+		var urlBinding = customerBinding{}
+		var opt FormA
+		err := ctx.MustBindWith(&opt, urlBinding)
+		if err != nil {
+			...
+		}
+		...
+	}
+}
+```
+
 ### http2 server push
 
 http.Pusher is supported only **go1.8+**. See the [golang blog](https://blog.golang.org/h2push) for detail information.
diff --git a/binding/default_validator.go b/binding/default_validator.go
index ee69329..87fc4c6 100644
--- a/binding/default_validator.go
+++ b/binding/default_validator.go
@@ -20,15 +20,27 @@ type defaultValidator struct {
 
 type sliceValidateError []error
 
+// Error concatenates all error elements in sliceValidateError into a single string separated by \n.
 func (err sliceValidateError) Error() string {
-	var errMsgs []string
-	for i, e := range err {
-		if e == nil {
-			continue
+	n := len(err)
+	switch n {
+	case 0:
+		return ""
+	default:
+		var b strings.Builder
+		if err[0] != nil {
+			fmt.Fprintf(&b, "[%d]: %s", 0, err[0].Error())
+		}
+		if n > 1 {
+			for i := 1; i < n; i++ {
+				if err[i] != nil {
+					b.WriteString("\n")
+					fmt.Fprintf(&b, "[%d]: %s", i, err[i].Error())
+				}
+			}
 		}
-		errMsgs = append(errMsgs, fmt.Sprintf("[%d]: %s", i, e.Error()))
+		return b.String()
 	}
-	return strings.Join(errMsgs, "\n")
 }
 
 var _ StructValidator = &defaultValidator{}
diff --git a/binding/form_mapping.go b/binding/form_mapping.go
index 421c0f7..cb66dd4 100644
--- a/binding/form_mapping.go
+++ b/binding/form_mapping.go
@@ -34,6 +34,10 @@ func mapForm(ptr interface{}, form map[string][]string) error {
 	return mapFormByTag(ptr, form, "form")
 }
 
+func MapFormWithTag(ptr interface{}, form map[string][]string, tag string) error {
+	return mapFormByTag(ptr, form, tag)
+}
+
 var emptyField = reflect.StructField{}
 
 func mapFormByTag(ptr interface{}, form map[string][]string, tag string) error {


Test patch:
diff --git a/binding/default_validator_benchmark_test.go b/binding/default_validator_benchmark_test.go
new file mode 100644
index 0000000..839cf71
--- /dev/null
+++ b/binding/default_validator_benchmark_test.go
@@ -0,0 +1,20 @@
+package binding
+
+import (
+	"errors"
+	"strconv"
+	"testing"
+)
+
+func BenchmarkSliceValidateError(b *testing.B) {
+	const size int = 100
+	for i := 0; i < b.N; i++ {
+		e := make(sliceValidateError, size)
+		for j := 0; j < size; j++ {
+			e[j] = errors.New(strconv.Itoa(j))
+		}
+		if len(e.Error()) == 0 {
+			b.Errorf("error")
+		}
+	}
+}
diff --git a/binding/default_validator_test.go b/binding/default_validator_test.go
index e9c6de4..e9debe5 100644
--- a/binding/default_validator_test.go
+++ b/binding/default_validator_test.go
@@ -16,6 +16,26 @@ func TestSliceValidateError(t *testing.T) {
 		want string
 	}{
 		{"has nil elements", sliceValidateError{errors.New("test error"), nil}, "[0]: test error"},
+		{"has zero elements", sliceValidateError{}, ""},
+		{"has one element", sliceValidateError{errors.New("test one error")}, "[0]: test one error"},
+		{"has two elements",
+			sliceValidateError{
+				errors.New("first error"),
+				errors.New("second error"),
+			},
+			"[0]: first error\n[1]: second error",
+		},
+		{"has many elements",
+			sliceValidateError{
+				errors.New("first error"),
+				errors.New("second error"),
+				nil,
+				nil,
+				nil,
+				errors.New("last error"),
+			},
+			"[0]: first error\n[1]: second error\n[5]: last error",
+		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
diff --git a/binding/form_mapping_test.go b/binding/form_mapping_test.go
index 2675d46..608594e 100644
--- a/binding/form_mapping_test.go
+++ b/binding/form_mapping_test.go
@@ -145,6 +145,15 @@ func TestMappingForm(t *testing.T) {
 	assert.Equal(t, int(6), s.F)
 }
 
+func TestMapFormWithTag(t *testing.T) {
+	var s struct {
+		F int `externalTag:"field"`
+	}
+	err := MapFormWithTag(&s, map[string][]string{"field": {"6"}}, "externalTag")
+	assert.NoError(t, err)
+	assert.Equal(t, int(6), s.F)
+}
+
 func TestMappingTime(t *testing.T) {
 	var s struct {
 		Time      time.Time