From 3ba81c249036d309c5ff15f8973807fa2050adea Mon Sep 17 00:00:00 2001 From: Jacky Date: Wed, 8 Jan 2025 11:25:05 +0800 Subject: [PATCH] feat(errdocs): add documentation and code generation tool for error handling - Introduced a new Go script to generate documentation and error definitions in Markdown, TypeScript, and JavaScript formats. - Updated .gitignore to include new output directories for generated files. - Added a new documentation page detailing usage instructions for the code generation tool. - Enhanced the VitePress configuration to link to the new documentation page. --- .gitignore | 5 +- cmd/errdocs/generate.go | 308 ++++++++++++++++++++++++++++ docs/.vitepress/config.mts | 1 + docs/error-handler/docs-code-gen.md | 65 ++++++ 4 files changed, 378 insertions(+), 1 deletion(-) create mode 100644 cmd/errdocs/generate.go create mode 100644 docs/error-handler/docs-code-gen.md diff --git a/.gitignore b/.gitignore index 11765fa..fac54e0 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,7 @@ app.testing.ini docs/.vitepress/cache docs/.vitepress/dist node_modules -coverage.out \ No newline at end of file +coverage.out +err-docs-dist +err-ts-dist +err-js-dist diff --git a/cmd/errdocs/generate.go b/cmd/errdocs/generate.go new file mode 100644 index 0000000..9e82038 --- /dev/null +++ b/cmd/errdocs/generate.go @@ -0,0 +1,308 @@ +package main + +import ( + "bufio" + "fmt" + "log" + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/spf13/cast" + "github.com/uozi-tech/cosy" +) + +var ( + // Matches patterns like: e = cosy.NewErrorScope("user") and captures: + // group(1): variable name (e.g. e) + // group(2): scope name (e.g. user) + reScope = regexp.MustCompile(`(\w+)\s*=\s*cosy\.NewErrorScope\s*\(\s*"([^"]+)"\s*\)`) + + // Updated regex templates to support negative error codes + // Example: -1, -100, etc. + newTemplate = `%s\s*\.\s*New\s*\(\s*(-?[0-9]+)\s*,\s*"([^"]*)"\s*\)` + newWithParamsTemplate = `%s\s*\.\s*NewWithParams\s*\(\s*(-?[0-9]+)\s*,\s*"([^"]*)"\s*,` +) + +// parseResult stores mapping of scopeName -> []ErrorInfo for a file +type parseResult map[string][]cosy.Error + +func main() { + if len(os.Args) < 3 { + log.Println("Usage: go run cmd/errdocs/generate.go [wrapper] [trailing_comma]") + log.Println("Example: go run cmd/errdocs/generate.go ./project ts ./dist $gettext true") + return + } + + rootPath := os.Args[1] + docType := strings.ToLower(strings.TrimSpace(os.Args[2])) + + // Validate doc type + switch docType { + case "md", "ts", "js": + // valid type + default: + log.Fatalf("Invalid type: %s. Must be one of: md, ts, js", docType) + } + + // Output directory is required + if len(os.Args) < 4 { + log.Fatalf("Output directory is required") + } + outDir := os.Args[3] + + // Set wrapper function and trailing comma + wrapper := "$gettext" + trailingComma := true // default to true + if len(os.Args) > 4 { + wrapper = os.Args[4] + } + if len(os.Args) > 5 { + trailingComma = strings.ToLower(os.Args[5]) != "false" + } + + globalScopeMap := make(map[string][]cosy.Error) + + // find all .go files + err := filepath.Walk(rootPath, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + return nil + } + if filepath.Ext(path) == ".go" { + res, parseErr := parseGoFile(path) + if parseErr != nil { + log.Printf("[Error] Parse file %s error: %v\n", path, parseErr) + } + for scopeName, errArr := range res { + globalScopeMap[scopeName] = append(globalScopeMap[scopeName], errArr...) + } + } + return nil + }) + if err != nil { + log.Fatalf("[Error] Walk folder error: %v\n", err) + } + + // If no scope information is found, prompt and exit + if len(globalScopeMap) == 0 { + log.Println("No cosy.NewErrorScope(...) definitions or related error information found.") + return + } + + // Create output directory + if err := os.MkdirAll(outDir, 0755); err != nil { + log.Fatalf("[Error] Create output directory %s failed: %v\n", outDir, err) + } + + // Generate documents for each scope + for scope, errInfos := range globalScopeMap { + var outFile string + var writeErr error + + switch docType { + case "md": + outFile = filepath.Join(outDir, fmt.Sprintf("%s.md", strings.ToLower(strings.ReplaceAll(scope, " ", "_")))) + writeErr = generateMarkdown(scope, errInfos, outFile) + case "ts": + outFile = filepath.Join(outDir, fmt.Sprintf("%s.ts", strings.ToLower(strings.ReplaceAll(scope, " ", "_")))) + writeErr = generateTypeScript(errInfos, outFile, wrapper, trailingComma) + case "js": + outFile = filepath.Join(outDir, fmt.Sprintf("%s.js", strings.ToLower(strings.ReplaceAll(scope, " ", "_")))) + writeErr = generateJavaScript(errInfos, outFile, wrapper, trailingComma) + } + + if writeErr != nil { + log.Printf("[Error] Write to %s error: %v\n", outFile, writeErr) + } else { + log.Printf("[Generated] %s %s\n", docType, outFile) + } + } +} + +// parseGoFile parses a single .go file and returns a mapping of scopeName -> []ErrorInfo +func parseGoFile(filePath string) (parseResult, error) { + file, err := os.Open(filePath) + if err != nil { + return nil, err + } + defer file.Close() + + scopeVarMap := make(map[string]string) // varName -> scopeName + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + matches := reScope.FindStringSubmatch(line) + if len(matches) == 3 { + varName := matches[1] + scopeName := matches[2] + scopeVarMap[varName] = scopeName + } + } + + if len(scopeVarMap) == 0 { + return parseResult{}, nil + } + if _, err := file.Seek(0, 0); err != nil { + return nil, err + } + + // scopeName -> error array + res := make(parseResult) + for _, v := range scopeVarMap { + res[v] = []cosy.Error{} + } + + scanner = bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + + for varName, scopeName := range scopeVarMap { + reNew := regexp.MustCompile(fmt.Sprintf(newTemplate, regexp.QuoteMeta(varName))) + reNewWithParams := regexp.MustCompile(fmt.Sprintf(newWithParamsTemplate, regexp.QuoteMeta(varName))) + + // match .New(...) + matchesNew := reNew.FindAllStringSubmatch(line, -1) + for _, m := range matchesNew { + if len(m) == 3 { + codeStr := m[1] + msg := m[2] + res[scopeName] = append(res[scopeName], cosy.Error{Code: cast.ToInt32(codeStr), Message: msg}) + } + } + + // match .NewWithParams(...) + matchesNWP := reNewWithParams.FindAllStringSubmatch(line, -1) + for _, m := range matchesNWP { + if len(m) == 3 { + codeStr := m[1] + msg := m[2] + res[scopeName] = append(res[scopeName], cosy.Error{Code: cast.ToInt32(codeStr), Message: msg}) + } + } + } + } + + return res, scanner.Err() +} + +// capitalizeFirst capitalizes the first letter of a string +func capitalizeFirst(s string) string { + if s == "" { + return s + } + return strings.ToUpper(s[:1]) + s[1:] +} + +// generateMarkdown generates Markdown error code documentation for a single scopeName +func generateMarkdown(scopeName string, errInfos []cosy.Error, outPath string) error { + f, err := os.Create(outPath) + if err != nil { + return err + } + defer f.Close() + + // Write scope title + title := fmt.Sprintf("# %s\n\n", scopeName) + _, _ = f.WriteString(title) + + // Write table header + _, _ = f.WriteString("| Error Code | Error Message |\n") + _, _ = f.WriteString("| --- | --- |\n") + + // Write each error message + for _, e := range errInfos { + line := fmt.Sprintf("| %d | %s |\n", e.Code, capitalizeFirst(e.Message)) + _, _ = f.WriteString(line) + } + + return nil +} + +// escapeString escapes special characters in a string for JavaScript/TypeScript +func escapeString(s string) string { + s = strings.ReplaceAll(s, "'", "\\'") + s = strings.ReplaceAll(s, "\"", "\\\"") + return s +} + +// generateTypeScript generates TypeScript error code documentation +func generateTypeScript(errInfos []cosy.Error, outPath string, wrapper string, trailingComma bool) error { + f, err := os.Create(outPath) + if err != nil { + return err + } + defer f.Close() + + // Write export default opening + _, _ = f.WriteString("export default {\n") + + // Write each error message + for i, e := range errInfos { + var codeStr string + if e.Code < 0 { + codeStr = fmt.Sprintf("'%d'", e.Code) + } else { + codeStr = fmt.Sprintf("%d", e.Code) + } + + line := fmt.Sprintf(" %s: () => %s('%s')", + codeStr, + wrapper, + escapeString(capitalizeFirst(e.Message))) + + // Add comma if it's not the last item or if trailingComma is true + if i < len(errInfos)-1 || trailingComma { + line += "," + } + line += "\n" + _, _ = f.WriteString(line) + } + + // Write closing brace + _, _ = f.WriteString("}\n") + + return nil +} + +// generateJavaScript generates JavaScript error code documentation +func generateJavaScript(errInfos []cosy.Error, outPath string, wrapper string, trailingComma bool) error { + f, err := os.Create(outPath) + if err != nil { + return err + } + defer f.Close() + + // Write export default opening + _, _ = f.WriteString("module.exports = {\n") + + // Write each error message + for i, e := range errInfos { + var codeStr string + if e.Code < 0 { + codeStr = fmt.Sprintf("'%d'", e.Code) + } else { + codeStr = fmt.Sprintf("%d", e.Code) + } + + line := fmt.Sprintf(" %s: () => %s('%s')", + codeStr, + wrapper, + escapeString(capitalizeFirst(e.Message))) + + // Add comma if it's not the last item or if trailingComma is true + if i < len(errInfos)-1 || trailingComma { + line += "," + } + line += "\n" + _, _ = f.WriteString(line) + } + + // Write closing brace + _, _ = f.WriteString("}\n") + + return nil +} diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index c8c4365..d5573e6 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -93,6 +93,7 @@ export default defineConfig(({mode}) => { text: '错误处理', items: [ {text: '接口参考', link: '/error-handler'}, + {text: '文档和代码生成', link: '/error-handler/docs-code-gen'}, ] }, { diff --git a/docs/error-handler/docs-code-gen.md b/docs/error-handler/docs-code-gen.md new file mode 100644 index 0000000..3c60ce2 --- /dev/null +++ b/docs/error-handler/docs-code-gen.md @@ -0,0 +1,65 @@ +# 文档和代码生成 + +## 使用方法 + +```bash +go run cmd/errdocs/generate.go [wrapper] [trailing_comma] +``` + +| 参数 | 说明 | 是否必填 | 默认值 | +| --- | --- | --- | --- | +| project folder path | 项目根目录路径 | 是 | - | +| type | 生成文件类型:`md`/`ts`/`js` | 是 | - | +| output dir | 输出目录 | 是 | - | +| wrapper | 错误信息包装函数 | 否 | `$gettext` | +| trailing_comma | 是否在最后一项添加逗号 | 否 | `true` | + +示例: +```bash +# 生成 Markdown 文档 +go run cmd/errdocs/generate.go ./project md ./docs + +# 生成 TypeScript 错误定义,使用自定义 wrapper +go run cmd/errdocs/generate.go ./project ts ./src/errors t + +# 生成 JavaScript 错误定义,不使用末尾逗号 +go run cmd/errdocs/generate.go ./project js ./dist $gettext false +``` + +## 生成的文件示例 + +### Markdown 文档 +```markdown +# auth + +| Error Code | Error Message | +| --- | --- | +| 4031 | Token is empty | +| 4032 | Token convert to claims failed | +| -4033 | JWT expired | +``` + +### TypeScript 错误定义 +```ts +export default { + 4031: () => $gettext('Token is empty'), + 4032: () => $gettext('Token convert to claims failed'), + '-4033': () => $gettext('JWT expired'), +} +``` + +### JavaScript 错误定义 +```js +module.exports = { + 4031: () => $gettext('Token is empty'), + 4032: () => $gettext('Token convert to claims failed'), + '-4033': () => $gettext('JWT expired'), +} +``` + +注意: +1. 错误信息的首字母会自动转换为大写 +2. 负数错误码在 TypeScript 和 JavaScript 中会使用字符串形式 +3. 包含引号的错误信息会自动进行转义处理 +4. 默认会在最后一项添加逗号,可以通过参数禁用 +