Repository: deepmap/oapi-codegen
Pull request #112 (2020-01-02 17:27:54)

Fixing query parsing of date parameters
Looking at [this issue](https://github.com/deepmap/oapi-codegen/issues/104), I neglected to handle query parsing on the server side when implementing the custom Date type.

This attempts to rectify that, with the caveat that this could probably stand a more elegant solution if additional custom types are added.


Patch:
diff --git a/examples/petstore-expanded/chi/api/petstore.gen.go b/examples/petstore-expanded/chi/api/petstore.gen.go
index 1a3a817..b757bfa 100644
--- a/examples/petstore-expanded/chi/api/petstore.gen.go
+++ b/examples/petstore-expanded/chi/api/petstore.gen.go
@@ -161,8 +161,11 @@ func FindPetByIdCtx(next http.Handler) http.Handler {
 
 // Handler creates http.Handler with routing matching OpenAPI spec.
 func Handler(si ServerInterface) http.Handler {
-	r := chi.NewRouter()
+	return HandlerFromMux(si, chi.NewRouter())
+}
 
+// HandlerFromMux creates http.Handler with routing matching OpenAPI spec based on the provided mux.
+func HandlerFromMux(si ServerInterface, r *chi.Mux) http.Handler {
 	r.Group(func(r chi.Router) {
 		r.Use(FindPetsCtx)
 		r.Get("/pets", si.FindPets)
diff --git a/pkg/codegen/templates/chi-handler.tmpl b/pkg/codegen/templates/chi-handler.tmpl
index 335a1e6..f6bfad2 100644
--- a/pkg/codegen/templates/chi-handler.tmpl
+++ b/pkg/codegen/templates/chi-handler.tmpl
@@ -1,7 +1,10 @@
 // Handler creates http.Handler with routing matching OpenAPI spec.
 func Handler(si ServerInterface) http.Handler {
-  r := chi.NewRouter()
+  return HandlerFromMux(si, chi.NewRouter())
+}
 
+// HandlerFromMux creates http.Handler with routing matching OpenAPI spec based on the provided mux.
+func HandlerFromMux(si ServerInterface, r *chi.Mux) http.Handler {
 {{range .}}r.Group(func(r chi.Router) {
   r.Use({{.OperationId}}Ctx)
   r.{{.Method | lower | title }}("{{.Path | swaggerUriToChiUri}}", si.{{.OperationId}})
diff --git a/pkg/codegen/templates/templates.gen.go b/pkg/codegen/templates/templates.gen.go
index fedb94f..0c7f6a6 100644
--- a/pkg/codegen/templates/templates.gen.go
+++ b/pkg/codegen/templates/templates.gen.go
@@ -75,8 +75,11 @@ func (a {{.TypeName}}) MarshalJSON() ([]byte, error) {
 `,
 	"chi-handler.tmpl": `// Handler creates http.Handler with routing matching OpenAPI spec.
 func Handler(si ServerInterface) http.Handler {
-  r := chi.NewRouter()
+  return HandlerFromMux(si, chi.NewRouter())
+}
 
+// HandlerFromMux creates http.Handler with routing matching OpenAPI spec based on the provided mux.
+func HandlerFromMux(si ServerInterface, r *chi.Mux) http.Handler {
 {{range .}}r.Group(func(r chi.Router) {
   r.Use({{.OperationId}}Ctx)
   r.{{.Method | lower | title }}("{{.Path | swaggerUriToChiUri}}", si.{{.OperationId}})
diff --git a/pkg/runtime/bindparam.go b/pkg/runtime/bindparam.go
index f4b1c31..18a4130 100644
--- a/pkg/runtime/bindparam.go
+++ b/pkg/runtime/bindparam.go
@@ -21,6 +21,7 @@ import (
 	"reflect"
 	"strings"
 
+	"github.com/deepmap/oapi-codegen/pkg/types"
 	"github.com/labstack/echo/v4"
 )
 
@@ -299,6 +300,7 @@ func BindQueryParameter(style string, explode bool, required bool, paramName str
 			// different things based on destination type.
 			values, found := queryParams[paramName]
 			var err error
+
 			switch k {
 			case reflect.Slice:
 				// In the slice case, we simply use the arguments provided by
@@ -430,6 +432,12 @@ func BindQueryParameter(style string, explode bool, required bool, paramName str
 // We don't try to be smart here, if the field exists as a query argument,
 // set its value.
 func bindParamsToExplodedObject(paramName string, values url.Values, dest interface{}) error {
+	// special handling for custom types
+	switch dest.(type) {
+	case *types.Date:
+		return BindStringToObject(values.Get(paramName), dest)
+	}
+
 	v := reflect.Indirect(reflect.ValueOf(dest))
 	if v.Type().Kind() != reflect.Struct {
 		return echo.NewHTTPError(http.StatusInternalServerError,
diff --git a/pkg/runtime/bindstring.go b/pkg/runtime/bindstring.go
index 384427d..f3397a2 100644
--- a/pkg/runtime/bindstring.go
+++ b/pkg/runtime/bindstring.go
@@ -19,6 +19,8 @@ import (
 	"reflect"
 	"strconv"
 	"time"
+
+	"github.com/deepmap/oapi-codegen/pkg/types"
 )
 
 // This function takes a string, and attempts to assign it to the destination
@@ -68,13 +70,21 @@ func BindStringToObject(src string, dst interface{}) error {
 			v.SetBool(val)
 		}
 	case reflect.Struct:
-		// Time is a special case of a struct that we handle
-		if dstTime, ok := dst.(*time.Time); ok {
+		switch dstType := dst.(type) {
+		case *time.Time:
+			// Time is a special case of a struct that we handle
 			parsedTime, err := time.Parse(time.RFC3339Nano, src)
 			if err != nil {
 				return fmt.Errorf("error parsing '%s' as RFC3339 time: %s", src, err)
 			}
-			*dstTime = parsedTime
+			*dstType = parsedTime
+			return nil
+		case *types.Date:
+			parsedTime, err := time.Parse(types.DateFormat, src)
+			if err != nil {
+				return fmt.Errorf("error parsing '%s' as date: %s", src, err)
+			}
+			dstType.Time = parsedTime
 			return nil
 		}
 		fallthrough
diff --git a/pkg/types/date.go b/pkg/types/date.go
index 5d981a2..bdf94a9 100644
--- a/pkg/types/date.go
+++ b/pkg/types/date.go
@@ -5,14 +5,14 @@ import (
 	"time"
 )
 
-const dateFormat = "2006-01-02"
+const DateFormat = "2006-01-02"
 
 type Date struct {
 	time.Time
 }
 
 func (d Date) MarshalJSON() ([]byte, error) {
-	return json.Marshal(d.Time.Format(dateFormat))
+	return json.Marshal(d.Time.Format(DateFormat))
 }
 
 func (d *Date) UnmarshalJSON(data []byte) error {
@@ -21,7 +21,7 @@ func (d *Date) UnmarshalJSON(data []byte) error {
 	if err != nil {
 		return err
 	}
-	parsed, err := time.Parse(dateFormat, dateStr)
+	parsed, err := time.Parse(DateFormat, dateStr)
 	if err != nil {
 		return err
 	}


Test patch:
diff --git a/internal/test/server/server.gen.go b/internal/test/server/server.gen.go
index c30b55f..32f5d35 100644
--- a/internal/test/server/server.gen.go
+++ b/internal/test/server/server.gen.go
@@ -361,8 +361,11 @@ func UpdateResource3Ctx(next http.Handler) http.Handler {
 
 // Handler creates http.Handler with routing matching OpenAPI spec.
 func Handler(si ServerInterface) http.Handler {
-	r := chi.NewRouter()
+	return HandlerFromMux(si, chi.NewRouter())
+}
 
+// HandlerFromMux creates http.Handler with routing matching OpenAPI spec based on the provided mux.
+func HandlerFromMux(si ServerInterface, r *chi.Mux) http.Handler {
 	r.Group(func(r chi.Router) {
 		r.Use(GetSimpleCtx)
 		r.Get("/get-simple", si.GetSimple)
diff --git a/pkg/runtime/bindparam_test.go b/pkg/runtime/bindparam_test.go
index c475183..fcb8275 100644
--- a/pkg/runtime/bindparam_test.go
+++ b/pkg/runtime/bindparam_test.go
@@ -16,7 +16,9 @@ package runtime
 import (
 	"net/url"
 	"testing"
+	"time"
 
+	"github.com/deepmap/oapi-codegen/pkg/types"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -229,24 +231,43 @@ func TestSplitParameter(t *testing.T) {
 }
 
 func TestBindQueryParameter(t *testing.T) {
-	type ID struct {
-		FirstName *string `json:"firstName"`
-		LastName  *string `json:"lastName"`
-		Role      string  `json:"role"`
-	}
-
-	expectedName := "Alex"
-	expectedDeepObject := &ID{FirstName: &expectedName, Role: "admin"}
-
-	actual := new(ID)
-	paramName := "id"
-	queryParams := url.Values{
-		"id[firstName]": {"Alex"},
-		"id[role]":      {"admin"},
-		"foo":           {"bar"},
-	}
-
-	err := BindQueryParameter("deepObject", true, false, paramName, queryParams, &actual)
-	assert.NoError(t, err)
-	assert.Equal(t, expectedDeepObject, actual)
+	t.Run("deepObject", func(t *testing.T) {
+		type ID struct {
+			FirstName *string     `json:"firstName"`
+			LastName  *string     `json:"lastName"`
+			Role      string      `json:"role"`
+			Birthday  *types.Date `json:"birthday"`
+		}
+
+		expectedName := "Alex"
+		expectedDeepObject := &ID{
+			FirstName: &expectedName,
+			Role:      "admin",
+			Birthday:  &types.Date{time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)},
+		}
+
+		actual := new(ID)
+		paramName := "id"
+		queryParams := url.Values{
+			"id[firstName]": {"Alex"},
+			"id[role]":      {"admin"},
+			"foo":           {"bar"},
+			"id[birthday]":  {"2020-01-01"},
+		}
+
+		err := BindQueryParameter("deepObject", true, false, paramName, queryParams, &actual)
+		assert.NoError(t, err)
+		assert.Equal(t, expectedDeepObject, actual)
+	})
+
+	t.Run("form", func(t *testing.T) {
+		expected := &types.Date{time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)}
+		birthday := &types.Date{}
+		queryParams := url.Values{
+			"birthday": {"2020-01-01"},
+		}
+		err := BindQueryParameter("form", true, false, "birthday", queryParams, &birthday)
+		assert.NoError(t, err)
+		assert.Equal(t, expected, birthday)
+	})
 }