生成其他协议

在开发过程中,我们可能需要生成不同语言的协议文件。niuhe插件支持 go, ts, vite 等协议代码生成。同时为了方便其他语言使用。将 niuhe 协议以 json 格式输出了完整的协议内容,可以很方便地被其他语言解析和使用。以下是如何生成其他语言协议的步骤:

1. 配置 .config.json5

首先,在 .config.json5langs 中添加 protocol 配置项.

2. protocol 定义

配置 protocol 后, 生成代码时会在 docs 文件下生成 protocol.json 文件。协议定义为:

declare namespace Protocol {
    interface StringEnum {
        value: string // 枚举值 - 数字也是 string
        name: string // 字段名
        desc: string // 描述
    }
    // 枚举类型
    interface Enum {
        mode: string
        type: 'string' | 'integer'
        name: string
        desc: string
        values: StringEnum[]
    }
    interface Field {
        label: string// required, optional, repeated, mapping
        type: string // 'integer', 'decimal', 'float', 'long', 'string', 'boolean', 'message', 'enum', 'stringenum','file' 'dict' 'any'
        name: string // 字段名
        desc: string //  描述
        ref?: string // 引用的结构
    }
    interface Message {
        mode: string //api
        name: string // 名称
        desc: string // 描述
        fields: Field[]
    }
    interface Route {
        method: string // post, get[, put, patch, delete, head, options]
        url: string // /api/sys/ts/
        mode: string // api
        desc: string // 描述
        req?: Message
        rsp?: Message
        
    }
    interface Docs {
        app: string
        /** api 路由信息 */
        routes: Route[]
        /** 数组 */
        enums: {
            [mode_name: string]: Enum
        }
        /** 消息类型 */
        messages: {
            [mode_name: string]: Message
        }
    }
}

go 语言解析为 kotlin 版例子

通过解析上述协议文件可得到请求方法, 路径, 入参和出参的详细定义。下面为 go 语言解析出 kotlin 版本的一个例子。

package protocol

import (
	"encoding/json"
	"fmt"
	"strings"
	"unicode"
)

type StringEnum struct {
	Value string `json:"vallue"` // 枚举值 - 数字也是 string
	Name  string `json:"name"`   // 字段名
	Desc  string `json:"desc"`   // 描述
}

// 枚举类型
type Enum struct {
	Mode   string       `json:"mode"`
	Etype  string       `json:"type"` // 'string' | 'integer'
	Name   string       `json:"name"`
	Desc   string       `json:"desc"`
	Values []StringEnum `json:"values"`
}
type Field struct {
	Label string // required, optional, repeated, mapping
	Type  string `json:"type"` // 'integer', 'decimal', 'float', 'long', 'string', 'boolean', 'message', 'enum', 'stringenum','file' 'dict' 'any'
	Name  string `json:"name"` // 字段名
	Desc  string `json:"desc"` //  描述
	Ref   string `json:"ref"`  // 引用的结构
}
type Message struct {
	Mode   string   `json:"mode"` //api
	Name   string   `json:"name"` // 名称
	Desc   string   `json:"desc"` // 描述
	Fields []*Field `json:"fields"`
}
type Route struct {
	Method string   `json:"method"` // post, get[, put, patch, delete, head, options]
	Url    string   `json:"url"`    // /api/sys/ts/
	Mode   string   `json:"mode"`   // api
	Desc   string   `json:"desc"`   // 描述
	Req    *Message `json:"req"`
	Rsp    *Message `json:"rsp"`
}
type Docs struct {
	App string `json:"app"`
	/** api 路由信息 */
	Routes []Route `json:"routes"`
	/** 数组 */
	Enums map[string]*Enum `json:"enums"`
	/** 消息类型 */
	Messages map[string]*Message `json:"messages"`
}

// 解析结果
type ParseResult struct {
	Interfaces string
	Beans      string
}

func (doc *Docs) getKtType(meta *Field) string {
	switch meta.Type {
	case "integer":
		return "Int"
	case "decimal":
		return "Float"
	case "float":
		return "Float"
	case "long":
		return "Long"
	case "stringenum":
	case "string":
		return "String"
	case "boolean":
		return "Boolean"
	case "message":
		if meta.Ref != "" {
			return doc.fmtName(meta.Ref)
		}
		return doc.fmtName(meta.Ref)
	case "enum":
		return "Int"
	case "file":
		return "Any?"
	case "any":
		return "Any?"
	}
	return "Any?"
}
func (doc *Docs) getKtLabel(meta *Field) string {
	switch meta.Label {
	case "required":
		return doc.getKtType(meta)
	case "optional":
		return doc.getKtType(meta) + "?"
	case "repeated":
		return "List<" + doc.getKtType(meta) + ">?"
	case "mapping":
		return "Map<String, " + doc.getKtType(meta) + ">?"
	}
	return doc.getKtType(meta)
}
func getKtDefault(ntype string) string {
	switch ntype {
	case "integer":
		return "0"
	case "decimal":
		return "0f"
	case "float":
		return "0f"
	case "long":
		return "0"
	case "string":
		return "\"\""
	case "boolean":
		return "false"
	case "message":
		return "null"
	case "enum":
		return "0"
	case "file":
		return "null"
	case "any":
		return "null"
	}
	return "null"
}
func (doc *Docs) fmtName(name string) string {
	// 从 Hrhelper.StringEnum 返回 StringEnum
	if strings.Contains(name, ".") {
		return name[strings.Index(name, ".")+1:]
	}
	return name
}

// toCamelCase 函数用于将下划线分隔的字符串转换为驼峰命名法
func (doc *Docs) toCamelCase(s string) string {
	// 按下划线分割字符串
	parts := strings.Split(s, "_")
	var result strings.Builder

	for i, part := range parts {
		if i == 0 {
			// 第一个部分保持原样
			result.WriteString(part)
		} else {
			// 后续部分首字母大写
			for j, r := range part {
				if j == 0 {
					result.WriteRune(unicode.ToUpper(r))
				} else {
					result.WriteRune(r)
				}
			}
		}
	}

	return result.String()
}

func (doc *Docs) buildBean(name string, item *Message) string {
	var lines strings.Builder
	lines.WriteString("@Serializable\n")
	lines.WriteString("data class " + doc.fmtName(item.Name) + "(\n")

	for _, field := range item.Fields {
		if field.Desc != "" {
			lines.WriteString("\n\t/** " + field.Desc + " */\n")
		}
		lines.WriteString("\t@SerialName(\"" + field.Name + "\")\n")
		lines.WriteString("\tval " + doc.toCamelCase(field.Name) + ": " + doc.getKtLabel(field) + " = " + getKtDefault(field.Type) + ",\n")
	}
	lines.WriteString(") {}\n")
	return lines.String()
}

func (doc *Docs) buildApi(route Route) string {
	var lines strings.Builder
	lines.WriteString("\t/**\n")
	if route.Desc != "" {
		lines.WriteString("\t * " + route.Desc + "\n\t *\n")
	}
	req := route.Req

	if len(req.Fields) > 0 {
		lines.WriteString("\t * 请求参数:\n")
		for _, value := range req.Fields {
			lines.WriteString("\t * @param " + doc.toCamelCase(value.Name) + " " + value.Desc + "\n")
		}
	}
	lines.WriteString("\t */\n")
	lines.WriteString("\t@FormUrlEncoded\n")
	lines.WriteString(fmt.Sprintf("\t@%v(\"%v\")\n", strings.ToUpper(route.Method), route.Url))
	rspRef := route.Rsp
	if len(req.Fields) > 0 {
		lines.WriteString("\tsuspend fun " + doc.fmtMethodName(route.Url, route.Method) + "(\n")
		for _, value := range req.Fields {
			lines.WriteString("\t\t@Field(\"" + value.Name + "\") " + doc.toCamelCase(value.Name) + ": " + doc.getKtLabel(value) + ",\n")
		}
		lines.WriteString("\t): CommRsp<" + doc.fmtName(rspRef.Name) + ">\n")
	} else {
		lines.WriteString("\tsuspend fun " + doc.fmtMethodName(route.Url, route.Method) + "(): CommRsp<" + doc.fmtName(rspRef.Name) + ">\n")
	}

	return lines.String()
}

// 将 /api/user/info/, post 转换为 postUserInfo
func (doc *Docs) fmtMethodName(path, method string) string {
	method = strings.ToLower(method)
	tmps := []string{method}

	segs := strings.Split(path, "/")
	if len(segs) > 2 {
		// 多余两个的取最后两个
		segs = segs[len(segs)-3:]
	}
	tmps = append(tmps, segs...)
	return doc.toCamelCase(strings.Join(tmps, "_"))
}

// 解析 protocol 文档
// docs 为 protocol.json 文件内容
func Parse(appName string, docs []byte) (*ParseResult, error) {
	info := &Docs{}
	err := json.Unmarshal(docs, info)
	if err != nil {
		return nil, fmt.Errorf("failed to unmarshal protocol docs: %w", err)
	}
	var interfaces strings.Builder
	interfaces.WriteString("\nimport kotlinx.serialization.SerialName\n")
	interfaces.WriteString("interface " + appName + "{\n")

	for _, route := range info.Routes {
		interfaces.WriteString(info.buildApi(route))
	}
	interfaces.WriteString("}\n")
	result := &ParseResult{
		Interfaces: interfaces.String(),
	}

	var beans strings.Builder
	beans.WriteString("\nimport kotlinx.serialization.SerialName\n")
	beans.WriteString("import kotlinx.serialization.Serializable\n\n")
	for name, msg := range info.Messages {
		beans.WriteString(info.buildBean(name, msg))
	}
	result.Beans = beans.String()
	return result, nil

}

例子调用

调用例子如下:

func ShowKtProtocol() {
    docPath := "you protocol.json path"
    docs, err := os.ReadFile(docPath)
    if err != nil {
            niuhe.LogInfo("read docs error: %v", err)
            return
    }
    result, err := protocol.Parse("HrHelper", docs)
    if err != nil {
            niuhe.LogInfo("%v", err)
            return
    }
    // 保存到对应的文件
    niuhe.LogInfo("%v", result.Interfaces)
    niuhe.LogInfo("%v", result.Beans)

}

最佳实践

可结合 niuhe/.config.json5 中的配置 endcmd:[] 在生成代码后执行某个命令将上述解析的结果保存到对应位置获得更佳体验