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

Allow binders to be chained together to create multi-source binder #2311

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
45 changes: 37 additions & 8 deletions binder.go
Expand Up @@ -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:
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
Expand All @@ -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)
}
Expand All @@ -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)
}
Expand All @@ -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)
}
Expand All @@ -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)
}
Expand All @@ -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)
}
Expand Down
75 changes: 74 additions & 1 deletion binder_test.go
Expand Up @@ -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", &notExisting).
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", &notExisting).
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) {
Expand Down