diff --git a/README.md b/README.md index 9a4d1e9..ee6e98e 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,29 @@ api.Walk(func(path string, endpoint *swagger.Endpoint) { }) ``` +### Custom Types + +For types implementing `json.Marshaler` whose JSON output does not match their Go types (such as `time.Time`), +it is possible to override the default scanning of types and define a property manually. + +For example: + +```go +type Person struct { + Name string `json:"name"` + Birthday time.Time `json:"birthday"` +} + +func main() { + RegisterCustomType(time.Time{}, Property{ + Type: "string", + Format: "date-time", + }) +} +``` + +`time.Time` is automatically registered as a custom type. + ## Complete Example ```go diff --git a/endpoint/builder.go b/endpoint/builder.go index ca39333..7ab4d72 100644 --- a/endpoint/builder.go +++ b/endpoint/builder.go @@ -112,6 +112,19 @@ func Query(name, typ, description string, required bool) Option { return parameter(p) } +// FormData defines a formData parameter for the endpoint; name, typ, description, and required correspond to the matching +// swagger fields +func FormData(name, typ, description string, required bool) Option { + p := swagger.Parameter{ + Name: name, + In: "formData", + Type: typ, + Description: description, + Required: required, + } + return parameter(p) +} + // Body defines a body parameter for the swagger endpoint as would commonly be used for the POST, PUT, and PATCH methods // prototype should be a struct or a pointer to struct that swag can use to reflect upon the return type // t represents the Type of the body diff --git a/examples/builtin/main.go b/examples/builtin/main.go index cb9d9af..12a8997 100644 --- a/examples/builtin/main.go +++ b/examples/builtin/main.go @@ -17,6 +17,7 @@ package main import ( "io" "net/http" + "time" "github.com/savaki/swag" "github.com/savaki/swag/endpoint" @@ -35,11 +36,13 @@ type Category struct { // Pet example from the swagger pet store type Pet struct { - ID int64 `json:"id"` - Category Category `json:"category"` - Name string `json:"name"` - PhotoUrls []string `json:"photoUrls"` - Tags []string `json:"tags"` + ID int64 `json:"id"` + Category Category `json:"category"` + Name string `json:"name"` + PhotoUrls []string `json:"photoUrls"` + Tags []string `json:"tags"` + CreatedAt time.Time `json:"createdAt"` + DeletedAt *time.Time `json:"deletedAt"` } func main() { diff --git a/swagger/api.go b/swagger/api.go index e710021..efd71d8 100644 --- a/swagger/api.go +++ b/swagger/api.go @@ -46,6 +46,7 @@ type Property struct { Ref string `json:"$ref,omitempty"` Example string `json:"example,omitempty"` Items *Items `json:"items,omitempty"` + Pattern string `json:"pattern,omitempty"` } // Contact represents the contact entity from the swagger definition; used by Info diff --git a/swagger/reflect.go b/swagger/reflect.go index f8c7a27..fd4154c 100644 --- a/swagger/reflect.go +++ b/swagger/reflect.go @@ -17,11 +17,84 @@ package swagger import ( "reflect" "strings" + "time" ) -func inspect(t reflect.Type, jsonTag string) Property { +var customTypes map[reflect.Type]Property + +func init() { + customTypes = map[reflect.Type]Property{} + + RegisterCustomType(time.Time{}, Property{ + Type: "string", + Format: "date-time", + }) +} + +// RegisterCustomType maps a reflect.Type to a pre-defined Property. This can be +// used to handle types that implement json.Marshaler or other interfaces. +// For example, a property with a Go type of time.Time would be represented as +// an object when it should be a string. +// +// RegisterCustomType(time.Time{}, Property{ +// Type: "string", +// Format: "date-time", +// }) +// +// Pointers to registered types will resolve to the same Property value unless +// that pointer type has also been registered as a custom type. +// +// For example: registering time.Time will also apply to *time.Time, unless +// *time.Time has also been registered. +func RegisterCustomType(v interface{}, p Property) { + t := reflect.TypeOf(v) + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + p.GoType = t + customTypes[t] = p +} + +func inspect(t reflect.Type, tag *reflect.StructTag) Property { + var jsonTag string + var desc string + var example string + var enum []string + if tag != nil { + jsonTag = tag.Get("json") + desc = tag.Get("description") + example = tag.Get("example") + if enums := tag.Get("enum"); enums != "" { + enum = strings.Split(enums, "|") + } + } + if p, ok := customTypes[t]; ok { + if p.Description == "" { + p.Description = desc + } + if p.Example == "" { + p.Example = example + } + return p + } + + if t.Kind() == reflect.Ptr { + if p, ok := customTypes[t.Elem()]; ok { + if p.Description == "" { + p.Description = desc + } + if p.Example == "" { + p.Example = example + } + return p + } + } + p := Property{ - GoType: t, + GoType: t, + Description: desc, + Example: example, + Enum: enum, } if strings.Contains(jsonTag, ",string") { @@ -58,6 +131,7 @@ func inspect(t reflect.Type, jsonTag string) Property { case reflect.Ptr: p.GoType = t.Elem() + p.Type = getType(t.Elem()) name := makeName(p.GoType) p.Ref = makeRef(name) @@ -100,6 +174,24 @@ func inspect(t reflect.Type, jsonTag string) Property { return p } +func getType(t reflect.Type) string { + switch t.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Int64, reflect.Uint64: + return "integer" + + case reflect.Float64, reflect.Float32: + return "number" + + case reflect.Bool: + return "boolean" + + case reflect.String: + return "string" + default: + return "" + } +} + func defineObject(v interface{}) Object { var required []string @@ -122,7 +214,7 @@ func defineObject(v interface{}) Object { } if t.Kind() != reflect.Struct { - p := inspect(t, "") + p := inspect(t, nil) return Object{ IsArray: isArray, GoType: t, @@ -141,6 +233,11 @@ func defineObject(v interface{}) Object { continue } + // skip fields tagged with `swagger:"-"` + if field.Tag.Get("swagger") == "-" { + continue + } + // determine the json name of the field name := strings.TrimSpace(field.Tag.Get("json")) if name == "" || strings.HasPrefix(name, ",") { @@ -167,7 +264,7 @@ func defineObject(v interface{}) Object { required = append(required, name) } - p := inspect(field.Type, field.Tag.Get("json")) + p := inspect(field.Type, &field.Tag) properties[name] = p } @@ -193,6 +290,9 @@ func define(v interface{}) map[string]Object { dirty = false for _, d := range objMap { for _, p := range d.Properties { + if _, ok := customTypes[p.GoType]; ok { + continue + } if p.GoType.Kind() == reflect.Struct { name := makeName(p.GoType) if _, exists := objMap[name]; !exists { diff --git a/swagger/reflect_test.go b/swagger/reflect_test.go index 80ab248..a15f004 100644 --- a/swagger/reflect_test.go +++ b/swagger/reflect_test.go @@ -17,10 +17,10 @@ package swagger import ( "bytes" "encoding/json" + "fmt" "io/ioutil" "testing" - - "fmt" + "time" "github.com/stretchr/testify/assert" ) @@ -38,6 +38,10 @@ type Pet struct { IntArray []int String string StringArray []string + Time time.Time + TimePtr *time.Time + + Skip string `swagger:"-"` unexported string } @@ -57,7 +61,7 @@ func TestDefine(t *testing.T) { obj, ok := v["swaggerPet"] assert.True(t, ok) assert.False(t, obj.IsArray) - assert.Equal(t, 8, len(obj.Properties)) + assert.Equal(t, 10, len(obj.Properties)) content := map[string]Object{} data, err := ioutil.ReadFile("testdata/pet.json") @@ -132,6 +136,17 @@ func TestHonorJsonIgnore(t *testing.T) { assert.Equal(t, 0, len(obj.Properties), "expected zero exposed properties") } +func TestCustomTypes(t *testing.T) { + type ContainsCustomType struct { + TestTime time.Time `json:"testTime"` + } + + obj := defineObject(ContainsCustomType{}) + + assert.Contains(t, obj.Properties, "testTime") + assert.EqualValues(t, "string", obj.Properties["testTime"].Type) +} + func TestIgnoreUnexported(t *testing.T) { type Test struct { Exported string diff --git a/swagger/testdata/pet.json b/swagger/testdata/pet.json index c272e74..0ec8348 100644 --- a/swagger/testdata/pet.json +++ b/swagger/testdata/pet.json @@ -33,6 +33,14 @@ "type": "string" } }, + "Time": { + "type": "string", + "format": "date-time" + }, + "TimePtr": { + "type": "string", + "format": "date-time" + }, "friend": { "$ref": "#/definitions/swaggerPerson" }, @@ -53,4 +61,4 @@ } } } -} \ No newline at end of file +} diff --git a/swagger/util.go b/swagger/util.go index 086fd10..1a2a6df 100644 --- a/swagger/util.go +++ b/swagger/util.go @@ -20,6 +20,16 @@ import ( "strings" ) +var useFullPath int8 = 1 + +func UseFullPathInName(flag bool) { + if flag { + useFullPath = 1 + } else { + useFullPath = 0 + } +} + func makeRef(name string) string { return fmt.Sprintf("#/definitions/%v", name) } @@ -30,6 +40,12 @@ type reflectType interface { } func makeName(t reflectType) string { - name := filepath.Base(t.PkgPath()) + t.Name() + var name string + pkgPath := t.PkgPath() + if pkgPath == "" || useFullPath == 0 { + name = t.Name() + } else { + name = filepath.Base(pkgPath) + t.Name() + } return strings.Replace(name, "-", "_", -1) }