-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(errdocs): add documentation and code generation tool for error h…
…andling - 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.
- Loading branch information
Showing
4 changed files
with
378 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 <project folder path> <type: md|ts|js> <output dir> [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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
# 文档和代码生成 | ||
|
||
## 使用方法 | ||
|
||
```bash | ||
go run cmd/errdocs/generate.go <project folder path> <type: md|ts|js> <output dir> [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. 默认会在最后一项添加逗号,可以通过参数禁用 | ||
|