Repository: deepmap/oapi-codegen
Pull request #116 (2020-01-06 10:31:00)

Object query parameters not styled (#115)
A possible solution to the issue in which a object is specified as a query parameter which then needs to be styled at runtime to build the full request URL.


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/codegen.go b/pkg/codegen/codegen.go
index 0caf4dd..9af10fe 100644
--- a/pkg/codegen/codegen.go
+++ b/pkg/codegen/codegen.go
@@ -19,6 +19,7 @@ import (
 	"bytes"
 	"fmt"
 	"go/format"
+	"regexp"
 	"sort"
 	"strings"
 	"text/template"
@@ -56,28 +57,28 @@ type goImports []goImport
 
 var (
 	allGoImports = goImports{
-		{lookFor: "base64.", packageName: "encoding/base64"},
-		{lookFor: "bytes.", packageName: "bytes"},
-		{lookFor: "chi.", packageName: "github.com/go-chi/chi"},
-		{lookFor: "context.", packageName: "context"},
-		{lookFor: "echo.", packageName: "github.com/labstack/echo/v4"},
-		{lookFor: "errors.", packageName: "github.com/pkg/errors"},
-		{lookFor: "fmt.", packageName: "fmt"},
-		{lookFor: "gzip.", packageName: "compress/gzip"},
-		{lookFor: "http.", packageName: "net/http"},
-		{lookFor: "io.", packageName: "io"},
-		{lookFor: "ioutil.", packageName: "io/ioutil"},
-		{lookFor: "json.", packageName: "encoding/json"},
-		{lookFor: "openapi3.", packageName: "github.com/getkin/kin-openapi/openapi3"},
-		{lookFor: "openapi_types.", alias: "openapi_types", packageName: "github.com/deepmap/oapi-codegen/pkg/types"},
-		{lookFor: "path.", packageName: "path"},
-		{lookFor: "runtime.", packageName: "github.com/deepmap/oapi-codegen/pkg/runtime"},
-		{lookFor: "strings.", packageName: "strings"},
-		{lookFor: "time.Duration", packageName: "time"},
-		{lookFor: "time.Time", packageName: "time"},
-		{lookFor: "url.", packageName: "net/url"},
-		{lookFor: "xml.", packageName: "encoding/xml"},
-		{lookFor: "yaml.", packageName: "gopkg.in/yaml.v2"},
+		{lookFor: "base64\\.", packageName: "encoding/base64"},
+		{lookFor: "bytes\\.", packageName: "bytes"},
+		{lookFor: "chi\\.", packageName: "github.com/go-chi/chi"},
+		{lookFor: "context\\.", packageName: "context"},
+		{lookFor: "echo\\.", packageName: "github.com/labstack/echo/v4"},
+		{lookFor: "errors\\.", packageName: "github.com/pkg/errors"},
+		{lookFor: "fmt\\.", packageName: "fmt"},
+		{lookFor: "gzip\\.", packageName: "compress/gzip"},
+		{lookFor: "http\\.", packageName: "net/http"},
+		{lookFor: "io\\.", packageName: "io"},
+		{lookFor: "ioutil\\.", packageName: "io/ioutil"},
+		{lookFor: "json\\.", packageName: "encoding/json"},
+		{lookFor: "openapi3\\.", packageName: "github.com/getkin/kin-openapi/openapi3"},
+		{lookFor: "openapi_types\\.", alias: "openapi_types", packageName: "github.com/deepmap/oapi-codegen/pkg/types"},
+		{lookFor: "path\\.", packageName: "path"},
+		{lookFor: "runtime\\.", packageName: "github.com/deepmap/oapi-codegen/pkg/runtime"},
+		{lookFor: "strings\\.", packageName: "strings"},
+		{lookFor: "time\\.Duration", packageName: "time"},
+		{lookFor: "time\\.Time", packageName: "time"},
+		{lookFor: "url\\.", packageName: "net/url"},
+		{lookFor: "xml\\.", packageName: "encoding/xml"},
+		{lookFor: "yaml\\.", packageName: "gopkg.in/yaml.v2"},
 	}
 )
 
@@ -154,10 +155,13 @@ func Generate(swagger *openapi3.Swagger, packageName string, opts Options) (stri
 	w := bufio.NewWriter(&buf)
 
 	// Based on module prefixes, figure out which optional imports are required.
-	// TODO: this is error prone, use tighter matches
 	for _, str := range []string{typeDefinitions, chiServerOut, echoServerOut, clientOut, clientWithResponsesOut, inlinedSpec} {
 		for _, goImport := range allGoImports {
-			if strings.Contains(str, goImport.lookFor) {
+			match, err := regexp.MatchString(fmt.Sprintf("[^a-zA-Z0-9_]%s", goImport.lookFor), str)
+			if err != nil {
+				return "", errors.Wrap(err, "error figuring out imports")
+			}
+			if match {
 				imports = append(imports, goImport.String())
 			}
 		}
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/runtime/styleparam.go b/pkg/runtime/styleparam.go
index f6439e8..3edcce6 100644
--- a/pkg/runtime/styleparam.go
+++ b/pkg/runtime/styleparam.go
@@ -14,6 +14,7 @@
 package runtime
 
 import (
+	"errors"
 	"fmt"
 	"reflect"
 	"sort"
@@ -48,6 +49,8 @@ func StyleParam(style string, explode bool, paramName string, value interface{})
 		return styleSlice(style, explode, paramName, sliceVal)
 	case reflect.Struct:
 		return styleStruct(style, explode, paramName, value)
+	case reflect.Map:
+		return styleMap(style, explode, paramName, value)
 	default:
 		return stylePrimitive(style, explode, paramName, value)
 	}
@@ -161,6 +164,28 @@ func styleStruct(style string, explode bool, paramName string, value interface{}
 		fieldDict[fieldName] = str
 	}
 
+	return processFieldDict(style, explode, paramName, fieldDict)
+}
+
+func styleMap(style string, explode bool, paramName string, value interface{}) (string, error) {
+	dict, ok := value.(map[string]interface{})
+	if !ok {
+		return "", errors.New("map not of type map[string]interface{}")
+	}
+
+	fieldDict := make(map[string]string)
+	for fieldName, value := range dict {
+		str, err := primitiveToString(value)
+		if err != nil {
+			return "", fmt.Errorf("error formatting '%s': %s", paramName, err)
+		}
+		fieldDict[fieldName] = str
+	}
+
+	return processFieldDict(style, explode, paramName, fieldDict)
+}
+
+func processFieldDict(style string, explode bool, paramName string, fieldDict map[string]string) (string, error) {
 	var parts []string
 
 	// This works for everything except deepObject. We'll handle that one
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)
+	})
 }
diff --git a/pkg/runtime/styleparam_test.go b/pkg/runtime/styleparam_test.go
index e7f4b7f..d908b59 100644
--- a/pkg/runtime/styleparam_test.go
+++ b/pkg/runtime/styleparam_test.go
@@ -30,6 +30,9 @@ func TestStyleParam(t *testing.T) {
 		FirstName: "Alex",
 		Role:      "admin",
 	}
+	dict := map[string]interface{}{}
+	dict["firstName"] = "Alex"
+	dict["role"] = "admin"
 
 	// ---------------------------- Simple Style -------------------------------
 
@@ -57,6 +60,14 @@ func TestStyleParam(t *testing.T) {
 	assert.NoError(t, err)
 	assert.EqualValues(t, "firstName=Alex,role=admin", result)
 
+	result, err = StyleParam("simple", false, "id", dict)
+	assert.NoError(t, err)
+	assert.EqualValues(t, "firstName,Alex,role,admin", result)
+
+	result, err = StyleParam("simple", true, "id", dict)
+	assert.NoError(t, err)
+	assert.EqualValues(t, "firstName=Alex,role=admin", result)
+
 	// ----------------------------- Label Style -------------------------------
 
 	result, err = StyleParam("label", false, "id", primitive)
@@ -83,6 +94,14 @@ func TestStyleParam(t *testing.T) {
 	assert.NoError(t, err)
 	assert.EqualValues(t, ".firstName=Alex.role=admin", result)
 
+	result, err = StyleParam("label", false, "id", dict)
+	assert.NoError(t, err)
+	assert.EqualValues(t, ".firstName,Alex,role,admin", result)
+
+	result, err = StyleParam("label", true, "id", dict)
+	assert.NoError(t, err)
+	assert.EqualValues(t, ".firstName=Alex.role=admin", result)
+
 	// ----------------------------- Matrix Style ------------------------------
 
 	result, err = StyleParam("matrix", false, "id", primitive)
@@ -109,6 +128,14 @@ func TestStyleParam(t *testing.T) {
 	assert.NoError(t, err)
 	assert.EqualValues(t, ";firstName=Alex;role=admin", result)
 
+	result, err = StyleParam("matrix", false, "id", dict)
+	assert.NoError(t, err)
+	assert.EqualValues(t, ";id=firstName,Alex,role,admin", result)
+
+	result, err = StyleParam("matrix", true, "id", dict)
+	assert.NoError(t, err)
+	assert.EqualValues(t, ";firstName=Alex;role=admin", result)
+
 	// ------------------------------ Form Style -------------------------------
 	result, err = StyleParam("form", false, "id", primitive)
 	assert.NoError(t, err)
@@ -134,6 +161,14 @@ func TestStyleParam(t *testing.T) {
 	assert.NoError(t, err)
 	assert.EqualValues(t, "firstName=Alex&role=admin", result)
 
+	result, err = StyleParam("form", false, "id", dict)
+	assert.NoError(t, err)
+	assert.EqualValues(t, "id=firstName,Alex,role,admin", result)
+
+	result, err = StyleParam("form", true, "id", dict)
+	assert.NoError(t, err)
+	assert.EqualValues(t, "firstName=Alex&role=admin", result)
+
 	// ------------------------  spaceDelimited Style --------------------------
 
 	result, err = StyleParam("spaceDelimited", false, "id", primitive)
@@ -156,6 +191,11 @@ func TestStyleParam(t *testing.T) {
 	result, err = StyleParam("spaceDelimited", true, "id", object)
 	assert.Error(t, err)
 
+	result, err = StyleParam("spaceDelimited", false, "id", dict)
+	assert.Error(t, err)
+
+	result, err = StyleParam("spaceDelimited", true, "id", dict)
+	assert.Error(t, err)
 	// -------------------------  pipeDelimited Style --------------------------
 
 	result, err = StyleParam("pipeDelimited", false, "id", primitive)
@@ -178,6 +218,12 @@ func TestStyleParam(t *testing.T) {
 	result, err = StyleParam("pipeDelimited", true, "id", object)
 	assert.Error(t, err)
 
+	result, err = StyleParam("pipeDelimited", false, "id", dict)
+	assert.Error(t, err)
+
+	result, err = StyleParam("pipeDelimited", true, "id", dict)
+	assert.Error(t, err)
+
 	// ---------------------------  deepObject Style ---------------------------
 	result, err = StyleParam("deepObject", false, "id", primitive)
 	assert.Error(t, err)
@@ -198,6 +244,10 @@ func TestStyleParam(t *testing.T) {
 	assert.NoError(t, err)
 	assert.EqualValues(t, "id[firstName]=Alex&id[role]=admin", result)
 
+	result, err = StyleParam("deepObject", true, "id", dict)
+	assert.NoError(t, err)
+	assert.EqualValues(t, "id[firstName]=Alex&id[role]=admin", result)
+
 	// Misc tests
 	// Test type aliases
 	type StrType string