Repository: deepmap/oapi-codegen
Pull request #212 (2020-07-24 14:11:18)

feat: Support custom type name
I would like to implement issue #25.

## Approach

- Add `x-go-type` extension property to specify a Go type name directly.
- Set the type name into `Schema.GoType` field
  - An undefined type in the schema is also set as it is. To allow to specify a type that is defined in other go files.
- Add handling the following base types.
  - int8, int16, uint ~ uint64

## Example

schema(snippet):
```yaml
openapi: 3.0.0
# ...
paths:
  '/pet/{id}':
    parameters:
      - schema:
          type: integer
          x-go-type: uint64 # <- custom property
        name: id
        in: path
# ...
components:
  schemas:
    Pet:
      type: object
      properties:
        id:
          type: integer
          x-go-type: uint64 # <- custom property
        createdAt:
          type: string
          x-go-type: CustomDate # <- custom property
# ...
```

generated go file(snippet):
```go
// ...
type Pet struct {
	CreatedAt *CustomDate `json:"createdAt,omitempty"` // <- CustomDate is defined in the other file.
	Id        uint64      `json:"id"`
}
// ...
func (siw *ServerInterfaceWrapper) GetPetByID(w http.ResponseWriter, r *http.Request) {
    // ...
	var id uint64 // <- custom type
	err = runtime.BindStyledParameter("simple", false, "id", chi.URLParam(r, "id"), &id)
	// ...
}
```

## Checklist

- [x] Passed `go test ./...`
- [x] Confirmed that the binding to the variable was successful with `chi-server`

bellwood4486: This is my first PR. Please let me know if there is any missing information.
Thank you.
tzapu: this would be extremely useful, any chance of getting it included?
bellwood4486: Hi, @deepmap-marcinr 
I'd appreciate it if you could look at this PR.
It seems that some other people are happy to have this extension property.
Thank you.

Patch:
diff --git a/pkg/codegen/extension.go b/pkg/codegen/extension.go
new file mode 100644
index 0000000..4aaca67
--- /dev/null
+++ b/pkg/codegen/extension.go
@@ -0,0 +1,25 @@
+package codegen
+
+import (
+	"encoding/json"
+	"fmt"
+
+	"github.com/pkg/errors"
+)
+
+const (
+	extPropGoType = "x-go-type"
+)
+
+func extTypeName(extPropValue interface{}) (string, error) {
+	raw, ok := extPropValue.(json.RawMessage)
+	if !ok {
+		return "", fmt.Errorf("failed to convert type: %T", extPropValue)
+	}
+	var name string
+	if err := json.Unmarshal(raw, &name); err != nil {
+		return "", errors.Wrap(err, "failed to unmarshal json")
+	}
+
+	return name, nil
+}
diff --git a/pkg/codegen/schema.go b/pkg/codegen/schema.go
index c9ad2ea..cbee286 100644
--- a/pkg/codegen/schema.go
+++ b/pkg/codegen/schema.go
@@ -134,12 +134,22 @@ func GenerateGoSchema(sref *openapi3.SchemaRef, path []string) (Schema, error) {
 		return mergedSchema, nil
 	}
 
-	// Schema type and format, eg. string / binary
-	t := schema.Type
-
 	outSchema := Schema{
 		RefType: refType,
 	}
+
+	// Check for custom Go type extension
+	if extension, ok := schema.Extensions[extPropGoType]; ok {
+		typeName, err := extTypeName(extension)
+		if err != nil {
+			return outSchema, errors.Wrapf(err, "invalid value for %q", extPropGoType)
+		}
+		outSchema.GoType = typeName
+		return outSchema, nil
+	}
+
+	// Schema type and format, eg. string / binary
+	t := schema.Type
 	// Handle objects and empty schemas first as a special case
 	if t == "" || t == "object" {
 		var outType string
diff --git a/pkg/runtime/bindstring.go b/pkg/runtime/bindstring.go
index 4983279..e43d2e5 100644
--- a/pkg/runtime/bindstring.go
+++ b/pkg/runtime/bindstring.go
@@ -48,12 +48,18 @@ func BindStringToObject(src string, dst interface{}) error {
 	}
 
 	switch t.Kind() {
-	case reflect.Int, reflect.Int32, reflect.Int64:
+	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
 		var val int64
 		val, err = strconv.ParseInt(src, 10, 64)
 		if err == nil {
 			v.SetInt(val)
 		}
+	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+		var val uint64
+		val, err = strconv.ParseUint(src, 10, 64)
+		if err == nil {
+			v.SetUint(val)
+		}
 	case reflect.String:
 		v.SetString(src)
 		err = nil


Test patch:
diff --git a/pkg/codegen/extension_test.go b/pkg/codegen/extension_test.go
new file mode 100644
index 0000000..64681a6
--- /dev/null
+++ b/pkg/codegen/extension_test.go
@@ -0,0 +1,50 @@
+package codegen
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_extTypeName(t *testing.T) {
+	type args struct {
+		extPropValue interface{}
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    string
+		wantErr bool
+	}{
+		{
+			name:    "success",
+			args:    args{json.RawMessage(`"uint64"`)},
+			want:    "uint64",
+			wantErr: false,
+		},
+		{
+			name:    "type conversion error",
+			args:    args{nil},
+			want:    "",
+			wantErr: true,
+		},
+		{
+			name:    "json unmarshal error",
+			args:    args{json.RawMessage("invalid json format")},
+			want:    "",
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := extTypeName(tt.args.extPropValue)
+			if tt.wantErr {
+				assert.Error(t, err)
+				return
+			}
+			assert.NoError(t, err)
+			assert.Equal(t, tt.want, got)
+		})
+	}
+}
diff --git a/pkg/runtime/bindstring_test.go b/pkg/runtime/bindstring_test.go
index 193f54e..8909e81 100644
--- a/pkg/runtime/bindstring_test.go
+++ b/pkg/runtime/bindstring_test.go
@@ -32,13 +32,21 @@ func TestBindStringToObject(t *testing.T) {
 	assert.Error(t, BindStringToObject("foo", &i))
 	assert.Error(t, BindStringToObject("1,2,3", &i))
 
-	var i64 int64
-	assert.NoError(t, BindStringToObject("124", &i64))
-	assert.Equal(t, int64(124), i64)
+	var i8 int8
+	assert.NoError(t, BindStringToObject("12", &i8))
+	assert.Equal(t, int8(12), i8)
 
-	assert.Error(t, BindStringToObject("5.7", &i64))
-	assert.Error(t, BindStringToObject("foo", &i64))
-	assert.Error(t, BindStringToObject("1,2,3", &i64))
+	assert.Error(t, BindStringToObject("5.7", &i8))
+	assert.Error(t, BindStringToObject("foo", &i8))
+	assert.Error(t, BindStringToObject("1,2,3", &i8))
+
+	var i16 int16
+	assert.NoError(t, BindStringToObject("12", &i16))
+	assert.Equal(t, int16(12), i16)
+
+	assert.Error(t, BindStringToObject("5.7", &i16))
+	assert.Error(t, BindStringToObject("foo", &i16))
+	assert.Error(t, BindStringToObject("1,2,3", &i16))
 
 	var i32 int32
 	assert.NoError(t, BindStringToObject("12", &i32))
@@ -48,6 +56,54 @@ func TestBindStringToObject(t *testing.T) {
 	assert.Error(t, BindStringToObject("foo", &i32))
 	assert.Error(t, BindStringToObject("1,2,3", &i32))
 
+	var i64 int64
+	assert.NoError(t, BindStringToObject("124", &i64))
+	assert.Equal(t, int64(124), i64)
+
+	assert.Error(t, BindStringToObject("5.7", &i64))
+	assert.Error(t, BindStringToObject("foo", &i64))
+	assert.Error(t, BindStringToObject("1,2,3", &i64))
+
+	var u uint
+	assert.NoError(t, BindStringToObject("5", &u))
+	assert.Equal(t, uint(5), u)
+
+	assert.Error(t, BindStringToObject("5.7", &u))
+	assert.Error(t, BindStringToObject("foo", &u))
+	assert.Error(t, BindStringToObject("1,2,3", &u))
+
+	var u8 uint8
+	assert.NoError(t, BindStringToObject("12", &u8))
+	assert.Equal(t, uint8(12), u8)
+
+	assert.Error(t, BindStringToObject("5.7", &u8))
+	assert.Error(t, BindStringToObject("foo", &u8))
+	assert.Error(t, BindStringToObject("1,2,3", &u8))
+
+	var u16 uint16
+	assert.NoError(t, BindStringToObject("12", &u16))
+	assert.Equal(t, uint16(12), u16)
+
+	assert.Error(t, BindStringToObject("5.7", &u16))
+	assert.Error(t, BindStringToObject("foo", &u16))
+	assert.Error(t, BindStringToObject("1,2,3", &u16))
+
+	var u32 uint32
+	assert.NoError(t, BindStringToObject("12", &u32))
+	assert.Equal(t, uint32(12), u32)
+
+	assert.Error(t, BindStringToObject("5.7", &u32))
+	assert.Error(t, BindStringToObject("foo", &u32))
+	assert.Error(t, BindStringToObject("1,2,3", &u32))
+
+	var u64 uint64
+	assert.NoError(t, BindStringToObject("124", &u64))
+	assert.Equal(t, uint64(124), u64)
+
+	assert.Error(t, BindStringToObject("5.7", &u64))
+	assert.Error(t, BindStringToObject("foo", &u64))
+	assert.Error(t, BindStringToObject("1,2,3", &u64))
+
 	var b bool
 	assert.NoError(t, BindStringToObject("True", &b))
 	assert.Equal(t, true, b)