From 510f12dd258cf07c8e4ef7e30eb0951f881842ea Mon Sep 17 00:00:00 2001 From: toimtoimtoim Date: Sun, 23 Oct 2022 22:42:01 +0300 Subject: [PATCH] Allow binders to be chained together to creat multi-source binder --- binder.go | 45 ++++++++++++++++++++++++------ binder_test.go | 75 +++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 111 insertions(+), 9 deletions(-) diff --git a/binder.go b/binder.go index 5a6cf9d9b..3a3e04212 100644 --- a/binder.go +++ b/binder.go @@ -20,6 +20,13 @@ import ( ```go var length int64 err := echo.QueryParamsBinder(c).Int64("length", &length).BindError() + ``` + + Binders can be chained together with `UseBefore` method. Where left side binder values have priority over right side binder. + + Example: + ```go + b := PathParamsBinder(c).UseBefore(QueryParamsBinder(c)) ``` For every supported type there are following methods: @@ -169,6 +176,28 @@ func FormFieldBinder(c Context) *ValueBinder { return vb } +// UseBefore creates new binder that binds left side binder first and if this does not result value the right side +// binder is used (receiver binder has priority over argument binder). +func (b *ValueBinder) UseBefore(after *ValueBinder) *ValueBinder { + oldValueFunc := b.ValueFunc + b.ValueFunc = func(sourceParam string) string { + if v := oldValueFunc(sourceParam); v != "" { + return v + } + return after.ValueFunc(sourceParam) + } + + oldValuesFunc := b.ValuesFunc + b.ValuesFunc = func(sourceParam string) []string { + if v := oldValuesFunc(sourceParam); v != nil { + return v + } + return after.ValuesFunc(sourceParam) + } + + return b +} + // FailFast set internal flag to indicate if binding methods will return early (without binding) when previous bind failed // NB: call this method before any other binding methods as it modifies binding methods behaviour func (b *ValueBinder) FailFast(value bool) *ValueBinder { @@ -1236,7 +1265,7 @@ func (b *ValueBinder) durations(sourceParam string, values []string, dest *[]tim // Example: 1609180603 bind to 2020-12-28T18:36:43.000000000+00:00 // // Note: -// * time.Time{} (param is empty) and time.Unix(0,0) (param = "0") are not equal +// - time.Time{} (param is empty) and time.Unix(0,0) (param = "0") are not equal func (b *ValueBinder) UnixTime(sourceParam string, dest *time.Time) *ValueBinder { return b.unixTime(sourceParam, dest, false, time.Second) } @@ -1247,7 +1276,7 @@ func (b *ValueBinder) UnixTime(sourceParam string, dest *time.Time) *ValueBinder // Example: 1609180603 bind to 2020-12-28T18:36:43.000000000+00:00 // // Note: -// * time.Time{} (param is empty) and time.Unix(0,0) (param = "0") are not equal +// - time.Time{} (param is empty) and time.Unix(0,0) (param = "0") are not equal func (b *ValueBinder) MustUnixTime(sourceParam string, dest *time.Time) *ValueBinder { return b.unixTime(sourceParam, dest, true, time.Second) } @@ -1257,7 +1286,7 @@ func (b *ValueBinder) MustUnixTime(sourceParam string, dest *time.Time) *ValueBi // Example: 1647184410140 bind to 2022-03-13T15:13:30.140000000+00:00 // // Note: -// * time.Time{} (param is empty) and time.Unix(0,0) (param = "0") are not equal +// - time.Time{} (param is empty) and time.Unix(0,0) (param = "0") are not equal func (b *ValueBinder) UnixTimeMilli(sourceParam string, dest *time.Time) *ValueBinder { return b.unixTime(sourceParam, dest, false, time.Millisecond) } @@ -1268,7 +1297,7 @@ func (b *ValueBinder) UnixTimeMilli(sourceParam string, dest *time.Time) *ValueB // Example: 1647184410140 bind to 2022-03-13T15:13:30.140000000+00:00 // // Note: -// * time.Time{} (param is empty) and time.Unix(0,0) (param = "0") are not equal +// - time.Time{} (param is empty) and time.Unix(0,0) (param = "0") are not equal func (b *ValueBinder) MustUnixTimeMilli(sourceParam string, dest *time.Time) *ValueBinder { return b.unixTime(sourceParam, dest, true, time.Millisecond) } @@ -1280,8 +1309,8 @@ func (b *ValueBinder) MustUnixTimeMilli(sourceParam string, dest *time.Time) *Va // Example: 999999999 binds to 1970-01-01T00:00:00.999999999+00:00 // // Note: -// * time.Time{} (param is empty) and time.Unix(0,0) (param = "0") are not equal -// * Javascript's Number type only has about 53 bits of precision (Number.MAX_SAFE_INTEGER = 9007199254740991). Compare it to 1609180603123456789 in example. +// - time.Time{} (param is empty) and time.Unix(0,0) (param = "0") are not equal +// - Javascript's Number type only has about 53 bits of precision (Number.MAX_SAFE_INTEGER = 9007199254740991). Compare it to 1609180603123456789 in example. func (b *ValueBinder) UnixTimeNano(sourceParam string, dest *time.Time) *ValueBinder { return b.unixTime(sourceParam, dest, false, time.Nanosecond) } @@ -1294,8 +1323,8 @@ func (b *ValueBinder) UnixTimeNano(sourceParam string, dest *time.Time) *ValueBi // Example: 999999999 binds to 1970-01-01T00:00:00.999999999+00:00 // // Note: -// * time.Time{} (param is empty) and time.Unix(0,0) (param = "0") are not equal -// * Javascript's Number type only has about 53 bits of precision (Number.MAX_SAFE_INTEGER = 9007199254740991). Compare it to 1609180603123456789 in example. +// - time.Time{} (param is empty) and time.Unix(0,0) (param = "0") are not equal +// - Javascript's Number type only has about 53 bits of precision (Number.MAX_SAFE_INTEGER = 9007199254740991). Compare it to 1609180603123456789 in example. func (b *ValueBinder) MustUnixTimeNano(sourceParam string, dest *time.Time) *ValueBinder { return b.unixTime(sourceParam, dest, true, time.Nanosecond) } diff --git a/binder_test.go b/binder_test.go index 0b27cae64..e30781aea 100644 --- a/binder_test.go +++ b/binder_test.go @@ -157,8 +157,81 @@ func TestFormFieldBinder(t *testing.T) { assert.Equal(t, "foo", texta) assert.Equal(t, int64(1), id) assert.Equal(t, int64(2), nr) - assert.Equal(t, []int64{5, 3, 4}, slice) assert.Equal(t, []int64{}, notExisting) + + // NB: when binding forms take note that this implementation uses standard library form parsing + // which parses form data from BOTH URL and BODY if content type is not MIMEMultipartForm + assert.Equal(t, []int64{5, 3, 4}, slice) // so we have value from body and query here +} + +func TestValueBinder_UseBefore(t *testing.T) { + c := createTestContext("/dosomething?lang=en&slice=51&slice=52&csv=1", nil, map[string]string{ + "id": "999", + "slice": "1", + "csv": "2", + // no `lang` here so value should be taken from query + }) + + // bound path params should have priority over query params + b := PathParamsBinder(c).UseBefore(QueryParamsBinder(c)) + + var lang string + var csv int8 + id := int64(42) + var slice = make([]int64, 0) + var notExisting = make([]int64, 0) + err := b. + Int8("csv", &csv). + Int64("id", &id). + String("lang", &lang). + Int64s("slice", &slice). + Int64s("notExisting", ¬Existing). + BindError() + + assert.NoError(t, err) + assert.Equal(t, int8(2), csv) // from path params because path has priority + assert.Equal(t, []int64{1}, slice) // from path params because path has priority, we test slices here + assert.Equal(t, int64(999), id) // from path params because path has priority also query does not contain this param + + assert.Equal(t, "en", lang) // from query params because path does not have it + + assert.Equal(t, []int64{}, notExisting) // no value +} + +func TestValueBinder_UseBefore_3binders(t *testing.T) { + body := `first=3&second=23&third=33` + req := httptest.NewRequest(http.MethodPost, "/dosomething?first=2&second=22", strings.NewReader(body)) + req.Header.Set(HeaderContentLength, strconv.Itoa(len(body))) + req.Header.Set(HeaderContentType, MIMEApplicationForm) + + rec := httptest.NewRecorder() + c := (New()).NewContext(req, rec) + c.SetParamNames("first") + c.SetParamValues("1") + + // bound params priority: + // 1. Path params + // 2. Query params + // 3. Form fields + b := PathParamsBinder(c).UseBefore(QueryParamsBinder(c)).UseBefore(FormFieldBinder(c)) + + var first int64 + var second int64 + var third int64 + var notExisting = make([]int64, 0) + err := b. + Int64("first", &first). + Int64("second", &second). + Int64("third", &third). + Int64s("notExisting", ¬Existing). + BindError() + + assert.NoError(t, err) + assert.Equal(t, int64(1), first) // from path params because path has priority + assert.Equal(t, int64(22), second) // from query params because query has priority over form and path does not have this param + assert.Equal(t, int64(33), third) // from form params because path and query does not have this param + + assert.Equal(t, []int64{}, notExisting) // no value } func TestValueBinder_errorStopsBinding(t *testing.T) {