diff --git a/common.go b/common.go index 469b561..fb1fac6 100644 --- a/common.go +++ b/common.go @@ -45,36 +45,36 @@ func GetUriPathParamList(uriPath string) []*define.ParamConfig { // Author : go_developer@163.com<白茶清欢> // // Date : 11:52 2024/12/24 -func GetDataType(docParamType string, formatType string) consts.DataType { +func GetDataType(docParamType string, formatType string) string { docParamType = strings.ToLower(docParamType) formatType = strings.ToLower(formatType) if len(formatType) == 0 { formatType = docParamType } switch docParamType { - case "integer": + case consts.SwaggerDataTypeInteger: if formatType == "int64" { - return consts.DataTypeInt + return consts.DataTypeInt.String() } - return consts.DataTypeUint + return consts.DataTypeUint.String() case "string", "apikey": - return consts.DataTypeString + return consts.DataTypeString.String() case "object": - return consts.DataTypeMapStrAny + return consts.DataTypeMapStrAny.String() case "boolean": - return consts.DataTypeBool + return consts.DataTypeBool.String() case "number", "float", "double", "float32", "float64": - return consts.DataTypeFloat + return consts.DataTypeFloat.String() case "array": if formatType == "integer" { - return consts.DataTypeSliceInt + return consts.DataTypeSliceInt.String() } else if formatType == "string" { - return consts.DataTypeSliceString + return consts.DataTypeSliceString.String() } else { - return consts.DataTypeSliceAny + return consts.DataTypeSliceAny.String() } default: - return consts.DataTypeAny + return consts.DataTypeAny.String() } } @@ -137,7 +137,7 @@ func ExpandArrayParam(swaggerDoc *define.Swagger, ref string, rootPath string) [ res = append(res, &define.ParamConfig{ Location: consts.RequestDataLocationBody.String(), Path: pathPrefix + "{{#idx#}}." + itemKey, - Type: GetDataType(itemConfig.Type, "").String(), + Type: GetDataType(itemConfig.Type, ""), Title: pathPrefix + itemKey, Description: pathPrefix + itemKey, Required: false, @@ -158,7 +158,7 @@ func ExpandArrayResult(swaggerDoc *define.Swagger, ref string, rootPath string) res = append(res, &define.ResultConfig{ Location: consts.ResponseDataLocationBody.String(), Path: pathPrefix + "{{#idx#}}." + itemKey, - Type: GetDataType(itemConfig.Type, "").String(), + Type: GetDataType(itemConfig.Type, ""), Title: pathPrefix + itemKey, Description: pathPrefix + itemKey, }) diff --git a/define/generate.go b/define/generate.go index 7ecfe1b..4aad3d6 100644 --- a/define/generate.go +++ b/define/generate.go @@ -30,13 +30,15 @@ type DocParseResult struct { type UriBaseConfig struct { Uri string `json:"uri"` // 接口路由 Method string `json:"method"` // 接口请求方法 - ContentType string `json:"content_type"` // 接口请求类型 - OutputContentType string `json:"output_content_type"` // 输出数据类型 + ContentType []string `json:"content_type"` // 接口请求类型 + OutputContentType []string `json:"output_content_type"` // 输出数据类型 TagList []string `json:"tag_list"` // 接口标签列表 Summary string `json:"summary"` // 接口摘要描述 Description string `json:"description"` // 接口详细描述 ParamList []*ParamConfig `json:"param_list"` // 参数列表 ResultList []*ResultConfig `json:"result_list"` // 返回值列表 + Deprecated bool `json:"deprecated"` // 是否弃用 + OutputStrict bool `json:"output_strict"` // 严格模式 } // ParamConfig 参数配置 @@ -45,12 +47,13 @@ type UriBaseConfig struct { // // Date : 16:23 2024/8/19 type ParamConfig struct { - Location string `json:"location"` // 参数位置 - Path string `json:"path"` // 参数路径 - Type string `json:"type"` // 参数类型 - Title string `json:"title"` // 参数标题 - Description string `json:"description"` // 参数描述 - Required bool `json:"required"` // 是否必传 + Location string `json:"location"` // 参数位置 + Path string `json:"path"` // 参数路径 + Type string `json:"type"` // 参数类型 + Title string `json:"title"` // 参数标题 + Description string `json:"description"` // 参数描述 + Required bool `json:"required"` // 是否必传 + Enum []string `json:"enum"` // 枚举值列表 } // ResultConfig 返回值配置 diff --git a/define/openapi.go b/define/openapi.go index e8da7ae..f536588 100644 --- a/define/openapi.go +++ b/define/openapi.go @@ -44,6 +44,7 @@ type PathConfig struct { Head *PathItemOperationConfig `json:"head,omitempty"` // 定义适用于此路径的 HEAD 操作。 Patch *PathItemOperationConfig `json:"patch,omitempty"` // 定义适用于此路径的 PATCH 操作。 Trace *PathItemOperationConfig `json:"trace,omitempty"` // 定义适用于此路径的 TRACE 操作。 + Connect *PathItemOperationConfig `json:"connect,omitempty"` // 定义适用于此路径的 CONNECT 操作。 } // PathItemConfig 接口的具体配置 @@ -99,17 +100,17 @@ type ExternalDocs struct { // // Date : 12:29 2024/7/19 type PathConfigParameter struct { - Name string `json:"name"` // 参数名称 - In string `json:"in"` // 必选. 参数的位置,可能的值有 "query", "header", "path" 或 "cookie"。 - Description string `json:"description"` // 对此参数的简要描述,这里可以包含使用示例。CommonMark syntax可以被用来呈现富文本格式. - Required bool `json:"required"` // 标明此参数是否是必选参数。如果 参数位置 的值是 path,那么这个参数一定是 必选 的因此这里的值必须是true。其他的则视情况而定。此字段的默认值是false。 - Deprecated bool `json:"deprecated"` // 标明一个参数是被弃用的而且应该尽快移除对它的使用。 - Schema *Schema `json:"schema,omitempty"` // 定义适用于此参数的类型结构。 Schema 对象 | Reference 对象 - AllowEmptyValue bool `json:"allowEmptyValue"` // 设置是否允许传递空参数,这只在参数值为query时有效,默认值是false。如果同时指定了style属性且值为n/a(无法被序列化),那么此字段 allowEmptyValue应该被忽略。 - Style string `json:"style"` // 描述根据参数值类型的不同如何序列化参数。默认值为(基于in字段的值):query 对应 form;path 对应 simple; header 对应 simple; cookie 对应 form。 - Explode bool `json:"explode"` // 当这个值为true时,参数值类型为array或object的参数使用数组内的值或对象的键值对生成带分隔符的参数值。对于其他类型的参数,这个字段没有任何影响。当 style 是 form时,这里的默认值是 true,对于其他 style 值类型,默认值是false。 - AllowReserved bool `json:"allowReserved"` // 决定此参数的值是否允许不使用%号编码使用定义于 RFC3986内的保留字符 :/?#[]@!$&'()*+,;=。 这个属性仅用于in的值是query时,此字段的默认值是false。 - Ref string `json:"$ref"` // 一个允许引用规范内部的其他部分或外部规范的对象。 Reference 对象 定义于 JSON Reference 且遵循相同的结构、行为和规则。 + Name string `json:"name"` // 参数名称 + In string `json:"in"` // 必选. 参数的位置,可能的值有 "query", "header", "path" 或 "cookie"。 + Description string `json:"description"` // 对此参数的简要描述,这里可以包含使用示例。CommonMark syntax可以被用来呈现富文本格式. + Required bool `json:"required"` // 标明此参数是否是必选参数。如果 参数位置 的值是 path,那么这个参数一定是 必选 的因此这里的值必须是true。其他的则视情况而定。此字段的默认值是false。 + Deprecated bool `json:"deprecated"` // 标明一个参数是被弃用的而且应该尽快移除对它的使用。 + Schema *Schema `json:"schema,omitempty,omitempty"` // 定义适用于此参数的类型结构。 Schema 对象 | Reference 对象 + AllowEmptyValue bool `json:"allowEmptyValue"` // 设置是否允许传递空参数,这只在参数值为query时有效,默认值是false。如果同时指定了style属性且值为n/a(无法被序列化),那么此字段 allowEmptyValue应该被忽略。 + Style string `json:"style,omitempty"` // 描述根据参数值类型的不同如何序列化参数。默认值为(基于in字段的值):query 对应 form;path 对应 simple; header 对应 simple; cookie 对应 form。 + Explode bool `json:"explode,omitempty"` // 当这个值为true时,参数值类型为array或object的参数使用数组内的值或对象的键值对生成带分隔符的参数值。对于其他类型的参数,这个字段没有任何影响。当 style 是 form时,这里的默认值是 true,对于其他 style 值类型,默认值是false。 + AllowReserved bool `json:"allowReserved,omitempty"` // 决定此参数的值是否允许不使用%号编码使用定义于 RFC3986内的保留字符 :/?#[]@!$&'()*+,;=。 这个属性仅用于in的值是query时,此字段的默认值是false。 + Ref string `json:"$ref,omitempty"` // 一个允许引用规范内部的其他部分或外部规范的对象。 Reference 对象 定义于 JSON Reference 且遵循相同的结构、行为和规则。 } // Schema ... @@ -118,18 +119,21 @@ type PathConfigParameter struct { // // Date : 12:32 2024/7/19 type Schema struct { - Nullable bool `json:"nullable"` // 对于定义的schema,允许发送 null 值。默认值是 false. + Nullable bool `json:"nullable,omitempty"` // 对于定义的schema,允许发送 null 值。默认值是 false. Discriminator *SchemaDiscriminator `json:"discriminator,omitempty"` // 说白了, 就是一个字段可能是不同的数据结构。。。 - ReadOnly bool `json:"readOnly"` // 仅与 Schema "properties" 定义有关。 声明此属性是 "readonly" 的。这意味着它可以作为 response 的一部分但不应该作为 request 的一部分被发送。如果一个 property 的 readOnly 被标记为 true 且在 required 列表中,required 将只作用于 response。一个 property 的 readOnly 和 writeOnly 不允许同时被标记为 true。默认值是 false。 - WriteOnly bool `json:"writeOnly"` // 仅与 Schema "properties" 定义有关。声明此 property 为 "write only"。所以它可以作为 request 的一部分而不应该作为 response 的一部分被发送。如果一个 property 的 writeOnly 被标记为 true 且在 required 列表中,required 将只作用于 request。一个 property 的 readOnly 和 writeOnly 不能同时被标记为 true。默认值是 false。 - Xml *XML `json:"xml,omitempty"` // 这只能用于 properties schemas,在 root schemas 中没有效果。 + ReadOnly bool `json:"readOnly,omitempty"` // 仅与 Schema "properties" 定义有关。 声明此属性是 "readonly" 的。这意味着它可以作为 response 的一部分但不应该作为 request 的一部分被发送。如果一个 property 的 readOnly 被标记为 true 且在 required 列表中,required 将只作用于 response。一个 property 的 readOnly 和 writeOnly 不允许同时被标记为 true。默认值是 false。 + WriteOnly bool `json:"writeOnly,omitempty"` // 仅与 Schema "properties" 定义有关。声明此 property 为 "write only"。所以它可以作为 request 的一部分而不应该作为 response 的一部分被发送。如果一个 property 的 writeOnly 被标记为 true 且在 required 列表中,required 将只作用于 request。一个 property 的 readOnly 和 writeOnly 不能同时被标记为 true。默认值是 false。 + Xml *XML `json:"xml,omitempty"` // 这只能用于 properties schemas,在root schemas 中没有效果。 ExternalDocs *ExternalDocs `json:"externalDocs,omitempty"` // 此 schema 附加的外部文档。 Example string `json:"example,omitempty"` // 一个用于示范此 schema实例的示例,可以是任意格式。为了表达无法用 JSON 或 YAML 格式呈现的示例,可以使用 string 类型的值,且在必要的地方需要使用字符转义。 - Deprecated bool `json:"deprecated"` // 表示一个 schema 是废弃的,应该逐渐被放弃使用。默认值是 false. + Deprecated bool `json:"deprecated,omitempty"` // 表示一个 schema 是废弃的,应该逐渐被放弃使用。默认值是 false. Properties map[string]*Property `json:"properties,omitempty"` // 数据字段 => 数据规则 Required []string `json:"required,omitempty"` // 必传属性列表 Enum []any `json:"enum,omitempty"` // 枚举值列表 - Type string `json:"type"` // 类型 + Type string `json:"type,omitempty"` // 类型 + Items *PropertyXOf `json:"items,omitempty"` // items 必须存在如果 type 的值是 array。 + Ref string `json:"$ref,omitempty"` // 类型引用 + Format string `json:"format,omitempty"` // 格式化类型 } // Property 是从 JSON Schema 提取出来的,但是做了一些调整以适应 OpenAPI Specification。 @@ -138,11 +142,11 @@ type Schema struct { // // Date : 17:05 2024/7/19 type Property struct { - Type string `json:"type"` // 数据类型, swagger本身的定义 - Format string `json:"format"` // 对应编程语言中的数据类型描述 + Type string `json:"type,omitempty"` // 数据类型, swagger本身的定义 + Format string `json:"format,omitempty"` // 对应编程语言中的数据类型描述 Enum []any `json:"enum,omitempty"` // 枚举值列表 - Default any `json:"default"` // 默认值 : 不同于 JSON Schema,这个值必须符合定义与相同级别的 Schema 对象 中定义的类型,比如 type 是 string,那么 default 可以是 "foo" 但不能是 1。 - Description string `json:"description"` // 数据描述, CommonMark syntax可以被用来呈现富文本格式. + Default any `json:"default,omitempty"` // 默认值 : 不同于 JSON Schema,这个值必须符合定义与相同级别的 Schema 对象 中定义的类型,比如 type 是 string,那么 default 可以是 "foo" 但不能是 1。 + Description string `json:"description,omitempty"` // 数据描述, CommonMark syntax可以被用来呈现富文本格式. AllOf []*PropertyXOf `json:"allOf,omitempty"` // type 是一个对象, allOf 指向对象描述 OneOf []*PropertyXOf `json:"oneOf,omitempty"` // type 是一个对象, allOf 指向对象描述 AnyOf []*PropertyXOf `json:"anyOf,omitempty"` // type 是一个对象, allOf 指向对象描述 @@ -191,10 +195,10 @@ type XML struct { // // Date : 17:17 2024/7/19 type RequestBody struct { - Required bool `json:"required"` // 指定请求体是不是应该被包含在请求中,默认值是false。 - Description string `json:"description"` // 对请求体的简要描述,可以包含使用示例,CommonMark syntax可以被用来呈现富文本格式 - Content map[string]*Media `json:"content"` // content_type => 相应数据描述的映射 必选. 请求体的内容。请求体的属性key是一个媒体类型或者媒体类型范围,值是对应媒体类型的示例数据。对于能匹配多个key的请求,定义更明确的请求会更优先被匹配。比如text/plain会覆盖text/*的定义。 - Ref string `json:"$ref"` // 一个允许引用规范内部的其他部分或外部规范的对象。 Reference 对象 定义于 JSON Reference 且遵循相同的结构、行为和规则。 + Required bool `json:"required"` // 指定请求体是不是应该被包含在请求中,默认值是false。 + Description string `json:"description,omitempty"` // 对请求体的简要描述,可以包含使用示例,CommonMark syntax可以被用来呈现富文本格式 + Content map[string]*Media `json:"content,omitempty"` // content_type => 相应数据描述的映射 必选. 请求体的内容。请求体的属性key是一个媒体类型或者媒体类型范围,值是对应媒体类型的示例数据。对于能匹配多个key的请求,定义更明确的请求会更优先被匹配。比如text/plain会覆盖text/*的定义。 + Ref string `json:"$ref,omitempty"` // 一个允许引用规范内部的其他部分或外部规范的对象。 Reference 对象 定义于 JSON Reference 且遵循相同的结构、行为和规则。 } // Media 本质即为不一样 content_type 对应的数据结构定义 @@ -203,10 +207,10 @@ type RequestBody struct { // // Date : 17:21 2024/7/19 type Media struct { - Schema *Schema `json:"schema"` // 定义此媒体类型的结构。 - Example string `json:"example"` // 媒体类型的示例。示例对象应该符合此媒体类型的格式, 这里指定的example对象 object is mutually exclusive of the examples object. 而且如果引用的schema也包含示例,在这里指定的example值将会覆盖schema提供的示例。 - Examples map[string]*Example `json:"examples"` // 媒体类型的示例,每个媒体对象的值都应该匹配它对应的媒体类型的格式。 The examples object is mutually exclusive of the example object. 而且如果引用的schema也包含示例,在这里指定的example值将会覆盖schema提供的示例。 - Encoding map[string]*Encoding `json:"encoding"` // 属性名与编码信息的映射。每个属性名必须存在于schema属性的key中,当媒体类型等于multipart或application/x-www-form-urlencoded时,编码对象信息仅适用于requestBody。 + Schema *Schema `json:"schema,omitempty"` // 定义此媒体类型的结构。 + Example string `json:"example,omitempty"` // 媒体类型的示例。示例对象应该符合此媒体类型的格式, 这里指定的example对象 object is mutually exclusive of the examples object. 而且如果引用的schema也包含示例,在这里指定的example值将会覆盖schema提供的示例。 + Examples map[string]*Example `json:"examples,omitempty"` // 媒体类型的示例,每个媒体对象的值都应该匹配它对应的媒体类型的格式。 The examples object is mutually exclusive of the example object. 而且如果引用的schema也包含示例,在这里指定的example值将会覆盖schema提供的示例。 + Encoding map[string]*Encoding `json:"encoding,omitempty"` // 属性名与编码信息的映射。每个属性名必须存在于schema属性的key中,当媒体类型等于multipart或application/x-www-form-urlencoded时,编码对象信息仅适用于requestBody。 } // Encoding 一个编码定义仅适用于一个结构属性 @@ -215,11 +219,11 @@ type Media struct { // // Date : 17:28 2024/7/19 type Encoding struct { - ContentType string `json:"content_type"` // 对具体属性的 Content-Type的编码。默认值取决于属性的类型:application/octet-stream编码适用于binary格式的string;text/plain适用于其他原始值;application/json适用于object;对于array值类型的默认值取决于数组内元素的类型,默认值可以是明确的媒体类型(比如application/json), 或者通配符类型的媒体类型(比如image/*), 又或者是用分号分隔的两种媒体类型。 - Headers map[string]*Header `json:"headers"` // 提供附加信息的请求头键值对映射。比如Content-Disposition、Content-Type各自描述了不同的信息而且在这里将会被忽略,如果请求体的媒体类型不是multipart,这个属性将会被忽略。 - Style string `json:"style"` // 描述一个属性根据它的类型将会被如何序列化。查看Parameter 对象的style属性可以得到更多详细信息。这个属性的行为与query参数相同,包括默认值的定义。如果请求体的媒体类型不是application/x-www-form-urlencoded,这个属性将会被忽略。 - Explode bool `json:"explode"` // 当这个值为true时,类型为array或object的属性值会为数组的每个元素或对象的每个键值对分开生成参数。这个属性对其他数据类型没有影响。当style为form时,这个属性的默认值是true,对于其他的style类型,这个属性的默认值是false。这个属性会被忽略如果请求体的媒体类型不是application/x-www-form-urlencoded。 - AllowReserved bool `json:"allowReserved"` // 决定此参数的值是否允许不使用%号编码使用定义于 RFC3986内的保留字符 :/?#[]@!$&'()*+,;=。 这个属性仅用于in的值是query时,此字段的默认值是false。 这个属性会被忽略如果请求体的媒体类型不是application/x-www-form-urlencoded。 + ContentType string `json:"content_type,omitempty"` // 对具体属性的 Content-Type的编码。默认值取决于属性的类型:application/octet-stream编码适用于binary格式的string;text/plain适用于其他原始值;application/json适用于object;对于array值类型的默认值取决于数组内元素的类型,默认值可以是明确的媒体类型(比如application/json), 或者通配符类型的媒体类型(比如image/*), 又或者是用分号分隔的两种媒体类型。 + Headers map[string]*Header `json:"headers,omitempty"` // 提供附加信息的请求头键值对映射。比如Content-Disposition、Content-Type各自描述了不同的信息而且在这里将会被忽略,如果请求体的媒体类型不是multipart,这个属性将会被忽略。 + Style string `json:"style,omitempty"` // 描述一个属性根据它的类型将会被如何序列化。查看Parameter 对象的style属性可以得到更多详细信息。这个属性的行为与query参数相同,包括默认值的定义。如果请求体的媒体类型不是application/x-www-form-urlencoded,这个属性将会被忽略。 + Explode bool `json:"explode,omitempty"` // 当这个值为true时,类型为array或object的属性值会为数组的每个元素或对象的每个键值对分开生成参数。这个属性对其他数据类型没有影响。当style为form时,这个属性的默认值是true,对于其他的style类型,这个属性的默认值是false。这个属性会被忽略如果请求体的媒体类型不是application/x-www-form-urlencoded。 + AllowReserved bool `json:"allowReserved,omitempty"` // 决定此参数的值是否允许不使用%号编码使用定义于 RFC3986内的保留字符 :/?#[]@!$&'()*+,;=。 这个属性仅用于in的值是query时,此字段的默认值是false。 这个属性会被忽略如果请求体的媒体类型不是application/x-www-form-urlencoded。 } // Header 对象除了以下改动之外与 Parameter 对象 一致: @@ -239,10 +243,10 @@ type Header PathConfigParameter // // Date : 17:24 2024/7/19 type Example struct { - Summary string `json:"summary"` // example 的简要描述。 - Description string `json:"description"` // example 的详细描述。CommonMark syntax可以被用来呈现富文本格式. - Value any `json:"value"` // 嵌入的字面量 example。 value 字段和 externalValue 字段是互斥的。无法使用 JSON 或 YAML 表示的媒体类型可以使用字符串值来表示。 - ExternalValue string `json:"externalValue"` // 指向字面 example 的一个 URL。这提供了引用无法被包含在 JSON 或 YAML 文档中的 example。value 字段和 externalValue 字段是互斥的。 + Summary string `json:"summary,omitempty"` // example 的简要描述。 + Description string `json:"description,omitempty"` // example 的详细描述。CommonMark syntax可以被用来呈现富文本格式. + Value any `json:"value,omitempty"` // 嵌入的字面量 example。 value 字段和 externalValue 字段是互斥的。无法使用 JSON 或 YAML 表示的媒体类型可以使用字符串值来表示。 + ExternalValue string `json:"externalValue,omitempty"` // 指向字面 example 的一个 URL。这提供了引用无法被包含在 JSON 或 YAML 文档中的 example。value 字段和 externalValue 字段是互斥的。 } // Response 响应的数据结构 @@ -251,10 +255,10 @@ type Example struct { // // Date : 17:38 2024/7/19 type Response struct { - Description string `json:"description"` // 必选. 对响应的简短描述。CommonMark syntax可以被用来呈现富文本格式. - Headers map[string]*Header `json:"headers"` // 映射HTTP头名称到其定义。RFC7230 规定了HTTP头名称不区分大小写。如果一个响应头使用"Content-Type"作为HTTP头名称,它会被忽略。 - Content map[string]*Media `json:"content"` // 一个包含描述预期响应负载的映射。使用 media type 或 media type range 作为键,以响应的描述作为值。当一个响应匹配多个键时,只有最明确的键才适用。比如:text/plain 会覆盖 text/* - Ref string `json:"$ref"` // 引用描述 + Description string `json:"description" required:"true"` // 必选. 对响应的简短描述。CommonMark syntax可以被用来呈现富文本格式. + Headers map[string]*Header `json:"headers,omitempty"` // 映射HTTP头名称到其定义。RFC7230 规定了HTTP头名称不区分大小写。如果一个响应头使用"Content-Type"作为HTTP头名称,它会被忽略。 + Content map[string]*Media `json:"content,omitempty"` // 一个包含描述预期响应负载的映射。使用 media type 或 media type range 作为键,以响应的描述作为值。当一个响应匹配多个键时,只有最明确的键才适用。比如:text/plain 会覆盖 text/* + Ref string `json:"$ref,omitempty"` // 引用描述 } // Info 信息 @@ -263,12 +267,12 @@ type Response struct { // // Date : 16:10 2024/4/19 type Info struct { - Description string `json:"description"` // 对应用的简短描述。 CommonMark syntax 可以被用来表示富文本呈现。 - Title string `json:"title" required:"true"` // 必选. 应用的名称。 - TermsOfService string `json:"termsOfService"` // 指向服务条款的 URL 地址,必须是 URL 地址格式。 - Contact *Contact `json:"contact,omitempty"` // 联系方式 - License *License `json:"license,omitempty"` // 开源协议 - Version string `json:"version" required:"true"` // 必选. API 文档的版本信息(注意:这个版本和开放 API 规范版本没有任何关系)。 + Description string `json:"description,omitempty"` // 对应用的简短描述。 CommonMark syntax 可以被用来表示富文本呈现。 + Title string `json:"title,omitempty" required:"true"` // 必选. 应用的名称。 + TermsOfService string `json:"termsOfService,omitempty"` // 指向服务条款的 URL 地址,必须是 URL 地址格式。 + Contact *Contact `json:"contact,omitempty,omitempty"` // 联系方式 + License *License `json:"license,omitempty"` // 开源协议 + Version string `json:"version" required:"true"` // 必选. API 文档的版本信息(注意:这个版本和开放 API 规范版本没有任何关系)。 } // Contact 联系人信息 @@ -277,9 +281,9 @@ type Info struct { // // Date : 16:08 2024/4/19 type Contact struct { - Name string `json:"name"` // 人或组织的名称。 - Url string `json:"url"` // 指向联系人信息的 URL 地址,必须是 URL 地址格式。 - Email string `json:"email"` // 人或组织的 email 地址,必须是 email 地址格式 + Name string `json:"name,omitempty"` // 人或组织的名称。 + Url string `json:"url,omitempty"` // 指向联系人信息的 URL 地址,必须是 URL 地址格式。 + Email string `json:"email,omitempty"` // 人或组织的 email 地址,必须是 email 地址格式 } // License 开源协议 @@ -288,8 +292,8 @@ type Contact struct { // // Date : 16:09 2024/4/19 type License struct { - Name string `json:"name"` // 开源协议名 - Url string `json:"url"` // 开源协议地址 + Name string `json:"name,omitempty"` // 开源协议名 + Url string `json:"url,omitempty"` // 开源协议地址 } // ServerItem server 对象结构 @@ -298,9 +302,9 @@ type License struct { // // Date : 14:18 2024/7/19 type ServerItem struct { - Url string `json:"url" required:"true"` // 必选. 指向目标主机的 URL 地址。这个 URL 地址支持服务器变量而且可能是相对路径,表示主机路径是相对于本文档所在的路径。当一个变量被命名为类似{brackets}时需要替换此变量。 - Description string `json:"description"` // 一个可选的字符串,用来描述此 URL 地址。CommonMark syntax可以被用来呈现富文本格式. - Variables map[string]*ServerItemVariable `json:"variables"` // 当 url 中出现变量名的时候, 会从次变量中读取数据替换到url中. 变量名 -> 变量配置 + Url string `json:"url,omitempty" required:"true"` // 必选. 指向目标主机的 URL 地址。这个 URL 地址支持服务器变量而且可能是相对路径,表示主机路径是相对于本文档所在的路径。当一个变量被命名为类似{brackets}时需要替换此变量。 + Description string `json:"description,omitempty"` // 一个可选的字符串,用来描述此 URL 地址。CommonMark syntax可以被用来呈现富文本格式. + Variables map[string]*ServerItemVariable `json:"variables,omitempty"` // 当 url 中出现变量名的时候, 会从次变量中读取数据替换到url中. 变量名 -> 变量配置 } // ServerItemVariable ... @@ -309,9 +313,9 @@ type ServerItem struct { // // Date : 14:22 2024/7/19 type ServerItemVariable struct { - Default string `json:"default"` // 变量默认值 - Description string `json:"description"` // 变量描述 - Enum []string `json:"enum"` // 变量枚举值 + Default string `json:"default,omitempty"` // 变量默认值 + Description string `json:"description,omitempty"` // 变量描述 + Enum []string `json:"enum,omitempty"` // 变量枚举值 } // TagItem 每一个标签数据结构 @@ -320,6 +324,6 @@ type ServerItemVariable struct { // // Date : 12:18 2024/7/19 type TagItem struct { - Name string `json:"name"` // 标签名称 - Description string `json:"description"` // 标签描述 + Name string `json:"name,omitempty"` // 标签名称 + Description string `json:"description,omitempty"` // 标签描述 } diff --git a/define/tag.go b/define/tag.go new file mode 100644 index 0000000..4f89323 --- /dev/null +++ b/define/tag.go @@ -0,0 +1,33 @@ +// Package define ... +// +// Description : define ... +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 2025-02-11 21:51 +package define + +const ( + TagJson = "json" + TagXml = "xml" + TagYaml = "yaml" + TagYml = "yml" + TagForm = "form" + TagBinding = "binding" + TagValidate = "validate" + TagErr = "err" + TagMsg = "msg" + TagDesc = "desc" + TagDescription = "description" + TagD = "d" + TagDefault = "default" + TagDeprecated = "deprecated" + TagSummary = "summary" + TagPath = "path" // 接口的请求路径 + TagMethod = "method" // 接口的请求方法 + TagUriTag = "tag" // 接口的tag + TagOutputStrict = "output_strict" // 接口数据是否为严格模式 : 严格模式, 响应数据必须是结构体/map,非严格模式返回任意值 + TagErrMsg = "err" // 验证失败错误信息tag + TagContentType = "content_type" + TagOutputContentType = "output_content_type" +) diff --git a/define/uri.go b/define/uri.go new file mode 100644 index 0000000..72b15e5 --- /dev/null +++ b/define/uri.go @@ -0,0 +1,20 @@ +// Package define ... +// +// Description : define ... +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 2025-02-13 21:29 +package define + +// UriConfig 接口基础配置 +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 21:29 2025/2/13 +type UriConfig struct { + Path string `json:"path"` // 接口路由, 必须配置 + RequestMethod string `json:"request_method"` // 接口请求方法, 必须配置 + TagList []string `json:"tag_list"` // 接口分组 + Desc string `json:"desc"` // 接口描述 +} diff --git a/generate.go b/generate.go index 5b60bea..85dc618 100644 --- a/generate.go +++ b/generate.go @@ -8,8 +8,13 @@ package api_doc import ( + "errors" + "fmt" "git.zhangdeman.cn/gateway/api-doc/define" "git.zhangdeman.cn/zhangdeman/consts" + "git.zhangdeman.cn/zhangdeman/wrapper" + "net/http" + "reflect" "strings" ) @@ -18,26 +23,44 @@ import ( // Author : go_developer@163.com<白茶清欢> // // Date : 15:56 2024/7/22 -func NewOpenapiDoc() *Generate { +func NewOpenapiDoc(info *define.Info, servers []*define.ServerItem) *Generate { + if nil == info { + info = &define.Info{ + Description: "openapi接口文档", + Title: "openapi接口文档", + TermsOfService: "", + Contact: nil, + License: nil, + Version: "0.0.1", + } + } + if len(info.Version) == 0 { + info.Version = "0.0.1" + } + if nil == info.License { + info.License = &define.License{ + Name: consts.LicenseApache20, + Url: consts.LicenseUrlTable[consts.LicenseApache20], + } + } + if nil == info.Contact { + info.Contact = &define.Contact{ + Name: "研发人员(developer)", + Url: "", + Email: "", + } + } + if nil == servers { + servers = []*define.ServerItem{} + } return &Generate{ + readMethodList: []string{ + http.MethodGet, http.MethodHead, http.MethodConnect, http.MethodOptions, http.MethodTrace, + }, docData: &define.OpenapiDoc{ - Openapi: consts.SwaggerDocVersion3, - Info: &define.Info{ - Description: "openapi接口文档", - Title: "openapi接口文档", - TermsOfService: "", - Contact: &define.Contact{ - Name: "", - Url: "", - Email: "", - }, - License: &define.License{ - Name: "", - Url: "", - }, - Version: "", - }, - Servers: make([]*define.ServerItem, 0), + Openapi: consts.SwaggerDocVersion3, + Info: info, + Servers: servers, Components: &define.Components{Schemas: map[string]*define.Schema{}}, Tags: make([]*define.TagItem, 0), Paths: make(map[string]*define.PathConfig), @@ -51,7 +74,8 @@ func NewOpenapiDoc() *Generate { // // Date : 15:57 2024/7/22 type Generate struct { - docData *define.OpenapiDoc + docData *define.OpenapiDoc + readMethodList []string } // SetLicense 设置文档协议 @@ -121,23 +145,6 @@ func (g *Generate) AddServer(serverDomain string, serverDesc string, serverVaria } } -/** -type PathItemOperationConfig struct { - Tags []string `json:"tags"` // 用于控制API文档的标签列表,标签可以用于在逻辑上分组对资源的操作或作为其它用途的先决条件。 - Summary string `json:"summary"` // 对此操作行为的简短描述。 - Description string `json:"description"` // 对此操作行为的详细解释。CommonMark syntax可以被用来呈现富文本格式. - ExternalDocs *ExternalDocs `json:"externalDocs,omitempty"` // 附加的外部文档。 - OperationID string `json:"operationId"` // 用于标识此操作的唯一字符串,这个id在此API内包含的所有操作中必须是唯一的。相关的工具和库可能会使用此operationId来唯一的标识一个操作,因此推荐在命名时符合一般的编程命名习惯 - Parameters []*PathConfigParameter `json:"parameters,omitempty"` // 定义可用于此操作的参数列表,如果一个同名的参数已经存在于 Path Item,那么这里的定义会覆盖它但是不能移除上面的定义。这个列表不允许包含重复的参数,参数的唯一性由 name 和 location 的组合来确定。这个列表可以使用 Reference 对象 来连接定义于 OpenAPI 对象 components/parameters 的参数。 - RequestBody *RequestBody `json:"requestBody,omitempty"` // 可用于此操作的请求体。requestBody 只能被用于HTTP 1.1 规范 RFC7231 中明确定义了包含请求体的请求方法,在其他没有明确定义的请求方法中,requestBody的消费者应该应该忽略requestBody。 - Responses map[string]*Response `json:"responses"` // 描述一个操作可能发生的响应的响应码与响应包含的响应体的对象。default字段可以用来标记一个响应适用于其他未被规范明确定义的HTTP响应码的默认响应。一个Responses 对象必须至少包含一个响应码,而且是成功的响应。 - Callbacks map[string]any `json:"callbacks,omitempty"` // 一组相对于父操作的可能出现的回调映射,映射中的每一个键都唯一的映射一个 Callback 对象 - Deprecated bool `json:"deprecated"` // 声明此操作已经被废弃,使用者应该尽量避免使用此操作,默认的值是 false。 - Security any `json:"security,omitempty"` // 声明那种安全机制可用于此操作。这个列表可以包含多种可用于此操作的安全需求对象,但是在认证一个请求时应该仅使用其中一种。这里的定义会覆盖任何在顶层 security 中的安全声明,因此可以声明一个空数组来变相的移除顶层的安全声明。 - Servers []*ServerItem `json:"servers,omitempty"` // 一个可用于此操作的额外的 server 数组,这里的定义会覆盖 Path Item 对象 或 顶层的定义。 -} -*/ - // AddApi 新增Api // // Author : go_developer@163.com<白茶清欢> @@ -149,7 +156,7 @@ type PathItemOperationConfig struct { // &define.UriBaseConfig{ // Uri: "/foo/bar", // Method: http.MethodPost, -// ContentType: "application/json", +// ContentType: ["application/json"], // TagList: []string{"测试标签"}, // Summary: "这是一份示例基础配置", // Description: "这是一份示例基础配置", @@ -159,6 +166,503 @@ type PathItemOperationConfig struct { // // resultList : 返回值列表 func (g *Generate) AddApi(baseCfg *define.UriBaseConfig, paramList []*define.ParamConfig, resultList []*define.ResultConfig) error { - return nil } + +// AddApiFromInAndOut 通过请求参数的 +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 14:22 2025/2/9 +func (g *Generate) AddApiFromInAndOut(paramType reflect.Type, resultType reflect.Type) error { + if paramType.Kind() == reflect.Ptr { + paramType = paramType.Elem() + } + if resultType.Kind() == reflect.Ptr { + resultType = resultType.Elem() + } + baseCfg, err := g.parseBaseUriConfig(paramType) + if nil != err { + return err + } + if _, exist := g.docData.Paths[baseCfg.Uri]; !exist { + g.docData.Paths[baseCfg.Uri] = &define.PathConfig{} + } + // 接口文档初始化 + cfg := g.getApiDocBaseCfg(baseCfg, paramType) + + if wrapper.ArrayType(g.readMethodList).Has(baseCfg.Method) >= 0 { + cfg.RequestBody = nil // get类请求没有request body + // 参数解析 + g.ParseReadConfigParam(baseCfg, cfg, paramType) + } else { + // post类解析 + paramSchemaName := g.AddComponentsSchema("", paramType.PkgPath(), paramType) + for _, itemType := range baseCfg.ContentType { + cfg.RequestBody.Content[itemType] = &define.Media{ + Schema: &define.Schema{ + Ref: g.getSchemaRef(paramSchemaName), + }, + Example: "", + Examples: nil, + Encoding: nil, + } + } + } + // 无论什么请求, 对于result解析逻辑一致 + resultSchemaName := g.AddComponentsSchema("", resultType.PkgPath(), resultType) + for _, itemOutputType := range baseCfg.OutputContentType { + cfg.Responses[fmt.Sprintf("%v", http.StatusOK)].Content[itemOutputType] = &define.Media{ + Schema: &define.Schema{ + Ref: g.getSchemaRef(resultSchemaName), + }, + Example: "", + Examples: nil, + Encoding: nil, + } + } + g.setApiDoc(baseCfg, cfg) + return nil +} + +// setApiDoc 设置文档配置 +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 16:13 2025/2/14 +func (g *Generate) setApiDoc(baseCfg *define.UriBaseConfig, apiDocCfg *define.PathItemOperationConfig) { + switch baseCfg.Method { + case http.MethodGet: + g.docData.Paths[baseCfg.Uri].Get = apiDocCfg + case http.MethodHead: + g.docData.Paths[baseCfg.Uri].Head = apiDocCfg + case http.MethodPost: + g.docData.Paths[baseCfg.Uri].Post = apiDocCfg + case http.MethodPut: + g.docData.Paths[baseCfg.Uri].Put = apiDocCfg + case http.MethodPatch: + g.docData.Paths[baseCfg.Uri].Patch = apiDocCfg + case http.MethodDelete: + g.docData.Paths[baseCfg.Uri].Delete = apiDocCfg + case http.MethodConnect: + g.docData.Paths[baseCfg.Uri].Connect = apiDocCfg + case http.MethodOptions: + g.docData.Paths[baseCfg.Uri].Options = apiDocCfg + case http.MethodTrace: + g.docData.Paths[baseCfg.Uri].Trace = apiDocCfg + default: + panic("unknown method: " + baseCfg.Method) + } +} + +// getApiDocBaseCfg 获取接口文档的基础配置 +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 16:10 2025/2/14 +func (g *Generate) getApiDocBaseCfg(baseCfg *define.UriBaseConfig, paramType reflect.Type) *define.PathItemOperationConfig { + defaultPkgPath := wrapper.String(strings.TrimLeft(baseCfg.Uri, "/")).SnakeCaseToCamel() + cfg := &define.PathItemOperationConfig{ + Tags: baseCfg.TagList, + Summary: baseCfg.Summary, + Description: baseCfg.Description, + ExternalDocs: nil, + OperationID: baseCfg.Method + "-" + defaultPkgPath, + Parameters: make([]*define.PathConfigParameter, 0), + RequestBody: &define.RequestBody{ + Required: true, + Description: "", + Content: map[string]*define.Media{}, + Ref: "", + }, + Responses: map[string]*define.Response{ + fmt.Sprintf("%v", http.StatusOK): &define.Response{ + Content: map[string]*define.Media{}, + }, + }, + Callbacks: nil, + Deprecated: baseCfg.Deprecated, + Security: nil, + Servers: nil, + } + // 解析绑定在url中的参数 + if paramList := define.UriParamRegexp.FindAllString(baseCfg.Uri, -1); len(paramList) > 0 { + for _, param := range paramList { + param = strings.TrimPrefix(param, "{") + param = strings.TrimSuffix(param, "}") + cfg.Parameters = append(cfg.Parameters, &define.PathConfigParameter{ + Name: param, + In: consts.SwaggerParameterInPath, + Description: param, + Required: true, + Deprecated: false, + Schema: &define.Schema{ + Type: consts.SwaggerDataTypeString, + Format: consts.DataTypeString.String(), + }, + AllowEmptyValue: false, + }) + } + } + return cfg +} + +// ParseReadConfigParam 解析get类请求参数 +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 11:55 2025/2/14 +func (g *Generate) ParseReadConfigParam(requestCfg *define.UriBaseConfig, baseReqCfg *define.PathItemOperationConfig, inputType reflect.Type) { + if inputType.Kind() == reflect.Ptr { + inputType = inputType.Elem() + } + if inputType.Kind() != reflect.Struct { + panic(requestCfg.Uri + " : request param not struct") + } + if nil == baseReqCfg.Parameters { + baseReqCfg.Parameters = make([]*define.PathConfigParameter, 0) + } + for i := 0; i < inputType.NumField(); i++ { + propertyName := ParseStructField.GetParamName(inputType.Field(i)) + if propertyName == "-" { + continue + } + realInputTypeFormat := inputType.Field(i).Type.Kind().String() + fieldType := inputType.Field(i).Type + if inputType.Field(i).Type.Kind() == reflect.Ptr { + fieldType = inputType.Field(i).Type.Elem() + } + if fieldType.Kind() == reflect.Ptr || + fieldType.Kind() == reflect.Struct || + fieldType.Kind() == reflect.Map || + fieldType.Kind() == reflect.Array || + fieldType.Kind() == reflect.Slice { + if convertType := g.realBaseType2SwaggerType(fieldType.String()); !strings.HasPrefix(convertType, "[]") && convertType != inputType.Field(i).Type.Kind().String() { + // 针对基础类型指针 + baseReqCfg.Parameters = append(baseReqCfg.Parameters, &define.PathConfigParameter{ + Name: ParseStructField.GetParamName(inputType.Field(i)), + In: consts.SwaggerParameterInQuery, + Description: ParseStructField.GetParamDesc(inputType.Field(i)), + Required: ValidateRule.IsRequired(inputType.Field(i)), + Deprecated: ParseStructField.Deprecated(inputType.Field(i)), + Schema: &define.Schema{ + Type: g.realBaseType2SwaggerType(inputType.Field(i).Type.String()), + Items: nil, + Ref: "", + Format: realInputTypeFormat, + }, + AllowEmptyValue: false, + Style: "", + Explode: false, + AllowReserved: false, + Ref: "", + }) + continue + } + } else { + baseReqCfg.Parameters = append(baseReqCfg.Parameters, &define.PathConfigParameter{ + Name: ParseStructField.GetParamName(inputType.Field(i)), + In: consts.SwaggerParameterInQuery, + Description: ParseStructField.GetParamDesc(inputType.Field(i)), + Required: ValidateRule.IsRequired(inputType.Field(i)), + Deprecated: ParseStructField.Deprecated(inputType.Field(i)), + Schema: &define.Schema{ + Type: g.realBaseType2SwaggerType(inputType.Field(i).Type.String()), + Items: nil, + Ref: "", + Format: realInputTypeFormat, + }, + AllowEmptyValue: false, + Style: "", + Explode: false, + AllowReserved: false, + Ref: "", + }) + } + } + switch requestCfg.Method { + case http.MethodGet: + g.docData.Paths[requestCfg.Uri].Get = baseReqCfg + case http.MethodHead: + g.docData.Paths[requestCfg.Uri].Head = baseReqCfg + case http.MethodConnect: + g.docData.Paths[requestCfg.Uri].Connect = baseReqCfg + case http.MethodOptions: + g.docData.Paths[requestCfg.Uri].Options = baseReqCfg + case http.MethodTrace: + g.docData.Paths[requestCfg.Uri].Trace = baseReqCfg + } +} + +// AddComponentsSchema 添加schema +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 15:25 2025/2/8 +func (g *Generate) AddComponentsSchema(rootSchemaName string, pkgPath string, inputType reflect.Type) string { + inputNameArr := strings.Split(inputType.Name(), ".") + inputName := inputNameArr[len(inputNameArr)-1] + schemaName := strings.ReplaceAll(pkgPath+"."+inputName, "/", "-") + if schemaName == "-" { + // 忽略的属性 + return schemaName + } + if _, exist := g.docData.Components.Schemas[schemaName]; exist { + // 已存在, 无需重复生成 + return schemaName + } + if _, exist := g.docData.Components.Schemas[schemaName]; !exist { + s := &define.Schema{ + Nullable: false, + Deprecated: false, + Properties: make(map[string]*define.Property), + Required: make([]string, 0), + Enum: make([]any, 0), + Type: consts.SwaggerDataTypeObject, // TODO : 区分数组 + Ref: g.getSchemaRef(schemaName), + } + if len(rootSchemaName) == 0 || inputType.Kind() == reflect.Struct { + s.Ref = "" + } + g.docData.Components.Schemas[schemaName] = s + } + if inputType.Kind() == reflect.Map { + // map, 直接添加公共 + g.docData.Components.Schemas[schemaName].Type = consts.SwaggerDataTypeObject + return schemaName + } + // 数组 + if inputType.Kind() == reflect.Slice || inputType.Kind() == reflect.Array { + if len(rootSchemaName) == 0 { + g.docData.Components.Schemas[schemaName].Type = consts.SwaggerDataTypeArray + sliceItemType := g.parseSliceItem(schemaName, inputType) + g.docData.Components.Schemas[schemaName].Items = &define.PropertyXOf{Ref: g.getSchemaRef(sliceItemType)} + } else { + sliceItemType := g.parseSliceItem(schemaName, inputType) + g.docData.Components.Schemas[rootSchemaName].Properties[schemaName] = &define.Property{ + Type: consts.SwaggerDataTypeArray, + Format: inputType.String(), + Items: &define.PropertyXOf{Ref: g.getSchemaRef(sliceItemType)}, + } + } + + return schemaName + } + // 结构体 + if inputType.Kind() == reflect.Struct { + if len(rootSchemaName) == 0 { + + } else { + // g.docData.Components.Schemas[schemaName].Properties[""] = schemaName + } + // g.docData.Components.Schemas[schemaName].Ref = consts.SwaggerDataTypeObject + for i := 0; i < inputType.NumField(); i++ { + propertyName := ParseStructField.GetParamName(inputType.Field(i)) + if propertyName == "-" { + continue + } + if inputType.Field(i).Type.Kind() == reflect.Ptr || + inputType.Field(i).Type.Kind() == reflect.Struct || + inputType.Field(i).Type.Kind() == reflect.Map || + inputType.Field(i).Type.Kind() == reflect.Array || + inputType.Field(i).Type.Kind() == reflect.Slice { + if convertType := g.realBaseType2SwaggerType(inputType.Field(i).Type.String()); !strings.HasPrefix(convertType, "[]") && convertType != inputType.Field(i).Type.Kind().String() { + // 针对基础类型指针 + g.docData.Components.Schemas[schemaName].Properties[propertyName] = &define.Property{ + Type: g.realBaseType2SwaggerType(convertType), + Format: inputType.Field(i).Type.String(), + Default: ParseStructField.GetDefaultValue(inputType.Field(i)), + Description: ParseStructField.GetParamDesc(inputType.Field(i)), + } + continue + } + if inputType.Field(i).Type.Kind() == reflect.Struct || + inputType.Field(i).Type.Kind() == reflect.Map { + g.docData.Components.Schemas[schemaName].Properties[propertyName] = &define.Property{ + Type: consts.SwaggerDataTypeObject, + Format: inputType.Field(i).Type.String(), + Description: ParseStructField.GetParamDesc(inputType.Field(i)), + Properties: map[string]*define.Property{}, + } + } else if inputType.Field(i).Type.Kind() == reflect.Array || + inputType.Field(i).Type.Kind() == reflect.Slice { + g.docData.Components.Schemas[schemaName].Properties[propertyName] = &define.Property{ + Type: consts.SwaggerDataTypeArray, + Format: inputType.Field(i).Type.String(), + Description: ParseStructField.GetParamDesc(inputType.Field(i)), + Items: &define.PropertyXOf{ + Ref: g.getSchemaRef(g.parseSliceItem(schemaName, inputType.Field(i).Type)), + }, + Properties: map[string]*define.Property{}, + } + } else if inputType.Field(i).Type.Kind() == reflect.Ptr { + + } else { + g.AddComponentsSchema(schemaName, propertyName, inputType.Field(i).Type) + } + + } else { + g.docData.Components.Schemas[schemaName].Properties[propertyName] = &define.Property{ + Type: g.realBaseType2SwaggerType(inputType.Field(i).Type.String()), + Format: inputType.Field(i).Type.String(), + Default: ParseStructField.GetDefaultValue(inputType.Field(i)), + Description: ParseStructField.GetParamDesc(inputType.Field(i)), + } + } + // 设置参数各种属性 + g.setStructFieldProperty(schemaName, inputType.Field(i)) + } + return schemaName + } + // 指针 + if inputType.Kind() == reflect.Ptr { + convertType := g.realBaseType2SwaggerType(inputType.String()) + if convertType == inputType.String() { + // 非基础数据类型 + return g.AddComponentsSchema(schemaName, inputType.Elem().String(), inputType.Elem()) + } else { + g.docData.Components.Schemas[schemaName].Properties[schemaName] = &define.Property{ + Type: convertType, + Format: inputType.String(), + Properties: map[string]*define.Property{}, + } + } + } + return schemaName +} + +// parseSliceItem 解析数组每一项 +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 21:33 2025/2/8 +func (g *Generate) parseSliceItem(rootSchemaName string, inputType reflect.Type) string { + if inputType.Kind() != reflect.Slice && inputType.Kind() != reflect.Array { + // 不是数组 + return "" + } + sliceValue := reflect.MakeSlice(inputType, 1, 1) + sliceItemType := sliceValue.Index(0).Type() + g.AddComponentsSchema(rootSchemaName, sliceItemType.PkgPath(), sliceItemType) + if len(sliceItemType.PkgPath()) == 0 { + return sliceItemType.String() + } + return sliceItemType.PkgPath() + "." + sliceItemType.Name() +} + +// getSchemaRef 获取引用的类型 +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 14:25 2025/2/9 +func (g *Generate) getSchemaRef(schemaName string) string { + if "" == schemaName { + return "" + } + return "#/components/schemas/" + strings.ReplaceAll(schemaName, "/", "-") +} + +// realType2SwaggerType golang 真实数据类型转换为golang数据类型 +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 20:25 2025/2/11 +func (g *Generate) realBaseType2SwaggerType(realType string) string { + switch realType { + case "bool", "*bool": + return consts.SwaggerDataTypeBoolean + case "string", "*string": + return consts.SwaggerDataTypeString + case "byte", "*byte": + return consts.SwaggerDataTypeByte + case "float32", "*float32", "float64", "*float64": + return consts.SwaggerDataTypeDouble + case "int", "*int", "uint", "*uint", "int64", "*int64", "uint64", "*uint64": + return consts.SwaggerDataTypeInteger + case "int8", "*int8", "uint8", "*uint8", "int16", "*int16", "uint16", "*uint16", "int32", "*int32", "uint32", "*uint32": + return consts.SwaggerDataTypeInteger + default: + return realType + } +} + +// realType2SwaggerType ... +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 21:20 2025/2/11 +func (g *Generate) realType2SwaggerType(realType string) string { + if strings.HasPrefix(realType, "[]") { + return consts.SwaggerDataTypeArray + } + return g.realBaseType2SwaggerType(realType) +} + +// setStructFieldProperty 添加struct_field各种属性 +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 16:13 2025/2/13 +func (g *Generate) setStructFieldProperty(schemaName string, structField reflect.StructField) { + paramName := ParseStructField.GetParamName(structField) + if ValidateRule.IsRequired(structField) { + g.docData.Components.Schemas[schemaName].Required = append(g.docData.Components.Schemas[schemaName].Required, paramName) + } + g.docData.Components.Schemas[schemaName].Properties[ParseStructField.GetParamName(structField)].Enum = ValidateRule.Enum(structField) +} + +// parseBaseUriConfig 通过Meta字段解析Uri基础配置信息 +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 15:13 2025/2/14 +func (g *Generate) parseBaseUriConfig(paramType reflect.Type) (*define.UriBaseConfig, error) { + // 解析meta信息 + metaField, metaFieldExist := paramType.FieldByName("Meta") + if !metaFieldExist { + return nil, errors.New("Meta field not found") + } + res := &define.UriBaseConfig{ + Uri: "", + Method: "", + ContentType: nil, + OutputContentType: nil, + TagList: nil, + Summary: "", + Description: "", + ParamList: nil, + ResultList: nil, + Deprecated: false, + } + res.Uri = metaField.Tag.Get(define.TagPath) + res.Method = strings.ToUpper(metaField.Tag.Get(define.TagMethod)) + res.Description = metaField.Tag.Get(define.TagDesc) + res.TagList = strings.Split(metaField.Tag.Get(define.TagUriTag), ",") + // 解析第一个返回值, 要求必须是结构体或者是map + outputStrictModel := metaField.Tag.Get(define.TagOutputStrict) + res.OutputStrict = outputStrictModel == "1" || outputStrictModel == "true" + deprecated := metaField.Tag.Get(define.TagDeprecated) + res.Deprecated = deprecated == "1" || deprecated == "true" + res.ContentType = strings.Split(metaField.Tag.Get(define.TagContentType), ",") + if len(res.ContentType) == 0 { + if wrapper.ArrayType(g.readMethodList).Has(res.Method) >= 0 { + // get类请求 + res.ContentType = []string{consts.MimeTypeXWWWFormUrlencoded} + } else { + res.ContentType = []string{consts.MimeTypeJson} + } + } + res.OutputContentType = strings.Split(metaField.Tag.Get(define.TagOutputContentType), ",") + if len(res.OutputContentType) == 0 { + // 未设置响应类型默认JSON数据 + res.OutputContentType = []string{consts.MimeTypeJson} + } + res.Summary = ParseStructField.Summary(metaField) + if res.Method == "" { + return nil, errors.New("baseCfg.Method is empty") + } + if res.Uri == "" { + return nil, errors.New("baseCfg.Uri is empty") + } + return res, nil +} diff --git a/go.mod b/go.mod index 67c3dea..c828600 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module git.zhangdeman.cn/gateway/api-doc go 1.22.2 require ( - git.zhangdeman.cn/zhangdeman/consts v0.0.0-20250207132005-330777d80591 + git.zhangdeman.cn/zhangdeman/consts v0.0.0-20250208020330-a50062af46a1 git.zhangdeman.cn/zhangdeman/serialize v0.0.0-20241223084948-de2e49144fcd git.zhangdeman.cn/zhangdeman/wrapper v0.0.0-20250124091620-c757e551a8c9 github.com/tidwall/gjson v1.18.0 diff --git a/go.sum b/go.sum index ef348cb..78d410d 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,5 @@ -git.zhangdeman.cn/zhangdeman/consts v0.0.0-20250207125648-b75d2ec7f3c1 h1:qWqq6dWW2eIpaXvLBUjTrbOzX1xrSw/nswz2ZL7a8Zw= -git.zhangdeman.cn/zhangdeman/consts v0.0.0-20250207125648-b75d2ec7f3c1/go.mod h1:IXXaZkb7vGzGnGM5RRWrASAuwrVSNxuoe0DmeXx5g6k= -git.zhangdeman.cn/zhangdeman/consts v0.0.0-20250207130030-67b5d5cc6121 h1:48wtD5FTfQ6AiHWrFHg/IaCxzNWYRbuGlENWDhn0Fbw= -git.zhangdeman.cn/zhangdeman/consts v0.0.0-20250207130030-67b5d5cc6121/go.mod h1:IXXaZkb7vGzGnGM5RRWrASAuwrVSNxuoe0DmeXx5g6k= -git.zhangdeman.cn/zhangdeman/consts v0.0.0-20250207132005-330777d80591 h1:P58+JwVhycrAFqE2Eq25N9y5lDokYBUz+oLxCKk44BE= -git.zhangdeman.cn/zhangdeman/consts v0.0.0-20250207132005-330777d80591/go.mod h1:IXXaZkb7vGzGnGM5RRWrASAuwrVSNxuoe0DmeXx5g6k= +git.zhangdeman.cn/zhangdeman/consts v0.0.0-20250208020330-a50062af46a1 h1:vv4X72I6s6XcTi0ykj2v/cgMZyseFyE2LkS4WloICCs= +git.zhangdeman.cn/zhangdeman/consts v0.0.0-20250208020330-a50062af46a1/go.mod h1:IXXaZkb7vGzGnGM5RRWrASAuwrVSNxuoe0DmeXx5g6k= git.zhangdeman.cn/zhangdeman/op_type v0.0.0-20240122104027-4928421213c0 h1:gUDlQMuJ4xNfP2Abl1Msmpa3fASLWYkNlqDFF/6GN0Y= git.zhangdeman.cn/zhangdeman/op_type v0.0.0-20240122104027-4928421213c0/go.mod h1:VHb9qmhaPDAQDcS6vUiDCamYjZ4R5lD1XtVsh55KsMI= git.zhangdeman.cn/zhangdeman/serialize v0.0.0-20241223084948-de2e49144fcd h1:q7GG14qgXKB4MEXQFOe7/UYebsqMfPaSX80TcPdOosI= diff --git a/parser_test.go b/parser_test.go index 1bcfa94..93348dd 100644 --- a/parser_test.go +++ b/parser_test.go @@ -12,26 +12,79 @@ import ( "fmt" "git.zhangdeman.cn/gateway/api-doc/define" "git.zhangdeman.cn/zhangdeman/serialize" - "os" - "os/user" + "reflect" "testing" ) +type Meta struct { +} + // Test_parser_Openapi3 测试数据结构定义正确性 // // Author : go_developer@163.com<白茶清欢> // // Date : 17:55 2024/7/19 func Test_parser_Openapi3(t *testing.T) { - current, _ := user.Current() - byteData, _ := os.ReadFile(current.HomeDir + "/Downloads/test-openapi-doc.json") - var data define.OpenapiDoc - err := json.Unmarshal(byteData, &data) - if nil != err { - fmt.Println("解析失败 : " + err.Error()) - } else { - fmt.Println("解析成功") + type User struct { + Meta `json:"-" deprecated:"false" path:"/user/detail" method:"POST" desc:"测试接口" tag:"用户,搜索" content_type:"application/json" output_content_type:"application/json"` + Name string `json:"name" d:"zhang" desc:"用户姓名" binding:"required"` + Age string `json:"age" d:"18" desc:"年龄" binding:"required,oneof=12 13 18 90"` } + type UserDelete struct { + Meta `json:"-" deprecated:"false" path:"/user/detail" method:"DELETE" desc:"测试接口" tag:"用户,搜索" content_type:"application/json" output_content_type:"application/json"` + Name string `json:"name" d:"zhang" desc:"用户姓名" binding:"required"` + Age string `json:"age" d:"18" desc:"年龄" binding:"required,oneof=12 13 18 90"` + } + type UserPut struct { + Meta `json:"-" deprecated:"false" path:"/user/put/{put_user_id}" method:"PUT" desc:"测试接口" tag:"用户,搜索" content_type:"application/json" output_content_type:"application/json"` + Name string `json:"name" d:"zhang" desc:"用户姓名" binding:"required"` + Age string `json:"age" d:"18" desc:"年龄" binding:"required,oneof=12 13 18 90"` + } + type UserGet struct { + Meta `json:"-" deprecated:"false" path:"/user/detail/get/{put_user_id}" method:"GET" desc:"测试接口" tag:"用户,搜索" content_type:"application/json" output_content_type:"application/json"` + Name string `json:"name" d:"zhang" desc:"用户姓名" binding:"required"` + Age string `json:"age" d:"18" desc:"年龄" binding:"required,oneof=12 13 18 90"` + } + type UserHead struct { + Meta `json:"-" deprecated:"false" path:"/user/detail/head/{put_user_id}" method:"HEAD" desc:"测试接口" tag:"用户,搜索" content_type:"application/json" output_content_type:"application/json"` + Name string `json:"name" d:"zhang" desc:"用户姓名" binding:"required"` + Age string `json:"age" d:"18" desc:"年龄" binding:"required,oneof=12 13 18 90"` + } + type List struct { + Total int64 `json:"total" binding:"required"` + UserList []User `json:"user_list"` + } + var o List + var f User + var fd UserDelete + var up UserPut + var ug UserGet + var uh UserHead + g := NewOpenapiDoc(nil, []*define.ServerItem{ + &define.ServerItem{ + Url: "http://127.0.0.1/v1", + Description: "v1接口", + Variables: map[string]*define.ServerItemVariable{ + "test": &define.ServerItemVariable{ + Default: "123456", + Description: "1111", + Enum: nil, + }, + }, + }, + &define.ServerItem{ + Url: "http://127.0.0.1/v2", + Description: "v2接口", + Variables: nil, + }, + }) + g.AddApiFromInAndOut(reflect.TypeOf(f), reflect.TypeOf(o)) + g.AddApiFromInAndOut(reflect.TypeOf(fd), reflect.TypeOf(o)) + g.AddApiFromInAndOut(reflect.TypeOf(up), reflect.TypeOf(o)) + g.AddApiFromInAndOut(reflect.TypeOf(ug), reflect.TypeOf(o)) + g.AddApiFromInAndOut(reflect.TypeOf(uh), reflect.TypeOf(o)) + byteData, _ := json.Marshal(g.docData) + fmt.Println(string(byteData)) } func TestParseForSwagger(t *testing.T) { diff --git a/struct_field.go b/struct_field.go new file mode 100644 index 0000000..f6bb6e3 --- /dev/null +++ b/struct_field.go @@ -0,0 +1,147 @@ +// Package api_doc ... +// +// Description : api_doc ... +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 2025-02-12 22:15 +package api_doc + +import ( + "git.zhangdeman.cn/gateway/api-doc/define" + "reflect" + "strconv" + "strings" +) + +var ( + ParseStructField = parseStructField{} +) + +type parseStructField struct { +} + +// GetParamName 获取参数名称 +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 21:58 2025/2/11 +func (psf parseStructField) GetParamName(structField reflect.StructField) string { + paramNameTagList := []string{ + define.TagJson, define.TagForm, + define.TagXml, define.TagYaml, + define.TagYml, + } + for _, tag := range paramNameTagList { + tagVal := structField.Tag.Get(tag) + if tagVal != "" { + return tagVal + } + } + // 未设置相关字段, 则字段名即为参数名 + return structField.Name +} + +// GetParamDesc ... +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 22:01 2025/2/11 +func (psf parseStructField) GetParamDesc(structField reflect.StructField) string { + descTagList := []string{define.TagDesc, define.TagDescription} + for _, tag := range descTagList { + tagVal := structField.Tag.Get(tag) + if tagVal != "" { + return tagVal + } + } + // 没有显示的设置参数描述, 则使用参数名作为参数描述 + return psf.GetParamName(structField) +} + +// GetDefaultValue 获取默认值 +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 22:05 2025/2/11 +func (psf parseStructField) GetDefaultValue(structField reflect.StructField) any { + defaultTagList := []string{define.TagD, define.TagDefault} + fieldType := structField.Type.Kind().String() + for _, tag := range defaultTagList { + if val, exist := structField.Tag.Lookup(tag); exist && val != "" { + if strings.HasPrefix(fieldType, "int") { + i, _ := strconv.Atoi(val) + return i + } + if strings.HasPrefix(fieldType, "uint") { + uintVal, _ := strconv.ParseUint(val, 10, 64) + return uintVal + } + if strings.HasPrefix(fieldType, "float") { + floatVal, _ := strconv.ParseFloat(val, 64) + return floatVal + } + if strings.HasPrefix(fieldType, "string") { + return val + } + if strings.HasPrefix(fieldType, "bool") { + if val == "true" { + return true + } else { + return false + } + } + return val + } + } + return nil +} + +// GetValidateRule 获取验证规则 +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 15:30 2025/2/13 +func (psf parseStructField) GetValidateRule(structField reflect.StructField) string { + defaultTagList := []string{define.TagValidate, define.TagBinding} + for _, tag := range defaultTagList { + if tagVal, exist := structField.Tag.Lookup(tag); exist && len(tagVal) > 0 { + return tagVal + } + } + return "" +} + +// Deprecated 是否弃用 +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 21:12 2025/2/13 +func (psf parseStructField) Deprecated(structField reflect.StructField) bool { + defaultTagList := []string{define.TagDeprecated} + for _, tag := range defaultTagList { + if tagVal, exist := structField.Tag.Lookup(tag); exist && (tagVal == "1" || strings.ToLower(tagVal) == "true") { + return true + } + } + return false +} + +// Summary 摘要信息 +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 15:15 2025/2/14 +func (psf parseStructField) Summary(structField reflect.StructField) string { + defaultTagList := []string{define.TagSummary} + for _, tag := range defaultTagList { + if tagVal, exist := structField.Tag.Lookup(tag); exist && len(tagVal) > 0 { + return tagVal + } + } + paramName := psf.GetParamName(structField) + if paramName == "-" { + return "" + } + return paramName +} diff --git a/swagger.go b/swagger.go index 5df17fe..a05fdb4 100644 --- a/swagger.go +++ b/swagger.go @@ -54,7 +54,7 @@ func buildSwagger2GlobalSecurityParamList(swaggerDoc *define.Swagger) []*define. result = append(result, &define.ParamConfig{ Location: GetParamLocation(paramConfig.In).String(), Path: paramName, - Type: GetDataType(paramConfig.Type, "").String(), + Type: GetDataType(paramConfig.Type, ""), Title: paramName, Description: paramConfig.Description, Required: false, @@ -72,8 +72,8 @@ func buildUriList(swaggerDoc *define.Swagger) ([]*define.UriBaseConfig, error) { uriResult := &define.UriBaseConfig{ Uri: swaggerDoc.BasePath + "/" + strings.TrimLeft(itemUri, "/"), Method: strings.ToUpper(requestMethod), - ContentType: methodConfig.Consumes[0], - OutputContentType: methodConfig.Produces[0], + ContentType: methodConfig.Consumes, + OutputContentType: methodConfig.Produces, TagList: methodConfig.Tags, Summary: methodConfig.Summary, Description: methodConfig.Description, @@ -120,7 +120,7 @@ func buildSwagger2ParamConfig(swaggerDoc *define.Swagger, paramConfigList []*def paramConfigBuildConfig := &define.ParamConfig{ Location: GetParamLocation(paramConfig.In).String(), Path: paramConfig.Name, - Type: GetDataType(paramConfig.Type, paramConfig.Format).String(), + Type: GetDataType(paramConfig.Type, paramConfig.Format), Title: paramConfig.Name, Description: paramConfig.Description, Required: paramConfig.Required, @@ -143,7 +143,7 @@ func buildSwagger2ParamConfig(swaggerDoc *define.Swagger, paramConfigList []*def paramConfigBuildConfig := &define.ParamConfig{ Location: GetParamLocation(paramConfig.In).String(), Path: paramName, - Type: GetDataType(paramMoreConfig.Type, "").String(), + Type: GetDataType(paramMoreConfig.Type, ""), Title: paramName, Description: paramMoreConfig.Description, Required: requiredTable[paramName], @@ -204,7 +204,7 @@ func buildSwagger2ResultConfig(swaggerDoc *define.Swagger, resultConfig map[stri responseType = consts.DataTypeAny.String() } else { schemaType = responseTypeDefine.Schema.Type - responseType = GetDataType(responseTypeDefine.Schema.Type, "").String() + responseType = GetDataType(responseTypeDefine.Schema.Type, "") } resCfg := &define.ResultConfig{ Location: consts.ResponseDataLocationBody.String(), @@ -223,7 +223,7 @@ func buildSwagger2ResultConfig(swaggerDoc *define.Swagger, resultConfig map[stri res = append(res, &define.ResultConfig{ Location: consts.ResponseDataLocationBody.String(), Path: responseKey, - Type: GetDataType(responseKeyConfig.Type, "").String(), + Type: GetDataType(responseKeyConfig.Type, ""), Title: responseKey, Description: responseKey, }) diff --git a/validate_rule.go b/validate_rule.go new file mode 100644 index 0000000..a4fa52c --- /dev/null +++ b/validate_rule.go @@ -0,0 +1,102 @@ +// Package api_doc ... +// +// Description : api_doc ... +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 2025-02-13 15:26 +package api_doc + +import ( + "git.zhangdeman.cn/zhangdeman/consts" + "reflect" + "strconv" + "strings" +) + +var ( + ValidateRule = validateRule{} +) + +type validateRule struct{} + +// IsRequired 判断是否必传 +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 15:32 2025/2/13 +func (r validateRule) IsRequired(structField reflect.StructField) bool { + ruleTable := r.getValidateRuleTable(structField) + _, exist := ruleTable[consts.ValidatorRuleCommonRequired.String()] + // 存在即为必传 + return exist +} + +// Enum 获取枚举值 +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 17:23 2025/2/13 +func (r validateRule) Enum(structField reflect.StructField) []any { + ruleTable := r.getValidateRuleTable(structField) + oneOfValue, _ := ruleTable[consts.ValidatorRuleCommonOneOf.String()] + if len(oneOfValue) == 0 { + return []any{} + } + fieldType := structField.Type.Kind().String() + valStrArr := strings.Split(oneOfValue, " ") + anySlice := make([]any, 0) + for _, val := range valStrArr { + if strings.HasPrefix(fieldType, "int") { + i, _ := strconv.Atoi(val) + anySlice = append(anySlice, i) + continue + } + if strings.HasPrefix(fieldType, "uint") { + uintVal, _ := strconv.ParseUint(val, 10, 64) + anySlice = append(anySlice, uintVal) + continue + } + if strings.HasPrefix(fieldType, "float") { + floatVal, _ := strconv.ParseFloat(val, 64) + anySlice = append(anySlice, floatVal) + continue + } + if strings.HasPrefix(fieldType, "string") { + anySlice = append(anySlice, val) + continue + } + if strings.HasPrefix(fieldType, "bool") { + if val == "true" { + anySlice = append(anySlice, true) + } else { + anySlice = append(anySlice, false) + } + continue + } + } + return anySlice +} + +// getValidateRuleTable 解析验证规则表 +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 15:29 2025/2/13 +func (r validateRule) getValidateRuleTable(structField reflect.StructField) map[string]string { + res := map[string]string{} + ruleStr := ParseStructField.GetValidateRule(structField) + if len(ruleStr) == 0 { + return res + } + expressList := strings.Split(ruleStr, ",") + for _, item := range expressList { + if strings.Contains(item, "=") { + arr := strings.Split(item, "=") + res[strings.TrimSpace(arr[0])] = strings.Join(arr[1:], "=") + } else { + res[strings.TrimSpace(item)] = "" + } + } + return res +}