diff --git a/.github/workflows/echo.yml b/.github/workflows/echo.yml index 2aec272d..fb8c5020 100644 --- a/.github/workflows/echo.yml +++ b/.github/workflows/echo.yml @@ -59,7 +59,7 @@ jobs: go test -race --coverprofile=coverage.coverprofile --covermode=atomic ./... - name: Upload coverage to Codecov - if: success() && matrix.go == 1.13 && matrix.os == 'ubuntu-latest' + if: success() && matrix.go == 1.15 && matrix.os == 'ubuntu-latest' uses: codecov/codecov-action@v1 with: token: diff --git a/Makefile b/Makefile index c369913a..bedb8bd2 100644 --- a/Makefile +++ b/Makefile @@ -23,5 +23,12 @@ test: ## Run tests race: ## Run tests with data race detector @go test -race ${PKG_LIST} +benchmark: ## Run benchmarks + @go test -run="-" -bench=".*" ${PKG_LIST} + help: ## Display this help screen @grep -h -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + +goversion ?= "1.12" +test_version: ## Run tests inside Docker with given version (defaults to 1.12 oldest supported). Example: make test_version goversion=1.13 + @docker run --rm -it -v $(shell pwd):/project golang:$(goversion) /bin/sh -c "cd /project && make check" diff --git a/binder.go b/binder.go new file mode 100644 index 00000000..9f0ca654 --- /dev/null +++ b/binder.go @@ -0,0 +1,1234 @@ +package echo + +import ( + "fmt" + "net/http" + "strconv" + "strings" + "time" +) + +/** + Following functions provide handful of methods for binding to Go native types from request query or path parameters. + * QueryParamsBinder(c) - binds query parameters (source URL) + * PathParamsBinder(c) - binds path parameters (source URL) + * FormFieldBinder(c) - binds form fields (source URL + body) + + Example: + ```go + var length int64 + err := echo.QueryParamsBinder(c).Int64("length", &length).BindError() + ``` + + For every supported type there are following methods: + * ("param", &destination) - if parameter value exists then binds it to given destination of that type i.e Int64(...). + * Must("param", &destination) - parameter value is required to exist, binds it to given destination of that type i.e MustInt64(...). + * s("param", &destination) - (for slices) if parameter values exists then binds it to given destination of that type i.e Int64s(...). + * Musts("param", &destination) - (for slices) parameter value is required to exist, binds it to given destination of that type i.e MustInt64s(...). + + for some slice types `BindWithDelimiter("param", &dest, ",")` supports splitting parameter values before type conversion is done + i.e. URL `/api/search?id=1,2,3&id=1` can be bind to `[]int64{1,2,3,1}` + + `FailFast` flags binder to stop binding after first bind error during binder call chain. Enabled by default. + `BindError()` returns first bind error from binder and resets errors in binder. Useful along with `FailFast()` method + to do binding and returns on first problem + `BindErrors()` returns all bind errors from binder and resets errors in binder. + + Types that are supported: + * bool + * float32 + * float64 + * int + * int8 + * int16 + * int32 + * int64 + * uint + * uint8/byte (does not support `bytes()`. Use BindUnmarshaler/CustomFunc to convert value from base64 etc to []byte{}) + * uint16 + * uint32 + * uint64 + * string + * time + * duration + * BindUnmarshaler() interface + * UnixTime() - converts unix time (integer) to time.Time + * UnixTimeNano() - converts unix time with nano second precision (integer) to time.Time + * CustomFunc() - callback function for your custom conversion logic. Signature `func(values []string) []error` +*/ + +// BindingError represents an error that occurred while binding request data. +type BindingError struct { + // Field is the field name where value binding failed + Field string `json:"field"` + // Values of parameter that failed to bind. + Values []string `json:"-"` + *HTTPError +} + +// NewBindingError creates new instance of binding error +func NewBindingError(sourceParam string, values []string, message interface{}, internalError error) error { + return &BindingError{ + Field: sourceParam, + Values: values, + HTTPError: &HTTPError{ + Code: http.StatusBadRequest, + Message: message, + Internal: internalError, + }, + } +} + +// Error returns error message +func (be *BindingError) Error() string { + return fmt.Sprintf("%s, field=%s", be.HTTPError.Error(), be.Field) +} + +// ValueBinder provides utility methods for binding query or path parameter to various Go built-in types +type ValueBinder struct { + // failFast is flag for binding methods to return without attempting to bind when previous binding already failed + failFast bool + errors []error + + // ValueFunc is used to get single parameter (first) value from request + ValueFunc func(sourceParam string) string + // ValuesFunc is used to get all values for parameter from request. i.e. `/api/search?ids=1&ids=2` + ValuesFunc func(sourceParam string) []string + // ErrorFunc is used to create errors. Allows you to use your own error type, that for example marshals to your specific json response + ErrorFunc func(sourceParam string, values []string, message interface{}, internalError error) error +} + +// QueryParamsBinder creates query parameter value binder +func QueryParamsBinder(c Context) *ValueBinder { + return &ValueBinder{ + failFast: true, + ValueFunc: func(sourceParam string) string { + return c.QueryParam(sourceParam) + }, + ValuesFunc: func(sourceParam string) []string { + values, ok := c.QueryParams()[sourceParam] + if !ok { + return nil + } + return values + }, + ErrorFunc: NewBindingError, + } +} + +// PathParamsBinder creates path parameter value binder +func PathParamsBinder(c Context) *ValueBinder { + return &ValueBinder{ + failFast: true, + ValueFunc: func(sourceParam string) string { + return c.Param(sourceParam) + }, + ValuesFunc: func(sourceParam string) []string { + // path parameter should not have multiple values so getting values does not make sense but lets not error out here + value := c.Param(sourceParam) + if value == "" { + return nil + } + return []string{value} + }, + ErrorFunc: NewBindingError, + } +} + +// FormFieldBinder creates form field value binder +// For all requests, FormFieldBinder parses the raw query from the URL and uses query params as form fields +// +// For POST, PUT, and PATCH requests, it also reads the request body, parses it +// as a form and uses query params as form fields. Request body parameters take precedence over URL query +// string values in r.Form. +// +// 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 +// See https://golang.org/pkg/net/http/#Request.ParseForm +func FormFieldBinder(c Context) *ValueBinder { + vb := &ValueBinder{ + failFast: true, + ValueFunc: func(sourceParam string) string { + return c.Request().FormValue(sourceParam) + }, + ErrorFunc: NewBindingError, + } + vb.ValuesFunc = func(sourceParam string) []string { + if c.Request().Form == nil { + // this is same as `Request().FormValue()` does internally + _ = c.Request().ParseMultipartForm(32 << 20) + } + values, ok := c.Request().Form[sourceParam] + if !ok { + return nil + } + return values + } + + return vb +} + +// 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 { + b.failFast = value + return b +} + +func (b *ValueBinder) setError(err error) { + if b.errors == nil { + b.errors = []error{err} + return + } + b.errors = append(b.errors, err) +} + +// BindError returns first seen bind error and resets/empties binder errors for further calls +func (b *ValueBinder) BindError() error { + if b.errors == nil { + return nil + } + err := b.errors[0] + b.errors = nil // reset errors so next chain will start from zero + return err +} + +// BindErrors returns all bind errors and resets/empties binder errors for further calls +func (b *ValueBinder) BindErrors() []error { + if b.errors == nil { + return nil + } + errors := b.errors + b.errors = nil // reset errors so next chain will start from zero + return errors +} + +// CustomFunc binds parameter values with Func. Func is called only when parameter values exist. +func (b *ValueBinder) CustomFunc(sourceParam string, customFunc func(values []string) []error) *ValueBinder { + return b.customFunc(sourceParam, customFunc, false) +} + +// MustCustomFunc requires parameter values to exist to be bind with Func. Returns error when value does not exist. +func (b *ValueBinder) MustCustomFunc(sourceParam string, customFunc func(values []string) []error) *ValueBinder { + return b.customFunc(sourceParam, customFunc, true) +} + +func (b *ValueBinder) customFunc(sourceParam string, customFunc func(values []string) []error, valueMustExist bool) *ValueBinder { + if b.failFast && b.errors != nil { + return b + } + + values := b.ValuesFunc(sourceParam) + if len(values) == 0 { + if valueMustExist { + b.setError(b.ErrorFunc(sourceParam, []string{}, "required field value is empty", nil)) + } + return b + } + if errs := customFunc(values); errs != nil { + b.errors = append(b.errors, errs...) + } + return b +} + +// String binds parameter to string variable +func (b *ValueBinder) String(sourceParam string, dest *string) *ValueBinder { + if b.failFast && b.errors != nil { + return b + } + + value := b.ValueFunc(sourceParam) + if value == "" { + return b + } + *dest = value + return b +} + +// MustString requires parameter value to exist to be bind to string variable. Returns error when value does not exist +func (b *ValueBinder) MustString(sourceParam string, dest *string) *ValueBinder { + if b.failFast && b.errors != nil { + return b + } + + value := b.ValueFunc(sourceParam) + if value == "" { + b.setError(b.ErrorFunc(sourceParam, []string{value}, "required field value is empty", nil)) + return b + } + *dest = value + return b +} + +// Strings binds parameter values to slice of string +func (b *ValueBinder) Strings(sourceParam string, dest *[]string) *ValueBinder { + if b.failFast && b.errors != nil { + return b + } + + value := b.ValuesFunc(sourceParam) + if value == nil { + return b + } + *dest = value + return b +} + +// MustStrings requires parameter values to exist to be bind to slice of string variables. Returns error when value does not exist +func (b *ValueBinder) MustStrings(sourceParam string, dest *[]string) *ValueBinder { + if b.failFast && b.errors != nil { + return b + } + + value := b.ValuesFunc(sourceParam) + if value == nil { + b.setError(b.ErrorFunc(sourceParam, []string{}, "required field value is empty", nil)) + return b + } + *dest = value + return b +} + +// BindUnmarshaler binds parameter to destination implementing BindUnmarshaler interface +func (b *ValueBinder) BindUnmarshaler(sourceParam string, dest BindUnmarshaler) *ValueBinder { + if b.failFast && b.errors != nil { + return b + } + + tmp := b.ValueFunc(sourceParam) + if tmp == "" { + return b + } + + if err := dest.UnmarshalParam(tmp); err != nil { + b.setError(b.ErrorFunc(sourceParam, []string{tmp}, "failed to bind field value to BindUnmarshaler interface", err)) + } + return b +} + +// MustBindUnmarshaler requires parameter value to exist to be bind to destination implementing BindUnmarshaler interface. +// Returns error when value does not exist +func (b *ValueBinder) MustBindUnmarshaler(sourceParam string, dest BindUnmarshaler) *ValueBinder { + if b.failFast && b.errors != nil { + return b + } + + value := b.ValueFunc(sourceParam) + if value == "" { + b.setError(b.ErrorFunc(sourceParam, []string{value}, "required field value is empty", nil)) + return b + } + + if err := dest.UnmarshalParam(value); err != nil { + b.setError(b.ErrorFunc(sourceParam, []string{value}, "failed to bind field value to BindUnmarshaler 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 { + return b.bindWithDelimiter(sourceParam, dest, delimiter, false) +} + +// MustBindWithDelimiter requires parameter value to exist to be bind destination by suitable conversion function. +// Delimiter is used before conversion to split parameter value to separate values +func (b *ValueBinder) MustBindWithDelimiter(sourceParam string, dest interface{}, delimiter string) *ValueBinder { + return b.bindWithDelimiter(sourceParam, dest, delimiter, true) +} + +func (b *ValueBinder) bindWithDelimiter(sourceParam string, dest interface{}, delimiter string, valueMustExist bool) *ValueBinder { + if b.failFast && b.errors != nil { + return b + } + values := b.ValuesFunc(sourceParam) + if len(values) == 0 { + if valueMustExist { + b.setError(b.ErrorFunc(sourceParam, []string{}, "required field value is empty", nil)) + } + return b + } + tmpValues := make([]string, 0, len(values)) + for _, v := range values { + tmpValues = append(tmpValues, strings.Split(v, delimiter)...) + } + + switch d := dest.(type) { + case *[]string: + *d = tmpValues + return b + case *[]bool: + return b.bools(sourceParam, tmpValues, d) + case *[]int64, *[]int32, *[]int16, *[]int8, *[]int: + return b.ints(sourceParam, tmpValues, d) + case *[]uint64, *[]uint32, *[]uint16, *[]uint8, *[]uint: // *[]byte is same as *[]uint8 + return b.uints(sourceParam, tmpValues, d) + case *[]float64, *[]float32: + return b.floats(sourceParam, tmpValues, d) + case *[]time.Duration: + return b.durations(sourceParam, tmpValues, d) + default: + // support only cases when destination is slice + // does not support time.Time as it needs argument (layout) for parsing or BindUnmarshaler + b.setError(b.ErrorFunc(sourceParam, []string{}, "unsupported bind type", nil)) + return b + } +} + +// Int64 binds parameter to int64 variable +func (b *ValueBinder) Int64(sourceParam string, dest *int64) *ValueBinder { + return b.intValue(sourceParam, dest, 64, false) +} + +// MustInt64 requires parameter value to exist to be bind to int64 variable. Returns error when value does not exist +func (b *ValueBinder) MustInt64(sourceParam string, dest *int64) *ValueBinder { + return b.intValue(sourceParam, dest, 64, true) +} + +// Int32 binds parameter to int32 variable +func (b *ValueBinder) Int32(sourceParam string, dest *int32) *ValueBinder { + return b.intValue(sourceParam, dest, 32, false) +} + +// MustInt32 requires parameter value to exist to be bind to int32 variable. Returns error when value does not exist +func (b *ValueBinder) MustInt32(sourceParam string, dest *int32) *ValueBinder { + return b.intValue(sourceParam, dest, 32, true) +} + +// Int16 binds parameter to int16 variable +func (b *ValueBinder) Int16(sourceParam string, dest *int16) *ValueBinder { + return b.intValue(sourceParam, dest, 16, false) +} + +// MustInt16 requires parameter value to exist to be bind to int16 variable. Returns error when value does not exist +func (b *ValueBinder) MustInt16(sourceParam string, dest *int16) *ValueBinder { + return b.intValue(sourceParam, dest, 16, true) +} + +// Int8 binds parameter to int8 variable +func (b *ValueBinder) Int8(sourceParam string, dest *int8) *ValueBinder { + return b.intValue(sourceParam, dest, 8, false) +} + +// MustInt8 requires parameter value to exist to be bind to int8 variable. Returns error when value does not exist +func (b *ValueBinder) MustInt8(sourceParam string, dest *int8) *ValueBinder { + return b.intValue(sourceParam, dest, 8, true) +} + +// Int binds parameter to int variable +func (b *ValueBinder) Int(sourceParam string, dest *int) *ValueBinder { + return b.intValue(sourceParam, dest, 0, false) +} + +// MustInt requires parameter value to exist to be bind to int variable. Returns error when value does not exist +func (b *ValueBinder) MustInt(sourceParam string, dest *int) *ValueBinder { + return b.intValue(sourceParam, dest, 0, true) +} + +func (b *ValueBinder) intValue(sourceParam string, dest interface{}, bitSize int, valueMustExist bool) *ValueBinder { + if b.failFast && b.errors != nil { + return b + } + + value := b.ValueFunc(sourceParam) + if value == "" { + if valueMustExist { + b.setError(b.ErrorFunc(sourceParam, []string{}, "required field value is empty", nil)) + } + return b + } + + return b.int(sourceParam, value, dest, bitSize) +} + +func (b *ValueBinder) int(sourceParam string, value string, dest interface{}, bitSize int) *ValueBinder { + n, err := strconv.ParseInt(value, 10, bitSize) + if err != nil { + if bitSize == 0 { + b.setError(b.ErrorFunc(sourceParam, []string{value}, "failed to bind field value to int", err)) + } else { + b.setError(b.ErrorFunc(sourceParam, []string{value}, fmt.Sprintf("failed to bind field value to int%v", bitSize), err)) + } + return b + } + + switch d := dest.(type) { + case *int64: + *d = n + case *int32: + *d = int32(n) + case *int16: + *d = int16(n) + case *int8: + *d = int8(n) + case *int: + *d = int(n) + } + return b +} + +func (b *ValueBinder) intsValue(sourceParam string, dest interface{}, valueMustExist bool) *ValueBinder { + if b.failFast && b.errors != nil { + return b + } + + values := b.ValuesFunc(sourceParam) + if len(values) == 0 { + if valueMustExist { + b.setError(b.ErrorFunc(sourceParam, values, "required field value is empty", nil)) + } + return b + } + return b.ints(sourceParam, values, dest) +} + +func (b *ValueBinder) ints(sourceParam string, values []string, dest interface{}) *ValueBinder { + switch d := dest.(type) { + case *[]int64: + tmp := make([]int64, len(values)) + for i, v := range values { + b.int(sourceParam, v, &tmp[i], 64) + if b.failFast && b.errors != nil { + return b + } + } + if b.errors == nil { + *d = tmp + } + case *[]int32: + tmp := make([]int32, len(values)) + for i, v := range values { + b.int(sourceParam, v, &tmp[i], 32) + if b.failFast && b.errors != nil { + return b + } + } + if b.errors == nil { + *d = tmp + } + case *[]int16: + tmp := make([]int16, len(values)) + for i, v := range values { + b.int(sourceParam, v, &tmp[i], 16) + if b.failFast && b.errors != nil { + return b + } + } + if b.errors == nil { + *d = tmp + } + case *[]int8: + tmp := make([]int8, len(values)) + for i, v := range values { + b.int(sourceParam, v, &tmp[i], 8) + if b.failFast && b.errors != nil { + return b + } + } + if b.errors == nil { + *d = tmp + } + case *[]int: + tmp := make([]int, len(values)) + for i, v := range values { + b.int(sourceParam, v, &tmp[i], 0) + if b.failFast && b.errors != nil { + return b + } + } + if b.errors == nil { + *d = tmp + } + } + return b +} + +// Int64s binds parameter to slice of int64 +func (b *ValueBinder) Int64s(sourceParam string, dest *[]int64) *ValueBinder { + return b.intsValue(sourceParam, dest, false) +} + +// MustInt64s requires parameter value to exist to be bind to int64 slice variable. Returns error when value does not exist +func (b *ValueBinder) MustInt64s(sourceParam string, dest *[]int64) *ValueBinder { + return b.intsValue(sourceParam, dest, true) +} + +// Int32s binds parameter to slice of int32 +func (b *ValueBinder) Int32s(sourceParam string, dest *[]int32) *ValueBinder { + return b.intsValue(sourceParam, dest, false) +} + +// MustInt32s requires parameter value to exist to be bind to int32 slice variable. Returns error when value does not exist +func (b *ValueBinder) MustInt32s(sourceParam string, dest *[]int32) *ValueBinder { + return b.intsValue(sourceParam, dest, true) +} + +// Int16s binds parameter to slice of int16 +func (b *ValueBinder) Int16s(sourceParam string, dest *[]int16) *ValueBinder { + return b.intsValue(sourceParam, dest, false) +} + +// MustInt16s requires parameter value to exist to be bind to int16 slice variable. Returns error when value does not exist +func (b *ValueBinder) MustInt16s(sourceParam string, dest *[]int16) *ValueBinder { + return b.intsValue(sourceParam, dest, true) +} + +// Int8s binds parameter to slice of int8 +func (b *ValueBinder) Int8s(sourceParam string, dest *[]int8) *ValueBinder { + return b.intsValue(sourceParam, dest, false) +} + +// MustInt8s requires parameter value to exist to be bind to int8 slice variable. Returns error when value does not exist +func (b *ValueBinder) MustInt8s(sourceParam string, dest *[]int8) *ValueBinder { + return b.intsValue(sourceParam, dest, true) +} + +// Ints binds parameter to slice of int +func (b *ValueBinder) Ints(sourceParam string, dest *[]int) *ValueBinder { + return b.intsValue(sourceParam, dest, false) +} + +// MustInts requires parameter value to exist to be bind to int slice variable. Returns error when value does not exist +func (b *ValueBinder) MustInts(sourceParam string, dest *[]int) *ValueBinder { + return b.intsValue(sourceParam, dest, true) +} + +// Uint64 binds parameter to uint64 variable +func (b *ValueBinder) Uint64(sourceParam string, dest *uint64) *ValueBinder { + return b.uintValue(sourceParam, dest, 64, false) +} + +// MustUint64 requires parameter value to exist to be bind to uint64 variable. Returns error when value does not exist +func (b *ValueBinder) MustUint64(sourceParam string, dest *uint64) *ValueBinder { + return b.uintValue(sourceParam, dest, 64, true) +} + +// Uint32 binds parameter to uint32 variable +func (b *ValueBinder) Uint32(sourceParam string, dest *uint32) *ValueBinder { + return b.uintValue(sourceParam, dest, 32, false) +} + +// MustUint32 requires parameter value to exist to be bind to uint32 variable. Returns error when value does not exist +func (b *ValueBinder) MustUint32(sourceParam string, dest *uint32) *ValueBinder { + return b.uintValue(sourceParam, dest, 32, true) +} + +// Uint16 binds parameter to uint16 variable +func (b *ValueBinder) Uint16(sourceParam string, dest *uint16) *ValueBinder { + return b.uintValue(sourceParam, dest, 16, false) +} + +// MustUint16 requires parameter value to exist to be bind to uint16 variable. Returns error when value does not exist +func (b *ValueBinder) MustUint16(sourceParam string, dest *uint16) *ValueBinder { + return b.uintValue(sourceParam, dest, 16, true) +} + +// Uint8 binds parameter to uint8 variable +func (b *ValueBinder) Uint8(sourceParam string, dest *uint8) *ValueBinder { + return b.uintValue(sourceParam, dest, 8, false) +} + +// MustUint8 requires parameter value to exist to be bind to uint8 variable. Returns error when value does not exist +func (b *ValueBinder) MustUint8(sourceParam string, dest *uint8) *ValueBinder { + return b.uintValue(sourceParam, dest, 8, true) +} + +// Byte binds parameter to byte variable +func (b *ValueBinder) Byte(sourceParam string, dest *byte) *ValueBinder { + return b.uintValue(sourceParam, dest, 8, false) +} + +// MustByte requires parameter value to exist to be bind to byte variable. Returns error when value does not exist +func (b *ValueBinder) MustByte(sourceParam string, dest *byte) *ValueBinder { + return b.uintValue(sourceParam, dest, 8, true) +} + +// Uint binds parameter to uint variable +func (b *ValueBinder) Uint(sourceParam string, dest *uint) *ValueBinder { + return b.uintValue(sourceParam, dest, 0, false) +} + +// MustUint requires parameter value to exist to be bind to uint variable. Returns error when value does not exist +func (b *ValueBinder) MustUint(sourceParam string, dest *uint) *ValueBinder { + return b.uintValue(sourceParam, dest, 0, true) +} + +func (b *ValueBinder) uintValue(sourceParam string, dest interface{}, bitSize int, valueMustExist bool) *ValueBinder { + if b.failFast && b.errors != nil { + return b + } + + value := b.ValueFunc(sourceParam) + if value == "" { + if valueMustExist { + b.setError(b.ErrorFunc(sourceParam, []string{}, "required field value is empty", nil)) + } + return b + } + + return b.uint(sourceParam, value, dest, bitSize) +} + +func (b *ValueBinder) uint(sourceParam string, value string, dest interface{}, bitSize int) *ValueBinder { + n, err := strconv.ParseUint(value, 10, bitSize) + if err != nil { + if bitSize == 0 { + b.setError(b.ErrorFunc(sourceParam, []string{value}, "failed to bind field value to uint", err)) + } else { + b.setError(b.ErrorFunc(sourceParam, []string{value}, fmt.Sprintf("failed to bind field value to uint%v", bitSize), err)) + } + return b + } + + switch d := dest.(type) { + case *uint64: + *d = n + case *uint32: + *d = uint32(n) + case *uint16: + *d = uint16(n) + case *uint8: // byte is alias to uint8 + *d = uint8(n) + case *uint: + *d = uint(n) + } + return b +} + +func (b *ValueBinder) uintsValue(sourceParam string, dest interface{}, valueMustExist bool) *ValueBinder { + if b.failFast && b.errors != nil { + return b + } + + values := b.ValuesFunc(sourceParam) + if len(values) == 0 { + if valueMustExist { + b.setError(b.ErrorFunc(sourceParam, values, "required field value is empty", nil)) + } + return b + } + return b.uints(sourceParam, values, dest) +} + +func (b *ValueBinder) uints(sourceParam string, values []string, dest interface{}) *ValueBinder { + switch d := dest.(type) { + case *[]uint64: + tmp := make([]uint64, len(values)) + for i, v := range values { + b.uint(sourceParam, v, &tmp[i], 64) + if b.failFast && b.errors != nil { + return b + } + } + if b.errors == nil { + *d = tmp + } + case *[]uint32: + tmp := make([]uint32, len(values)) + for i, v := range values { + b.uint(sourceParam, v, &tmp[i], 32) + if b.failFast && b.errors != nil { + return b + } + } + if b.errors == nil { + *d = tmp + } + case *[]uint16: + tmp := make([]uint16, len(values)) + for i, v := range values { + b.uint(sourceParam, v, &tmp[i], 16) + if b.failFast && b.errors != nil { + return b + } + } + if b.errors == nil { + *d = tmp + } + case *[]uint8: // byte is alias to uint8 + tmp := make([]uint8, len(values)) + for i, v := range values { + b.uint(sourceParam, v, &tmp[i], 8) + if b.failFast && b.errors != nil { + return b + } + } + if b.errors == nil { + *d = tmp + } + case *[]uint: + tmp := make([]uint, len(values)) + for i, v := range values { + b.uint(sourceParam, v, &tmp[i], 0) + if b.failFast && b.errors != nil { + return b + } + } + if b.errors == nil { + *d = tmp + } + } + return b +} + +// Uint64s binds parameter to slice of uint64 +func (b *ValueBinder) Uint64s(sourceParam string, dest *[]uint64) *ValueBinder { + return b.uintsValue(sourceParam, dest, false) +} + +// MustUint64s requires parameter value to exist to be bind to uint64 slice variable. Returns error when value does not exist +func (b *ValueBinder) MustUint64s(sourceParam string, dest *[]uint64) *ValueBinder { + return b.uintsValue(sourceParam, dest, true) +} + +// Uint32s binds parameter to slice of uint32 +func (b *ValueBinder) Uint32s(sourceParam string, dest *[]uint32) *ValueBinder { + return b.uintsValue(sourceParam, dest, false) +} + +// MustUint32s requires parameter value to exist to be bind to uint32 slice variable. Returns error when value does not exist +func (b *ValueBinder) MustUint32s(sourceParam string, dest *[]uint32) *ValueBinder { + return b.uintsValue(sourceParam, dest, true) +} + +// Uint16s binds parameter to slice of uint16 +func (b *ValueBinder) Uint16s(sourceParam string, dest *[]uint16) *ValueBinder { + return b.uintsValue(sourceParam, dest, false) +} + +// MustUint16s requires parameter value to exist to be bind to uint16 slice variable. Returns error when value does not exist +func (b *ValueBinder) MustUint16s(sourceParam string, dest *[]uint16) *ValueBinder { + return b.uintsValue(sourceParam, dest, true) +} + +// Uint8s binds parameter to slice of uint8 +func (b *ValueBinder) Uint8s(sourceParam string, dest *[]uint8) *ValueBinder { + return b.uintsValue(sourceParam, dest, false) +} + +// MustUint8s requires parameter value to exist to be bind to uint8 slice variable. Returns error when value does not exist +func (b *ValueBinder) MustUint8s(sourceParam string, dest *[]uint8) *ValueBinder { + return b.uintsValue(sourceParam, dest, true) +} + +// Uints binds parameter to slice of uint +func (b *ValueBinder) Uints(sourceParam string, dest *[]uint) *ValueBinder { + return b.uintsValue(sourceParam, dest, false) +} + +// MustUints requires parameter value to exist to be bind to uint slice variable. Returns error when value does not exist +func (b *ValueBinder) MustUints(sourceParam string, dest *[]uint) *ValueBinder { + return b.uintsValue(sourceParam, dest, true) +} + +// Bool binds parameter to bool variable +func (b *ValueBinder) Bool(sourceParam string, dest *bool) *ValueBinder { + return b.boolValue(sourceParam, dest, false) +} + +// MustBool requires parameter value to exist to be bind to bool variable. Returns error when value does not exist +func (b *ValueBinder) MustBool(sourceParam string, dest *bool) *ValueBinder { + return b.boolValue(sourceParam, dest, true) +} + +func (b *ValueBinder) boolValue(sourceParam string, dest *bool, valueMustExist bool) *ValueBinder { + if b.failFast && b.errors != nil { + return b + } + + value := b.ValueFunc(sourceParam) + if value == "" { + if valueMustExist { + b.setError(b.ErrorFunc(sourceParam, []string{}, "required field value is empty", nil)) + } + return b + } + return b.bool(sourceParam, value, dest) +} + +func (b *ValueBinder) bool(sourceParam string, value string, dest *bool) *ValueBinder { + n, err := strconv.ParseBool(value) + if err != nil { + b.setError(b.ErrorFunc(sourceParam, []string{value}, "failed to bind field value to bool", err)) + return b + } + + *dest = n + return b +} + +func (b *ValueBinder) boolsValue(sourceParam string, dest *[]bool, valueMustExist bool) *ValueBinder { + if b.failFast && b.errors != nil { + return b + } + + values := b.ValuesFunc(sourceParam) + if len(values) == 0 { + if valueMustExist { + b.setError(b.ErrorFunc(sourceParam, []string{}, "required field value is empty", nil)) + } + return b + } + return b.bools(sourceParam, values, dest) +} + +func (b *ValueBinder) bools(sourceParam string, values []string, dest *[]bool) *ValueBinder { + tmp := make([]bool, len(values)) + for i, v := range values { + b.bool(sourceParam, v, &tmp[i]) + if b.failFast && b.errors != nil { + return b + } + } + if b.errors == nil { + *dest = tmp + } + return b +} + +// Bools binds parameter values to slice of bool variables +func (b *ValueBinder) Bools(sourceParam string, dest *[]bool) *ValueBinder { + return b.boolsValue(sourceParam, dest, false) +} + +// MustBools requires parameter values to exist to be bind to slice of bool variables. Returns error when values does not exist +func (b *ValueBinder) MustBools(sourceParam string, dest *[]bool) *ValueBinder { + return b.boolsValue(sourceParam, dest, true) +} + +// Float64 binds parameter to float64 variable +func (b *ValueBinder) Float64(sourceParam string, dest *float64) *ValueBinder { + return b.floatValue(sourceParam, dest, 64, false) +} + +// MustFloat64 requires parameter value to exist to be bind to float64 variable. Returns error when value does not exist +func (b *ValueBinder) MustFloat64(sourceParam string, dest *float64) *ValueBinder { + return b.floatValue(sourceParam, dest, 64, true) +} + +// Float32 binds parameter to float32 variable +func (b *ValueBinder) Float32(sourceParam string, dest *float32) *ValueBinder { + return b.floatValue(sourceParam, dest, 32, false) +} + +// MustFloat32 requires parameter value to exist to be bind to float32 variable. Returns error when value does not exist +func (b *ValueBinder) MustFloat32(sourceParam string, dest *float32) *ValueBinder { + return b.floatValue(sourceParam, dest, 32, true) +} + +func (b *ValueBinder) floatValue(sourceParam string, dest interface{}, bitSize int, valueMustExist bool) *ValueBinder { + if b.failFast && b.errors != nil { + return b + } + + value := b.ValueFunc(sourceParam) + if value == "" { + if valueMustExist { + b.setError(b.ErrorFunc(sourceParam, []string{}, "required field value is empty", nil)) + } + return b + } + + return b.float(sourceParam, value, dest, bitSize) +} + +func (b *ValueBinder) float(sourceParam string, value string, dest interface{}, bitSize int) *ValueBinder { + n, err := strconv.ParseFloat(value, bitSize) + if err != nil { + b.setError(b.ErrorFunc(sourceParam, []string{value}, fmt.Sprintf("failed to bind field value to float%v", bitSize), err)) + return b + } + + switch d := dest.(type) { + case *float64: + *d = n + case *float32: + *d = float32(n) + } + return b +} + +func (b *ValueBinder) floatsValue(sourceParam string, dest interface{}, valueMustExist bool) *ValueBinder { + if b.failFast && b.errors != nil { + return b + } + + values := b.ValuesFunc(sourceParam) + if len(values) == 0 { + if valueMustExist { + b.setError(b.ErrorFunc(sourceParam, []string{}, "required field value is empty", nil)) + } + return b + } + return b.floats(sourceParam, values, dest) +} + +func (b *ValueBinder) floats(sourceParam string, values []string, dest interface{}) *ValueBinder { + switch d := dest.(type) { + case *[]float64: + tmp := make([]float64, len(values)) + for i, v := range values { + b.float(sourceParam, v, &tmp[i], 64) + if b.failFast && b.errors != nil { + return b + } + } + if b.errors == nil { + *d = tmp + } + case *[]float32: + tmp := make([]float32, len(values)) + for i, v := range values { + b.float(sourceParam, v, &tmp[i], 32) + if b.failFast && b.errors != nil { + return b + } + } + if b.errors == nil { + *d = tmp + } + } + return b +} + +// Float64s binds parameter values to slice of float64 variables +func (b *ValueBinder) Float64s(sourceParam string, dest *[]float64) *ValueBinder { + return b.floatsValue(sourceParam, dest, false) +} + +// MustFloat64s requires parameter values to exist to be bind to slice of float64 variables. Returns error when values does not exist +func (b *ValueBinder) MustFloat64s(sourceParam string, dest *[]float64) *ValueBinder { + return b.floatsValue(sourceParam, dest, true) +} + +// Float32s binds parameter values to slice of float32 variables +func (b *ValueBinder) Float32s(sourceParam string, dest *[]float32) *ValueBinder { + return b.floatsValue(sourceParam, dest, false) +} + +// MustFloat32s requires parameter values to exist to be bind to slice of float32 variables. Returns error when values does not exist +func (b *ValueBinder) MustFloat32s(sourceParam string, dest *[]float32) *ValueBinder { + return b.floatsValue(sourceParam, dest, true) +} + +// Time binds parameter to time.Time variable +func (b *ValueBinder) Time(sourceParam string, dest *time.Time, layout string) *ValueBinder { + return b.time(sourceParam, dest, layout, false) +} + +// MustTime requires parameter value to exist to be bind to time.Time variable. Returns error when value does not exist +func (b *ValueBinder) MustTime(sourceParam string, dest *time.Time, layout string) *ValueBinder { + return b.time(sourceParam, dest, layout, true) +} + +func (b *ValueBinder) time(sourceParam string, dest *time.Time, layout string, valueMustExist bool) *ValueBinder { + if b.failFast && b.errors != nil { + return b + } + + value := b.ValueFunc(sourceParam) + if value == "" { + if valueMustExist { + b.setError(b.ErrorFunc(sourceParam, []string{value}, "required field value is empty", nil)) + } + return b + } + t, err := time.Parse(layout, value) + if err != nil { + b.setError(b.ErrorFunc(sourceParam, []string{value}, "failed to bind field value to Time", err)) + return b + } + *dest = t + return b +} + +// Times binds parameter values to slice of time.Time variables +func (b *ValueBinder) Times(sourceParam string, dest *[]time.Time, layout string) *ValueBinder { + return b.times(sourceParam, dest, layout, false) +} + +// MustTimes requires parameter values to exist to be bind to slice of time.Time variables. Returns error when values does not exist +func (b *ValueBinder) MustTimes(sourceParam string, dest *[]time.Time, layout string) *ValueBinder { + return b.times(sourceParam, dest, layout, true) +} + +func (b *ValueBinder) times(sourceParam string, dest *[]time.Time, layout string, valueMustExist bool) *ValueBinder { + if b.failFast && b.errors != nil { + return b + } + + values := b.ValuesFunc(sourceParam) + if len(values) == 0 { + if valueMustExist { + b.setError(b.ErrorFunc(sourceParam, []string{}, "required field value is empty", nil)) + } + return b + } + + tmp := make([]time.Time, len(values)) + for i, v := range values { + t, err := time.Parse(layout, v) + if err != nil { + b.setError(b.ErrorFunc(sourceParam, []string{v}, "failed to bind field value to Time", err)) + if b.failFast { + return b + } + continue + } + tmp[i] = t + } + if b.errors == nil { + *dest = tmp + } + return b +} + +// Duration binds parameter to time.Duration variable +func (b *ValueBinder) Duration(sourceParam string, dest *time.Duration) *ValueBinder { + return b.duration(sourceParam, dest, false) +} + +// MustDuration requires parameter value to exist to be bind to time.Duration variable. Returns error when value does not exist +func (b *ValueBinder) MustDuration(sourceParam string, dest *time.Duration) *ValueBinder { + return b.duration(sourceParam, dest, true) +} + +func (b *ValueBinder) duration(sourceParam string, dest *time.Duration, valueMustExist bool) *ValueBinder { + if b.failFast && b.errors != nil { + return b + } + + value := b.ValueFunc(sourceParam) + if value == "" { + if valueMustExist { + b.setError(b.ErrorFunc(sourceParam, []string{value}, "required field value is empty", nil)) + } + return b + } + t, err := time.ParseDuration(value) + if err != nil { + b.setError(b.ErrorFunc(sourceParam, []string{value}, "failed to bind field value to Duration", err)) + return b + } + *dest = t + return b +} + +// Durations binds parameter values to slice of time.Duration variables +func (b *ValueBinder) Durations(sourceParam string, dest *[]time.Duration) *ValueBinder { + return b.durationsValue(sourceParam, dest, false) +} + +// MustDurations requires parameter values to exist to be bind to slice of time.Duration variables. Returns error when values does not exist +func (b *ValueBinder) MustDurations(sourceParam string, dest *[]time.Duration) *ValueBinder { + return b.durationsValue(sourceParam, dest, true) +} + +func (b *ValueBinder) durationsValue(sourceParam string, dest *[]time.Duration, valueMustExist bool) *ValueBinder { + if b.failFast && b.errors != nil { + return b + } + + values := b.ValuesFunc(sourceParam) + if len(values) == 0 { + if valueMustExist { + b.setError(b.ErrorFunc(sourceParam, []string{}, "required field value is empty", nil)) + } + return b + } + return b.durations(sourceParam, values, dest) +} + +func (b *ValueBinder) durations(sourceParam string, values []string, dest *[]time.Duration) *ValueBinder { + tmp := make([]time.Duration, len(values)) + for i, v := range values { + t, err := time.ParseDuration(v) + if err != nil { + b.setError(b.ErrorFunc(sourceParam, []string{v}, "failed to bind field value to Duration", err)) + if b.failFast { + return b + } + continue + } + tmp[i] = t + } + if b.errors == nil { + *dest = tmp + } + return b +} + +// UnixTime binds parameter to time.Time variable (in local Time corresponding to the given Unix time). +// +// 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 +func (b *ValueBinder) UnixTime(sourceParam string, dest *time.Time) *ValueBinder { + return b.unixTime(sourceParam, dest, false, false) +} + +// MustUnixTime requires parameter value to exist to be bind to time.Duration variable (in local Time corresponding +// to the given Unix time). Returns error when value does not exist. +// +// 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 +func (b *ValueBinder) MustUnixTime(sourceParam string, dest *time.Time) *ValueBinder { + return b.unixTime(sourceParam, dest, true, false) +} + +// UnixTimeNano binds parameter to time.Time variable (in local Time corresponding to the given Unix time in nano second 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 +// 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. +func (b *ValueBinder) UnixTimeNano(sourceParam string, dest *time.Time) *ValueBinder { + return b.unixTime(sourceParam, dest, false, true) +} + +// MustUnixTimeNano requires parameter value to exist to be bind to time.Duration variable (in local Time corresponding +// to the given Unix time value in nano second precision). Returns error when value does not exist. +// +// Example: 1609180603123456789 binds to 2020-12-28T18:36:43.123456789+00:00 +// Example: 1000000000 binds to 1970-01-01T00:00:01.000000000+00:00 +// 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. +func (b *ValueBinder) MustUnixTimeNano(sourceParam string, dest *time.Time) *ValueBinder { + return b.unixTime(sourceParam, dest, true, true) +} + +func (b *ValueBinder) unixTime(sourceParam string, dest *time.Time, valueMustExist bool, isNano bool) *ValueBinder { + if b.failFast && b.errors != nil { + return b + } + + value := b.ValueFunc(sourceParam) + if value == "" { + if valueMustExist { + b.setError(b.ErrorFunc(sourceParam, []string{value}, "required field value is empty", nil)) + } + return b + } + + n, err := strconv.ParseInt(value, 10, 64) + if err != nil { + b.setError(b.ErrorFunc(sourceParam, []string{value}, "failed to bind field value to Time", err)) + return b + } + + if isNano { + *dest = time.Unix(0, n) + } else { + *dest = time.Unix(n, 0) + } + return b +} diff --git a/binder_external_test.go b/binder_external_test.go new file mode 100644 index 00000000..f1aecb52 --- /dev/null +++ b/binder_external_test.go @@ -0,0 +1,130 @@ +// run tests as external package to get real feel for API +package echo_test + +import ( + "encoding/base64" + "fmt" + "github.com/labstack/echo/v4" + "log" + "net/http" + "net/http/httptest" +) + +func ExampleValueBinder_BindErrors() { + // example route function that binds query params to different destinations and returns all bind errors in one go + routeFunc := func(c echo.Context) error { + var opts struct { + Active bool + IDs []int64 + } + length := int64(50) // default length is 50 + + b := echo.QueryParamsBinder(c) + + errs := b.Int64("length", &length). + Int64s("ids", &opts.IDs). + Bool("active", &opts.Active). + BindErrors() // returns all errors + if errs != nil { + for _, err := range errs { + bErr := err.(*echo.BindingError) + log.Printf("in case you want to access what field: %s values: %v failed", bErr.Field, bErr.Values) + } + return fmt.Errorf("%v fields failed to bind", len(errs)) + } + fmt.Printf("active = %v, length = %v, ids = %v", opts.Active, length, opts.IDs) + + return c.JSON(http.StatusOK, opts) + } + + e := echo.New() + c := e.NewContext( + httptest.NewRequest(http.MethodGet, "/api/endpoint?active=true&length=25&ids=1&ids=2&ids=3", nil), + httptest.NewRecorder(), + ) + + _ = routeFunc(c) + + // Output: active = true, length = 25, ids = [1 2 3] +} + +func ExampleValueBinder_BindError() { + // example route function that binds query params to different destinations and stops binding on first bind error + failFastRouteFunc := func(c echo.Context) error { + var opts struct { + Active bool + IDs []int64 + } + length := int64(50) // default length is 50 + + // create binder that stops binding at first error + b := echo.QueryParamsBinder(c) + + err := b.Int64("length", &length). + Int64s("ids", &opts.IDs). + Bool("active", &opts.Active). + BindError() // returns first binding error + if err != nil { + bErr := err.(*echo.BindingError) + return fmt.Errorf("my own custom error for field: %s values: %v", bErr.Field, bErr.Values) + } + fmt.Printf("active = %v, length = %v, ids = %v\n", opts.Active, length, opts.IDs) + + return c.JSON(http.StatusOK, opts) + } + + e := echo.New() + c := e.NewContext( + httptest.NewRequest(http.MethodGet, "/api/endpoint?active=true&length=25&ids=1&ids=2&ids=3", nil), + httptest.NewRecorder(), + ) + + _ = failFastRouteFunc(c) + + // Output: active = true, length = 25, ids = [1 2 3] +} + +func ExampleValueBinder_CustomFunc() { + // example route function that binds query params using custom function closure + routeFunc := func(c echo.Context) error { + length := int64(50) // default length is 50 + var binary []byte + + b := echo.QueryParamsBinder(c) + errs := b.Int64("length", &length). + CustomFunc("base64", func(values []string) []error { + if len(values) == 0 { + return nil + } + decoded, err := base64.URLEncoding.DecodeString(values[0]) + if err != nil { + // in this example we use only first param value but url could contain multiple params in reality and + // therefore in theory produce multiple binding errors + return []error{echo.NewBindingError("base64", values[0:1], "failed to decode base64", err)} + } + binary = decoded + return nil + }). + BindErrors() // returns all errors + + if errs != nil { + for _, err := range errs { + bErr := err.(*echo.BindingError) + log.Printf("in case you want to access what field: %s values: %v failed", bErr.Field, bErr.Values) + } + return fmt.Errorf("%v fields failed to bind", len(errs)) + } + fmt.Printf("length = %v, base64 = %s", length, binary) + + return c.JSON(http.StatusOK, "ok") + } + + e := echo.New() + c := e.NewContext( + httptest.NewRequest(http.MethodGet, "/api/endpoint?length=25&base64=SGVsbG8gV29ybGQ%3D", nil), + httptest.NewRecorder(), + ) + _ = routeFunc(c) + + // Output: length = 25, base64 = Hello World +} diff --git a/binder_go1.15_test.go b/binder_go1.15_test.go new file mode 100644 index 00000000..018628c3 --- /dev/null +++ b/binder_go1.15_test.go @@ -0,0 +1,265 @@ +// +build go1.15 + +package echo + +/** + Since version 1.15 time.Time and time.Duration error message pattern has changed (values are wrapped now in \"\") + So pre 1.15 these tests fail with similar error: + + expected: "code=400, message=failed to bind field value to Duration, internal=time: invalid duration \"nope\", field=param" + actual : "code=400, message=failed to bind field value to Duration, internal=time: invalid duration nope, field=param" +*/ + +import ( + "errors" + "github.com/stretchr/testify/assert" + "io" + "net/http" + "net/http/httptest" + "testing" + "time" +) + +func createTestContext15(URL string, body io.Reader, pathParams map[string]string) Context { + e := New() + req := httptest.NewRequest(http.MethodGet, URL, body) + if body != nil { + req.Header.Set(HeaderContentType, MIMEApplicationJSON) + } + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + if len(pathParams) > 0 { + names := make([]string, 0) + values := make([]string, 0) + for name, value := range pathParams { + names = append(names, name) + values = append(values, value) + } + c.SetParamNames(names...) + c.SetParamValues(values...) + } + + return c +} + +func TestValueBinder_TimeError(t *testing.T) { + var testCases = []struct { + name string + givenFailFast bool + givenBindErrors []error + whenURL string + whenMust bool + whenLayout string + expectValue time.Time + expectError string + }{ + { + name: "nok, conversion fails, value is not changed", + whenURL: "/search?param=nope¶m=100", + expectValue: time.Time{}, + expectError: "code=400, message=failed to bind field value to Time, internal=parsing time \"nope\": extra text: \"nope\", field=param", + }, + { + name: "nok (must), conversion fails, value is not changed", + whenMust: true, + whenURL: "/search?param=nope¶m=100", + expectValue: time.Time{}, + expectError: "code=400, message=failed to bind field value to Time, internal=parsing time \"nope\": extra text: \"nope\", field=param", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + c := createTestContext15(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.MustTime("param", &dest, tc.whenLayout).BindError() + } else { + err = b.Time("param", &dest, tc.whenLayout).BindError() + } + + assert.Equal(t, tc.expectValue, dest) + if tc.expectError != "" { + assert.EqualError(t, err, tc.expectError) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestValueBinder_TimesError(t *testing.T) { + var testCases = []struct { + name string + givenFailFast bool + givenBindErrors []error + whenURL string + whenMust bool + whenLayout string + expectValue []time.Time + expectError string + }{ + { + name: "nok, fail fast without binding value", + givenFailFast: true, + whenURL: "/search?param=1¶m=100", + expectValue: []time.Time(nil), + expectError: "code=400, message=failed to bind field value to Time, internal=parsing time \"1\" as \"2006-01-02T15:04:05Z07:00\": cannot parse \"1\" as \"2006\", field=param", + }, + { + name: "nok, conversion fails, value is not changed", + whenURL: "/search?param=nope¶m=100", + expectValue: []time.Time(nil), + expectError: "code=400, message=failed to bind field value to Time, internal=parsing time \"nope\" as \"2006-01-02T15:04:05Z07:00\": cannot parse \"nope\" as \"2006\", field=param", + }, + { + name: "nok (must), conversion fails, value is not changed", + whenMust: true, + whenURL: "/search?param=nope¶m=100", + expectValue: []time.Time(nil), + expectError: "code=400, message=failed to bind field value to Time, internal=parsing time \"nope\" as \"2006-01-02T15:04:05Z07:00\": cannot parse \"nope\" as \"2006\", field=param", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + c := createTestContext15(tc.whenURL, nil, nil) + b := QueryParamsBinder(c).FailFast(tc.givenFailFast) + b.errors = tc.givenBindErrors + + layout := time.RFC3339 + if tc.whenLayout != "" { + layout = tc.whenLayout + } + + var dest []time.Time + var err error + if tc.whenMust { + err = b.MustTimes("param", &dest, layout).BindError() + } else { + err = b.Times("param", &dest, layout).BindError() + } + + assert.Equal(t, tc.expectValue, dest) + if tc.expectError != "" { + assert.EqualError(t, err, tc.expectError) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestValueBinder_DurationError(t *testing.T) { + var testCases = []struct { + name string + givenFailFast bool + givenBindErrors []error + whenURL string + whenMust bool + expectValue time.Duration + expectError string + }{ + { + name: "nok, conversion fails, value is not changed", + whenURL: "/search?param=nope¶m=100", + expectValue: 0, + expectError: "code=400, message=failed to bind field value to Duration, internal=time: invalid duration \"nope\", field=param", + }, + { + name: "nok (must), conversion fails, value is not changed", + whenMust: true, + whenURL: "/search?param=nope¶m=100", + expectValue: 0, + expectError: "code=400, message=failed to bind field value to Duration, internal=time: invalid duration \"nope\", field=param", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + c := createTestContext15(tc.whenURL, nil, nil) + b := QueryParamsBinder(c).FailFast(tc.givenFailFast) + if tc.givenFailFast { + b.errors = []error{errors.New("previous error")} + } + + var dest time.Duration + var err error + if tc.whenMust { + err = b.MustDuration("param", &dest).BindError() + } else { + err = b.Duration("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_DurationsError(t *testing.T) { + var testCases = []struct { + name string + givenFailFast bool + givenBindErrors []error + whenURL string + whenMust bool + expectValue []time.Duration + expectError string + }{ + { + name: "nok, fail fast without binding value", + givenFailFast: true, + whenURL: "/search?param=1¶m=100", + expectValue: []time.Duration(nil), + expectError: "code=400, message=failed to bind field value to Duration, internal=time: missing unit in duration \"1\", field=param", + }, + { + name: "nok, conversion fails, value is not changed", + whenURL: "/search?param=nope¶m=100", + expectValue: []time.Duration(nil), + expectError: "code=400, message=failed to bind field value to Duration, internal=time: invalid duration \"nope\", field=param", + }, + { + name: "nok (must), conversion fails, value is not changed", + whenMust: true, + whenURL: "/search?param=nope¶m=100", + expectValue: []time.Duration(nil), + expectError: "code=400, message=failed to bind field value to Duration, internal=time: invalid duration \"nope\", field=param", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + c := createTestContext15(tc.whenURL, nil, nil) + b := QueryParamsBinder(c).FailFast(tc.givenFailFast) + b.errors = tc.givenBindErrors + + var dest []time.Duration + var err error + if tc.whenMust { + err = b.MustDurations("param", &dest).BindError() + } else { + err = b.Durations("param", &dest).BindError() + } + + assert.Equal(t, tc.expectValue, dest) + if tc.expectError != "" { + assert.EqualError(t, err, tc.expectError) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/binder_test.go b/binder_test.go new file mode 100644 index 00000000..946906a9 --- /dev/null +++ b/binder_test.go @@ -0,0 +1,2757 @@ +// run tests as external package to get real feel for API +package echo + +import ( + "encoding/json" + "errors" + "fmt" + "github.com/stretchr/testify/assert" + "io" + "net/http" + "net/http/httptest" + "strconv" + "strings" + "testing" + "time" +) + +func createTestContext(URL string, body io.Reader, pathParams map[string]string) Context { + e := New() + req := httptest.NewRequest(http.MethodGet, URL, body) + if body != nil { + req.Header.Set(HeaderContentType, MIMEApplicationJSON) + } + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + if len(pathParams) > 0 { + names := make([]string, 0) + values := make([]string, 0) + for name, value := range pathParams { + names = append(names, name) + values = append(values, value) + } + c.SetParamNames(names...) + c.SetParamValues(values...) + } + + return c +} + +func TestBindingError_Error(t *testing.T) { + err := NewBindingError("id", []string{"1", "nope"}, "bind failed", errors.New("internal error")) + assert.EqualError(t, err, `code=400, message=bind failed, internal=internal error, field=id`) + + bErr := err.(*BindingError) + assert.Equal(t, 400, bErr.Code) + assert.Equal(t, "bind failed", bErr.Message) + assert.Equal(t, errors.New("internal error"), bErr.Internal) + + assert.Equal(t, "id", bErr.Field) + assert.Equal(t, []string{"1", "nope"}, bErr.Values) +} + +func TestBindingError_ErrorJSON(t *testing.T) { + err := NewBindingError("id", []string{"1", "nope"}, "bind failed", errors.New("internal error")) + + resp, err := json.Marshal(err) + + assert.Equal(t, `{"field":"id","message":"bind failed"}`, string(resp)) +} + +func TestPathParamsBinder(t *testing.T) { + c := createTestContext("/api/user/999", nil, map[string]string{ + "id": "1", + "nr": "2", + "slice": "3", + }) + b := PathParamsBinder(c) + + id := int64(99) + nr := int64(88) + var slice = make([]int64, 0) + var notExisting = make([]int64, 0) + err := b.Int64("id", &id). + Int64("nr", &nr). + Int64s("slice", &slice). + Int64s("not_existing", ¬Existing). + BindError() + + assert.NoError(t, err) + assert.Equal(t, int64(1), id) + assert.Equal(t, int64(2), nr) + assert.Equal(t, []int64{3}, slice) // binding params to slice does not make sense but it should not panic either + assert.Equal(t, []int64{}, notExisting) // binding params to slice does not make sense but it should not panic either +} + +func TestQueryParamsBinder_FailFast(t *testing.T) { + var testCases = []struct { + name string + whenURL string + givenFailFast bool + expectError []string + }{ + { + name: "ok, FailFast=true stops at first error", + whenURL: "/api/user/999?nr=en&id=nope", + givenFailFast: true, + expectError: []string{ + `code=400, message=failed to bind field value to int64, internal=strconv.ParseInt: parsing "nope": invalid syntax, field=id`, + }, + }, + { + name: "ok, FailFast=false encounters all errors", + whenURL: "/api/user/999?nr=en&id=nope", + givenFailFast: false, + expectError: []string{ + `code=400, message=failed to bind field value to int64, internal=strconv.ParseInt: parsing "nope": invalid syntax, field=id`, + `code=400, message=failed to bind field value to int64, internal=strconv.ParseInt: parsing "en": invalid syntax, field=nr`, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + c := createTestContext(tc.whenURL, nil, map[string]string{"id": "999"}) + b := QueryParamsBinder(c).FailFast(tc.givenFailFast) + id := int64(99) + nr := int64(88) + errs := b.Int64("id", &id). + Int64("nr", &nr). + BindErrors() + + assert.Len(t, errs, len(tc.expectError)) + for _, err := range errs { + assert.Contains(t, tc.expectError, err.Error()) + } + }) + } +} + +func TestFormFieldBinder(t *testing.T) { + e := New() + body := `texta=foo&slice=5` + req := httptest.NewRequest(http.MethodPost, "/api/search?id=1&nr=2&slice=3&slice=4", strings.NewReader(body)) + req.Header.Set(HeaderContentLength, strconv.Itoa(len(body))) + req.Header.Set(HeaderContentType, MIMEApplicationForm) + + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + b := FormFieldBinder(c) + + var texta string + id := int64(99) + nr := int64(88) + var slice = make([]int64, 0) + var notExisting = make([]int64, 0) + err := b. + Int64s("slice", &slice). + Int64("id", &id). + Int64("nr", &nr). + String("texta", &texta). + Int64s("notExisting", ¬Existing). + BindError() + + assert.NoError(t, err) + 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) +} + +func TestValueBinder_errorStopsBinding(t *testing.T) { + // this test documents "feature" that binding multiple params can change destination if it was binded before + // failing parameter binding + + c := createTestContext("/api/user/999?id=1&nr=nope", nil, nil) + b := QueryParamsBinder(c) + + id := int64(99) // will be changed before nr binding fails + nr := int64(88) // will not be changed + err := b.Int64("id", &id). + Int64("nr", &nr). + BindError() + + assert.EqualError(t, err, "code=400, message=failed to bind field value to int64, internal=strconv.ParseInt: parsing \"nope\": invalid syntax, field=nr") + assert.Equal(t, int64(1), id) + assert.Equal(t, int64(88), nr) +} + +func TestValueBinder_BindError(t *testing.T) { + c := createTestContext("/api/user/999?nr=en&id=nope", nil, nil) + b := QueryParamsBinder(c) + + id := int64(99) + nr := int64(88) + err := b.Int64("id", &id). + Int64("nr", &nr). + BindError() + + assert.EqualError(t, err, "code=400, message=failed to bind field value to int64, internal=strconv.ParseInt: parsing \"nope\": invalid syntax, field=id") + assert.Nil(t, b.errors) + assert.Nil(t, b.BindError()) +} + +func TestValueBinder_GetValues(t *testing.T) { + var testCases = []struct { + name string + whenValuesFunc func(sourceParam string) []string + expect []int64 + expectError string + }{ + { + name: "ok, default implementation", + expect: []int64{1, 101}, + }, + { + name: "ok, values returns nil", + whenValuesFunc: func(sourceParam string) []string { + return nil + }, + expect: []int64(nil), + }, + { + name: "ok, values returns empty slice", + whenValuesFunc: func(sourceParam string) []string { + return []string{} + }, + expect: []int64(nil), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + c := createTestContext("/search?nr=en&id=1&id=101", nil, nil) + b := QueryParamsBinder(c) + if tc.whenValuesFunc != nil { + b.ValuesFunc = tc.whenValuesFunc + } + + var IDs []int64 + err := b.Int64s("id", &IDs).BindError() + + assert.Equal(t, tc.expect, IDs) + if tc.expectError != "" { + assert.EqualError(t, err, tc.expectError) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestValueBinder_CustomFuncWithError(t *testing.T) { + c := createTestContext("/search?nr=en&id=1&id=101", nil, nil) + b := QueryParamsBinder(c) + + id := int64(99) + givenCustomFunc := func(values []string) []error { + assert.Equal(t, []string{"1", "101"}, values) + + return []error{ + errors.New("first error"), + errors.New("second error"), + } + } + err := b.CustomFunc("id", givenCustomFunc).BindError() + + assert.Equal(t, int64(99), id) + assert.EqualError(t, err, "first error") +} + +func TestValueBinder_CustomFunc(t *testing.T) { + var testCases = []struct { + name string + givenFailFast bool + givenFuncErrors []error + whenURL string + expectParamValues []string + expectValue interface{} + expectErrors []string + }{ + { + name: "ok, binds value", + whenURL: "/search?nr=en&id=1&id=100", + expectParamValues: []string{"1", "100"}, + expectValue: int64(1000), + }, + { + name: "ok, params values empty, value is not changed", + whenURL: "/search?nr=en", + expectParamValues: []string{}, + expectValue: int64(99), + }, + { + name: "nok, previous errors fail fast without binding value", + givenFailFast: true, + whenURL: "/search?nr=en&id=1&id=100", + expectParamValues: []string{"1", "100"}, + expectValue: int64(99), + expectErrors: []string{"previous error"}, + }, + { + name: "nok, func returns errors", + givenFuncErrors: []error{ + errors.New("first error"), + errors.New("second error"), + }, + whenURL: "/search?nr=en&id=1&id=100", + expectParamValues: []string{"1", "100"}, + expectValue: int64(99), + expectErrors: []string{"first error", "second error"}, + }, + } + + 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")} + } + + id := int64(99) + givenCustomFunc := func(values []string) []error { + assert.Equal(t, tc.expectParamValues, values) + if tc.givenFuncErrors == nil { + id = 1000 // emulated conversion and setting value + return nil + } + return tc.givenFuncErrors + } + errs := b.CustomFunc("id", givenCustomFunc).BindErrors() + + assert.Equal(t, tc.expectValue, id) + if tc.expectErrors != nil { + assert.Len(t, errs, len(tc.expectErrors)) + for _, err := range errs { + assert.Contains(t, tc.expectErrors, err.Error()) + } + } else { + assert.Nil(t, errs) + } + }) + } +} + +func TestValueBinder_MustCustomFunc(t *testing.T) { + var testCases = []struct { + name string + givenFailFast bool + givenFuncErrors []error + whenURL string + expectParamValues []string + expectValue interface{} + expectErrors []string + }{ + { + name: "ok, binds value", + whenURL: "/search?nr=en&id=1&id=100", + expectParamValues: []string{"1", "100"}, + expectValue: int64(1000), + }, + { + name: "nok, params values empty, returns error, value is not changed", + whenURL: "/search?nr=en", + expectParamValues: []string{}, + expectValue: int64(99), + expectErrors: []string{"code=400, message=required field value is empty, field=id"}, + }, + { + name: "nok, previous errors fail fast without binding value", + givenFailFast: true, + whenURL: "/search?nr=en&id=1&id=100", + expectParamValues: []string{"1", "100"}, + expectValue: int64(99), + expectErrors: []string{"previous error"}, + }, + { + name: "nok, func returns errors", + givenFuncErrors: []error{ + errors.New("first error"), + errors.New("second error"), + }, + whenURL: "/search?nr=en&id=1&id=100", + expectParamValues: []string{"1", "100"}, + expectValue: int64(99), + expectErrors: []string{"first error", "second error"}, + }, + } + + 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")} + } + + id := int64(99) + givenCustomFunc := func(values []string) []error { + assert.Equal(t, tc.expectParamValues, values) + if tc.givenFuncErrors == nil { + id = 1000 // emulated conversion and setting value + return nil + } + return tc.givenFuncErrors + } + errs := b.MustCustomFunc("id", givenCustomFunc).BindErrors() + + assert.Equal(t, tc.expectValue, id) + if tc.expectErrors != nil { + assert.Len(t, errs, len(tc.expectErrors)) + for _, err := range errs { + assert.Contains(t, tc.expectErrors, err.Error()) + } + } else { + assert.Nil(t, errs) + } + }) + } +} + +func TestValueBinder_String(t *testing.T) { + var testCases = []struct { + name string + givenFailFast bool + givenBindErrors []error + whenURL string + whenMust bool + expectValue string + expectError string + }{ + { + name: "ok, binds value", + whenURL: "/search?param=en¶m=de", + expectValue: "en", + }, + { + name: "ok, params values empty, value is not changed", + whenURL: "/search?nr=en", + expectValue: "default", + }, + { + name: "nok, previous errors fail fast without binding value", + givenFailFast: true, + whenURL: "/search?nr=en&id=1&id=100", + expectValue: "default", + expectError: "previous error", + }, + { + name: "ok (must), binds value", + whenMust: true, + whenURL: "/search?param=en¶m=de", + expectValue: "en", + }, + { + name: "ok (must), params values empty, returns error, value is not changed", + whenMust: true, + whenURL: "/search?nr=en", + expectValue: "default", + 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?nr=en&id=1&id=100", + expectValue: "default", + expectError: "previous error", + }, + } + + 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 := "default" + var err error + if tc.whenMust { + err = b.MustString("param", &dest).BindError() + } else { + err = b.String("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_Strings(t *testing.T) { + var testCases = []struct { + name string + givenFailFast bool + givenBindErrors []error + whenURL string + whenMust bool + expectValue []string + expectError string + }{ + { + name: "ok, binds value", + whenURL: "/search?param=en¶m=de", + expectValue: []string{"en", "de"}, + }, + { + name: "ok, params values empty, value is not changed", + whenURL: "/search?nr=en", + expectValue: []string{"default"}, + }, + { + name: "nok, previous errors fail fast without binding value", + givenFailFast: true, + whenURL: "/search?nr=en&id=1&id=100", + expectValue: []string{"default"}, + expectError: "previous error", + }, + { + name: "ok (must), binds value", + whenMust: true, + whenURL: "/search?param=en¶m=de", + expectValue: []string{"en", "de"}, + }, + { + name: "ok (must), params values empty, returns error, value is not changed", + whenMust: true, + whenURL: "/search?nr=en", + expectValue: []string{"default"}, + 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?nr=en&id=1&id=100", + expectValue: []string{"default"}, + expectError: "previous error", + }, + } + + 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 := []string{"default"} + var err error + if tc.whenMust { + err = b.MustStrings("param", &dest).BindError() + } else { + err = b.Strings("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_Int64_intValue(t *testing.T) { + var testCases = []struct { + name string + givenFailFast bool + givenBindErrors []error + whenURL string + whenMust bool + expectValue int64 + expectError string + }{ + { + name: "ok, binds value", + whenURL: "/search?param=1¶m=100", + expectValue: 1, + }, + { + name: "ok, params values empty, value is not changed", + whenURL: "/search?nope=1", + expectValue: 99, + }, + { + name: "nok, previous errors fail fast without binding value", + givenFailFast: true, + whenURL: "/search?param=1¶m=100", + expectValue: 99, + expectError: "previous error", + }, + { + name: "nok, conversion fails, value is not changed", + whenURL: "/search?param=nope¶m=100", + expectValue: 99, + expectError: "code=400, message=failed to bind field value to int64, internal=strconv.ParseInt: parsing \"nope\": invalid syntax, field=param", + }, + { + name: "ok (must), binds value", + whenMust: true, + whenURL: "/search?param=1¶m=100", + expectValue: 1, + }, + { + name: "ok (must), params values empty, returns error, value is not changed", + whenMust: true, + whenURL: "/search?nope=1", + expectValue: 99, + 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¶m=100", + expectValue: 99, + expectError: "previous error", + }, + { + name: "nok (must), conversion fails, value is not changed", + whenMust: true, + whenURL: "/search?param=nope¶m=100", + expectValue: 99, + expectError: "code=400, message=failed to bind field value to int64, 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 := int64(99) + var err error + if tc.whenMust { + err = b.MustInt64("param", &dest).BindError() + } else { + err = b.Int64("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_Int_errorMessage(t *testing.T) { + // int/uint (without byte size) has a little bit different error message so test these separately + c := createTestContext("/search?param=nope", nil, nil) + b := QueryParamsBinder(c).FailFast(false) + + destInt := 99 + destUint := uint(98) + errs := b.Int("param", &destInt).Uint("param", &destUint).BindErrors() + + assert.Equal(t, 99, destInt) + assert.Equal(t, uint(98), destUint) + assert.EqualError(t, errs[0], `code=400, message=failed to bind field value to int, internal=strconv.ParseInt: parsing "nope": invalid syntax, field=param`) + assert.EqualError(t, errs[1], `code=400, message=failed to bind field value to uint, internal=strconv.ParseUint: parsing "nope": invalid syntax, field=param`) +} + +func TestValueBinder_Uint64_uintValue(t *testing.T) { + var testCases = []struct { + name string + givenFailFast bool + givenBindErrors []error + whenURL string + whenMust bool + expectValue uint64 + expectError string + }{ + { + name: "ok, binds value", + whenURL: "/search?param=1¶m=100", + expectValue: 1, + }, + { + name: "ok, params values empty, value is not changed", + whenURL: "/search?nope=1", + expectValue: 99, + }, + { + name: "nok, previous errors fail fast without binding value", + givenFailFast: true, + whenURL: "/search?param=1¶m=100", + expectValue: 99, + expectError: "previous error", + }, + { + name: "nok, conversion fails, value is not changed", + whenURL: "/search?param=nope¶m=100", + expectValue: 99, + expectError: "code=400, message=failed to bind field value to uint64, internal=strconv.ParseUint: parsing \"nope\": invalid syntax, field=param", + }, + { + name: "ok (must), binds value", + whenMust: true, + whenURL: "/search?param=1¶m=100", + expectValue: 1, + }, + { + name: "ok (must), params values empty, returns error, value is not changed", + whenMust: true, + whenURL: "/search?nope=1", + expectValue: 99, + 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¶m=100", + expectValue: 99, + expectError: "previous error", + }, + { + name: "nok (must), conversion fails, value is not changed", + whenMust: true, + whenURL: "/search?param=nope¶m=100", + expectValue: 99, + expectError: "code=400, message=failed to bind field value to uint64, internal=strconv.ParseUint: 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 := uint64(99) + var err error + if tc.whenMust { + err = b.MustUint64("param", &dest).BindError() + } else { + err = b.Uint64("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_Int_Types(t *testing.T) { + type target struct { + int64 int64 + mustInt64 int64 + uint64 uint64 + mustUint64 uint64 + + int32 int32 + mustInt32 int32 + uint32 uint32 + mustUint32 uint32 + + int16 int16 + mustInt16 int16 + uint16 uint16 + mustUint16 uint16 + + int8 int8 + mustInt8 int8 + uint8 uint8 + mustUint8 uint8 + + byte byte + mustByte byte + + int int + mustInt int + uint uint + mustUint uint + } + types := []string{ + "int64=1", + "mustInt64=2", + "uint64=3", + "mustUint64=4", + + "int32=5", + "mustInt32=6", + "uint32=7", + "mustUint32=8", + + "int16=9", + "mustInt16=10", + "uint16=11", + "mustUint16=12", + + "int8=13", + "mustInt8=14", + "uint8=15", + "mustUint8=16", + + "byte=17", + "mustByte=18", + + "int=19", + "mustInt=20", + "uint=21", + "mustUint=22", + } + c := createTestContext("/search?"+strings.Join(types, "&"), nil, nil) + b := QueryParamsBinder(c) + + dest := target{} + err := b. + Int64("int64", &dest.int64). + MustInt64("mustInt64", &dest.mustInt64). + Uint64("uint64", &dest.uint64). + MustUint64("mustUint64", &dest.mustUint64). + Int32("int32", &dest.int32). + MustInt32("mustInt32", &dest.mustInt32). + Uint32("uint32", &dest.uint32). + MustUint32("mustUint32", &dest.mustUint32). + Int16("int16", &dest.int16). + MustInt16("mustInt16", &dest.mustInt16). + Uint16("uint16", &dest.uint16). + MustUint16("mustUint16", &dest.mustUint16). + Int8("int8", &dest.int8). + MustInt8("mustInt8", &dest.mustInt8). + Uint8("uint8", &dest.uint8). + MustUint8("mustUint8", &dest.mustUint8). + Byte("byte", &dest.byte). + MustByte("mustByte", &dest.mustByte). + Int("int", &dest.int). + MustInt("mustInt", &dest.mustInt). + Uint("uint", &dest.uint). + MustUint("mustUint", &dest.mustUint). + BindError() + + assert.NoError(t, err) + assert.Equal(t, int64(1), dest.int64) + assert.Equal(t, int64(2), dest.mustInt64) + assert.Equal(t, uint64(3), dest.uint64) + assert.Equal(t, uint64(4), dest.mustUint64) + + assert.Equal(t, int32(5), dest.int32) + assert.Equal(t, int32(6), dest.mustInt32) + assert.Equal(t, uint32(7), dest.uint32) + assert.Equal(t, uint32(8), dest.mustUint32) + + assert.Equal(t, int16(9), dest.int16) + assert.Equal(t, int16(10), dest.mustInt16) + assert.Equal(t, uint16(11), dest.uint16) + assert.Equal(t, uint16(12), dest.mustUint16) + + assert.Equal(t, int8(13), dest.int8) + assert.Equal(t, int8(14), dest.mustInt8) + assert.Equal(t, uint8(15), dest.uint8) + assert.Equal(t, uint8(16), dest.mustUint8) + + assert.Equal(t, uint8(17), dest.byte) + assert.Equal(t, uint8(18), dest.mustByte) + + assert.Equal(t, 19, dest.int) + assert.Equal(t, 20, dest.mustInt) + assert.Equal(t, uint(21), dest.uint) + assert.Equal(t, uint(22), dest.mustUint) +} + +func TestValueBinder_Int64s_intsValue(t *testing.T) { + var testCases = []struct { + name string + givenFailFast bool + givenBindErrors []error + whenURL string + whenMust bool + expectValue []int64 + expectError string + }{ + { + name: "ok, binds value", + whenURL: "/search?param=1¶m=2¶m=1", + expectValue: []int64{1, 2, 1}, + }, + { + name: "ok, params values empty, value is not changed", + whenURL: "/search?nope=1", + expectValue: []int64{99}, + }, + { + name: "nok, previous errors fail fast without binding value", + givenFailFast: true, + whenURL: "/search?param=1¶m=100", + expectValue: []int64{99}, + expectError: "previous error", + }, + { + name: "nok, conversion fails, value is not changed", + whenURL: "/search?param=nope¶m=100", + expectValue: []int64{99}, + expectError: "code=400, message=failed to bind field value to int64, internal=strconv.ParseInt: parsing \"nope\": invalid syntax, field=param", + }, + { + name: "ok (must), binds value", + whenMust: true, + whenURL: "/search?param=1¶m=2¶m=1", + expectValue: []int64{1, 2, 1}, + }, + { + name: "ok (must), params values empty, returns error, value is not changed", + whenMust: true, + whenURL: "/search?nope=1", + expectValue: []int64{99}, + 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¶m=100", + expectValue: []int64{99}, + expectError: "previous error", + }, + { + name: "nok (must), conversion fails, value is not changed", + whenMust: true, + whenURL: "/search?param=nope¶m=100", + expectValue: []int64{99}, + expectError: "code=400, message=failed to bind field value to int64, 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 := []int64{99} // when values are set with bind - contents before bind is gone + var err error + if tc.whenMust { + err = b.MustInt64s("param", &dest).BindError() + } else { + err = b.Int64s("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_Uint64s_uintsValue(t *testing.T) { + var testCases = []struct { + name string + givenFailFast bool + givenBindErrors []error + whenURL string + whenMust bool + expectValue []uint64 + expectError string + }{ + { + name: "ok, binds value", + whenURL: "/search?param=1¶m=2¶m=1", + expectValue: []uint64{1, 2, 1}, + }, + { + name: "ok, params values empty, value is not changed", + whenURL: "/search?nope=1", + expectValue: []uint64{99}, + }, + { + name: "nok, previous errors fail fast without binding value", + givenFailFast: true, + whenURL: "/search?param=1¶m=100", + expectValue: []uint64{99}, + expectError: "previous error", + }, + { + name: "nok, conversion fails, value is not changed", + whenURL: "/search?param=nope¶m=100", + expectValue: []uint64{99}, + expectError: "code=400, message=failed to bind field value to uint64, internal=strconv.ParseUint: parsing \"nope\": invalid syntax, field=param", + }, + { + name: "ok (must), binds value", + whenMust: true, + whenURL: "/search?param=1¶m=2¶m=1", + expectValue: []uint64{1, 2, 1}, + }, + { + name: "ok (must), params values empty, returns error, value is not changed", + whenMust: true, + whenURL: "/search?nope=1", + expectValue: []uint64{99}, + 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¶m=100", + expectValue: []uint64{99}, + expectError: "previous error", + }, + { + name: "nok (must), conversion fails, value is not changed", + whenMust: true, + whenURL: "/search?param=nope¶m=100", + expectValue: []uint64{99}, + expectError: "code=400, message=failed to bind field value to uint64, internal=strconv.ParseUint: 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 := []uint64{99} // when values are set with bind - contents before bind is gone + var err error + if tc.whenMust { + err = b.MustUint64s("param", &dest).BindError() + } else { + err = b.Uint64s("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_Ints_Types(t *testing.T) { + type target struct { + int64 []int64 + mustInt64 []int64 + uint64 []uint64 + mustUint64 []uint64 + + int32 []int32 + mustInt32 []int32 + uint32 []uint32 + mustUint32 []uint32 + + int16 []int16 + mustInt16 []int16 + uint16 []uint16 + mustUint16 []uint16 + + int8 []int8 + mustInt8 []int8 + uint8 []uint8 + mustUint8 []uint8 + + int []int + mustInt []int + uint []uint + mustUint []uint + } + types := []string{ + "int64=1", + "mustInt64=2", + "uint64=3", + "mustUint64=4", + + "int32=5", + "mustInt32=6", + "uint32=7", + "mustUint32=8", + + "int16=9", + "mustInt16=10", + "uint16=11", + "mustUint16=12", + + "int8=13", + "mustInt8=14", + "uint8=15", + "mustUint8=16", + + "int=19", + "mustInt=20", + "uint=21", + "mustUint=22", + } + url := "/search?" + for _, v := range types { + url = url + "&" + v + "&" + v + } + c := createTestContext(url, nil, nil) + b := QueryParamsBinder(c) + + dest := target{} + err := b. + Int64s("int64", &dest.int64). + MustInt64s("mustInt64", &dest.mustInt64). + Uint64s("uint64", &dest.uint64). + MustUint64s("mustUint64", &dest.mustUint64). + Int32s("int32", &dest.int32). + MustInt32s("mustInt32", &dest.mustInt32). + Uint32s("uint32", &dest.uint32). + MustUint32s("mustUint32", &dest.mustUint32). + Int16s("int16", &dest.int16). + MustInt16s("mustInt16", &dest.mustInt16). + Uint16s("uint16", &dest.uint16). + MustUint16s("mustUint16", &dest.mustUint16). + Int8s("int8", &dest.int8). + MustInt8s("mustInt8", &dest.mustInt8). + Uint8s("uint8", &dest.uint8). + MustUint8s("mustUint8", &dest.mustUint8). + Ints("int", &dest.int). + MustInts("mustInt", &dest.mustInt). + Uints("uint", &dest.uint). + MustUints("mustUint", &dest.mustUint). + BindError() + + assert.NoError(t, err) + assert.Equal(t, []int64{1, 1}, dest.int64) + assert.Equal(t, []int64{2, 2}, dest.mustInt64) + assert.Equal(t, []uint64{3, 3}, dest.uint64) + assert.Equal(t, []uint64{4, 4}, dest.mustUint64) + + assert.Equal(t, []int32{5, 5}, dest.int32) + assert.Equal(t, []int32{6, 6}, dest.mustInt32) + assert.Equal(t, []uint32{7, 7}, dest.uint32) + assert.Equal(t, []uint32{8, 8}, dest.mustUint32) + + assert.Equal(t, []int16{9, 9}, dest.int16) + assert.Equal(t, []int16{10, 10}, dest.mustInt16) + assert.Equal(t, []uint16{11, 11}, dest.uint16) + assert.Equal(t, []uint16{12, 12}, dest.mustUint16) + + assert.Equal(t, []int8{13, 13}, dest.int8) + assert.Equal(t, []int8{14, 14}, dest.mustInt8) + assert.Equal(t, []uint8{15, 15}, dest.uint8) + assert.Equal(t, []uint8{16, 16}, dest.mustUint8) + + assert.Equal(t, []int{19, 19}, dest.int) + assert.Equal(t, []int{20, 20}, dest.mustInt) + assert.Equal(t, []uint{21, 21}, dest.uint) + assert.Equal(t, []uint{22, 22}, dest.mustUint) +} + +func TestValueBinder_Ints_Types_FailFast(t *testing.T) { + // FailFast() should stop parsing and return early + errTmpl := "code=400, message=failed to bind field value to %v, internal=strconv.Parse%v: parsing \"nope\": invalid syntax, field=param" + c := createTestContext("/search?param=1¶m=nope¶m=2", nil, nil) + + var dest64 []int64 + err := QueryParamsBinder(c).FailFast(true).Int64s("param", &dest64).BindError() + assert.Equal(t, []int64(nil), dest64) + assert.EqualError(t, err, fmt.Sprintf(errTmpl, "int64", "Int")) + + var dest32 []int32 + err = QueryParamsBinder(c).FailFast(true).Int32s("param", &dest32).BindError() + assert.Equal(t, []int32(nil), dest32) + assert.EqualError(t, err, fmt.Sprintf(errTmpl, "int32", "Int")) + + var dest16 []int16 + err = QueryParamsBinder(c).FailFast(true).Int16s("param", &dest16).BindError() + assert.Equal(t, []int16(nil), dest16) + assert.EqualError(t, err, fmt.Sprintf(errTmpl, "int16", "Int")) + + var dest8 []int8 + err = QueryParamsBinder(c).FailFast(true).Int8s("param", &dest8).BindError() + assert.Equal(t, []int8(nil), dest8) + assert.EqualError(t, err, fmt.Sprintf(errTmpl, "int8", "Int")) + + var dest []int + err = QueryParamsBinder(c).FailFast(true).Ints("param", &dest).BindError() + assert.Equal(t, []int(nil), dest) + assert.EqualError(t, err, fmt.Sprintf(errTmpl, "int", "Int")) + + var destu64 []uint64 + err = QueryParamsBinder(c).FailFast(true).Uint64s("param", &destu64).BindError() + assert.Equal(t, []uint64(nil), destu64) + assert.EqualError(t, err, fmt.Sprintf(errTmpl, "uint64", "Uint")) + + var destu32 []uint32 + err = QueryParamsBinder(c).FailFast(true).Uint32s("param", &destu32).BindError() + assert.Equal(t, []uint32(nil), destu32) + assert.EqualError(t, err, fmt.Sprintf(errTmpl, "uint32", "Uint")) + + var destu16 []uint16 + err = QueryParamsBinder(c).FailFast(true).Uint16s("param", &destu16).BindError() + assert.Equal(t, []uint16(nil), destu16) + assert.EqualError(t, err, fmt.Sprintf(errTmpl, "uint16", "Uint")) + + var destu8 []uint8 + err = QueryParamsBinder(c).FailFast(true).Uint8s("param", &destu8).BindError() + assert.Equal(t, []uint8(nil), destu8) + assert.EqualError(t, err, fmt.Sprintf(errTmpl, "uint8", "Uint")) + + var destu []uint + err = QueryParamsBinder(c).FailFast(true).Uints("param", &destu).BindError() + assert.Equal(t, []uint(nil), destu) + assert.EqualError(t, err, fmt.Sprintf(errTmpl, "uint", "Uint")) +} + +func TestValueBinder_Bool(t *testing.T) { + var testCases = []struct { + name string + givenFailFast bool + givenBindErrors []error + whenURL string + whenMust bool + expectValue bool + expectError string + }{ + { + name: "ok, binds value", + whenURL: "/search?param=true¶m=1", + expectValue: true, + }, + { + name: "ok, params values empty, value is not changed", + whenURL: "/search?nope=1", + expectValue: false, + }, + { + name: "nok, previous errors fail fast without binding value", + givenFailFast: true, + whenURL: "/search?param=1¶m=100", + expectValue: false, + expectError: "previous error", + }, + { + name: "nok, conversion fails, value is not changed", + whenURL: "/search?param=nope¶m=100", + expectValue: false, + expectError: "code=400, message=failed to bind field value to bool, internal=strconv.ParseBool: parsing \"nope\": invalid syntax, field=param", + }, + { + name: "ok (must), binds value", + whenMust: true, + whenURL: "/search?param=1¶m=100", + expectValue: true, + }, + { + name: "ok (must), params values empty, returns error, value is not changed", + whenMust: true, + whenURL: "/search?nope=1", + expectValue: false, + 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¶m=100", + expectValue: false, + expectError: "previous error", + }, + { + name: "nok (must), conversion fails, value is not changed", + whenMust: true, + whenURL: "/search?param=nope¶m=100", + expectValue: false, + expectError: "code=400, message=failed to bind field value to bool, internal=strconv.ParseBool: 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 := false + var err error + if tc.whenMust { + err = b.MustBool("param", &dest).BindError() + } else { + err = b.Bool("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_Bools(t *testing.T) { + var testCases = []struct { + name string + givenFailFast bool + givenBindErrors []error + whenURL string + whenMust bool + expectValue []bool + expectError string + }{ + { + name: "ok, binds value", + whenURL: "/search?param=true¶m=false¶m=1¶m=0", + expectValue: []bool{true, false, true, false}, + }, + { + name: "ok, params values empty, value is not changed", + whenURL: "/search?nope=1", + expectValue: []bool(nil), + }, + { + name: "nok, previous errors fail fast without binding value", + givenFailFast: true, + givenBindErrors: []error{errors.New("previous error")}, + whenURL: "/search?param=1¶m=100", + expectValue: []bool(nil), + expectError: "previous error", + }, + { + name: "nok, conversion fails, value is not changed", + whenURL: "/search?param=true¶m=nope¶m=100", + expectValue: []bool(nil), + expectError: "code=400, message=failed to bind field value to bool, internal=strconv.ParseBool: parsing \"nope\": invalid syntax, field=param", + }, + { + name: "nok, conversion fails fast, value is not changed", + givenFailFast: true, + whenURL: "/search?param=true¶m=nope¶m=100", + expectValue: []bool(nil), + expectError: "code=400, message=failed to bind field value to bool, internal=strconv.ParseBool: parsing \"nope\": invalid syntax, field=param", + }, + { + name: "ok (must), binds value", + whenMust: true, + whenURL: "/search?param=true¶m=false¶m=1¶m=0", + expectValue: []bool{true, false, true, false}, + }, + { + name: "ok (must), params values empty, returns error, value is not changed", + whenMust: true, + whenURL: "/search?nope=1", + expectValue: []bool(nil), + expectError: "code=400, message=required field value is empty, field=param", + }, + { + name: "nok (must), previous errors fail fast without binding value", + givenFailFast: true, + givenBindErrors: []error{errors.New("previous error")}, + whenMust: true, + whenURL: "/search?param=1¶m=100", + expectValue: []bool(nil), + expectError: "previous error", + }, + { + name: "nok (must), conversion fails, value is not changed", + whenMust: true, + whenURL: "/search?param=nope¶m=100", + expectValue: []bool(nil), + expectError: "code=400, message=failed to bind field value to bool, internal=strconv.ParseBool: 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) + b.errors = tc.givenBindErrors + + var dest []bool + var err error + if tc.whenMust { + err = b.MustBools("param", &dest).BindError() + } else { + err = b.Bools("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_Float64(t *testing.T) { + var testCases = []struct { + name string + givenFailFast bool + givenBindErrors []error + whenURL string + whenMust bool + expectValue float64 + expectError string + }{ + { + name: "ok, binds value", + whenURL: "/search?param=4.3¶m=1", + expectValue: 4.3, + }, + { + name: "ok, params values empty, value is not changed", + whenURL: "/search?nope=1", + expectValue: 1.123, + }, + { + name: "nok, previous errors fail fast without binding value", + givenFailFast: true, + whenURL: "/search?param=1¶m=100", + expectValue: 1.123, + expectError: "previous error", + }, + { + name: "nok, conversion fails, value is not changed", + whenURL: "/search?param=nope¶m=100", + expectValue: 1.123, + expectError: "code=400, message=failed to bind field value to float64, internal=strconv.ParseFloat: parsing \"nope\": invalid syntax, field=param", + }, + { + name: "ok (must), binds value", + whenMust: true, + whenURL: "/search?param=4.3¶m=100", + expectValue: 4.3, + }, + { + name: "ok (must), params values empty, returns error, value is not changed", + whenMust: true, + whenURL: "/search?nope=1", + expectValue: 1.123, + 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¶m=100", + expectValue: 1.123, + expectError: "previous error", + }, + { + name: "nok (must), conversion fails, value is not changed", + whenMust: true, + whenURL: "/search?param=nope¶m=100", + expectValue: 1.123, + expectError: "code=400, message=failed to bind field value to float64, internal=strconv.ParseFloat: 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 := 1.123 + var err error + if tc.whenMust { + err = b.MustFloat64("param", &dest).BindError() + } else { + err = b.Float64("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_Float64s(t *testing.T) { + var testCases = []struct { + name string + givenFailFast bool + givenBindErrors []error + whenURL string + whenMust bool + expectValue []float64 + expectError string + }{ + { + name: "ok, binds value", + whenURL: "/search?param=4.3¶m=0", + expectValue: []float64{4.3, 0}, + }, + { + name: "ok, params values empty, value is not changed", + whenURL: "/search?nope=1", + expectValue: []float64(nil), + }, + { + name: "nok, previous errors fail fast without binding value", + givenFailFast: true, + givenBindErrors: []error{errors.New("previous error")}, + whenURL: "/search?param=1¶m=100", + expectValue: []float64(nil), + expectError: "previous error", + }, + { + name: "nok, conversion fails, value is not changed", + whenURL: "/search?param=nope¶m=100", + expectValue: []float64(nil), + expectError: "code=400, message=failed to bind field value to float64, internal=strconv.ParseFloat: parsing \"nope\": invalid syntax, field=param", + }, + { + name: "nok, conversion fails fast, value is not changed", + givenFailFast: true, + whenURL: "/search?param=0¶m=nope¶m=100", + expectValue: []float64(nil), + expectError: "code=400, message=failed to bind field value to float64, internal=strconv.ParseFloat: parsing \"nope\": invalid syntax, field=param", + }, + { + name: "ok (must), binds value", + whenMust: true, + whenURL: "/search?param=4.3¶m=0", + expectValue: []float64{4.3, 0}, + }, + { + name: "ok (must), params values empty, returns error, value is not changed", + whenMust: true, + whenURL: "/search?nope=1", + expectValue: []float64(nil), + expectError: "code=400, message=required field value is empty, field=param", + }, + { + name: "nok (must), previous errors fail fast without binding value", + givenFailFast: true, + givenBindErrors: []error{errors.New("previous error")}, + whenMust: true, + whenURL: "/search?param=1¶m=100", + expectValue: []float64(nil), + expectError: "previous error", + }, + { + name: "nok (must), conversion fails, value is not changed", + whenMust: true, + whenURL: "/search?param=nope¶m=100", + expectValue: []float64(nil), + expectError: "code=400, message=failed to bind field value to float64, internal=strconv.ParseFloat: 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) + b.errors = tc.givenBindErrors + + var dest []float64 + var err error + if tc.whenMust { + err = b.MustFloat64s("param", &dest).BindError() + } else { + err = b.Float64s("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_Float32(t *testing.T) { + var testCases = []struct { + name string + givenNoFailFast bool + givenBindErrors []error + whenURL string + whenMust bool + expectValue float32 + expectError string + }{ + { + name: "ok, binds value", + whenURL: "/search?param=4.3¶m=1", + expectValue: 4.3, + }, + { + name: "ok, params values empty, value is not changed", + whenURL: "/search?nope=1", + expectValue: 1.123, + }, + { + name: "nok, previous errors fail fast without binding value", + givenNoFailFast: true, + whenURL: "/search?param=1¶m=100", + expectValue: 1.123, + expectError: "previous error", + }, + { + name: "nok, conversion fails, value is not changed", + whenURL: "/search?param=nope¶m=100", + expectValue: 1.123, + expectError: "code=400, message=failed to bind field value to float32, internal=strconv.ParseFloat: parsing \"nope\": invalid syntax, field=param", + }, + { + name: "ok (must), binds value", + whenMust: true, + whenURL: "/search?param=4.3¶m=100", + expectValue: 4.3, + }, + { + name: "ok (must), params values empty, returns error, value is not changed", + whenMust: true, + whenURL: "/search?nope=1", + expectValue: 1.123, + expectError: "code=400, message=required field value is empty, field=param", + }, + { + name: "nok (must), previous errors fail fast without binding value", + givenNoFailFast: true, + whenMust: true, + whenURL: "/search?param=1¶m=100", + expectValue: 1.123, + expectError: "previous error", + }, + { + name: "nok (must), conversion fails, value is not changed", + whenMust: true, + whenURL: "/search?param=nope¶m=100", + expectValue: 1.123, + expectError: "code=400, message=failed to bind field value to float32, internal=strconv.ParseFloat: 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.givenNoFailFast) + if tc.givenNoFailFast { + b.errors = []error{errors.New("previous error")} + } + + dest := float32(1.123) + var err error + if tc.whenMust { + err = b.MustFloat32("param", &dest).BindError() + } else { + err = b.Float32("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_Float32s(t *testing.T) { + var testCases = []struct { + name string + givenFailFast bool + givenBindErrors []error + whenURL string + whenMust bool + expectValue []float32 + expectError string + }{ + { + name: "ok, binds value", + whenURL: "/search?param=4.3¶m=0", + expectValue: []float32{4.3, 0}, + }, + { + name: "ok, params values empty, value is not changed", + whenURL: "/search?nope=1", + expectValue: []float32(nil), + }, + { + name: "nok, previous errors fail fast without binding value", + givenFailFast: true, + givenBindErrors: []error{errors.New("previous error")}, + whenURL: "/search?param=1¶m=100", + expectValue: []float32(nil), + expectError: "previous error", + }, + { + name: "nok, conversion fails, value is not changed", + whenURL: "/search?param=nope¶m=100", + expectValue: []float32(nil), + expectError: "code=400, message=failed to bind field value to float32, internal=strconv.ParseFloat: parsing \"nope\": invalid syntax, field=param", + }, + { + name: "nok, conversion fails fast, value is not changed", + givenFailFast: true, + whenURL: "/search?param=0¶m=nope¶m=100", + expectValue: []float32(nil), + expectError: "code=400, message=failed to bind field value to float32, internal=strconv.ParseFloat: parsing \"nope\": invalid syntax, field=param", + }, + { + name: "ok (must), binds value", + whenMust: true, + whenURL: "/search?param=4.3¶m=0", + expectValue: []float32{4.3, 0}, + }, + { + name: "ok (must), params values empty, returns error, value is not changed", + whenMust: true, + whenURL: "/search?nope=1", + expectValue: []float32(nil), + expectError: "code=400, message=required field value is empty, field=param", + }, + { + name: "nok (must), previous errors fail fast without binding value", + givenFailFast: true, + givenBindErrors: []error{errors.New("previous error")}, + whenMust: true, + whenURL: "/search?param=1¶m=100", + expectValue: []float32(nil), + expectError: "previous error", + }, + { + name: "nok (must), conversion fails, value is not changed", + whenMust: true, + whenURL: "/search?param=nope¶m=100", + expectValue: []float32(nil), + expectError: "code=400, message=failed to bind field value to float32, internal=strconv.ParseFloat: 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) + b.errors = tc.givenBindErrors + + var dest []float32 + var err error + if tc.whenMust { + err = b.MustFloat32s("param", &dest).BindError() + } else { + err = b.Float32s("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_Time(t *testing.T) { + exampleTime, _ := time.Parse(time.RFC3339, "2020-12-23T09:45:31+02:00") + var testCases = []struct { + name string + givenFailFast bool + givenBindErrors []error + whenURL string + whenMust bool + whenLayout string + expectValue time.Time + expectError string + }{ + { + name: "ok, binds value", + whenURL: "/search?param=2020-12-23T09:45:31%2B02:00¶m=2000-01-02T09:45:31%2B00:00", + whenLayout: time.RFC3339, + 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¶m=100", + expectValue: time.Time{}, + expectError: "previous error", + }, + { + name: "ok (must), binds value", + whenMust: true, + whenURL: "/search?param=2020-12-23T09:45:31%2B02:00¶m=2000-01-02T09:45:31%2B00:00", + whenLayout: time.RFC3339, + 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¶m=100", + expectValue: time.Time{}, + expectError: "previous error", + }, + } + + 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.MustTime("param", &dest, tc.whenLayout).BindError() + } else { + err = b.Time("param", &dest, tc.whenLayout).BindError() + } + + assert.Equal(t, tc.expectValue, dest) + if tc.expectError != "" { + assert.EqualError(t, err, tc.expectError) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestValueBinder_Times(t *testing.T) { + exampleTime, _ := time.Parse(time.RFC3339, "2020-12-23T09:45:31+02:00") + exampleTime2, _ := time.Parse(time.RFC3339, "2000-01-02T09:45:31+00:00") + var testCases = []struct { + name string + givenFailFast bool + givenBindErrors []error + whenURL string + whenMust bool + whenLayout string + expectValue []time.Time + expectError string + }{ + { + name: "ok, binds value", + whenURL: "/search?param=2020-12-23T09:45:31%2B02:00¶m=2000-01-02T09:45:31%2B00:00", + whenLayout: time.RFC3339, + expectValue: []time.Time{exampleTime, exampleTime2}, + }, + { + name: "ok, params values empty, value is not changed", + whenURL: "/search?nope=1", + expectValue: []time.Time(nil), + }, + { + name: "nok, previous errors fail fast without binding value", + givenFailFast: true, + givenBindErrors: []error{errors.New("previous error")}, + whenURL: "/search?param=1¶m=100", + expectValue: []time.Time(nil), + expectError: "previous error", + }, + { + name: "ok (must), binds value", + whenMust: true, + whenURL: "/search?param=2020-12-23T09:45:31%2B02:00¶m=2000-01-02T09:45:31%2B00:00", + whenLayout: time.RFC3339, + expectValue: []time.Time{exampleTime, exampleTime2}, + }, + { + name: "ok (must), params values empty, returns error, value is not changed", + whenMust: true, + whenURL: "/search?nope=1", + expectValue: []time.Time(nil), + expectError: "code=400, message=required field value is empty, field=param", + }, + { + name: "nok (must), previous errors fail fast without binding value", + givenFailFast: true, + givenBindErrors: []error{errors.New("previous error")}, + whenMust: true, + whenURL: "/search?param=1¶m=100", + expectValue: []time.Time(nil), + expectError: "previous error", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + c := createTestContext(tc.whenURL, nil, nil) + b := QueryParamsBinder(c).FailFast(tc.givenFailFast) + b.errors = tc.givenBindErrors + + layout := time.RFC3339 + if tc.whenLayout != "" { + layout = tc.whenLayout + } + + var dest []time.Time + var err error + if tc.whenMust { + err = b.MustTimes("param", &dest, layout).BindError() + } else { + err = b.Times("param", &dest, layout).BindError() + } + + assert.Equal(t, tc.expectValue, dest) + if tc.expectError != "" { + assert.EqualError(t, err, tc.expectError) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestValueBinder_Duration(t *testing.T) { + example := 42 * time.Second + var testCases = []struct { + name string + givenFailFast bool + givenBindErrors []error + whenURL string + whenMust bool + expectValue time.Duration + expectError string + }{ + { + name: "ok, binds value", + whenURL: "/search?param=42s¶m=1ms", + expectValue: example, + }, + { + name: "ok, params values empty, value is not changed", + whenURL: "/search?nope=1", + expectValue: 0, + }, + { + name: "nok, previous errors fail fast without binding value", + givenFailFast: true, + whenURL: "/search?param=1¶m=100", + expectValue: 0, + expectError: "previous error", + }, + { + name: "ok (must), binds value", + whenMust: true, + whenURL: "/search?param=42s¶m=1ms", + expectValue: example, + }, + { + name: "ok (must), params values empty, returns error, value is not changed", + whenMust: true, + whenURL: "/search?nope=1", + expectValue: 0, + 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¶m=100", + expectValue: 0, + expectError: "previous error", + }, + } + + 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 time.Duration + var err error + if tc.whenMust { + err = b.MustDuration("param", &dest).BindError() + } else { + err = b.Duration("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_Durations(t *testing.T) { + exampleDuration := 42 * time.Second + exampleDuration2 := 1 * time.Millisecond + var testCases = []struct { + name string + givenFailFast bool + givenBindErrors []error + whenURL string + whenMust bool + expectValue []time.Duration + expectError string + }{ + { + name: "ok, binds value", + whenURL: "/search?param=42s¶m=1ms", + expectValue: []time.Duration{exampleDuration, exampleDuration2}, + }, + { + name: "ok, params values empty, value is not changed", + whenURL: "/search?nope=1", + expectValue: []time.Duration(nil), + }, + { + name: "nok, previous errors fail fast without binding value", + givenFailFast: true, + givenBindErrors: []error{errors.New("previous error")}, + whenURL: "/search?param=1¶m=100", + expectValue: []time.Duration(nil), + expectError: "previous error", + }, + { + name: "ok (must), binds value", + whenMust: true, + whenURL: "/search?param=42s¶m=1ms", + expectValue: []time.Duration{exampleDuration, exampleDuration2}, + }, + { + name: "ok (must), params values empty, returns error, value is not changed", + whenMust: true, + whenURL: "/search?nope=1", + expectValue: []time.Duration(nil), + expectError: "code=400, message=required field value is empty, field=param", + }, + { + name: "nok (must), previous errors fail fast without binding value", + givenFailFast: true, + givenBindErrors: []error{errors.New("previous error")}, + whenMust: true, + whenURL: "/search?param=1¶m=100", + expectValue: []time.Duration(nil), + expectError: "previous error", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + c := createTestContext(tc.whenURL, nil, nil) + b := QueryParamsBinder(c).FailFast(tc.givenFailFast) + b.errors = tc.givenBindErrors + + var dest []time.Duration + var err error + if tc.whenMust { + err = b.MustDurations("param", &dest).BindError() + } else { + err = b.Durations("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_BindUnmarshaler(t *testing.T) { + exampleTime, _ := time.Parse(time.RFC3339, "2020-12-23T09:45:31+02:00") + + var testCases = []struct { + name string + givenFailFast bool + givenBindErrors []error + whenURL string + whenMust bool + expectValue Timestamp + expectError string + }{ + { + name: "ok, binds value", + whenURL: "/search?param=2020-12-23T09:45:31%2B02:00¶m=2000-01-02T09:45:31%2B00:00", + expectValue: Timestamp(exampleTime), + }, + { + name: "ok, params values empty, value is not changed", + whenURL: "/search?nope=1", + expectValue: Timestamp{}, + }, + { + name: "nok, previous errors fail fast without binding value", + givenFailFast: true, + whenURL: "/search?param=1¶m=100", + expectValue: Timestamp{}, + expectError: "previous error", + }, + { + name: "nok, conversion fails, value is not changed", + whenURL: "/search?param=nope¶m=100", + expectValue: Timestamp{}, + expectError: "code=400, message=failed to bind field value to BindUnmarshaler interface, internal=parsing time \"nope\" as \"2006-01-02T15:04:05Z07:00\": cannot parse \"nope\" as \"2006\", field=param", + }, + { + name: "ok (must), binds value", + whenMust: true, + whenURL: "/search?param=2020-12-23T09:45:31%2B02:00¶m=2000-01-02T09:45:31%2B00:00", + expectValue: Timestamp(exampleTime), + }, + { + name: "ok (must), params values empty, returns error, value is not changed", + whenMust: true, + whenURL: "/search?nope=1", + expectValue: Timestamp{}, + 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¶m=100", + expectValue: Timestamp{}, + expectError: "previous error", + }, + { + name: "nok (must), conversion fails, value is not changed", + whenMust: true, + whenURL: "/search?param=nope¶m=100", + expectValue: Timestamp{}, + expectError: "code=400, message=failed to bind field value to BindUnmarshaler interface, internal=parsing time \"nope\" as \"2006-01-02T15:04:05Z07:00\": cannot parse \"nope\" as \"2006\", 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 Timestamp + var err error + if tc.whenMust { + err = b.MustBindUnmarshaler("param", &dest).BindError() + } else { + err = b.BindUnmarshaler("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 + whenURL string + expect interface{} + }{ + { + name: "ok, strings", + expect: []string{"1", "2", "1"}, + }, + { + name: "ok, int64", + expect: []int64{1, 2, 1}, + }, + { + name: "ok, int32", + expect: []int32{1, 2, 1}, + }, + { + name: "ok, int16", + expect: []int16{1, 2, 1}, + }, + { + name: "ok, int8", + expect: []int8{1, 2, 1}, + }, + { + name: "ok, int", + expect: []int{1, 2, 1}, + }, + { + name: "ok, uint64", + expect: []uint64{1, 2, 1}, + }, + { + name: "ok, uint32", + expect: []uint32{1, 2, 1}, + }, + { + name: "ok, uint16", + expect: []uint16{1, 2, 1}, + }, + { + name: "ok, uint8", + expect: []uint8{1, 2, 1}, + }, + { + name: "ok, uint", + expect: []uint{1, 2, 1}, + }, + { + name: "ok, float64", + expect: []float64{1, 2, 1}, + }, + { + name: "ok, float32", + expect: []float32{1, 2, 1}, + }, + { + name: "ok, bool", + whenURL: "/search?param=1,false¶m=true", + expect: []bool{true, false, true}, + }, + { + name: "ok, Duration", + whenURL: "/search?param=1s,42s¶m=1ms", + expect: []time.Duration{1 * time.Second, 42 * time.Second, 1 * time.Millisecond}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + URL := "/search?param=1,2¶m=1" + if tc.whenURL != "" { + URL = tc.whenURL + } + c := createTestContext(URL, nil, nil) + b := QueryParamsBinder(c) + + switch tc.expect.(type) { + case []string: + var dest []string + assert.NoError(t, b.BindWithDelimiter("param", &dest, ",").BindError()) + assert.Equal(t, tc.expect, dest) + case []int64: + var dest []int64 + assert.NoError(t, b.BindWithDelimiter("param", &dest, ",").BindError()) + assert.Equal(t, tc.expect, dest) + case []int32: + var dest []int32 + assert.NoError(t, b.BindWithDelimiter("param", &dest, ",").BindError()) + assert.Equal(t, tc.expect, dest) + case []int16: + var dest []int16 + assert.NoError(t, b.BindWithDelimiter("param", &dest, ",").BindError()) + assert.Equal(t, tc.expect, dest) + case []int8: + var dest []int8 + assert.NoError(t, b.BindWithDelimiter("param", &dest, ",").BindError()) + assert.Equal(t, tc.expect, dest) + case []int: + var dest []int + assert.NoError(t, b.BindWithDelimiter("param", &dest, ",").BindError()) + assert.Equal(t, tc.expect, dest) + case []uint64: + var dest []uint64 + assert.NoError(t, b.BindWithDelimiter("param", &dest, ",").BindError()) + assert.Equal(t, tc.expect, dest) + case []uint32: + var dest []uint32 + assert.NoError(t, b.BindWithDelimiter("param", &dest, ",").BindError()) + assert.Equal(t, tc.expect, dest) + case []uint16: + var dest []uint16 + assert.NoError(t, b.BindWithDelimiter("param", &dest, ",").BindError()) + assert.Equal(t, tc.expect, dest) + case []uint8: + var dest []uint8 + assert.NoError(t, b.BindWithDelimiter("param", &dest, ",").BindError()) + assert.Equal(t, tc.expect, dest) + case []uint: + var dest []uint + assert.NoError(t, b.BindWithDelimiter("param", &dest, ",").BindError()) + assert.Equal(t, tc.expect, dest) + case []float64: + var dest []float64 + assert.NoError(t, b.BindWithDelimiter("param", &dest, ",").BindError()) + assert.Equal(t, tc.expect, dest) + case []float32: + var dest []float32 + assert.NoError(t, b.BindWithDelimiter("param", &dest, ",").BindError()) + assert.Equal(t, tc.expect, dest) + case []bool: + var dest []bool + assert.NoError(t, b.BindWithDelimiter("param", &dest, ",").BindError()) + assert.Equal(t, tc.expect, dest) + case []time.Duration: + var dest []time.Duration + assert.NoError(t, b.BindWithDelimiter("param", &dest, ",").BindError()) + assert.Equal(t, tc.expect, dest) + default: + assert.Fail(t, "invalid type") + } + }) + } +} + +func TestValueBinder_BindWithDelimiter(t *testing.T) { + var testCases = []struct { + name string + givenFailFast bool + givenBindErrors []error + whenURL string + whenMust bool + expectValue []int64 + expectError string + }{ + { + name: "ok, binds value", + whenURL: "/search?param=1,2¶m=1", + expectValue: []int64{1, 2, 1}, + }, + { + name: "ok, params values empty, value is not changed", + whenURL: "/search?nope=1", + expectValue: []int64(nil), + }, + { + name: "nok, previous errors fail fast without binding value", + givenFailFast: true, + whenURL: "/search?param=1¶m=100", + expectValue: []int64(nil), + expectError: "previous error", + }, + { + name: "nok, conversion fails, value is not changed", + whenURL: "/search?param=nope¶m=100", + expectValue: []int64(nil), + expectError: "code=400, message=failed to bind field value to int64, internal=strconv.ParseInt: parsing \"nope\": invalid syntax, field=param", + }, + { + name: "ok (must), binds value", + whenMust: true, + whenURL: "/search?param=1,2¶m=1", + expectValue: []int64{1, 2, 1}, + }, + { + name: "ok (must), params values empty, returns error, value is not changed", + whenMust: true, + whenURL: "/search?nope=1", + expectValue: []int64(nil), + 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¶m=100", + expectValue: []int64(nil), + expectError: "previous error", + }, + { + name: "nok (must), conversion fails, value is not changed", + whenMust: true, + whenURL: "/search?param=nope¶m=100", + expectValue: []int64(nil), + expectError: "code=400, message=failed to bind field value to int64, 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")} + } + + var dest []int64 + var err error + if tc.whenMust { + err = b.MustBindWithDelimiter("param", &dest, ",").BindError() + } else { + err = b.BindWithDelimiter("param", &dest, ",").BindError() + } + + assert.Equal(t, tc.expectValue, dest) + if tc.expectError != "" { + assert.EqualError(t, err, tc.expectError) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestBindWithDelimiter_invalidType(t *testing.T) { + c := createTestContext("/search?param=1¶m=100", nil, nil) + b := QueryParamsBinder(c) + + var dest []BindUnmarshaler + err := b.BindWithDelimiter("param", &dest, ",").BindError() + assert.Equal(t, []BindUnmarshaler(nil), dest) + assert.EqualError(t, err, "code=400, message=unsupported bind type, field=param") +} + +func TestValueBinder_UnixTime(t *testing.T) { + exampleTime, _ := time.Parse(time.RFC3339, "2020-12-28T18:36:43+00:00") // => 1609180603 + 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 seconds", + whenURL: "/search?param=1609180603¶m=1609180604", + expectValue: exampleTime, + }, + { + name: "ok, binds value, unix time over int32 value", + whenURL: "/search?param=2147483648¶m=1609180604", + expectValue: time.Unix(2147483648, 0), + }, + { + 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¶m=100", + expectValue: time.Time{}, + expectError: "previous error", + }, + { + name: "nok, conversion fails, value is not changed", + whenURL: "/search?param=nope¶m=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=1609180603¶m=1609180604", + 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¶m=100", + expectValue: time.Time{}, + expectError: "previous error", + }, + { + name: "nok (must), conversion fails, value is not changed", + whenMust: true, + whenURL: "/search?param=nope¶m=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.MustUnixTime("param", &dest).BindError() + } else { + err = b.UnixTime("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 + exampleTimeNanoBelowSec, _ := time.Parse(time.RFC3339Nano, "1970-01-01T00:00:00.999999999+00:00") + 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 nano seconds (sec precision)", + whenURL: "/search?param=1609180603000000000¶m=1609180604", + expectValue: exampleTime, + }, + { + name: "ok, binds value, unix time in nano seconds", + whenURL: "/search?param=1609180603123456789¶m=1609180604", + expectValue: exampleTimeNano, + }, + { + name: "ok, binds value, unix time in nano seconds (below 1 sec)", + whenURL: "/search?param=999999999¶m=1609180604", + expectValue: exampleTimeNanoBelowSec, + }, + { + 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¶m=100", + expectValue: time.Time{}, + expectError: "previous error", + }, + { + name: "nok, conversion fails, value is not changed", + whenURL: "/search?param=nope¶m=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=1609180603000000000¶m=1609180604", + 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¶m=100", + expectValue: time.Time{}, + expectError: "previous error", + }, + { + name: "nok (must), conversion fails, value is not changed", + whenMust: true, + whenURL: "/search?param=nope¶m=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.MustUnixTimeNano("param", &dest).BindError() + } else { + err = b.UnixTimeNano("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 BenchmarkDefaultBinder_BindInt64_single(b *testing.B) { + type Opts struct { + Param int64 `query:"param"` + } + c := createTestContext("/search?param=1¶m=100", nil, nil) + + b.ReportAllocs() + b.ResetTimer() + binder := new(DefaultBinder) + for i := 0; i < b.N; i++ { + var dest Opts + _ = binder.Bind(&dest, c) + } +} + +func BenchmarkValueBinder_BindInt64_single(b *testing.B) { + c := createTestContext("/search?param=1¶m=100", nil, nil) + + b.ReportAllocs() + b.ResetTimer() + type Opts struct { + Param int64 + } + binder := QueryParamsBinder(c) + for i := 0; i < b.N; i++ { + var dest Opts + _ = binder.Int64("param", &dest.Param).BindError() + } +} + +func BenchmarkRawFunc_Int64_single(b *testing.B) { + c := createTestContext("/search?param=1¶m=100", nil, nil) + + rawFunc := func(input string, defaultValue int64) (int64, bool) { + if input == "" { + return defaultValue, true + } + n, err := strconv.Atoi(input) + if err != nil { + return 0, false + } + return int64(n), true + } + + b.ReportAllocs() + b.ResetTimer() + type Opts struct { + Param int64 + } + for i := 0; i < b.N; i++ { + var dest Opts + if n, ok := rawFunc(c.QueryParam("param"), 1); ok { + dest.Param = n + } + } +} + +func BenchmarkDefaultBinder_BindInt64_10_fields(b *testing.B) { + type Opts struct { + Int64 int64 `query:"int64"` + Int32 int32 `query:"int32"` + Int16 int16 `query:"int16"` + Int8 int8 `query:"int8"` + String string `query:"string"` + + Uint64 uint64 `query:"uint64"` + Uint32 uint32 `query:"uint32"` + Uint16 uint16 `query:"uint16"` + Uint8 uint8 `query:"uint8"` + Strings []string `query:"strings"` + } + c := createTestContext("/search?int64=1&int32=2&int16=3&int8=4&string=test&uint64=5&uint32=6&uint16=7&uint8=8&strings=first&strings=second", nil, nil) + + b.ReportAllocs() + b.ResetTimer() + binder := new(DefaultBinder) + for i := 0; i < b.N; i++ { + var dest Opts + _ = binder.Bind(&dest, c) + if dest.Int64 != 1 { + b.Fatalf("int64!=1") + } + } +} + +func BenchmarkValueBinder_BindInt64_10_fields(b *testing.B) { + type Opts struct { + Int64 int64 `query:"int64"` + Int32 int32 `query:"int32"` + Int16 int16 `query:"int16"` + Int8 int8 `query:"int8"` + String string `query:"string"` + + Uint64 uint64 `query:"uint64"` + Uint32 uint32 `query:"uint32"` + Uint16 uint16 `query:"uint16"` + Uint8 uint8 `query:"uint8"` + Strings []string `query:"strings"` + } + c := createTestContext("/search?int64=1&int32=2&int16=3&int8=4&string=test&uint64=5&uint32=6&uint16=7&uint8=8&strings=first&strings=second", nil, nil) + + b.ReportAllocs() + b.ResetTimer() + binder := QueryParamsBinder(c) + for i := 0; i < b.N; i++ { + var dest Opts + _ = binder. + Int64("int64", &dest.Int64). + Int32("int32", &dest.Int32). + Int16("int16", &dest.Int16). + Int8("int8", &dest.Int8). + String("string", &dest.String). + Uint64("int64", &dest.Uint64). + Uint32("int32", &dest.Uint32). + Uint16("int16", &dest.Uint16). + Uint8("int8", &dest.Uint8). + Strings("strings", &dest.Strings). + BindError() + if dest.Int64 != 1 { + b.Fatalf("int64!=1") + } + } +}