From 61146e6b836bd11b81f5c0ce28fa14a71c74a168 Mon Sep 17 00:00:00 2001 From: Xyedo Date: Mon, 8 Jul 2024 20:48:44 +0700 Subject: [PATCH] feat: composable type & map[string]any --- components/definition/definition.go | 209 ++++++++------- components/endpoint/endpoints.go | 8 +- components/fields/parsing.go | 46 +++- components/http/response/response.go | 240 ++++++++++++++++-- components/parameter/parameter.go | 56 ++-- example/models/generic.go | 5 + generate.go | 2 +- generate_test.go | 165 +++++++++++- testdata/expected_output/bft.json | 120 +++++++-- .../composable-array-of-object.json | 108 ++++++++ .../composable-array-of-primitive.json | 144 +++++++++++ .../expected_output/composable-object.json | 102 ++++++++ .../expected_output/composable-primitive.json | 120 +++++++++ testdata/expected_output/dnmt.json | 51 +++- testdata/expected_output/map.json | 81 ++++++ 15 files changed, 1276 insertions(+), 181 deletions(-) create mode 100644 testdata/expected_output/composable-array-of-object.json create mode 100644 testdata/expected_output/composable-array-of-primitive.json create mode 100644 testdata/expected_output/composable-object.json create mode 100644 testdata/expected_output/composable-primitive.json create mode 100644 testdata/expected_output/map.json diff --git a/components/definition/definition.go b/components/definition/definition.go index 21a177b..1152271 100644 --- a/components/definition/definition.go +++ b/components/definition/definition.go @@ -57,21 +57,25 @@ func (g DefinitionGenerator) CreateDefinition(t interface{}) { properties := make(map[string]DefinitionProperties) definitionName := fmt.Sprintf("%T", t) - reflectReturn := reflect.TypeOf(t) - switch reflectReturn.Kind() { + if strings.HasPrefix(definitionName, "map[string]") { + return + } + + reflectValue := reflect.ValueOf(t) + switch reflectValue.Kind() { case reflect.Slice: - reflectReturn = reflectReturn.Elem() - if reflectReturn.Kind() == reflect.Struct { - properties = g.createStructDefinitions(reflectReturn) + + if reflect.TypeOf(t).Elem().Kind() == reflect.Struct { + properties = g.createStructDefinitions(reflectValue) } definitionName, _ = strings.CutPrefix(definitionName, "[]") case reflect.Struct: - if reflectReturn == reflect.TypeOf(response.CustomResponse{}) { + if reflectValue.Type() == reflect.TypeOf(response.CustomResponse{}) { // if CustomResponseType, use Model struct in it g.CreateDefinition(t.(response.CustomResponse).Model) return } - properties = g.createStructDefinitions(reflectReturn) + properties = g.createStructDefinitions(reflectValue) } // merge embedded struct fields with other fields @@ -108,12 +112,18 @@ func (g DefinitionGenerator) findRequiredFields(properties map[string]Definition return requiredFields } -func (g DefinitionGenerator) createStructDefinitions(structType reflect.Type) map[string]DefinitionProperties { +func (g DefinitionGenerator) createStructDefinitions(structValue reflect.Value) map[string]DefinitionProperties { + if structValue.Kind() == reflect.Slice { + structValue = reflect.New(structValue.Type().Elem()).Elem() + } + properties := make(map[string]DefinitionProperties) - for i := 0; i < structType.NumField(); i++ { - field := structType.Field(i) - fieldType := fields.Type(field.Type.Kind().String()) - fieldJsonTag := fields.JsonTag(field) + for i := 0; i < structValue.NumField(); i++ { + field := structValue.Field(i) + fieldType := field.Type() + fieldKind := field.Type().Kind() + fieldStructType := structValue.Type().Field(i) + fieldJsonTag := fields.JsonTag(fieldStructType) // skip ignored tags if fieldJsonTag == "-" { @@ -121,126 +131,126 @@ func (g DefinitionGenerator) createStructDefinitions(structType reflect.Type) ma } // skip for function and channel types - if fieldType == "func" || fieldType == "chan" { + if fieldKind == reflect.Func || fieldKind == reflect.Chan { continue } // if item type is array, create Definition for array element type - switch fieldType { - case "array": - if field.Type.Elem().Kind() == reflect.Pointer { // []*type - if field.Type.Elem().Elem().Kind() == reflect.Struct { // []*struct + switch fieldKind { + case reflect.Array, reflect.Slice: + if field.Elem().Kind() == reflect.Pointer { // []*T + if field.Elem().Elem().Kind() == reflect.Struct { // []*struct properties[fieldJsonTag] = DefinitionProperties{ - Example: fields.ExampleTag(field), - Type: fieldType, + Example: fields.ExampleTag(fieldStructType), + Type: "array", Items: &DefinitionPropertiesItems{ - Ref: fmt.Sprintf("#/definitions/%s", field.Type.Elem().Elem().String()), + Ref: fmt.Sprintf("#/definitions/%s", fieldType.Elem().Elem().String()), }, - IsRequired: g.isRequired(field), + IsRequired: g.isRequired(fieldStructType), } - if structType == field.Type.Elem() { + if structValue.Type() == fieldType.Elem() { continue // prevent recursion } - g.CreateDefinition(reflect.New(field.Type.Elem().Elem()).Elem().Interface()) - } else { // []*other - itemType := fields.Type(field.Type.Elem().Elem().Kind().String()) + g.CreateDefinition(reflect.New(fieldType.Elem().Elem()).Elem().Interface()) + } else { // []*primitve_type + itemType := fields.Type(fieldType.Elem().Elem()) properties[fieldJsonTag] = DefinitionProperties{ - Example: fields.ExampleTag(field), - Type: fieldType, + Example: fields.ExampleTag(fieldStructType), + Type: "array", Items: &DefinitionPropertiesItems{ Type: itemType, }, - IsRequired: g.isRequired(field), + IsRequired: g.isRequired(fieldStructType), } } - } else if field.Type.Elem().Kind() == reflect.Struct { // []struct + } else if fieldType.Elem().Kind() == reflect.Struct { // []struct properties[fieldJsonTag] = DefinitionProperties{ - Example: fields.ExampleTag(field), - Type: fieldType, + Example: fields.ExampleTag(fieldStructType), + Type: "array", Items: &DefinitionPropertiesItems{ - Ref: fmt.Sprintf("#/definitions/%s", field.Type.Elem().String()), + Ref: fmt.Sprintf("#/definitions/%s", fieldType.Elem().String()), }, - IsRequired: g.isRequired(field), + IsRequired: g.isRequired(fieldStructType), } - if structType == field.Type.Elem() { + if structValue.Type() == fieldType.Elem() { continue // prevent recursion } - g.CreateDefinition(reflect.New(field.Type.Elem()).Elem().Interface()) + g.CreateDefinition(reflect.New(fieldType.Elem()).Elem().Interface()) } else { // []other properties[fieldJsonTag] = DefinitionProperties{ - Example: fields.ExampleTag(field), - Type: fieldType, + Example: fields.ExampleTag(fieldStructType), + Type: "array", Items: &DefinitionPropertiesItems{ - Type: fields.Type(field.Type.Elem().Kind().String()), + Type: fields.Type(fieldType.Elem()), }, - IsRequired: g.isRequired(field), + IsRequired: g.isRequired(fieldStructType), } } - case "struct": - isRequiredField := g.isRequired(field) - if field.Type.String() == "time.Time" { - properties[fieldJsonTag] = g.timeProperty(field, isRequiredField) - } else if field.Type.String() == "time.Duration" { - properties[fieldJsonTag] = g.durationProperty(field, isRequiredField) + case reflect.Struct: + isRequiredField := g.isRequired(fieldStructType) + if fieldType.String() == "time.Time" { + properties[fieldJsonTag] = g.timeProperty(fieldStructType, isRequiredField) + } else if fieldType.String() == "time.Duration" { + properties[fieldJsonTag] = g.durationProperty(fieldStructType, isRequiredField) } else { properties[fieldJsonTag] = DefinitionProperties{ - Example: fields.ExampleTag(field), - Ref: fmt.Sprintf("#/definitions/%s", field.Type.String()), + Example: fields.ExampleTag(fieldStructType), + Ref: fmt.Sprintf("#/definitions/%s", fieldType.String()), IsRequired: isRequiredField, } - g.CreateDefinition(reflect.New(field.Type).Elem().Interface()) + g.CreateDefinition(reflect.New(fieldType).Elem().Interface()) } - case "ptr": - if field.Type.Elem() == structType { // prevent recursion + case reflect.Pointer: + if fieldType.Elem() == structValue.Type() { // prevent recursion properties[fieldJsonTag] = DefinitionProperties{ - Example: fmt.Sprintf("Recursive Type: %s", field.Type.Elem().String()), + Example: fmt.Sprintf("Recursive Type: %s", fieldType.Elem().String()), } continue } - if field.Type.Elem().Kind() == reflect.Struct { - if field.Type.Elem().String() == "time.Time" { - properties[fieldJsonTag] = g.timeProperty(field, false) - } else if field.Type.String() == "time.Duration" { - properties[fieldJsonTag] = g.durationProperty(field, false) + if fieldType.Elem().Kind() == reflect.Struct { // *struct + if fieldType.Elem().String() == "time.Time" { + properties[fieldJsonTag] = g.timeProperty(fieldStructType, false) + } else if fieldType.String() == "time.Duration" { + properties[fieldJsonTag] = g.durationProperty(fieldStructType, false) } else { - properties[fieldJsonTag] = g.refProperty(field, false) - g.CreateDefinition(reflect.New(field.Type.Elem()).Elem().Interface()) + properties[fieldJsonTag] = g.refProperty(fieldStructType, false) + g.CreateDefinition(reflect.New(fieldType.Elem()).Elem().Interface()) } - } else if field.Type.Elem().Kind() == reflect.Array || field.Type.Elem().Kind() == reflect.Slice { - if field.Type.Elem().Elem().Kind() == reflect.Struct { + } else if fieldType.Elem().Kind() == reflect.Array || fieldType.Elem().Kind() == reflect.Slice { // *[]T + if fieldType.Elem().Elem().Kind() == reflect.Struct { properties[fieldJsonTag] = DefinitionProperties{ - Example: fields.ExampleTag(field), - Type: fields.Type(field.Type.Elem().Kind().String()), + Example: fields.ExampleTag(fieldStructType), + Type: fields.Type(fieldType.Elem()), Items: &DefinitionPropertiesItems{ - Ref: fmt.Sprintf("#/definitions/%s", field.Type.Elem().Elem().String()), + Ref: fmt.Sprintf("#/definitions/%s", fieldType.Elem().Elem().String()), }, } - if structType == field.Type.Elem().Elem() { + if structValue.Type() == fieldType.Elem().Elem() { continue // prevent recursion } - g.CreateDefinition(reflect.New(field.Type.Elem().Elem()).Elem().Interface()) + g.CreateDefinition(reflect.New(fieldType.Elem().Elem()).Elem().Interface()) } else { properties[fieldJsonTag] = DefinitionProperties{ - Example: fields.ExampleTag(field), - Type: fields.Type(field.Type.Elem().Kind().String()), + Example: fields.ExampleTag(fieldStructType), + Type: fields.Type(fieldType.Elem()), Items: &DefinitionPropertiesItems{ - Type: fields.Type(field.Type.Elem().Elem().Kind().String()), + Type: fields.Type(fieldType.Elem().Elem()), }, } } } else { properties[fieldJsonTag] = DefinitionProperties{ - Example: fields.ExampleTag(field), - Type: fields.Type(field.Type.Elem().Kind().String()), + Example: fields.ExampleTag(fieldStructType), + Type: fields.Type(fieldType.Elem()), } } - case "map": - name := fmt.Sprintf("%s.%s", structType.String(), fieldJsonTag) - mapKeyType := field.Type.Key() - mapValueType := field.Type.Elem() + case reflect.Map: + name := fmt.Sprintf("%s.%s", structValue.String(), fieldJsonTag) + mapKeyType := fieldType.Key() + mapValueType := fieldType.Elem() if mapValueType.Kind() == reflect.Ptr { mapValueType = mapValueType.Elem() } @@ -251,7 +261,7 @@ func (g DefinitionGenerator) createStructDefinitions(structType reflect.Type) ma g.Definitions[name] = Definition{ Type: "object", Properties: map[string]DefinitionProperties{ - fields.Type(mapKeyType.String()): { + fields.Type(mapKeyType): { Ref: fmt.Sprintf("#/definitions/%s", mapValueType.String()), }, }, @@ -260,24 +270,51 @@ func (g DefinitionGenerator) createStructDefinitions(structType reflect.Type) ma g.Definitions[name] = Definition{ Type: "object", Properties: map[string]DefinitionProperties{ - fields.Type(mapKeyType.String()): { - Example: fields.ExampleTag(field), - Type: fields.Type(mapValueType.String()), + fields.Type(mapKeyType): { + Example: fields.ExampleTag(fieldStructType), + Type: fields.Type(mapValueType), }, }, } } - case "interface": - // TODO: Find a way to get real model of interface{} - properties[fieldJsonTag] = DefinitionProperties{ - Example: fields.ExampleTag(field), - Type: "Ambiguous Type: interface{}", - IsRequired: g.isRequired(field), + case reflect.Interface: + fek := field.Elem().Kind() + if fek == 0 { + continue } + if fek == reflect.Pointer { + fek = field.Elem().Elem().Kind() + } + + if fields.IsPrimitiveValue(fek) { + continue + } + + if fek == reflect.Slice || fek == reflect.Array { + var k reflect.Kind + //[]*T + if field.Elem().Type().Elem().Kind() == reflect.Pointer { + k = field.Elem().Type().Elem().Elem().Kind() + + } else { //[]T + k = field.Elem().Type().Elem().Kind() + } + + if fields.IsPrimitiveValue(k) { + continue + } + } + + g.CreateDefinition( + reflect.New( + field.Elem().Type(), + ).Elem().Interface(), + ) + default: - properties[fieldJsonTag] = g.defaultProperty(field) + properties[fieldJsonTag] = g.defaultProperty(fieldStructType) } } @@ -313,7 +350,7 @@ func (g DefinitionGenerator) refProperty(field reflect.StructField, required boo func (g DefinitionGenerator) defaultProperty(field reflect.StructField) DefinitionProperties { return DefinitionProperties{ Example: fields.ExampleTag(field), - Type: fields.Type(field.Type.Kind().String()), + Type: fields.Type(field.Type), IsRequired: g.isRequired(field), } } diff --git a/components/endpoint/endpoints.go b/components/endpoint/endpoints.go index cc87c18..9ab5d2a 100644 --- a/components/endpoint/endpoints.go +++ b/components/endpoint/endpoints.go @@ -40,8 +40,8 @@ type JsonEndPoint struct { // It encapsulates the description and schema of a response object. // See: https://swagger.io/specification/v2/#response-object type JsonResponse struct { - Description string `json:"description"` - Schema *parameter.JsonResponseSchema `json:"schema,omitempty"` + Description string `json:"description"` + Schema *parameter.JsonResponseSchemaAllOf `json:"schema,omitempty"` } // EndPoint holds the details of an API endpoint, including HTTP method, path, parameters, @@ -143,7 +143,9 @@ func (e *EndPoint) BodyJsonParameter() *parameter.JsonParameter { In: "body", Description: "body", Required: true, - Schema: &bodySchema, + Schema: ¶meter.JsonResponseSchemaAllOf{ + AllOf: []parameter.JsonResponseSchema{bodySchema}, + }, } } diff --git a/components/fields/parsing.go b/components/fields/parsing.go index 9f74177..cb50d13 100644 --- a/components/fields/parsing.go +++ b/components/fields/parsing.go @@ -1,6 +1,7 @@ package fields import ( + "fmt" "reflect" "strconv" "strings" @@ -51,17 +52,48 @@ func IsRequired(field reflect.StructField) bool { // Type maps a string to its corresponding Swagger type according to the // Swagger Specification version 2 data types (https://swagger.io/specification/v2/#data-types). -func Type(t string) string { - if t == "interface" { +func Type(t reflect.Type) string { + + kind := t.Kind() + switch { + case kind == reflect.Pointer: + return Type(t.Elem()) + case kind == reflect.Interface: return "interface" - } else if strings.Contains(strings.ToLower(t), "int") { + case isInteger(kind): return "integer" - } else if t == "array" || t == "slice" { + case kind == reflect.Slice, kind == reflect.Array: return "array" - } else if t == "bool" { + case kind == reflect.Bool: return "boolean" - } else if t == "float64" || t == "float32" { + case kind == reflect.Float64, kind == reflect.Float32: return "number" + case kind == reflect.String: + return "string" + default: + panic(fmt.Sprintf("unsupported type: %s", kind)) } - return t +} + +func IsPrimitiveValue(valueKind reflect.Kind) bool { + return valueKind == reflect.Bool || + valueKind == reflect.Float64 || + valueKind == reflect.Float32 || + valueKind == reflect.String || + isInteger(valueKind) +} + +func isInteger(valueKind reflect.Kind) bool { + return containsKind([]reflect.Kind{ + reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, + }, valueKind) +} + +func containsKind(s []reflect.Kind, k reflect.Kind) bool { + for _, v := range s { + if v == k { + return true + } + } + return false } diff --git a/components/http/response/response.go b/components/http/response/response.go index 8e1b869..aa5c224 100644 --- a/components/http/response/response.go +++ b/components/http/response/response.go @@ -3,6 +3,7 @@ package response import ( "fmt" "reflect" + "strings" "github.com/go-swagno/swagno/components/fields" @@ -42,36 +43,118 @@ func NewResponseGenerator() *ResponseGenerator { // Generate generates a JSON response schema based on the provided model. // It uses reflection to determine the type of the model and constructs the appropriate JSON schema. // This function handles different types such as slices, maps, and structures to create a detailed and accurate schema. -func (g ResponseGenerator) Generate(model any) *parameter.JsonResponseSchema { +func (g ResponseGenerator) Generate(model any) *parameter.JsonResponseSchemaAllOf { switch reflect.TypeOf(model).Kind() { - case reflect.Slice: + case reflect.Slice, reflect.Array: sliceElementKind := reflect.TypeOf(model).Elem().Kind() if sliceElementKind == reflect.Struct { - return ¶meter.JsonResponseSchema{ - Type: "array", - Items: ¶meter.JsonResponseSchemeItems{ - Ref: strings.ReplaceAll(fmt.Sprintf("#/definitions/%s", reflect.TypeOf(model).Elem().String()), "[]", ""), + return ¶meter.JsonResponseSchemaAllOf{ + AllOf: []parameter.JsonResponseSchema{ + { + Type: "array", + Items: ¶meter.JsonResponseSchemeItems{ + Ref: strings.ReplaceAll(fmt.Sprintf("#/definitions/%s", reflect.TypeOf(model).Elem().String()), "[]", ""), + }, + }, }, } } else { - return ¶meter.JsonResponseSchema{ - Type: "array", - Items: ¶meter.JsonResponseSchemeItems{ - Type: fields.Type(sliceElementKind.String()), + return ¶meter.JsonResponseSchemaAllOf{ + AllOf: []parameter.JsonResponseSchema{ + { + Type: "array", + Items: ¶meter.JsonResponseSchemeItems{ + Type: fields.Type(reflect.TypeOf(model).Elem()), + }, + }, }, } } case reflect.Map: + modelType := reflect.TypeOf(model) + if modelType.Key().Kind() == reflect.String { + //for loop to iterate over the map and get the key and value types + properties := make(map[string]parameter.JsonResponseSchema) + modelValueOf := reflect.ValueOf(model) + for _, key := range modelValueOf.MapKeys() { + // mapIndex + + valueOfMap := modelValueOf.MapIndex(key).Elem() + // check if value is primitive type + var k reflect.Kind + var v reflect.Value + if valueOfMap.Type().Kind() == reflect.Pointer { + k = valueOfMap.Type().Elem().Kind() + v = valueOfMap.Elem() + } else { + k = valueOfMap.Type().Kind() + v = valueOfMap + } + + if fields.IsPrimitiveValue(k) { + properties[key.String()] = parameter.JsonResponseSchema{ + Type: fields.Type(valueOfMap.Type()), + Example: v.Interface(), + } + continue + } else { + model := g.Generate(valueOfMap.Interface()) + if model != nil { + properties[key.String()] = model.AllOf[0] + + } + } + + } + return ¶meter.JsonResponseSchemaAllOf{ + AllOf: []parameter.JsonResponseSchema{ + { + Type: "object", + Properties: properties, + }, + }, + } + } ref := strings.ReplaceAll(fmt.Sprintf("#/definitions/%T", model), "[]", "") - return ¶meter.JsonResponseSchema{ - Ref: ref, + return ¶meter.JsonResponseSchemaAllOf{ + AllOf: []parameter.JsonResponseSchema{ + { + Ref: ref, + }, + }, } default: - if hasStructFields(model) { - return ¶meter.JsonResponseSchema{ - Ref: strings.ReplaceAll(fmt.Sprintf("#/definitions/%T", model), "[]", ""), + if rv, ok := extractStructFields(model); ok { + parameters := make([]parameter.JsonResponseSchema, 0, rv.NumField()) + for i := 0; i < rv.NumField(); i++ { + field := rv.Field(i) + fieldStructType := rv.Type().Field(i) + + if field.Kind() != reflect.Interface { + continue + } + + field = field.Elem() + + if field.Kind() == 0 { + continue + } + + if param := getValueFromStruct(field, fieldStructType); param != nil { + parameters = append(parameters, *param) + } + } + return ¶meter.JsonResponseSchemaAllOf{ + AllOf: append( + []parameter.JsonResponseSchema{ + { + Ref: strings.ReplaceAll(fmt.Sprintf("#/definitions/%T", model), "[]", ""), + }, + }, + parameters..., + ), } } } @@ -79,16 +162,135 @@ func (g ResponseGenerator) Generate(model any) *parameter.JsonResponseSchema { return nil } -// hasStructFields checks if the given interface has fields in case it's a struct. -func hasStructFields(s interface{}) bool { +func getValueFromStruct(field reflect.Value, structField reflect.StructField) *parameter.JsonResponseSchema { + fieldJsonTag := fields.JsonTag(structField) + if fieldJsonTag == "-" { + return nil + } + + switch field.Kind() { + case reflect.Pointer: + return getValueFromStruct(field.Elem(), structField) + case reflect.Struct: + if field.Type().String() == "time.Time" { + return ¶meter.JsonResponseSchema{ + Type: "object", + Properties: map[string]parameter.JsonResponseSchema{ + fieldJsonTag: { + Type: "string", + }, + }, + } + } + if field.Type().String() == "time.Duration" { + return ¶meter.JsonResponseSchema{ + Type: "object", + Properties: map[string]parameter.JsonResponseSchema{ + fieldJsonTag: { + Type: "integer", + }, + }, + } + } + + return ¶meter.JsonResponseSchema{ + Type: "object", + Properties: map[string]parameter.JsonResponseSchema{ + fieldJsonTag: { + Ref: fmt.Sprintf("#/definitions/%s", field.Type().String()), + }, + }, + } + case reflect.Slice, reflect.Array: + var jsonSchemaItems *parameter.JsonResponseSchemeItems + + var v reflect.Value + if field.Type().Elem().Kind() == reflect.Pointer { + v = reflect.New(field.Type().Elem().Elem()).Elem() + } else { + v = reflect.New(field.Type().Elem()).Elem() + } + + primitiveValue := getValueFromStruct(v, structField) + if primitiveValue == nil { + return nil + } + + if props := primitiveValue.Properties[fieldJsonTag]; props.Ref != "" { + jsonSchemaItems = ¶meter.JsonResponseSchemeItems{ + Ref: props.Ref, + } + } else { + jsonSchemaItems = ¶meter.JsonResponseSchemeItems{ + Type: props.Type, + Items: props.Items, + } + } + + return ¶meter.JsonResponseSchema{ + Type: "object", + Properties: map[string]parameter.JsonResponseSchema{ + fieldJsonTag: { + Type: "array", + Items: jsonSchemaItems, + }, + }, + } + case reflect.Map: + properties := make(map[string]parameter.JsonResponseSchema) + for _, key := range field.MapKeys() { + // mapIndex + + valueOfMap := field.MapIndex(key).Elem() + // check if value is primitive type + if fields.IsPrimitiveValue(valueOfMap.Type().Kind()) { + properties[key.String()] = parameter.JsonResponseSchema{ + Type: fields.Type(valueOfMap.Type()), + Example: valueOfMap.Interface(), + } + continue + } else { + model := getValueFromStruct(valueOfMap, structField) + if model != nil { + properties[key.String()] = *model + } + } + + } + return ¶meter.JsonResponseSchema{ + Type: "object", + Properties: map[string]parameter.JsonResponseSchema{ + fieldJsonTag: { + Type: "object", + Properties: properties, + }, + }, + } + + default: // primitive types + return ¶meter.JsonResponseSchema{ + Type: "object", + Properties: map[string]parameter.JsonResponseSchema{ + fieldJsonTag: { + Type: fields.Type(field.Type()), + Example: fields.ExampleTag(structField), + }, + }, + } + } + +} + +// extractStructFields checks if the given interface has fields in case it's a struct. +func extractStructFields(s interface{}) (reflect.Value, bool) { rv := reflect.ValueOf(s) if rv.Kind() != reflect.Struct { - return false + return reflect.Value{}, false } numFields := rv.NumField() - return numFields > 0 + return rv, numFields > 0 } func (c CustomResponse) Description() string { diff --git a/components/parameter/parameter.go b/components/parameter/parameter.go index f1b162c..e7f4613 100644 --- a/components/parameter/parameter.go +++ b/components/parameter/parameter.go @@ -56,34 +56,40 @@ const ( // JsonParameter is the JSON model version of Parameter object used for API purposes // https://swagger.io/specification/v2/#parameterObject type JsonParameter struct { - Type string `json:"type,omitempty"` - Description string `json:"description"` - Name string `json:"name"` - In string `json:"in,omitempty"` - Required bool `json:"required"` - Schema *JsonResponseSchema `json:"schema,omitempty"` - Format string `json:"format,omitempty"` - Enum []interface{} `json:"enum,omitempty"` - Default interface{} `json:"default,omitempty"` - Min int64 `json:"minimum,omitempty"` - Max int64 `json:"maximum,omitempty"` - MinLen int64 `json:"minLength,omitempty"` - MaxLen int64 `json:"maxLength,omitempty"` - Pattern string `json:"pattern,omitempty"` - MaxItems int64 `json:"maxItems,omitempty"` - MinItems int64 `json:"minItems,omitempty"` - UniqueItems bool `json:"uniqueItems,omitempty"` - MultipleOf int64 `json:"multipleOf,omitempty"` - CollenctionFormat string `json:"collectionFormat,omitempty"` -} - -// JsonResponseSchema defines the schema for a JSON response as per the Swagger 2.0 specification. + Type string `json:"type,omitempty"` + Description string `json:"description"` + Name string `json:"name"` + In string `json:"in,omitempty"` + Required bool `json:"required"` + Schema *JsonResponseSchemaAllOf `json:"schema,omitempty"` + Format string `json:"format,omitempty"` + Enum []interface{} `json:"enum,omitempty"` + Default interface{} `json:"default,omitempty"` + Min int64 `json:"minimum,omitempty"` + Max int64 `json:"maximum,omitempty"` + MinLen int64 `json:"minLength,omitempty"` + MaxLen int64 `json:"maxLength,omitempty"` + Pattern string `json:"pattern,omitempty"` + MaxItems int64 `json:"maxItems,omitempty"` + MinItems int64 `json:"minItems,omitempty"` + UniqueItems bool `json:"uniqueItems,omitempty"` + MultipleOf int64 `json:"multipleOf,omitempty"` + CollenctionFormat string `json:"collectionFormat,omitempty"` +} + +// JsonResponseSchemaAllOf defines the schema for a JSON response as per the Swagger 2.0 specification. // It is used to describe the structure and type of a response returned by an API endpoint. // https://swagger.io/specification/v2/#schema-object +type JsonResponseSchemaAllOf struct { + AllOf []JsonResponseSchema `json:"allOf,omitempty"` +} + type JsonResponseSchema struct { - Ref string `json:"$ref,omitempty"` - Type string `json:"type,omitempty"` - Items *JsonResponseSchemeItems `json:"items,omitempty"` + Ref string `json:"$ref,omitempty"` + Type string `json:"type,omitempty"` + Properties map[string]JsonResponseSchema `json:"properties,omitempty"` + Items *JsonResponseSchemeItems `json:"items,omitempty"` + Example interface{} `json:"example,omitempty"` } // JsonResponseSchemeItems represents the individual items in a JsonResponseSchema, especially for arrays. diff --git a/example/models/generic.go b/example/models/generic.go index fdf350f..79f2c06 100644 --- a/example/models/generic.go +++ b/example/models/generic.go @@ -7,6 +7,11 @@ type PostBody struct { type EmptySuccessfulResponse struct{} +type Response struct { + Status string `json:"status"` + Data any `json:"data,omitempty"` + Errors any `json:"errors,omitempty"` +} type SuccessfulResponse struct { ID string `json:"ID" example:"1234-1234-1234-1234"` } diff --git a/generate.go b/generate.go index 1884328..badc582 100644 --- a/generate.go +++ b/generate.go @@ -16,7 +16,7 @@ func appendResponses(sourceResponses map[string]endpoint.JsonResponse, additiona responseGenerator := response.NewResponseGenerator() for _, resp := range additionalResponses { - var responseSchema *parameter.JsonResponseSchema + var responseSchema *parameter.JsonResponseSchemaAllOf switch respType := resp.(type) { case response.CustomResponse: diff --git a/generate_test.go b/generate_test.go index 0f281a1..f6ae5ff 100644 --- a/generate_test.go +++ b/generate_test.go @@ -18,10 +18,12 @@ import ( var desc = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed id malesuada lorem, et fermentum sapien. Vivamus non pharetra risus, in efficitur leo. Suspendisse sed metus sit amet mi laoreet imperdiet. Donec aliquam eros eu blandit feugiat. Quisque scelerisque justo ac vehicula bibendum. Fusce suscipit arcu nisl, eu maximus odio consequat quis. Curabitur fermentum eleifend tellus, lobortis hendrerit velit varius vitae." func TestSwaggerGeneration(t *testing.T) { + var id float64 = 1234 testCases := []struct { name string endpoints []*endpoint.EndPoint file string + debug bool }{ { name: "Basic Functionality Test", @@ -95,10 +97,166 @@ func TestSwaggerGeneration(t *testing.T) { }, file: "testdata/expected_output/dnmt.json", }, + { + name: "test map[string]any", + endpoints: []*endpoint.EndPoint{endpoint.New( + endpoint.PUT, + "/test-map", + endpoint.WithTags("product"), + endpoint.WithSuccessfulReturns([]response.Response{response.New( + map[string]any{ + "code": float64(200), + "data": map[string]any{ + "id": &id, + "id2": "asd", + "id3": 123.23, + "id4": []int{12, 34}, + "id5": []string{"asd", "asd"}, + }, + }, + "201", + "Request Accepted", + )}, + ), + )}, + file: "testdata/expected_output/map.json", + }, + { + name: "test composable type with primitive type", + endpoints: []*endpoint.EndPoint{ + endpoint.New( + endpoint.PUT, + "/composable-primitive", + endpoint.WithTags("product"), + endpoint.WithSuccessfulReturns([]response.Response{ + response.New( + models.Response{ + Status: "success", + Data: "asd", + }, + "200", + "Request Accepted", + ), + response.New( + models.Response{ + Status: "success", + Data: 1, + }, + "201", + "Request Accepted", + ), + response.New( + models.Response{ + Status: "success", + Data: true, + }, + "203", + "Request Accepted", + ), + response.New( + models.Response{ + Status: "success", + Data: &id, + }, + "204", + "Request Accepted", + ), + }), + ), + }, + file: "testdata/expected_output/composable-primitive.json", + debug: true, + }, + { + name: "test composable type with primitive array type", + endpoints: []*endpoint.EndPoint{ + endpoint.New( + endpoint.PUT, + "/composable-array-primitive", + endpoint.WithTags("product"), + endpoint.WithSuccessfulReturns([]response.Response{ + + response.New( + models.Response{ + Status: "success", + Data: []*float64{&id, &id}, + Errors: []float64{1, 2}, + }, + "200", + "Request Accepted", + ), + response.New( + models.Response{ + Status: "success", + Data: []string{"asd", "asd"}, + Errors: []int{1, 2}, + }, + "201", + "Request Accepted", + ), + response.New( + models.Response{ + Status: "success", + Data: []bool{true, false}, + Errors: []string{"asd", "asf"}, + }, + "202", + "Request Accepted", + ), + }, + ), + ), + }, + file: "testdata/expected_output/composable-array-of-primitive.json", + }, + { + name: "test composable type with object type", + endpoints: []*endpoint.EndPoint{ + endpoint.New( + endpoint.PUT, + "/composable-object", + endpoint.WithTags("product"), + endpoint.WithSuccessfulReturns([]response.Response{response.New( + models.Response{ + Status: "success", + Data: models.PostBody{}, + Errors: models.UnsuccessfulResponse{}, + }, + "200", + "Request Accepted", + )}, + ), + ), + }, + file: "testdata/expected_output/composable-object.json", + }, + + { + name: "test composable type with array of object type", + endpoints: []*endpoint.EndPoint{ + endpoint.New( + endpoint.PUT, + "/composable-array-of-object", + endpoint.WithTags("product"), + endpoint.WithSuccessfulReturns([]response.Response{response.New( + models.Response{ + Status: "success", + Data: []models.PostBody{}, + Errors: []models.UnsuccessfulResponse{}, + }, + "200", + "Request Accepted", + )}, + ), + ), + }, + file: "testdata/expected_output/composable-array-of-object.json", + }, } - // Iterate through test cases + // Iterate through test cases./ for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { expectedJsonData, err := os.ReadFile(tc.file) @@ -115,7 +273,10 @@ func TestSwaggerGeneration(t *testing.T) { got.AddEndpoints(tc.endpoints) got.generateSwaggerJson() - if diff := cmp.Diff(want, got, cmpopts.IgnoreFields(Swagger{}, "endpoints"), cmpopts.IgnoreFields(definition.DefinitionProperties{}, "Example", "IsRequired")); diff != "" { + if diff := cmp.Diff(want, got, cmpopts.IgnoreFields(Swagger{}, "endpoints"), + cmpopts.IgnoreFields(definition.DefinitionProperties{}, "Example", "IsRequired"), + cmpopts.SortSlices(func(a, b string) bool { return a < b }), + ); diff != "" { t.Errorf("JsonSwagger() mismatch (-expected +got):\n%s", diff) } }) diff --git a/testdata/expected_output/bft.json b/testdata/expected_output/bft.json index a8f2b41..91af481 100644 --- a/testdata/expected_output/bft.json +++ b/testdata/expected_output/bft.json @@ -8,9 +8,16 @@ "/product": { "get": { "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed id malesuada lorem, et fermentum sapien. Vivamus non pharetra risus, in efficitur leo. Suspendisse sed metus sit amet mi laoreet imperdiet. Donec aliquam eros eu blandit feugiat. Quisque scelerisque justo ac vehicula bibendum. Fusce suscipit arcu nisl, eu maximus odio consequat quis. Curabitur fermentum eleifend tellus, lobortis hendrerit velit varius vitae.", - "consumes": ["application/json"], - "produces": ["application/json", "application/xml"], - "tags": ["product"], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "product" + ], "summary": "this is a test summary", "operationId": "get-/product", "parameters": [], @@ -21,16 +28,27 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/models.UnsuccessfulResponse" + "allOf": [ + { + "$ref": "#/definitions/models.UnsuccessfulResponse" + } + ] } } } }, "post": { "description": "", - "consumes": ["application/json"], - "produces": ["application/json", "application/xml"], - "tags": ["product"], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "product" + ], "summary": "", "operationId": "post-/product", "parameters": [ @@ -41,7 +59,11 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/models.ProductPost" + "allOf": [ + { + "$ref": "#/definitions/models.ProductPost" + } + ] } } ], @@ -49,13 +71,21 @@ "201": { "description": "Request Accepted", "schema": { - "$ref": "#/definitions/models.SuccessfulResponse" + "allOf": [ + { + "$ref": "#/definitions/models.SuccessfulResponse" + } + ] } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/models.UnsuccessfulResponse" + "allOf": [ + { + "$ref": "#/definitions/models.UnsuccessfulResponse" + } + ] } } } @@ -64,9 +94,16 @@ "/product/{id}": { "get": { "description": "", - "consumes": ["application/json"], - "produces": ["application/json", "application/xml"], - "tags": ["product"], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "product" + ], "summary": "", "operationId": "get-/product/{id}", "parameters": [ @@ -82,13 +119,21 @@ "201": { "description": "Request Accepted", "schema": { - "$ref": "#/definitions/models.SuccessfulResponse" + "allOf": [ + { + "$ref": "#/definitions/models.SuccessfulResponse" + } + ] } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/models.UnsuccessfulResponse" + "allOf": [ + { + "$ref": "#/definitions/models.UnsuccessfulResponse" + } + ] } } } @@ -97,9 +142,16 @@ "/product/{id}/detail": { "get": { "description": "", - "consumes": ["application/json"], - "produces": ["application/json", "application/xml"], - "tags": ["product"], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "product" + ], "summary": "", "operationId": "get-/product/{id}/detail", "parameters": [ @@ -115,13 +167,21 @@ "201": { "description": "Request Accepted", "schema": { - "$ref": "#/definitions/models.SuccessfulResponse" + "allOf": [ + { + "$ref": "#/definitions/models.SuccessfulResponse" + } + ] } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/models.UnsuccessfulResponse" + "allOf": [ + { + "$ref": "#/definitions/models.UnsuccessfulResponse" + } + ] } } } @@ -138,7 +198,10 @@ }, "models.ProductPost": { "type": "object", - "required": ["name", "merchant_id"], + "required": [ + "name", + "merchant_id" + ], "properties": { "category_id": { "type": "integer", @@ -156,7 +219,9 @@ }, "models.SuccessfulResponse": { "type": "object", - "required": ["ID"], + "required": [ + "ID" + ], "properties": { "ID": { "type": "string", @@ -166,7 +231,9 @@ }, "models.UnsuccessfulResponse": { "type": "object", - "required": ["error_msg1"], + "required": [ + "error_msg1" + ], "properties": { "error_msg1": { "type": "string", @@ -175,5 +242,8 @@ } } }, - "schemes": ["http", "https"] -} + "schemes": [ + "http", + "https" + ] +} \ No newline at end of file diff --git a/testdata/expected_output/composable-array-of-object.json b/testdata/expected_output/composable-array-of-object.json new file mode 100644 index 0000000..9d91770 --- /dev/null +++ b/testdata/expected_output/composable-array-of-object.json @@ -0,0 +1,108 @@ +{ + "swagger": "2.0", + "info": { + "title": "Testing API", + "description": "", + "version": "v1.0.0" + }, + "paths": { + "/composable-array-of-object": { + "put": { + "description": "", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "product" + ], + "summary": "", + "operationId": "put-/composable-array-of-object", + "parameters": [], + "responses": { + "200": { + "description": "Request Accepted", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/models.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/models.PostBody" + } + } + } + }, + { + "type": "object", + "properties": { + "errors": { + "type": "array", + "items": { + "$ref": "#/definitions/models.UnsuccessfulResponse" + } + } + } + } + ] + } + } + } + } + } + }, + "basePath": "/", + "host": "", + "definitions": { + "models.PostBody": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "example": 123456 + }, + "name": { + "type": "string", + "example": "John Smith" + } + }, + "required": [ + "name", + "id" + ] + }, + "models.Response": { + "type": "object", + "properties": { + "status": { + "type": "string" + } + }, + "required": [ + "status" + ] + }, + "models.UnsuccessfulResponse": { + "type": "object", + "properties": { + "error_msg1": { + "type": "string" + } + }, + "required": [ + "error_msg1" + ] + } + }, + "schemes": [ + "http", + "https" + ] +} \ No newline at end of file diff --git a/testdata/expected_output/composable-array-of-primitive.json b/testdata/expected_output/composable-array-of-primitive.json new file mode 100644 index 0000000..9d64e36 --- /dev/null +++ b/testdata/expected_output/composable-array-of-primitive.json @@ -0,0 +1,144 @@ +{ + "swagger": "2.0", + "info": { + "title": "Testing API", + "description": "", + "version": "v1.0.0" + }, + "paths": { + "/composable-array-primitive": { + "put": { + "description": "", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "product" + ], + "summary": "", + "operationId": "put-/composable-array-primitive", + "parameters": [], + "responses": { + "200": { + "description": "Request Accepted", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/models.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "number" + } + } + } + }, + { + "type": "object", + "properties": { + "errors": { + "type": "array", + "items": { + "type": "number" + } + } + } + } + ] + } + }, + "201": { + "description": "Request Accepted", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/models.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + { + "type": "object", + "properties": { + "errors": { + "type": "array", + "items": { + "type": "integer" + } + } + } + } + ] + } + }, + "202": { + "description": "Request Accepted", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/models.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "boolean" + } + } + } + }, + { + "type": "object", + "properties": { + "errors": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + ] + } + } + } + } + } + }, + "basePath": "/", + "host": "", + "definitions": { + "models.Response": { + "type": "object", + "properties": { + "status": { + "type": "string" + } + }, + "required": [ + "status" + ] + } + }, + "schemes": [ + "http", + "https" + ] +} \ No newline at end of file diff --git a/testdata/expected_output/composable-object.json b/testdata/expected_output/composable-object.json new file mode 100644 index 0000000..770bea4 --- /dev/null +++ b/testdata/expected_output/composable-object.json @@ -0,0 +1,102 @@ +{ + "swagger": "2.0", + "info": { + "title": "Testing API", + "description": "", + "version": "v1.0.0" + }, + "paths": { + "/composable-object": { + "put": { + "description": "", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "product" + ], + "summary": "", + "operationId": "put-/composable-object", + "parameters": [], + "responses": { + "200": { + "description": "Request Accepted", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/models.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/models.PostBody" + } + } + }, + { + "type": "object", + "properties": { + "errors": { + "$ref": "#/definitions/models.UnsuccessfulResponse" + } + } + } + ] + } + } + } + } + } + }, + "basePath": "/", + "host": "", + "definitions": { + "models.PostBody": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "example": 123456 + }, + "name": { + "type": "string", + "example": "John Smith" + } + }, + "required": [ + "id", + "name" + ] + }, + "models.Response": { + "type": "object", + "properties": { + "status": { + "type": "string" + } + }, + "required": [ + "status" + ] + }, + "models.UnsuccessfulResponse": { + "type": "object", + "properties": { + "error_msg1": { + "type": "string" + } + }, + "required": [ + "error_msg1" + ] + } + }, + "schemes": [ + "http", + "https" + ] +} \ No newline at end of file diff --git a/testdata/expected_output/composable-primitive.json b/testdata/expected_output/composable-primitive.json new file mode 100644 index 0000000..c5e4732 --- /dev/null +++ b/testdata/expected_output/composable-primitive.json @@ -0,0 +1,120 @@ +{ + "swagger": "2.0", + "info": { + "title": "Testing API", + "description": "", + "version": "v1.0.0" + }, + "paths": { + "/composable-primitive": { + "put": { + "description": "", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "product" + ], + "summary": "", + "operationId": "put-/composable-primitive", + "parameters": [], + "responses": { + "200": { + "description": "Request Accepted", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/models.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + }, + "201": { + "description": "Request Accepted", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/models.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "integer" + } + } + } + ] + } + }, + "203": { + "description": "Request Accepted", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/models.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "boolean" + } + } + } + ] + } + }, + "204": { + "description": "Request Accepted", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/models.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "number" + } + } + } + ] + } + } + } + } + } + }, + "basePath": "/", + "host": "", + "definitions": { + "models.Response": { + "type": "object", + "properties": { + "status": { + "type": "string" + } + }, + "required": [ + "status" + ] + } + }, + "schemes": [ + "http", + "https" + ] +} \ No newline at end of file diff --git a/testdata/expected_output/dnmt.json b/testdata/expected_output/dnmt.json index 1bc23d4..af244b2 100644 --- a/testdata/expected_output/dnmt.json +++ b/testdata/expected_output/dnmt.json @@ -8,8 +8,13 @@ "/deeplynested": { "get": { "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed id malesuada lorem, et fermentum sapien. Vivamus non pharetra risus, in efficitur leo. Suspendisse sed metus sit amet mi laoreet imperdiet. Donec aliquam eros eu blandit feugiat. Quisque scelerisque justo ac vehicula bibendum. Fusce suscipit arcu nisl, eu maximus odio consequat quis. Curabitur fermentum eleifend tellus, lobortis hendrerit velit varius vitae.", - "consumes": ["application/json"], - "produces": ["application/json", "application/xml"], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], "tags": [], "summary": "this is a test summary", "operationId": "get-/deeplynested", @@ -18,7 +23,11 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/models.ComplexSuccessfulResponse" + "allOf": [ + { + "$ref": "#/definitions/models.ComplexSuccessfulResponse" + } + ] } } } @@ -27,8 +36,13 @@ "/arraydeeplynested": { "get": { "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed id malesuada lorem, et fermentum sapien. Vivamus non pharetra risus, in efficitur leo. Suspendisse sed metus sit amet mi laoreet imperdiet. Donec aliquam eros eu blandit feugiat. Quisque scelerisque justo ac vehicula bibendum. Fusce suscipit arcu nisl, eu maximus odio consequat quis. Curabitur fermentum eleifend tellus, lobortis hendrerit velit varius vitae.", - "consumes": ["application/json"], - "produces": ["application/json", "application/xml"], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], "tags": [], "summary": "this is a test summary", "operationId": "get-/arraydeeplynested", @@ -37,10 +51,14 @@ "200": { "description": "OK", "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/models.ComplexSuccessfulResponse" - } + "allOf": [ + { + "type": "array", + "items": { + "$ref": "#/definitions/models.ComplexSuccessfulResponse" + } + } + ] } } } @@ -61,7 +79,9 @@ }, "models.Deeply": { "type": "object", - "required": ["nested"], + "required": [ + "nested" + ], "properties": { "nested": { "$ref": "#/definitions/models.Nested" @@ -88,7 +108,9 @@ }, "models.Object": { "type": "object", - "required": ["name"], + "required": [ + "name" + ], "properties": { "name": { "type": "string", @@ -97,5 +119,8 @@ } } }, - "schemes": ["http", "https"] -} + "schemes": [ + "http", + "https" + ] +} \ No newline at end of file diff --git a/testdata/expected_output/map.json b/testdata/expected_output/map.json new file mode 100644 index 0000000..8417563 --- /dev/null +++ b/testdata/expected_output/map.json @@ -0,0 +1,81 @@ +{ + "swagger": "2.0", + "info": { + "title": "Testing API", + "description": "", + "version": "v1.0.0" + }, + "paths": { + "/test-map": { + "put": { + "description": "", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "product" + ], + "summary": "", + "operationId": "put-/test-map", + "parameters": [], + "responses": { + "201": { + "description": "Request Accepted", + "schema": { + "allOf": [ + { + "type": "object", + "properties": { + "code": { + "type": "number", + "example": 200 + }, + "data": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 1234 + }, + "id2": { + "type": "string", + "example": "asd" + }, + "id3": { + "type": "number", + "example": 123.23 + }, + "id4": { + "type": "array", + "items": { + "type": "integer" + } + }, + "id5": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + ] + } + } + } + } + } + }, + "basePath": "/", + "host": "", + "definitions": {}, + "schemes": [ + "http", + "https" + ] +} \ No newline at end of file