Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion bindparam.go
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,24 @@ func BindQueryParameter(style string, explode bool, required bool, paramName str
case reflect.Slice:
err = bindSplitPartsToDestinationArray(parts, output)
case reflect.Struct:
err = bindSplitPartsToDestinationStruct(paramName, parts, explode, output)
// Some struct types (e.g. types.Date, time.Time) are scalar values
// that should be bound from a single string, not decomposed as
// key-value objects. Detect these via the Binder and
// TextUnmarshaler interfaces.
switch v := output.(type) {
case Binder:
if len(parts) != 1 {
return fmt.Errorf("multiple values for single value parameter '%s'", paramName)
}
err = v.Bind(parts[0])
case encoding.TextUnmarshaler:
if len(parts) != 1 {
return fmt.Errorf("multiple values for single value parameter '%s'", paramName)
}
err = v.UnmarshalText([]byte(parts[0]))
default:
err = bindSplitPartsToDestinationStruct(paramName, parts, explode, output)
}
default:
if len(parts) == 0 {
if required {
Expand Down
101 changes: 101 additions & 0 deletions bindparam_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,107 @@ func TestBindQueryParameter(t *testing.T) {
assert.Equal(t, expected, birthday)
})

// Regression tests for https://github.com/oapi-codegen/runtime/issues/21
// types.Date should bind correctly as a query parameter in all configurations.
t.Run("date_form_explode_required", func(t *testing.T) {
expectedDate := types.Date{Time: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)}
var date types.Date
queryParams := url.Values{
"date": {"2023-01-01"},
}
err := BindQueryParameter("form", true, true, "date", queryParams, &date)
assert.NoError(t, err)
assert.Equal(t, expectedDate, date)
})

t.Run("date_form_explode_optional", func(t *testing.T) {
expectedDate := types.Date{Time: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)}
var date *types.Date
queryParams := url.Values{
"date": {"2023-01-01"},
}
err := BindQueryParameter("form", true, false, "date", queryParams, &date)
assert.NoError(t, err)
require.NotNil(t, date)
assert.Equal(t, expectedDate, *date)
})

t.Run("date_form_explode_optional_missing", func(t *testing.T) {
var date *types.Date
queryParams := url.Values{}
err := BindQueryParameter("form", true, false, "date", queryParams, &date)
assert.NoError(t, err)
assert.Nil(t, date)
})

t.Run("date_form_no_explode_required", func(t *testing.T) {
expectedDate := types.Date{Time: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)}
var date types.Date
queryParams := url.Values{
"date": {"2023-01-01"},
}
err := BindQueryParameter("form", false, true, "date", queryParams, &date)
assert.NoError(t, err)
assert.Equal(t, expectedDate, date)
})

t.Run("date_form_no_explode_optional", func(t *testing.T) {
expectedDate := types.Date{Time: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)}
var date *types.Date
queryParams := url.Values{
"date": {"2023-01-01"},
}
err := BindQueryParameter("form", false, false, "date", queryParams, &date)
assert.NoError(t, err)
require.NotNil(t, date)
assert.Equal(t, expectedDate, *date)
})

// time.Time has the same bug as types.Date for form/no-explode.
t.Run("time_form_no_explode_required", func(t *testing.T) {
expectedTime := time.Date(2020, 12, 9, 16, 9, 53, 0, time.UTC)
var ts time.Time
queryParams := url.Values{
"ts": {"2020-12-09T16:09:53Z"},
}
err := BindQueryParameter("form", false, true, "ts", queryParams, &ts)
assert.NoError(t, err)
assert.Equal(t, expectedTime, ts)
})

t.Run("date_in_struct_form_explode", func(t *testing.T) {
type Params struct {
Name string `json:"name"`
StartDate types.Date `json:"start_date"`
}
queryParams := url.Values{
"name": {"test"},
"start_date": {"2023-06-15"},
}
var params Params
err := BindQueryParameter("form", true, true, "params", queryParams, &params)
assert.NoError(t, err)
assert.Equal(t, "test", params.Name)
assert.Equal(t, types.Date{Time: time.Date(2023, 6, 15, 0, 0, 0, 0, time.UTC)}, params.StartDate)
})

t.Run("date_pointer_in_struct_form_explode", func(t *testing.T) {
type Params struct {
Name string `json:"name"`
StartDate *types.Date `json:"start_date"`
}
queryParams := url.Values{
"name": {"test"},
"start_date": {"2023-06-15"},
}
var params Params
err := BindQueryParameter("form", true, true, "params", queryParams, &params)
assert.NoError(t, err)
assert.Equal(t, "test", params.Name)
require.NotNil(t, params.StartDate)
assert.Equal(t, types.Date{Time: time.Date(2023, 6, 15, 0, 0, 0, 0, time.UTC)}, *params.StartDate)
})

t.Run("optional", func(t *testing.T) {
queryParams := url.Values{
"time": {"2020-12-09T16:09:53+00:00"},
Expand Down
15 changes: 15 additions & 0 deletions types/date.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,18 @@ func (d *Date) UnmarshalText(data []byte) error {
d.Time = parsed
return nil
}

// Bind implements the runtime.Binder interface so that Date is treated as a
// scalar value when binding query parameters rather than being decomposed as
// a struct with key-value pairs.
func (d *Date) Bind(src string) error {
if src == "" {
return nil
}
parsed, err := time.Parse(DateFormat, src)
if err != nil {
return err
}
d.Time = parsed
return nil
}