diff --git a/bindparam.go b/bindparam.go index 435caa4..c383687 100644 --- a/bindparam.go +++ b/bindparam.go @@ -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 { diff --git a/bindparam_test.go b/bindparam_test.go index eec3d3a..3712160 100644 --- a/bindparam_test.go +++ b/bindparam_test.go @@ -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, ¶ms) + 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, ¶ms) + 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"}, diff --git a/types/date.go b/types/date.go index 6663bdc..155751a 100644 --- a/types/date.go +++ b/types/date.go @@ -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 +}