Files
full/scripts/api-docgen/main.go

301 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"bytes"
"encoding/json"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
_ "bsm/full/module/base/ads/pb"
_ "bsm/full/module/base/cloud/pb"
_ "bsm/full/module/base/cms/pb"
_ "bsm/full/module/base/feedback/pb"
_ "bsm/full/module/base/initial/pb"
_ "bsm/full/module/base/passport/pb"
_ "bsm/full/module/base/sender/pb"
_ "bsm/full/module/ec/address/pb"
_ "bsm/full/module/ec/mall/pb"
_ "bsm/full/module/ec/market/pb"
_ "bsm/full/module/ec/order/pb"
_ "bsm/full/module/finance/wallet/pb"
_ "bsm/full/module/social/feed/pb"
_ "bsm/full/module/social/group/pb"
_ "bsm/full/module/social/relation/pb"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/reflect/protoregistry"
)
type moduleInfo struct {
Order int
Name string
Package protoreflect.FullName
Description string
}
var modules = []moduleInfo{
{1, "ads", "ads", "广告获取与投放内容读取。"},
{2, "cloud", "cloud", "云端相册、书签、网盘、笔记、私密数据、分享和空间管理。"},
{3, "cms", "cms", "站点、页面、文章、栏目和标签内容管理。"},
{4, "feedback", "feedback", "用户反馈提交与查询。"},
{5, "initial", "initial", "客户端初始化、基础数据和版本更新检查。"},
{6, "passport", "passport", "账号注册、登录、验证、找回密码和账户资料管理。"},
{7, "sender", "sender", "邮件、短信发送及验证码校验。"},
{8, "address", "address", "用户地址库管理。"},
{9, "mall", "mall", "商城广告、类目、运费、公告、商品、员工和店铺管理。"},
{10, "market", "market", "市场代理商、供应商及经营数据管理。"},
{11, "order", "order", "购物车、优惠券、订单汇总及商家订单管理。"},
{12, "wallet", "wallet", "钱包、支付方式、支付宝与微信支付。"},
{13, "feed", "feed", "动态发布、标签、时间线和动态设置。"},
{14, "group", "group", "群组及成员管理。"},
{15, "relation", "relation", "关注、好友和关系匹配。"},
}
func main() {
root, err := filepath.Abs(filepath.Join("..", ".."))
must(err)
outDir := filepath.Join(root, "wiki", "api")
must(os.MkdirAll(outDir, 0o755))
for _, module := range modules {
services := servicesFor(module.Package)
content := renderModule(module, services)
name := fmt.Sprintf("%02d-%s.md", module.Order, module.Name)
must(os.WriteFile(filepath.Join(outDir, name), content, 0o644))
}
}
func servicesFor(pkg protoreflect.FullName) []protoreflect.ServiceDescriptor {
var services []protoreflect.ServiceDescriptor
protoregistry.GlobalFiles.RangeFiles(func(file protoreflect.FileDescriptor) bool {
if file.Package() != pkg {
return true
}
for i := 0; i < file.Services().Len(); i++ {
services = append(services, file.Services().Get(i))
}
return true
})
sort.Slice(services, func(i, j int) bool { return services[i].FullName() < services[j].FullName() })
return services
}
func renderModule(module moduleInfo, services []protoreflect.ServiceDescriptor) []byte {
var out bytes.Buffer
fmt.Fprintf(&out, "# %s API\n\n%s\n\n", strings.ToUpper(module.Name), module.Description)
out.WriteString("> 本文档由 protobuf descriptor 生成。字段名采用 protobuf JSON 名称64 位整数在 JSON 中应按字符串处理。\n\n")
out.WriteString("## 接入方式\n\n")
fmt.Fprintf(&out, "- 动态 HTTP`POST /rpc/%s/{Service}/{Method}`\n", module.Package)
fmt.Fprintf(&out, "- 原生 gRPC`/%s.{Service}/{Method}`\n", module.Package)
fmt.Fprintf(&out, "- grpc-gateway`POST /%s.{Service}/{Method}`\n", module.Package)
out.WriteString("- 鉴权:需要登录的接口通过 `Authorization: <JWT>` 传递裸 JWT不添加 `Bearer` 前缀。\n")
out.WriteString("- 动态 HTTP 成功或失败均返回 HTTP 200业务状态见 `code`、`message`、`details`。\n\n")
out.WriteString("## 服务概览\n\n")
out.WriteString("| 服务 | 方法数 | 说明 |\n|---|---:|---|\n")
for _, service := range services {
fmt.Fprintf(&out, "| `%s` | %d | %s 服务 |\n", service.Name(), service.Methods().Len(), service.Name())
}
schemas := map[protoreflect.FullName]protoreflect.MessageDescriptor{}
enums := map[protoreflect.FullName]protoreflect.EnumDescriptor{}
for _, service := range services {
fmt.Fprintf(&out, "\n## %s\n\n", service.Name())
out.WriteString("| 方法 | 动态 HTTP 路径 | 请求 | 响应 | 类型 |\n|---|---|---|---|---|\n")
for i := 0; i < service.Methods().Len(); i++ {
method := service.Methods().Get(i)
kind := "Unary"
if method.IsStreamingClient() || method.IsStreamingServer() {
kind = "Streaming动态 HTTP 暂不支持)"
}
fmt.Fprintf(&out, "| `%s` | `POST /rpc/%s/%s/%s` | [`%s`](#%s) | [`%s`](#%s) | %s |\n",
method.Name(), module.Package, service.Name(), method.Name(),
method.Input().FullName(), anchor(method.Input().FullName()), method.Output().FullName(), anchor(method.Output().FullName()), kind)
collectMessage(method.Input(), schemas, enums)
collectMessage(method.Output(), schemas, enums)
}
for i := 0; i < service.Methods().Len(); i++ {
method := service.Methods().Get(i)
fmt.Fprintf(&out, "\n### %s.%s\n\n", service.Name(), method.Name())
fmt.Fprintf(&out, "- 动态 HTTP`POST /rpc/%s/%s/%s`\n", module.Package, service.Name(), method.Name())
fmt.Fprintf(&out, "- gRPC`/%s/%s`\n", service.FullName(), method.Name())
out.WriteString("- 请求示例:\n\n```json\n")
out.Write(exampleJSON(method.Input()))
out.WriteString("\n```\n")
}
}
messageNames := sortedMessageNames(schemas)
if len(messageNames) > 0 {
out.WriteString("\n## 消息结构\n")
}
for _, name := range messageNames {
renderMessage(&out, schemas[name], enums)
}
enumNames := make([]string, 0, len(enums))
for name := range enums {
enumNames = append(enumNames, string(name))
}
sort.Strings(enumNames)
if len(enumNames) > 0 {
out.WriteString("\n## 枚举\n")
}
for _, rawName := range enumNames {
renderEnum(&out, enums[protoreflect.FullName(rawName)])
}
return out.Bytes()
}
func collectMessage(message protoreflect.MessageDescriptor, messages map[protoreflect.FullName]protoreflect.MessageDescriptor, enums map[protoreflect.FullName]protoreflect.EnumDescriptor) {
if _, exists := messages[message.FullName()]; exists {
return
}
messages[message.FullName()] = message
for i := 0; i < message.Fields().Len(); i++ {
field := message.Fields().Get(i)
if field.Message() != nil {
collectMessage(field.Message(), messages, enums)
}
if field.Enum() != nil {
enums[field.Enum().FullName()] = field.Enum()
}
}
}
func renderMessage(out *bytes.Buffer, message protoreflect.MessageDescriptor, enums map[protoreflect.FullName]protoreflect.EnumDescriptor) {
fmt.Fprintf(out, "\n### %s\n\n", message.FullName())
if message.Fields().Len() == 0 {
out.WriteString("空消息:请求时发送 `{}`。\n")
return
}
out.WriteString("| JSON 字段 | 类型 | 规则 |\n|---|---|---|\n")
for i := 0; i < message.Fields().Len(); i++ {
field := message.Fields().Get(i)
rule := "可选"
if field.Cardinality() == protoreflect.Repeated {
rule = "数组"
}
if field.IsMap() {
rule = "对象映射"
}
if field.ContainingOneof() != nil && !field.ContainingOneof().IsSynthetic() {
rule = "oneof: " + string(field.ContainingOneof().Name())
}
fmt.Fprintf(out, "| `%s` | `%s` | %s |\n", field.JSONName(), fieldType(field), rule)
if field.Enum() != nil {
enums[field.Enum().FullName()] = field.Enum()
}
}
}
func renderEnum(out *bytes.Buffer, enum protoreflect.EnumDescriptor) {
fmt.Fprintf(out, "\n### %s\n\n", enum.FullName())
out.WriteString("| 名称 | 数值 |\n|---|---:|\n")
for i := 0; i < enum.Values().Len(); i++ {
value := enum.Values().Get(i)
fmt.Fprintf(out, "| `%s` | %d |\n", value.Name(), value.Number())
}
}
func fieldType(field protoreflect.FieldDescriptor) string {
var value string
switch field.Kind() {
case protoreflect.MessageKind, protoreflect.GroupKind:
value = string(field.Message().FullName())
case protoreflect.EnumKind:
value = string(field.Enum().FullName())
default:
value = field.Kind().String()
}
if field.IsMap() {
value = "map<" + fieldType(field.MapKey()) + ", " + fieldType(field.MapValue()) + ">"
} else if field.Cardinality() == protoreflect.Repeated {
value = "[]" + value
}
return value
}
func exampleJSON(message protoreflect.MessageDescriptor) []byte {
value := exampleMessage(message, map[protoreflect.FullName]bool{}, 0)
data, err := json.MarshalIndent(value, "", " ")
must(err)
return data
}
func exampleMessage(message protoreflect.MessageDescriptor, visiting map[protoreflect.FullName]bool, depth int) map[string]any {
if depth > 2 || visiting[message.FullName()] {
return map[string]any{}
}
visiting[message.FullName()] = true
defer delete(visiting, message.FullName())
result := map[string]any{}
seenOneof := map[protoreflect.Name]bool{}
for i := 0; i < message.Fields().Len(); i++ {
field := message.Fields().Get(i)
if oneof := field.ContainingOneof(); oneof != nil && !oneof.IsSynthetic() {
if seenOneof[oneof.Name()] {
continue
}
seenOneof[oneof.Name()] = true
}
value := exampleField(field, visiting, depth)
if field.Cardinality() == protoreflect.Repeated && !field.IsMap() {
value = []any{value}
}
if field.IsMap() {
value = map[string]any{"key": exampleField(field.MapValue(), visiting, depth)}
}
result[field.JSONName()] = value
}
return result
}
func exampleField(field protoreflect.FieldDescriptor, visiting map[protoreflect.FullName]bool, depth int) any {
switch field.Kind() {
case protoreflect.BoolKind:
return false
case protoreflect.StringKind:
return "string"
case protoreflect.BytesKind:
return "BASE64"
case protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind,
protoreflect.Uint64Kind, protoreflect.Fixed64Kind:
return "0"
case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind,
protoreflect.Uint32Kind, protoreflect.Fixed32Kind, protoreflect.FloatKind, protoreflect.DoubleKind:
return 0
case protoreflect.EnumKind:
if field.Enum().Values().Len() > 0 {
return string(field.Enum().Values().Get(0).Name())
}
return 0
case protoreflect.MessageKind, protoreflect.GroupKind:
return exampleMessage(field.Message(), visiting, depth+1)
default:
return nil
}
}
func sortedMessageNames(messages map[protoreflect.FullName]protoreflect.MessageDescriptor) []protoreflect.FullName {
names := make([]protoreflect.FullName, 0, len(messages))
for name := range messages {
names = append(names, name)
}
sort.Slice(names, func(i, j int) bool { return names[i] < names[j] })
return names
}
func anchor(name protoreflect.FullName) string {
return strings.ToLower(strings.ReplaceAll(string(name), ".", ""))
}
func must(err error) {
if err != nil {
panic(err)
}
}