diff --git a/define/tag.go b/define/tag.go index 78ae579..b517731 100644 --- a/define/tag.go +++ b/define/tag.go @@ -36,3 +36,8 @@ const ( TagNameOmitempty = "omitempty" TagNameEnumDescription = "enum-desc" // 枚举值描述: enum1:enum1-desc||enum2:enum2-desc ) + +type EnumValue struct { + Value string `json:"value" dc:"枚举值"` + Description string `json:"description" dc:"枚举值描述"` +} diff --git a/go.mod b/go.mod index d5b2768..eb91469 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.24.0 toolchain go1.24.1 require ( - git.zhangdeman.cn/zhangdeman/consts v0.0.0-20260105014807-92a7dc3bb97f + git.zhangdeman.cn/zhangdeman/consts v0.0.0-20260106015608-42e268aea545 git.zhangdeman.cn/zhangdeman/serialize v0.0.0-20251013044511-86c1a4a3a9dd git.zhangdeman.cn/zhangdeman/wrapper v0.0.0-20251225094759-09c64ba2541c github.com/gin-gonic/gin v1.11.0 @@ -24,6 +24,7 @@ require ( github.com/bytedance/sonic/loader v0.4.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect github.com/gabriel-vasile/mimetype v1.4.12 // indirect + github.com/getkin/kin-openapi v0.133.0 // indirect github.com/gin-contrib/sse v1.1.0 // indirect github.com/go-ini/ini v1.67.0 // indirect github.com/go-openapi/jsonpointer v0.22.4 // indirect @@ -41,14 +42,22 @@ require ( github.com/go-playground/validator/v10 v10.30.1 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-yaml v1.19.1 // indirect + github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/kr/text v0.2.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect + github.com/mailru/easyjson v0.9.1 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/mozillazg/go-pinyin v0.21.0 // indirect + github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect + github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/quic-go v0.58.0 // indirect github.com/sbabiv/xml2map v1.2.1 // indirect @@ -59,6 +68,7 @@ require ( github.com/tidwall/pretty v1.2.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.1 // indirect + github.com/woodsbury/decimal128 v1.4.0 // indirect go.uber.org/mock v0.6.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/arch v0.23.0 // indirect diff --git a/go.sum b/go.sum index 7ffc02b..12c2f9f 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ git.zhangdeman.cn/zhangdeman/consts v0.0.0-20260105014807-92a7dc3bb97f h1:2p5yxwk472XxY93UTky2Xl2doTU1uqR7vcKara/Dt1E= git.zhangdeman.cn/zhangdeman/consts v0.0.0-20260105014807-92a7dc3bb97f/go.mod h1:5p8CEKGBxi7qPtTXDI3HDmqKAfIm5i/aBWdrbkbdNjc= +git.zhangdeman.cn/zhangdeman/consts v0.0.0-20260106015608-42e268aea545 h1:A6UeeMcSqAlHUmA2coWIuiCS/W9ySylhZMa/0HbFDcQ= +git.zhangdeman.cn/zhangdeman/consts v0.0.0-20260106015608-42e268aea545/go.mod h1:5p8CEKGBxi7qPtTXDI3HDmqKAfIm5i/aBWdrbkbdNjc= git.zhangdeman.cn/zhangdeman/op_type v0.0.0-20251013024601-da007da2fb42 h1:VjYrb4adud7FHeiYS9XA0B/tOaJjfRejzQAlwimrrDc= git.zhangdeman.cn/zhangdeman/op_type v0.0.0-20251013024601-da007da2fb42/go.mod h1:VHb9qmhaPDAQDcS6vUiDCamYjZ4R5lD1XtVsh55KsMI= git.zhangdeman.cn/zhangdeman/serialize v0.0.0-20251013044511-86c1a4a3a9dd h1:kTZOpR8iHx27sUufMWVYhDZx9Q4h80j7RWlaR8GIBiU= @@ -26,6 +28,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ= +github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE= github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= @@ -41,6 +45,7 @@ github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3Zlm github.com/go-openapi/spec v0.22.3 h1:qRSmj6Smz2rEBxMnLRBMeBWxbbOvuOoElvSvObIgwQc= github.com/go-openapi/spec v0.22.3/go.mod h1:iIImLODL2loCh3Vnox8TY2YWYJZjMAKYyLH2Mu8lOZs= github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= @@ -82,6 +87,8 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= @@ -94,15 +101,29 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= +github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/mozillazg/go-pinyin v0.21.0 h1:Wo8/NT45z7P3er/9YSLHA3/kjZzbLz5hR7i+jGeIGao= +github.com/mozillazg/go-pinyin v0.21.0/go.mod h1:iR4EnMMRXkfpFVV5FMi4FNB6wGq9NV6uDWbUuPhP4Yc= +github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= +github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= +github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= +github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= +github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= @@ -111,6 +132,7 @@ github.com/quic-go/quic-go v0.58.0 h1:ggY2pvZaVdB9EyojxL1p+5mptkuHyX5MOSv4dgWF4U github.com/quic-go/quic-go v0.58.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/sbabiv/xml2map v1.2.1 h1:1lT7t0hhUvXZCkdxqtq4n8/ZCnwLWGq4rDuDv5XOoFE= github.com/sbabiv/xml2map v1.2.1/go.mod h1:2TPoAfcaM7+Sd4iriPvzyntb2mx7GY+kkQpB/GQa/eo= github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY= @@ -148,6 +170,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/woodsbury/decimal128 v1.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQsBgUlc= +github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= diff --git a/openapi/generate.go b/openapi/generate.go new file mode 100644 index 0000000..23c2219 --- /dev/null +++ b/openapi/generate.go @@ -0,0 +1,312 @@ +// Package openapi ... +// +// Description : openapi ... +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 2026-01-05 17:20 +package openapi + +import ( + "fmt" + "net/http" + "reflect" + "strings" + + "git.zhangdeman.cn/zhangdeman/api-doc/define" + "git.zhangdeman.cn/zhangdeman/consts" + "git.zhangdeman.cn/zhangdeman/wrapper/op_array" + "github.com/getkin/kin-openapi/openapi3" +) + +var ( + DocManager = &Generate{ + docTable: make(map[string]*openapi3.T), + } +) + +// NewOpenApiDoc 生成文档实例 +func NewOpenApiDoc(optionFunc ...OptionFunc) *openapi3.T { + t := &openapi3.T{ + Extensions: map[string]any{}, + OpenAPI: "3.1.0", + Components: &openapi3.Components{ + Extensions: map[string]any{}, + Origin: &openapi3.Origin{ + Key: &openapi3.Location{ + Line: 0, + Column: 0, + }, + Fields: make(map[string]openapi3.Location), + }, + Schemas: map[string]*openapi3.SchemaRef{}, + Parameters: map[string]*openapi3.ParameterRef{}, + Headers: map[string]*openapi3.HeaderRef{}, + RequestBodies: map[string]*openapi3.RequestBodyRef{}, + Responses: map[string]*openapi3.ResponseRef{}, + SecuritySchemes: map[string]*openapi3.SecuritySchemeRef{}, + Examples: map[string]*openapi3.ExampleRef{}, + Links: map[string]*openapi3.LinkRef{}, + Callbacks: map[string]*openapi3.CallbackRef{}, + }, + Info: &openapi3.Info{ + Extensions: map[string]any{}, + Origin: &openapi3.Origin{ + Key: &openapi3.Location{ + Line: 0, + Column: 0, + }, + Fields: make(map[string]openapi3.Location), + }, + Title: "服务 API接口 文档", + Description: "服务 API接口 文档", + TermsOfService: "", + Contact: &openapi3.Contact{ + Extensions: map[string]any{}, + Origin: &openapi3.Origin{ + Key: &openapi3.Location{ + Line: 0, + Column: 0, + }, + Fields: make(map[string]openapi3.Location), + }, + Name: "developer", + URL: "", + Email: "developer@test.com", + }, + License: &openapi3.License{ + Extensions: map[string]any{}, + Origin: &openapi3.Origin{ + Key: &openapi3.Location{ + Line: 0, + Column: 0, + }, + Fields: make(map[string]openapi3.Location), + }, + Name: consts.LicenseApache20, + URL: consts.LicenseUrlTable[consts.LicenseApache20], + }, + Version: "0.0.1", + }, + Paths: &openapi3.Paths{ + Extensions: map[string]any{}, + Origin: &openapi3.Origin{ + Key: &openapi3.Location{ + Line: 0, + Column: 0, + }, + Fields: make(map[string]openapi3.Location), + }, + }, + Security: []openapi3.SecurityRequirement{}, + Servers: []*openapi3.Server{ + { + Extensions: nil, + Origin: nil, + URL: "http://127.0.0.1:8080", + Description: "开发环境", + Variables: nil, + }, + }, + Tags: []*openapi3.Tag{}, + /*ExternalDocs: &openapi3.ExternalDocs{ + Extensions: map[string]any{}, + Origin: nil, + Description: "", + URL: "", + },*/ + } + for _, item := range optionFunc { + item(t) + } + return t +} + +// Generate 生成 OpenApi 标准规范的文档 +type Generate struct { + docTable map[string]*openapi3.T +} + +// DocData 获取一个文档数据 +func (g *Generate) DocData(docFlag string) *openapi3.T { + return g.docTable[docFlag] +} + +func (g *Generate) NewOpenApiDoc(docFlag string, docOption ...OptionFunc) *openapi3.T { + t := NewOpenApiDoc() + for _, item := range docOption { + item(t) + } + g.docTable[docFlag] = t + return t +} + +// AddApiDoc 添加接口文档 +func (g *Generate) AddApiDoc(docFlag string, apiMeta define.UriConfig, request any, response any) error { + var ( + err error + requestType reflect.Type + responseType reflect.Type + ok bool + ) + + // 初始化请求数据与响应数据类型 + if requestType, ok = request.(reflect.Type); !ok { + requestType = reflect.TypeOf(request) + if requestType.Kind() == reflect.Ptr { + requestType = requestType.Elem() + } + } + + if responseType, ok = response.(reflect.Type); !ok { + responseType = reflect.TypeOf(response) + if responseType.Kind() == reflect.Ptr { + responseType = responseType.Elem() + } + } + + schemaData := GenerateOpenAPISchema(requestType) + apiOperate, isRead := g.initApiConfig(docFlag, apiMeta) + requestTypeStr := requestType.String() + if isRead { + for paramName, paramConfig := range schemaData.Value.Properties { + apiOperate.Parameters = append(apiOperate.Parameters, &openapi3.ParameterRef{ + Extensions: nil, + Origin: nil, + Ref: "", + Value: &openapi3.Parameter{ + Extensions: nil, + Origin: nil, + Name: paramName, + In: strings.ToLower(consts.RequestDataLocationQuery.String()), + Description: paramConfig.Value.Description, + Style: "", + Explode: nil, + AllowEmptyValue: paramConfig.Value.AllowEmptyValue, + AllowReserved: false, + Deprecated: false, + Required: op_array.ArrayType(paramConfig.Value.Required).Has(paramName) >= 0, + Schema: paramConfig, + Example: nil, + Examples: nil, + Content: nil, + }, + }) + } + } else { + apiOperate.RequestBody = &openapi3.RequestBodyRef{ + Extensions: nil, + Origin: nil, + Ref: "", + Value: &openapi3.RequestBody{ + Extensions: nil, + Origin: nil, + Description: "", + Required: false, + Content: map[string]*openapi3.MediaType{ + consts.MimeTypeJson: { + Extensions: nil, + Origin: nil, + Schema: schemaData, + Example: nil, + Examples: nil, + Encoding: nil, + }, + }, + }, + } + } + + // 初始化接口配置 + if _, exist := g.docTable[docFlag].Components.Schemas[requestTypeStr]; !exist { + g.docTable[docFlag].Components.Schemas[requestTypeStr] = schemaData + } + responseTypeStr := responseType.String() + if _, exist := g.docTable[docFlag].Components.Schemas[responseTypeStr]; !exist { + g.docTable[docFlag].Components.Schemas[responseTypeStr] = GenerateOpenAPISchema(responseType) + } + desc := "请求成功" + apiOperate.Responses.Set(fmt.Sprintf("%v", http.StatusOK), &openapi3.ResponseRef{ + Extensions: nil, + Origin: nil, + Ref: "", + Value: &openapi3.Response{ + Extensions: nil, + Origin: nil, + Description: &desc, + Headers: nil, + Content: map[string]*openapi3.MediaType{ + consts.MimeTypeJson: { + Extensions: nil, + Origin: nil, + Schema: g.docTable[docFlag].Components.Schemas[responseTypeStr], + Example: nil, + Examples: nil, + Encoding: nil, + }, + }, + Links: nil, + }, + }) + return err +} + +// initApiConfig 初始化接口配置, 并返回 operation 以及 是否读请求 +func (g *Generate) initApiConfig(docFlag string, apiMeta define.UriConfig) (*openapi3.Operation, bool) { + if nil == g.docTable[docFlag].Paths.Value(apiMeta.Path) { + g.docTable[docFlag].Paths.Set(apiMeta.Path, &openapi3.PathItem{ + Extensions: nil, + Origin: nil, + Ref: "", + Summary: apiMeta.Desc, + Description: apiMeta.Desc, + Connect: nil, + Delete: nil, + Get: nil, + Head: nil, + Options: nil, + Patch: nil, + Post: nil, + Put: nil, + Trace: nil, + Servers: nil, + Parameters: nil, + }) + } + newOperate := openapi3.NewOperation() + newOperate.Parameters = make(openapi3.Parameters, 0) + newOperate.Responses = openapi3.NewResponses() + newOperate.Summary = apiMeta.Desc + newOperate.Description = apiMeta.Desc + newOperate.Tags = apiMeta.TagList + newOperate.OperationID = fmt.Sprintf("%v_%v", apiMeta.RequestMethod, apiMeta.Path) + isRead := false + switch apiMeta.RequestMethod { + case http.MethodGet: + g.docTable[docFlag].Paths.Value(apiMeta.Path).Get = newOperate + isRead = true + case http.MethodHead: + g.docTable[docFlag].Paths.Value(apiMeta.Path).Head = newOperate + isRead = true + case http.MethodConnect: + g.docTable[docFlag].Paths.Value(apiMeta.Path).Connect = newOperate + isRead = true + case http.MethodOptions: + g.docTable[docFlag].Paths.Value(apiMeta.Path).Options = newOperate + isRead = true + case http.MethodTrace: + g.docTable[docFlag].Paths.Value(apiMeta.Path).Trace = newOperate + isRead = true + // 读请求 + case http.MethodPost: + g.docTable[docFlag].Paths.Value(apiMeta.Path).Post = newOperate + case http.MethodPut: + g.docTable[docFlag].Paths.Value(apiMeta.Path).Put = newOperate + case http.MethodPatch: + g.docTable[docFlag].Paths.Value(apiMeta.Path).Patch = newOperate + case http.MethodDelete: + g.docTable[docFlag].Paths.Value(apiMeta.Path).Delete = newOperate + // 写请求 + } + return newOperate, isRead +} diff --git a/openapi/generate_test.go b/openapi/generate_test.go new file mode 100644 index 0000000..985a944 --- /dev/null +++ b/openapi/generate_test.go @@ -0,0 +1,54 @@ +// Package openapi ... +// +// Description : openapi ... +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 2026-01-06 11:48 +package openapi + +import ( + "encoding/json" + "fmt" + "net/http" + "testing" + "time" + + "git.zhangdeman.cn/zhangdeman/api-doc/define" +) + +func TestGenerate_AddApiDoc(t *testing.T) { + type Category struct { + ID int64 `json:"id" description:"分类ID" eg:"123" binding:"required,min=10,max=100"` + Sex string `json:"sex" dc:"性别" binding:"required,oneof=man woman" enum-desc:"man:男||woman:女"` + Name string `json:"name" description:"分类名称" eg:"baichaqinghuan" binding:"required,min=8"` + } + type Product struct { + ID int64 `json:"id,omitempty" description:"产品ID" example:"1001" required:"true" binding:"required"` + Name string `json:"name" description:"产品名称" example:"iPhone 13" minLength:"2" maxLength:"100" required:"true"` + Price float64 `json:"price" description:"价格" example:"6999.99" min:"0"` + Stock int `json:"stock" description:"库存" example:"100" min:"0"` + Tags []string `json:"tags" description:"标签"` + Attributes map[string]string `json:"attributes" description:"属性"` + CreatedAt time.Time `json:"created_at" description:"创建时间"` + UpdatedAt *time.Time `json:"updated_at,omitempty" description:"更新时间"` + Category *Category `json:"category,omitempty" description:"分类"` + } + docFlag := "demo" + DocManager.NewOpenApiDoc(docFlag) + DocManager.AddApiDoc(docFlag, define.UriConfig{ + Path: "/a/b/c", + RequestMethod: http.MethodGet, + TagList: []string{"test"}, + Desc: "测试接口", + }, Category{}, Product{}) + DocManager.AddApiDoc(docFlag, define.UriConfig{ + Path: "/a/b/c", + RequestMethod: http.MethodPost, + TagList: []string{"test"}, + Desc: "测试接口", + }, Category{}, Product{}) + // 输出 JSON + data, _ := json.MarshalIndent(DocManager.docTable[docFlag], "", " ") + fmt.Println(string(data)) +} diff --git a/openapi/option.go b/openapi/option.go new file mode 100644 index 0000000..3603a99 --- /dev/null +++ b/openapi/option.go @@ -0,0 +1,33 @@ +// Package openapi ... +// +// Description : openapi ... +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 2026-01-06 22:48 +package openapi + +import "github.com/getkin/kin-openapi/openapi3" + +// OptionFunc 设置文档选项 +type OptionFunc func(t *openapi3.T) + +// WithServers 设置文档服务器 +func WithServers(serverList openapi3.Servers) OptionFunc { + return func(t *openapi3.T) { + if len(serverList) == 0 { + return + } + t.Servers = serverList + } +} + +// WithInfo 文档基础信息 +func WithInfo(info *openapi3.Info) OptionFunc { + return func(t *openapi3.T) { + if nil == info { + return + } + t.Info = info + } +} diff --git a/openapi/schema.go b/openapi/schema.go new file mode 100644 index 0000000..db2a50f --- /dev/null +++ b/openapi/schema.go @@ -0,0 +1,242 @@ +// 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/util" + "github.com/getkin/kin-openapi/openapi3" +) + +// StructFieldInfo 结构体字段信息 +type StructFieldInfo struct { + IsString bool `json:"is_string" dc:"是否字符串"` + 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 map[string]string `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{ + IsString: validateRule.IsString, + 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: nil, + Max: nil, + MinLength: nil, + MaxLength: nil, + Pattern: validateRule.Regexp, + Format: field.Type.String(), + Enum: validateRule.Oneof, // 解析枚举值 + EnumDesc: util.ParseStructFieldTag.EnumDescription(field), // 解析枚举值描述 + OmitEmpty: false, + } + if info.IsString { + info.MinLength = validateRule.Min + info.MaxLength = validateRule.Max + } else { + info.Min = validateRule.Min + info.Max = validateRule.Max + } + + // 解析 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() + } + } + + schema := generateSchemaRecursive(tType, make(map[string]bool)) + return schema +} + +// 生成 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 + if nil == schema.Extensions { + schema.Extensions = map[string]any{} + } + schema.Extensions["x-enum-descriptions"] = info.EnumDesc + } +} + +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()} + } +} diff --git a/openapi/schema_test.go b/openapi/schema_test.go new file mode 100644 index 0000000..1a9aa61 --- /dev/null +++ b/openapi/schema_test.go @@ -0,0 +1,58 @@ +// Package openapi ... +// +// Description : openapi ... +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 2026-01-06 10:50 +package openapi + +import ( + "encoding/json" + "fmt" + "testing" + "time" + + "github.com/getkin/kin-openapi/openapi3" +) + +func TestParseStructField(t *testing.T) { + type Category struct { + ID int64 `json:"id" description:"分类ID"` + Name string `json:"name" description:"分类名称"` + } + type Product struct { + ID int64 `json:"id" description:"产品ID" example:"1001" required:"true"` + Name string `json:"name" description:"产品名称" example:"iPhone 13" minLength:"2" maxLength:"100" required:"true"` + Price float64 `json:"price" description:"价格" example:"6999.99" min:"0"` + Stock int `json:"stock" description:"库存" example:"100" min:"0"` + Tags []string `json:"tags" description:"标签"` + Attributes map[string]string `json:"attributes" description:"属性"` + CreatedAt time.Time `json:"created_at" description:"创建时间"` + UpdatedAt *time.Time `json:"updated_at,omitempty" description:"更新时间"` + Category *Category `json:"category,omitempty" description:"分类"` + } + + // 生成 Product 的 Schema + productSchema := GenerateOpenAPISchema(Product{}) + + // 创建完整的 OpenAPI 文档 + doc := &openapi3.T{ + OpenAPI: "3.0.0", + Info: &openapi3.Info{ + Title: "产品服务 API", + Description: "产品管理相关接口", + Version: "1.0.0", + }, + Components: &openapi3.Components{ + Schemas: map[string]*openapi3.SchemaRef{ + "Product": productSchema, + "Category": GenerateOpenAPISchema(Category{}), + }, + }, + } + + // 输出 JSON + data, _ := json.MarshalIndent(doc, "", " ") + fmt.Println(string(data)) +} diff --git a/struct_field.go b/util/struct_field.go similarity index 67% rename from struct_field.go rename to util/struct_field.go index 73b9dac..d5b3603 100644 --- a/struct_field.go +++ b/util/struct_field.go @@ -1,11 +1,11 @@ -// Package api_doc ... +// Package util ... // // Description : api_doc ... // // Author : go_developer@163.com<白茶清欢> // // Date : 2025-02-12 22:15 -package api_doc +package util import ( "reflect" @@ -13,6 +13,7 @@ import ( "strings" "git.zhangdeman.cn/zhangdeman/api-doc/define" + utilPkg "git.zhangdeman.cn/zhangdeman/util" ) var ( @@ -23,11 +24,7 @@ type parseStructFieldTag struct { } // GetParamName 获取参数名称 -// -// Author : go_developer@163.com<白茶清欢> -// -// Date : 21:58 2025/2/11 -func (psf parseStructFieldTag) GetParamName(structField reflect.StructField) string { +func (psf parseStructFieldTag) GetParamName(structField reflect.StructField) (string, bool) { paramNameTagList := []string{ define.TagJson, define.TagForm, define.TagXml, define.TagYaml, @@ -35,21 +32,33 @@ func (psf parseStructFieldTag) GetParamName(structField reflect.StructField) str } for _, tag := range paramNameTagList { tagVal := structField.Tag.Get(tag) - tagVal = strings.TrimSuffix(strings.TrimPrefix(tagVal, define.TagNameOmitempty+","), ","+define.TagNameOmitempty) - tagVal = strings.Trim(tagVal, ",") - if tagVal != "" && tagVal != define.TagNameOmitempty { - return tagVal + if tagVal == "" { + continue } + parts := strings.Split(tagVal, ",") + jsonName := "" + omit := false + if len(parts) > 0 { + if parts[0] == "-" { + return "", false // 跳过该字段 + } + if parts[0] != "" { + jsonName = parts[0] + } + } + for _, part := range parts[1:] { + switch part { + case define.TagNameOmitempty: + omit = true + } + } + return jsonName, omit } // 未设置相关字段, 则字段名即为参数名 - return structField.Name + return structField.Name, false } // GetParamDesc ... -// -// Author : go_developer@163.com<白茶清欢> -// -// Date : 22:01 2025/2/11 func (psf parseStructFieldTag) GetParamDesc(structField reflect.StructField) string { descTagList := []string{define.TagDc, define.TagDesc, define.TagDescription} for _, tag := range descTagList { @@ -59,14 +68,11 @@ func (psf parseStructFieldTag) GetParamDesc(structField reflect.StructField) str } } // 没有显示的设置参数描述, 则使用参数名作为参数描述 - return psf.GetParamName(structField) + paramName, _ := psf.GetParamName(structField) + return paramName } // GetDefaultValue 获取默认值 -// -// Author : go_developer@163.com<白茶清欢> -// -// Date : 22:05 2025/2/11 func (psf parseStructFieldTag) GetDefaultValue(structField reflect.StructField) any { defaultTagList := []string{define.TagD, define.TagDefault} fieldType := structField.Type.Kind().String() @@ -104,10 +110,6 @@ func (psf parseStructFieldTag) GetDefaultValue(structField reflect.StructField) } // GetValidateRule 获取验证规则 -// -// Author : go_developer@163.com<白茶清欢> -// -// Date : 15:30 2025/2/13 func (psf parseStructFieldTag) GetValidateRule(structField reflect.StructField) string { defaultTagList := []string{define.TagValidate, define.TagBinding} for _, tag := range defaultTagList { @@ -119,10 +121,6 @@ func (psf parseStructFieldTag) GetValidateRule(structField reflect.StructField) } // Deprecated 是否弃用 -// -// Author : go_developer@163.com<白茶清欢> -// -// Date : 21:12 2025/2/13 func (psf parseStructFieldTag) Deprecated(structField reflect.StructField) bool { defaultTagList := []string{define.TagDeprecated} for _, tag := range defaultTagList { @@ -134,10 +132,6 @@ func (psf parseStructFieldTag) Deprecated(structField reflect.StructField) bool } // Summary 摘要信息 -// -// Author : go_developer@163.com<白茶清欢> -// -// Date : 15:15 2025/2/14 func (psf parseStructFieldTag) Summary(structField reflect.StructField) string { defaultTagList := []string{define.TagSummary} for _, tag := range defaultTagList { @@ -145,21 +139,14 @@ func (psf parseStructFieldTag) Summary(structField reflect.StructField) string { return tagVal } } - paramName := psf.GetParamName(structField) - if paramName == "-" { - return "" - } + paramName, _ := psf.GetParamName(structField) return paramName } // EnumDescription .枚举值详细描述 -// -// Author : go_developer@163.com<白茶清欢> -// -// Date : 22:40 2025/2/18 func (psf parseStructFieldTag) EnumDescription(structField reflect.StructField) map[string]string { defaultTagList := []string{define.TagNameEnumDescription} - res := map[string]string{} + res := make(map[string]string) for _, tag := range defaultTagList { if tagVal, exist := structField.Tag.Lookup(tag); exist && len(tagVal) > 0 { tagVal = strings.ReplaceAll(tagVal, "###", "`") @@ -174,48 +161,43 @@ func (psf parseStructFieldTag) EnumDescription(structField reflect.StructField) return res } } - if len(res) == 0 { - return nil - } return res } // GetExampleValue 示例值 -// -// Author : go_developer@163.com<白茶清欢> -// -// Date : 14:42 2025/2/20 func (psf parseStructFieldTag) GetExampleValue(structField reflect.StructField) any { - descTagList := []string{define.TagEg, define.TagExample} - fieldType := structField.Type.Kind().String() - for _, tag := range descTagList { - val := strings.TrimSpace(structField.Tag.Get(tag)) - if val == "" { + exampleTagList := []string{define.TagEg, define.TagExample} + structFieldType := structField.Type + if structFieldType.Kind() == reflect.Ptr { + structFieldType = structFieldType.Elem() + } + fieldTypeKind := structFieldType.Kind() + for _, tag := range exampleTagList { + example := strings.TrimSpace(structField.Tag.Get(tag)) + if example == "" { continue } - 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 + switch fieldTypeKind { + case reflect.String: + return example + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + var res int64 + if err := utilPkg.ConvertAssign(&res, example); err == nil { + return res } + case reflect.Float32, reflect.Float64: + var res float64 + if err := utilPkg.ConvertAssign(&res, example); err == nil { + return res + } + case reflect.Bool: + var res bool + if err := utilPkg.ConvertAssign(&res, example); err == nil { + return res + } + default: + return example } - return val } return nil } diff --git a/util/validate_v10_parse.go b/util/validate_v10_parse.go new file mode 100644 index 0000000..24312ac --- /dev/null +++ b/util/validate_v10_parse.go @@ -0,0 +1,130 @@ +// Package util ... +// +// Description : util ... +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 2026-01-05 18:48 +package util + +import ( + "reflect" + "strings" + + "git.zhangdeman.cn/zhangdeman/consts" + utilPkg "git.zhangdeman.cn/zhangdeman/util" + "git.zhangdeman.cn/zhangdeman/wrapper/op_string" +) + +// ParseValidateRule 解析验证规则 +func ParseValidateRule(dataType reflect.Type, ruleStr string) ValidateRule { + if dataType.Kind() == reflect.Ptr { + dataType = dataType.Elem() + } + dataKind := dataType.Kind() + rule := ValidateRule{ + IsString: dataKind == reflect.String, + Omitempty: false, + Required: false, + Lte: nil, + Gte: nil, + Lt: nil, + Gt: nil, + Len: nil, + Max: nil, + Min: nil, + Eq: nil, + Ne: nil, + Oneof: nil, + } + ruleList := strings.Split(ruleStr, ",") + for _, itemRule := range ruleList { + itemRule = strings.ToLower(strings.TrimSpace(itemRule)) + if len(itemRule) == 0 { + continue + } + if !strings.Contains(itemRule, "=") { + // 一定是无需要值的验证规则 + switch itemRule { + case consts.ValidatorRuleCommonRequired.String(): // 必传 + rule.Required = true + case consts.ValidatorRuleCommonOmitempty.String(): // 为空则不校验 + rule.Omitempty = true + } + continue + } + itemRuleArr := strings.Split(itemRule, "=") + ruleType := itemRuleArr[0] + ruleValue := strings.Join(itemRuleArr[1:], "=") + if len(ruleValue) == 0 { + // 未配置值的校验规则 + continue + } + switch ruleType { + case consts.ValidatorRuleLte.String(), consts.ValidatorRuleGte.String(), consts.ValidatorRuleGt.String(), consts.ValidatorRuleLt.String(), // 数字取值范围 + consts.ValidatorRuleCommonLen.String(), consts.ValidatorRuleCommonMin.String(), consts.ValidatorRuleCommonMax.String(): // 长度取值范围 + var val float64 + if err := utilPkg.ConvertAssign(&val, ruleValue); nil == err { + switch ruleType { + case consts.ValidatorRuleLte.String(): + rule.Lte = &val + case consts.ValidatorRuleGte.String(): + rule.Gte = &val + case consts.ValidatorRuleGt.String(): + rule.Gt = &val + case consts.ValidatorRuleLt.String(): + rule.Lt = &val + case consts.ValidatorRuleCommonLen.String(): + rule.Len = &val + + case consts.ValidatorRuleCommonMin.String(): + rule.Min = &val + + case consts.ValidatorRuleCommonMax.String(): + rule.Max = &val + } + } + case consts.ValidatorRuleEq.String(): + rule.Eq = ruleValue + case consts.ValidatorRuleNe.String(): + rule.Ne = ruleValue + case consts.ValidatorRuleRegexp.String(): + rule.Regexp = ruleValue + case consts.ValidatorRuleCommonOneOf.String(): + rule.Oneof = make([]any, 0) + switch dataKind { + case reflect.String: + valList := strings.Split(ruleValue, " ") + for _, item := range valList { + rule.Oneof = append(rule.Oneof, item) + } + case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint, + reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int, + reflect.Float32, reflect.Float64: + valList := op_string.ToBaseTypeSlice[float64](ruleValue, " ").Value + for _, item := range valList { + rule.Oneof = append(rule.Oneof, item) + } + default: + } + } + } + return rule +} + +type ValidateRule struct { + IsString bool `json:"is_string" dc:"是否字符串"` + Omitempty bool `json:"omitempty" dc:"为空则不校验"` + Required bool `json:"required" dc:"必传校验"` + Lte *float64 `json:"lte" dc:"数字类型小于等于"` + Gte *float64 `json:"gte" dc:"数字类型大于等于"` + Lt *float64 `json:"lt" dc:"数字类型小于"` + Gt *float64 `json:"gt" dc:"数字类型大于"` + Len *float64 `json:"len" dc:"长度等于"` + Max *float64 `json:"max" dc:"长度小于等于"` + Min *float64 `json:"min" dc:"长度大于等于"` + Eq any `json:"eq" dc:"等于"` + Ne any `json:"ne" dc:"不等于"` + Oneof []any `json:"oneof" dc:"枚举值列表"` + Regexp string `json:"regexp" dc:"正则表达式"` +}