Files
api-doc/openapi/schema.go

233 lines
6.8 KiB
Go

// Package openapi ...
//
// Description : openapi 扩展版本,添加更多标签支持和类型映射
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 2026-01-05 17:27
package openapi
import (
"reflect"
"time"
"git.zhangdeman.cn/zhangdeman/api-doc/define"
"git.zhangdeman.cn/zhangdeman/api-doc/util"
"github.com/getkin/kin-openapi/openapi3"
)
// StructFieldInfo 结构体字段信息
type StructFieldInfo struct {
Name string `json:"name" dc:"结构体字段名"`
JSONName string `json:"json_name" dc:"json tag"`
Type reflect.Type `json:"type" dc:"字段类型"`
Description string `json:"description" dc:"参数描述"`
Example any `json:"example" dc:"示例值"`
Default any `json:"default" dc:"默认值"`
Required bool `json:"required" dc:"是否必传"`
Min *float64 `json:"min" dc:"最小值"`
Max *float64 `json:"max" dc:"最大值"`
MinLength *float64 `json:"min_length" dc:"最小长度"`
MaxLength *float64 `json:"max_length" dc:"最大长度"`
Pattern string `json:"pattern" dc:"模式"`
Format string `json:"format" dc:"格式"`
Enum []any `json:"enum" dc:"枚举值列表"`
EnumDesc []define.EnumValue `json:"enum_desc" dc:"枚举值详细描述"`
OmitEmpty bool `json:"omit_empty" dc:"是否可控"`
}
// ParseStructField 解析结构体字段信息
func ParseStructField(field reflect.StructField) *StructFieldInfo {
if !field.IsExported() {
return nil
}
// 解析验证规则
validateRule := util.ParseValidateRule(field.Type, util.ParseStructFieldTag.GetValidateRule(field))
info := &StructFieldInfo{
Name: field.Name,
JSONName: "",
Type: field.Type,
Description: util.ParseStructFieldTag.GetParamDesc(field), // 解析参数描述
Example: util.ParseStructFieldTag.GetExampleValue(field), // 解析示例值
Default: util.ParseStructFieldTag.GetDefaultValue(field), // 解析默认值
Required: validateRule.Required,
Min: validateRule.Min,
Max: validateRule.Max,
MinLength: validateRule.Min,
MaxLength: validateRule.Max,
Pattern: validateRule.Regexp,
Format: field.Type.String(),
Enum: validateRule.Oneof, // 解析枚举值
EnumDesc: util.ParseStructFieldTag.EnumDescription(field), // 解析枚举值描述
OmitEmpty: false,
}
// 解析 JSON tag
info.JSONName, info.OmitEmpty = util.ParseStructFieldTag.GetParamName(field)
return info
}
// GenerateOpenAPISchema 生成完整的 OpenAPI Schema
func GenerateOpenAPISchema(s any) *openapi3.SchemaRef {
var (
ok bool
tType reflect.Type
)
if tType, ok = s.(reflect.Type); !ok {
tType = reflect.TypeOf(s)
if tType.Kind() == reflect.Ptr {
tType = tType.Elem()
}
}
return generateSchemaRecursive(tType, make(map[string]bool))
}
// 生成 schema
func generateSchemaRecursive(t reflect.Type, seen map[string]bool) *openapi3.SchemaRef {
// 检查循环引用
typeName := t.PkgPath() + "." + t.Name()
if seen[typeName] && t.Kind() == reflect.Struct {
return &openapi3.SchemaRef{
Value: openapi3.NewObjectSchema(),
}
}
seen[typeName] = true
defer delete(seen, typeName)
// 处理不同类型
switch t.Kind() {
case reflect.Struct:
// 特殊处理 time.Time
if t == reflect.TypeOf(time.Time{}) {
schema := openapi3.NewDateTimeSchema()
return &openapi3.SchemaRef{Value: schema}
}
schema := openapi3.NewObjectSchema()
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
info := ParseStructField(field)
if info == nil {
continue
}
fieldSchema := generateSchemaFromType(info.Type, seen)
if fieldSchema == nil {
continue
}
// 应用字段信息到 Schema
applyFieldInfoToSchema(fieldSchema.Value, info)
if info.Required && !info.OmitEmpty {
schema.Required = append(schema.Required, info.JSONName)
}
schema.Properties[info.JSONName] = fieldSchema
}
return &openapi3.SchemaRef{Value: schema}
case reflect.Slice, reflect.Array:
elemType := t.Elem()
elemSchema := generateSchemaRecursive(elemType, seen)
if elemSchema == nil {
return nil
}
return &openapi3.SchemaRef{
Value: openapi3.NewArraySchema().WithItems(elemSchema.Value),
}
case reflect.Map:
if t.Key().Kind() != reflect.String {
// OpenAPI 只支持字符串键
return &openapi3.SchemaRef{
Value: openapi3.NewObjectSchema(),
}
}
valueType := t.Elem()
valueSchema := generateSchemaRecursive(valueType, seen)
if valueSchema == nil {
return nil
}
return &openapi3.SchemaRef{
Value: openapi3.NewObjectSchema().
WithAdditionalProperties(valueSchema.Value),
}
case reflect.Ptr:
return generateSchemaRecursive(t.Elem(), seen)
case reflect.Interface:
return &openapi3.SchemaRef{Value: openapi3.NewSchema()}
default:
return generatePrimitiveSchema(t)
}
}
func generateSchemaFromType(t reflect.Type, seen map[string]bool) *openapi3.SchemaRef {
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
return generateSchemaRecursive(t, seen)
}
func applyFieldInfoToSchema(schema *openapi3.Schema, info *StructFieldInfo) {
if info.Description != "" {
schema.Description = info.Description
}
if info.Example != nil {
schema.Example = info.Example
}
schema.Min = info.Min
if info.Max != nil {
schema.Max = info.Max
}
if info.MinLength != nil {
schema.MinLength = uint64(*info.MinLength)
}
if info.MaxLength != nil {
schema.MaxLength = openapi3.Ptr(uint64(*info.MaxLength))
}
if info.Pattern != "" {
schema.Pattern = info.Pattern
}
if info.Format != "" {
schema.Format = info.Format
}
if len(info.Enum) > 0 {
schema.Enum = info.Enum
/*for _, item := range info.Enum {
schema.Enum = append(schema.Enum, item.Value)
}*/
}
}
func generatePrimitiveSchema(t reflect.Type) *openapi3.SchemaRef {
switch t.Kind() {
case reflect.String:
return &openapi3.SchemaRef{Value: openapi3.NewStringSchema()}
case reflect.Bool:
return &openapi3.SchemaRef{Value: openapi3.NewBoolSchema()}
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32:
return &openapi3.SchemaRef{Value: openapi3.NewInt32Schema()}
case reflect.Int64:
return &openapi3.SchemaRef{Value: openapi3.NewInt64Schema()}
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32:
return &openapi3.SchemaRef{Value: openapi3.NewInt64Schema().WithMin(0)}
case reflect.Uint64:
return &openapi3.SchemaRef{Value: openapi3.NewInt64Schema().WithMin(0)}
case reflect.Float32:
return &openapi3.SchemaRef{Value: openapi3.NewFloat64Schema()}
case reflect.Float64:
return &openapi3.SchemaRef{Value: openapi3.NewFloat64Schema()}
default:
return &openapi3.SchemaRef{Value: openapi3.NewSchema()}
}
}