302 lines
11 KiB
Go
302 lines
11 KiB
Go
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: Bearer <token>` 传递凭证。\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())
|
||
fmt.Fprintf(&out, "- 白名单键:`%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)
|
||
}
|
||
}
|