Compare commits

27 Commits

Author SHA1 Message Date
def62b0aa9 Merge pull request '升级文档管理, 支持openapi3, 使用 github.com/getkin/kin-openapi/openapi3 实现' (#23) from feature/upgrade_api_doc_generate into master
Reviewed-on: #23
2026-01-06 23:26:36 +08:00
3ea5af0709 feat: 增加文档数据初始化逻辑 2026-01-06 23:25:14 +08:00
5d86daadeb feat: 枚举值增加 x-enum-descriptions 用于标记枚举值含义 2026-01-06 22:42:29 +08:00
a0090ad524 feat: 调试示例值 2026-01-06 22:08:31 +08:00
d1b5e16d35 feat: 修复分组、标签、描述不现实问题 2026-01-06 21:59:41 +08:00
d45d6e7890 feat: swagger server 2026-01-06 21:43:08 +08:00
b6821e1497 feat: 修复响应数据文档生成的BUG 2026-01-06 19:37:16 +08:00
088b813045 feat: 修复数字取值范围与字符长度限制显示错误问题 2026-01-06 19:17:22 +08:00
708e327ddf feat: 修复必传数据解析错误问题 2026-01-06 19:01:21 +08:00
8ed6735d48 feat: 调试必传参数 2026-01-06 18:52:57 +08:00
0dd0e8f22f feat: 完成基础文档生成 2026-01-06 18:05:07 +08:00
c1d5dd89d4 feat: 完胜增加schema逻辑 2026-01-06 11:57:02 +08:00
2f4e6851f3 feat: 完成openapi文档数据结构初始化 2026-01-06 11:11:07 +08:00
b03fc5acf9 feat: 完善结构体字段类型解析 2026-01-06 10:55:27 +08:00
9f2059837c feat: 优化ParseStructField方法 2026-01-06 10:05:29 +08:00
2cad79cf3d feat: 完成validate 10 验证规则解析 2026-01-06 09:59:40 +08:00
e40b2959d7 feat: 验证规则解析 2026-01-05 22:50:12 +08:00
b3b090a020 feat: validate v10 校验规则结构化处理 2026-01-05 19:00:10 +08:00
5ff7dac227 feat: 优化参数名称解析 2026-01-05 18:46:53 +08:00
f4447d50d6 feat: 修复枚举值赋值问题 2026-01-05 18:28:11 +08:00
1157973b65 feat: 增加枚举值解析 2026-01-05 18:23:23 +08:00
8ce50fd235 save code 2026-01-05 18:16:48 +08:00
20b6c48640 fix: 修复递归调用类型错误问题 2026-01-05 18:05:45 +08:00
2996fc4732 feat: 完善字段描述获取 2026-01-05 18:02:47 +08:00
d74f82d060 feat: 优化 struct_field 2026-01-05 17:47:12 +08:00
e031f56393 feat: 规划增加接口文档的方法 2026-01-05 17:25:25 +08:00
b9c5ed591a feat: 引入 github.com/getkin/kin-openapi/openapi3 库, 准备重写文档生成 2026-01-05 17:22:32 +08:00
10 changed files with 926 additions and 76 deletions

View File

@@ -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:"枚举值描述"`
}

12
go.mod
View File

@@ -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

24
go.sum
View File

@@ -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=

312
openapi/generate.go Normal file
View File

@@ -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
}

54
openapi/generate_test.go Normal file
View File

@@ -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))
}

33
openapi/option.go Normal file
View File

@@ -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
}
}

242
openapi/schema.go Normal file
View File

@@ -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()}
}
}

58
openapi/schema_test.go Normal file
View File

@@ -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))
}

View File

@@ -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
}

130
util/validate_v10_parse.go Normal file
View File

@@ -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:"正则表达式"`
}