diff --git a/README.md b/README.md index 3bcada6..18ae1c5 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,29 @@ Golang 1.16+ go get -u github.com/zc2638/swag@v1.4.3 ``` +## Use note: +The time.time type uses jsonTag to parse it into a string type +```go +type XXX struct{ + Now time.Time `json:",string"` +} +``` + +The compatibility is resolved as follows: +```go +type xxx struct { + MapSlicePtr map[string][]*string + MapSlice map[string][]string + MapSliceStructPtr map[string][]*Person + MapSliceStruct map[string][]Person + SliceStructPtr *[]*Person + SliceStruct *[]Person + SliceStringPtr *[]*string + SliceString *[]string +} + +``` + **Tip:** As of `v1.2.0`, lower versions are no longer compatible. In order to be compatible with most web frameworks, the overall architecture has been greatly changed. diff --git a/README_zh.md b/README_zh.md index 3cdf28c..0d9e048 100644 --- a/README_zh.md +++ b/README_zh.md @@ -24,6 +24,29 @@ Golang 1.16+ go get -u github.com/zc2638/swag@v1.4.3 ``` +## 使用注意: +time.time类型使用jsonTag来将其解析为string类型 +```go +type XXX struct{ + Now time.Time `json:",string"` +} +``` + +兼容的解析如下: +```go +type xxx struct { + MapSlicePtr map[string][]*string + MapSlice map[string][]string + MapSliceStructPtr map[string][]*Person + MapSliceStruct map[string][]Person + SliceStructPtr *[]*Person + SliceStruct *[]Person + SliceStringPtr *[]*string + SliceString *[]string +} + +``` + **Tip:** 从 `v1.2.0` 开始,低版本不再兼容。为了兼容大部分的web框架,整体架构做了很大的改动。 ## 默认 Swagger UI 服务器 diff --git a/api.go b/api.go index eb343ba..fda374d 100644 --- a/api.go +++ b/api.go @@ -41,6 +41,18 @@ type Object struct { // Property represents the property entity from the swagger definition type Property struct { + GoType reflect.Type `json:"-"` + Type string `json:"type,omitempty"` + Description string `json:"description,omitempty"` + Enum []string `json:"enum,omitempty"` + Format string `json:"format,omitempty"` + Ref string `json:"$ref,omitempty"` + Example string `json:"example,omitempty"` + Items *Items `json:"items,omitempty"` + AddPropertie *AdditionalProperties `json:"additionalProperties,omitempty"` +} + +type AdditionalProperties struct { GoType reflect.Type `json:"-"` Type string `json:"type,omitempty"` Description string `json:"description,omitempty"` diff --git a/reflect.go b/reflect.go index cca6842..66cebcf 100644 --- a/reflect.go +++ b/reflect.go @@ -15,10 +15,9 @@ package swag import ( + "github.com/zc2638/swag/types" "reflect" "strings" - - "github.com/zc2638/swag/types" ) func inspect(t reflect.Type, jsonTag string) Property { @@ -67,16 +66,41 @@ func inspect(t reflect.Type, jsonTag string) Property { name := makeName(p.GoType) p.Ref = makeRef(name) + case reflect.Map: + p.Type = "object" + p.AddPropertie = buildMapType(p.GoType) + // get the go reflect type of the map value + p.GoType = p.AddPropertie.GoType + case reflect.Slice: p.Type = types.Array.String() p.Items = &Items{} - p.GoType = t.Elem() // dereference the slice + // dereference the slice,get the element object of the slice + // Example: + // t ==> *[]*Struct|Struct|*string|string + // p.GoType.Kind() ==> []*Struct|Struct|*string|string + + //p.GoType ==> *Struct|Struct|*string|string + p.GoType = p.GoType.Elem() + switch p.GoType.Kind() { case reflect.Ptr: + //p.GoType ==> Struct|string p.GoType = p.GoType.Elem() - name := makeName(p.GoType) - p.Items.Ref = makeRef(name) + + // determine the type of element object in the slice + isPrimitive := isPrimitiveType(p.GoType.Name(), p.GoType) + + if isPrimitive { + // golang built-in primitive type + kind_type := jsonSchemaType(p.GoType.String(), p.GoType) + p.Items.Type = kind_type + } else { + // Struct types + name := makeName(p.GoType) + p.Items.Ref = makeRef(name) + } case reflect.Struct: name := makeName(p.GoType) @@ -106,6 +130,145 @@ func inspect(t reflect.Type, jsonTag string) Property { return p } +// buildMapType build map type swag info +func buildMapType(mapType reflect.Type) *AdditionalProperties { + + prop := AdditionalProperties{} + + // get the element object of the map + // Example: + // mapType ==> map[string][]*Struct|Struct|*string|string + // or mapTYpe ==> map[string]*Struct|Struct|*string|string + // mapType.Elem().Kind() ==> []*Struct|Struct|*string|string + // or mapType.Elem().Kind() ==> *Struct|Struct|*string|string + if mapType.Elem().Kind().String() != "interface" { + isSlice := isSliceOrArryType(mapType.Elem().Kind()) + if isSlice || isByteArrayType(mapType.Elem()) { + // if map value is slice + // Example: + // mapType.Elem()==> []*Struct|Struct|*string|string + mapType = mapType.Elem() + } + + // if map value is struct or built-in primitive type + // Example: + // mapType.Elem()==> *Struct|Struct|*string|string + isPrimitive := isPrimitiveType(mapType.Elem().Name(), mapType.Elem()) + + if isByteArrayType(mapType.Elem()) { + prop.Type = "string" + } else { + if isSlice { + prop.Type = types.Array.String() + prop.Items = &Items{} + if isPrimitive { + prop.Items.Type = jsonSchemaType(mapType.Elem().String(), mapType.Elem()) + prop.GoType = getGoType(mapType.Elem()) + } else { + prop.Items.Ref = makeMapRef(mapType.Elem().String()) + prop.GoType = getGoType(mapType.Elem()) + } + } else if isPrimitive { + prop.Type = jsonSchemaType(mapType.Elem().String(), mapType.Elem()) + prop.GoType = getGoType(mapType.Elem()) + } else { + prop.Ref = makeMapRef(mapType.Elem().String()) + prop.GoType = getGoType(mapType.Elem()) + } + } + } + + return &prop +} + +func getGoType(t reflect.Type) reflect.Type { + + var goType reflect.Type + + if t.Kind() == reflect.Ptr { + goType = t.Elem() + } else { + goType = t + } + + return goType +} + +func makeMapRef(typeName string) string { + type_name := strings.Trim(typeName, "*") + return makeRef(type_name) +} + +func isSliceOrArryType(t reflect.Kind) bool { + return t == reflect.Slice || t == reflect.Array +} + +// isByteArrayType +// Check if the data is of slice or array type +// while simultaneously verifying if the element type is of byte type(in go defined "type byte=uint8"). +func isByteArrayType(t reflect.Type) bool { + return (t.Kind() == reflect.Slice || t.Kind() == reflect.Array) && + t.Elem().Kind() == reflect.Uint8 +} + +// isPrimitiveType Whether it is a built-in primitive type +func isPrimitiveType(modelName string, modelType reflect.Type) bool { + var modelKind reflect.Kind + + if modelType.Kind() == reflect.Ptr { + modelKind = modelType.Elem().Kind() + } else { + modelKind = modelType.Kind() + } + + switch modelKind { + case reflect.Bool: + return true + case reflect.Float32, reflect.Float64, + reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return true + case reflect.String: + return true + } + + if len(modelName) == 0 { + return false + } + + _, ok := types.SchemaMap[modelName] + return ok +} + +func jsonSchemaType(modelName string, modelType reflect.Type) string { + var modelKind reflect.Kind + + if modelType.Kind() == reflect.Ptr { + modelKind = modelType.Elem().Kind() + } else { + modelKind = modelType.Kind() + } + + if mapped, ok := types.SchemaMap[modelName]; ok { + return mapped + } + + // check if original type is primitive + switch modelKind { + case reflect.Bool: + return types.Boolean.String() + case reflect.Float32, reflect.Float64: + return types.Number.String() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return types.Integer.String() + case reflect.String: + return types.String.String() + } + + return modelName // use as is (custom or struct) +} + func buildProperty(t reflect.Type) (map[string]Property, []string) { properties := make(map[string]Property) required := make([]string, 0) diff --git a/reflect_test.go b/reflect_test.go index 58d4876..2877c43 100644 --- a/reflect_test.go +++ b/reflect_test.go @@ -50,6 +50,24 @@ type Pet struct { Bool bool Enum string `json:"enum" enum:"a,b,c" example:"b"` Anonymous + MapSlicePtr map[string][]*string + MapSlice map[string][]string + MapSliceStructPtr map[string][]*Person + MapSliceStruct map[string][]Person + SliceStructPtr *[]*Person + SliceStruct *[]Person + SliceStringPtr *[]*string + SliceString *[]string + MapNestOptions *MapObj `json:"map_nest_options,omitempty"` +} + +type MapObj struct { + RuleOptions map[string]*MapOption `json:"rule_options"` +} + +type MapOption struct { + Name string `json:"name"` + SubOptions map[string]*MapOption `json:"sub_options,omitempty"` } type Empty struct { @@ -61,7 +79,7 @@ func TestDefine(t *testing.T) { obj, ok := v["github.com_zc2638_swag.Pet"] assert.True(t, ok) assert.False(t, obj.IsArray) - assert.Equal(t, 17, len(obj.Properties)) + assert.Equal(t, 26, len(obj.Properties)) content := make(map[string]Object) data, err := os.ReadFile("testdata/pet.json") diff --git a/testdata/pet.json b/testdata/pet.json index 9fba898..ee477f3 100644 --- a/testdata/pet.json +++ b/testdata/pet.json @@ -1,16 +1,48 @@ { - "github.com_zc2638_swag.Person": { + "github.com_zc2638_swag.MapObj": { "type": "object", "properties": { - "First": { + "rule_options": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/github.com_zc2638_swag.MapOption" + } + } + } + }, + "github.com_zc2638_swag.MapOption": { + "type": "object", + "properties": { + "match_src": { + "type": "string" + }, + "name": { "type": "string" + }, + "ops": { + "type": "array", + "items": { + "type": "string" + } + }, + "sub_options": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/github.com_zc2638_swag.MapOption" + } + }, + "values": { + "type": "array", + "items": { + "type": "string" + } } } }, - "Anonymous": { + "github.com_zc2638_swag.Person": { "type": "object", "properties": { - "AnyOne": { + "First": { "type": "string" } } @@ -21,72 +53,136 @@ "pointer" ], "properties": { + "AnyOne": { + "type": "string" + }, + "Bool": { + "type": "boolean" + }, + "Double": { + "type": "number", + "format": "double" + }, + "DoubleArray": { + "type": "array", + "items": { + "type": "number", + "format": "double" + } + }, + "Float": { + "type": "number", + "format": "float" + }, + "FloatArray": { + "type": "array", + "items": { + "type": "number", + "format": "float" + } + }, "Int": { "type": "integer", "format": "int32" }, - "IntArray": { + "Int64Array": { "type": "array", "items": { "type": "integer", - "format": "int32" + "format": "int64" } }, - "Int64Array": { + "IntArray": { "type": "array", "items": { "type": "integer", - "format": "int64" + "format": "int32" } }, - "String": { - "type": "string" + "MapSlice": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } }, - "StringSecondWay": { - "type": "string" + "MapSlicePtr": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } }, - "StringArray": { + "MapSliceStruct": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/definitions/github.com_zc2638_swag.Person" + } + } + }, + "MapSliceStructPtr": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/definitions/github.com_zc2638_swag.Person" + } + } + }, + "SliceString": { "type": "array", "items": { "type": "string" } }, - "Float": { - "type": "number", - "format": "float" - }, - "FloatArray": { + "SliceStringPtr": { "type": "array", "items": { - "type": "number", - "format": "float" + "type": "string" } }, - "Double": { - "type": "number", - "format": "double" + "SliceStruct": { + "type": "array", + "items": { + "$ref": "#/definitions/github.com_zc2638_swag.Person" + } }, - "DoubleArray": { + "SliceStructPtr": { "type": "array", "items": { - "type": "number", - "format": "double" + "$ref": "#/definitions/github.com_zc2638_swag.Person" } }, - "Bool": { - "type": "boolean" + "String": { + "type": "string" }, - "AnyOne": { + "StringArray": { + "type": "array", + "items": { + "type": "string" + } + }, + "StringSecondWay": { "type": "string" }, "enum": { "type": "string", - "enum": ["a","b","c"], + "enum": [ + "a", + "b", + "c" + ], "example": "b" }, "friend": { - "$ref": "#/definitions/github.com_zc2638_swag.Person", - "description": "description short expression" + "description": "description short expression", + "$ref": "#/definitions/github.com_zc2638_swag.Person" }, "friends": { "type": "array", @@ -95,6 +191,9 @@ "$ref": "#/definitions/github.com_zc2638_swag.Person" } }, + "map_nest_options": { + "$ref": "#/definitions/github.com_zc2638_swag.MapObj" + }, "pointer": { "$ref": "#/definitions/github.com_zc2638_swag.Person" }, diff --git a/types/types.go b/types/types.go index 0f375c7..f7eb4e4 100644 --- a/types/types.go +++ b/types/types.go @@ -45,6 +45,12 @@ type Context struct { PathParams map[string]string } +var SchemaMap = map[string]string{ + "time.Time": "string", + "time.Duration": "integer", + "json.Number": "number", +} + // AddURLParamsToContext returns a copy of parent in which the context value is set func AddURLParamsToContext(parent context.Context, params map[string]string) context.Context { routeVal := parent.Value(RouteContextKey)