1
0
mirror of https://github.com/labstack/echo.git synced 2025-01-12 01:22:21 +02:00

Add new value binding methods (UnixTimeMilli,TextUnmarshaler,JSONUnmarshaler) to ValueBinder

This commit is contained in:
toimtoimtoim 2022-03-13 17:30:02 +02:00 committed by Martti T
parent ec92fedf21
commit 59d2eaa4ac
2 changed files with 385 additions and 10 deletions

121
binder.go
View File

@ -1,6 +1,8 @@
package echo
import (
"encoding"
"encoding/json"
"fmt"
"net/http"
"strconv"
@ -52,8 +54,11 @@ import (
* time
* duration
* BindUnmarshaler() interface
* TextUnmarshaler() interface
* JSONUnmarshaler() interface
* UnixTime() - converts unix time (integer) to time.Time
* UnixTimeNano() - converts unix time with nano second precision (integer) to time.Time
* UnixTimeMilli() - converts unix time with millisecond precision (integer) to time.Time
* UnixTimeNano() - converts unix time with nanosecond precision (integer) to time.Time
* CustomFunc() - callback function for your custom conversion logic. Signature `func(values []string) []error`
*/
@ -321,6 +326,78 @@ func (b *ValueBinder) MustBindUnmarshaler(sourceParam string, dest BindUnmarshal
return b
}
// JSONUnmarshaler binds parameter to destination implementing json.Unmarshaler interface
func (b *ValueBinder) JSONUnmarshaler(sourceParam string, dest json.Unmarshaler) *ValueBinder {
if b.failFast && b.errors != nil {
return b
}
tmp := b.ValueFunc(sourceParam)
if tmp == "" {
return b
}
if err := dest.UnmarshalJSON([]byte(tmp)); err != nil {
b.setError(b.ErrorFunc(sourceParam, []string{tmp}, "failed to bind field value to json.Unmarshaler interface", err))
}
return b
}
// MustJSONUnmarshaler requires parameter value to exist to be bind to destination implementing json.Unmarshaler interface.
// Returns error when value does not exist
func (b *ValueBinder) MustJSONUnmarshaler(sourceParam string, dest json.Unmarshaler) *ValueBinder {
if b.failFast && b.errors != nil {
return b
}
tmp := b.ValueFunc(sourceParam)
if tmp == "" {
b.setError(b.ErrorFunc(sourceParam, []string{tmp}, "required field value is empty", nil))
return b
}
if err := dest.UnmarshalJSON([]byte(tmp)); err != nil {
b.setError(b.ErrorFunc(sourceParam, []string{tmp}, "failed to bind field value to json.Unmarshaler interface", err))
}
return b
}
// TextUnmarshaler binds parameter to destination implementing encoding.TextUnmarshaler interface
func (b *ValueBinder) TextUnmarshaler(sourceParam string, dest encoding.TextUnmarshaler) *ValueBinder {
if b.failFast && b.errors != nil {
return b
}
tmp := b.ValueFunc(sourceParam)
if tmp == "" {
return b
}
if err := dest.UnmarshalText([]byte(tmp)); err != nil {
b.setError(b.ErrorFunc(sourceParam, []string{tmp}, "failed to bind field value to encoding.TextUnmarshaler interface", err))
}
return b
}
// MustTextUnmarshaler requires parameter value to exist to be bind to destination implementing encoding.TextUnmarshaler interface.
// Returns error when value does not exist
func (b *ValueBinder) MustTextUnmarshaler(sourceParam string, dest encoding.TextUnmarshaler) *ValueBinder {
if b.failFast && b.errors != nil {
return b
}
tmp := b.ValueFunc(sourceParam)
if tmp == "" {
b.setError(b.ErrorFunc(sourceParam, []string{tmp}, "required field value is empty", nil))
return b
}
if err := dest.UnmarshalText([]byte(tmp)); err != nil {
b.setError(b.ErrorFunc(sourceParam, []string{tmp}, "failed to bind field value to encoding.TextUnmarshaler interface", err))
}
return b
}
// BindWithDelimiter binds parameter to destination by suitable conversion function.
// Delimiter is used before conversion to split parameter value to separate values
func (b *ValueBinder) BindWithDelimiter(sourceParam string, dest interface{}, delimiter string) *ValueBinder {
@ -1161,7 +1238,7 @@ func (b *ValueBinder) durations(sourceParam string, values []string, dest *[]tim
// Note:
// * 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, false)
return b.unixTime(sourceParam, dest, false, time.Second)
}
// MustUnixTime requires parameter value to exist to be bind to time.Duration variable (in local Time corresponding
@ -1172,10 +1249,31 @@ func (b *ValueBinder) UnixTime(sourceParam string, dest *time.Time) *ValueBinder
// Note:
// * 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, false)
return b.unixTime(sourceParam, dest, true, time.Second)
}
// UnixTimeNano binds parameter to time.Time variable (in local Time corresponding to the given Unix time in nano second precision).
// UnixTimeMilli binds parameter to time.Time variable (in local Time corresponding to the given Unix time in millisecond precision).
//
// 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
func (b *ValueBinder) UnixTimeMilli(sourceParam string, dest *time.Time) *ValueBinder {
return b.unixTime(sourceParam, dest, false, time.Millisecond)
}
// MustUnixTimeMilli requires parameter value to exist to be bind to time.Duration variable (in local Time corresponding
// to the given Unix time in millisecond precision). Returns error when value does not exist.
//
// 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
func (b *ValueBinder) MustUnixTimeMilli(sourceParam string, dest *time.Time) *ValueBinder {
return b.unixTime(sourceParam, dest, true, time.Millisecond)
}
// UnixTimeNano binds parameter to time.Time variable (in local Time corresponding to the given Unix time in nanosecond precision).
//
// Example: 1609180603123456789 binds to 2020-12-28T18:36:43.123456789+00:00
// Example: 1000000000 binds to 1970-01-01T00:00:01.000000000+00:00
@ -1185,7 +1283,7 @@ func (b *ValueBinder) MustUnixTime(sourceParam string, dest *time.Time) *ValueBi
// * 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, true)
return b.unixTime(sourceParam, dest, false, time.Nanosecond)
}
// MustUnixTimeNano requires parameter value to exist to be bind to time.Duration variable (in local Time corresponding
@ -1199,10 +1297,10 @@ func (b *ValueBinder) UnixTimeNano(sourceParam string, dest *time.Time) *ValueBi
// * 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, true)
return b.unixTime(sourceParam, dest, true, time.Nanosecond)
}
func (b *ValueBinder) unixTime(sourceParam string, dest *time.Time, valueMustExist bool, isNano bool) *ValueBinder {
func (b *ValueBinder) unixTime(sourceParam string, dest *time.Time, valueMustExist bool, precision time.Duration) *ValueBinder {
if b.failFast && b.errors != nil {
return b
}
@ -1221,10 +1319,13 @@ func (b *ValueBinder) unixTime(sourceParam string, dest *time.Time, valueMustExi
return b
}
if isNano {
*dest = time.Unix(0, n)
} else {
switch precision {
case time.Second:
*dest = time.Unix(n, 0)
case time.Millisecond:
*dest = time.Unix(n/1e3, (n%1e3)*1e6) // TODO: time.UnixMilli(n) exists since Go1.17 switch to that when min version allows
case time.Nanosecond:
*dest = time.Unix(0, n)
}
return b
}

View File

@ -7,6 +7,7 @@ import (
"fmt"
"github.com/stretchr/testify/assert"
"io"
"math/big"
"net/http"
"net/http/httptest"
"strconv"
@ -2187,6 +2188,188 @@ func TestValueBinder_BindUnmarshaler(t *testing.T) {
}
}
func TestValueBinder_JSONUnmarshaler(t *testing.T) {
example := big.NewInt(999)
var testCases = []struct {
name string
givenFailFast bool
givenBindErrors []error
whenURL string
whenMust bool
expectValue big.Int
expectError string
}{
{
name: "ok, binds value",
whenURL: "/search?param=999&param=998",
expectValue: *example,
},
{
name: "ok, params values empty, value is not changed",
whenURL: "/search?nope=1",
expectValue: big.Int{},
},
{
name: "nok, previous errors fail fast without binding value",
givenFailFast: true,
whenURL: "/search?param=1&param=100",
expectValue: big.Int{},
expectError: "previous error",
},
{
name: "nok, conversion fails, value is not changed",
whenURL: "/search?param=nope&param=xxx",
expectValue: big.Int{},
expectError: "code=400, message=failed to bind field value to json.Unmarshaler interface, internal=math/big: cannot unmarshal \"nope\" into a *big.Int, field=param",
},
{
name: "ok (must), binds value",
whenMust: true,
whenURL: "/search?param=999&param=998",
expectValue: *example,
},
{
name: "ok (must), params values empty, returns error, value is not changed",
whenMust: true,
whenURL: "/search?nope=1",
expectValue: big.Int{},
expectError: "code=400, message=required field value is empty, field=param",
},
{
name: "nok (must), previous errors fail fast without binding value",
givenFailFast: true,
whenMust: true,
whenURL: "/search?param=1&param=xxx",
expectValue: big.Int{},
expectError: "previous error",
},
{
name: "nok (must), conversion fails, value is not changed",
whenMust: true,
whenURL: "/search?param=nope&param=xxx",
expectValue: big.Int{},
expectError: "code=400, message=failed to bind field value to json.Unmarshaler interface, internal=math/big: cannot unmarshal \"nope\" into a *big.Int, field=param",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
c := createTestContext(tc.whenURL, nil, nil)
b := QueryParamsBinder(c).FailFast(tc.givenFailFast)
if tc.givenFailFast {
b.errors = []error{errors.New("previous error")}
}
var dest big.Int
var err error
if tc.whenMust {
err = b.MustJSONUnmarshaler("param", &dest).BindError()
} else {
err = b.JSONUnmarshaler("param", &dest).BindError()
}
assert.Equal(t, tc.expectValue, dest)
if tc.expectError != "" {
assert.EqualError(t, err, tc.expectError)
} else {
assert.NoError(t, err)
}
})
}
}
func TestValueBinder_TextUnmarshaler(t *testing.T) {
example := big.NewInt(999)
var testCases = []struct {
name string
givenFailFast bool
givenBindErrors []error
whenURL string
whenMust bool
expectValue big.Int
expectError string
}{
{
name: "ok, binds value",
whenURL: "/search?param=999&param=998",
expectValue: *example,
},
{
name: "ok, params values empty, value is not changed",
whenURL: "/search?nope=1",
expectValue: big.Int{},
},
{
name: "nok, previous errors fail fast without binding value",
givenFailFast: true,
whenURL: "/search?param=1&param=100",
expectValue: big.Int{},
expectError: "previous error",
},
{
name: "nok, conversion fails, value is not changed",
whenURL: "/search?param=nope&param=xxx",
expectValue: big.Int{},
expectError: "code=400, message=failed to bind field value to encoding.TextUnmarshaler interface, internal=math/big: cannot unmarshal \"nope\" into a *big.Int, field=param",
},
{
name: "ok (must), binds value",
whenMust: true,
whenURL: "/search?param=999&param=998",
expectValue: *example,
},
{
name: "ok (must), params values empty, returns error, value is not changed",
whenMust: true,
whenURL: "/search?nope=1",
expectValue: big.Int{},
expectError: "code=400, message=required field value is empty, field=param",
},
{
name: "nok (must), previous errors fail fast without binding value",
givenFailFast: true,
whenMust: true,
whenURL: "/search?param=1&param=xxx",
expectValue: big.Int{},
expectError: "previous error",
},
{
name: "nok (must), conversion fails, value is not changed",
whenMust: true,
whenURL: "/search?param=nope&param=xxx",
expectValue: big.Int{},
expectError: "code=400, message=failed to bind field value to encoding.TextUnmarshaler interface, internal=math/big: cannot unmarshal \"nope\" into a *big.Int, field=param",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
c := createTestContext(tc.whenURL, nil, nil)
b := QueryParamsBinder(c).FailFast(tc.givenFailFast)
if tc.givenFailFast {
b.errors = []error{errors.New("previous error")}
}
var dest big.Int
var err error
if tc.whenMust {
err = b.MustTextUnmarshaler("param", &dest).BindError()
} else {
err = b.TextUnmarshaler("param", &dest).BindError()
}
assert.Equal(t, tc.expectValue, dest)
if tc.expectError != "" {
assert.EqualError(t, err, tc.expectError)
} else {
assert.NoError(t, err)
}
})
}
}
func TestValueBinder_BindWithDelimiter_types(t *testing.T) {
var testCases = []struct {
name string
@ -2529,6 +2712,97 @@ func TestValueBinder_UnixTime(t *testing.T) {
}
}
func TestValueBinder_UnixTimeMilli(t *testing.T) {
exampleTime, _ := time.Parse(time.RFC3339Nano, "2022-03-13T15:13:30.140000000+00:00") // => 1647184410140
var testCases = []struct {
name string
givenFailFast bool
givenBindErrors []error
whenURL string
whenMust bool
expectValue time.Time
expectError string
}{
{
name: "ok, binds value, unix time in milliseconds",
whenURL: "/search?param=1647184410140&param=1647184410199",
expectValue: exampleTime,
},
{
name: "ok, params values empty, value is not changed",
whenURL: "/search?nope=1",
expectValue: time.Time{},
},
{
name: "nok, previous errors fail fast without binding value",
givenFailFast: true,
whenURL: "/search?param=1&param=100",
expectValue: time.Time{},
expectError: "previous error",
},
{
name: "nok, conversion fails, value is not changed",
whenURL: "/search?param=nope&param=100",
expectValue: time.Time{},
expectError: "code=400, message=failed to bind field value to Time, internal=strconv.ParseInt: parsing \"nope\": invalid syntax, field=param",
},
{
name: "ok (must), binds value",
whenMust: true,
whenURL: "/search?param=1647184410140&param=1647184410199",
expectValue: exampleTime,
},
{
name: "ok (must), params values empty, returns error, value is not changed",
whenMust: true,
whenURL: "/search?nope=1",
expectValue: time.Time{},
expectError: "code=400, message=required field value is empty, field=param",
},
{
name: "nok (must), previous errors fail fast without binding value",
givenFailFast: true,
whenMust: true,
whenURL: "/search?param=1&param=100",
expectValue: time.Time{},
expectError: "previous error",
},
{
name: "nok (must), conversion fails, value is not changed",
whenMust: true,
whenURL: "/search?param=nope&param=100",
expectValue: time.Time{},
expectError: "code=400, message=failed to bind field value to Time, internal=strconv.ParseInt: parsing \"nope\": invalid syntax, field=param",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
c := createTestContext(tc.whenURL, nil, nil)
b := QueryParamsBinder(c).FailFast(tc.givenFailFast)
if tc.givenFailFast {
b.errors = []error{errors.New("previous error")}
}
dest := time.Time{}
var err error
if tc.whenMust {
err = b.MustUnixTimeMilli("param", &dest).BindError()
} else {
err = b.UnixTimeMilli("param", &dest).BindError()
}
assert.Equal(t, tc.expectValue.UnixNano(), dest.UnixNano())
assert.Equal(t, tc.expectValue.In(time.UTC), dest.In(time.UTC))
if tc.expectError != "" {
assert.EqualError(t, err, tc.expectError)
} else {
assert.NoError(t, err)
}
})
}
}
func TestValueBinder_UnixTimeNano(t *testing.T) {
exampleTime, _ := time.Parse(time.RFC3339, "2020-12-28T18:36:43.000000000+00:00") // => 1609180603
exampleTimeNano, _ := time.Parse(time.RFC3339Nano, "2020-12-28T18:36:43.123456789+00:00") // => 1609180603123456789