forked from Finschia/finschia-sdk
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
docs: create ERRORS.md for x/module (Finschia#1059)
* chore: create an automatic error document generation module Signed-off-by: 170210 <[email protected]> * docs: generate ERRORS.md for x/module Signed-off-by: 170210 <[email protected]> * chore: add a ci to check generated error docs up-to-date Signed-off-by: 170210 <[email protected]> * chore: update changelog Signed-off-by: 170210 <[email protected]> * refactor: fix for lint Signed-off-by: 170210 <[email protected]> * style: format yml file Signed-off-by: 170210 <[email protected]> * fixup: fix for review Signed-off-by: 170210 <[email protected]> * fixup: fix for review Signed-off-by: 170210 <[email protected]> * fixup: refactor by comment Signed-off-by: 170210 <[email protected]> * fixup: fix for comment Signed-off-by: 170210 <[email protected]> * fixup: fix for comment Signed-off-by: 170210 <[email protected]> --------- Signed-off-by: 170210 <[email protected]>
- Loading branch information
1 parent
0dd215b
commit 75b473f
Showing
11 changed files
with
699 additions
and
2 deletions.
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
# Verify that generated code is up-to-date. | ||
|
||
name: Check generated code | ||
on: | ||
workflow_dispatch: | ||
pull_request: | ||
branches: | ||
- '*' | ||
|
||
permissions: | ||
contents: read | ||
|
||
jobs: | ||
check-error-doc: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- name: Setup Golang | ||
uses: actions/setup-go@v4 | ||
with: | ||
go-version: '1.20' | ||
|
||
- name: Checkout | ||
uses: actions/checkout@v3 | ||
with: | ||
fetch-depth: 1 | ||
|
||
- name: Check generated error docs | ||
run: | | ||
make error-doc-gen | ||
if ! git diff --stat --exit-code ; then | ||
echo ">> ERROR:" | ||
echo ">>" | ||
echo ">> Error documents require update (source files in x folder may have changed)." | ||
echo ">> Ensure your tools are up-to-date, re-run 'make error-doc' and update this PR." | ||
echo ">>" | ||
exit 1 | ||
fi |
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
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,175 @@ | ||
package generator | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"os" | ||
"path/filepath" | ||
"sort" | ||
"strings" | ||
|
||
"golang.org/x/text/cases" | ||
"golang.org/x/text/language" | ||
) | ||
|
||
type ErrorDocumentGenerator struct { | ||
targetPath string | ||
errorsFiles []string | ||
modules []string | ||
errorDocument map[string][]*moduleInfo | ||
} | ||
|
||
type moduleInfo struct { | ||
filepath string | ||
codespace string | ||
constDict map[string]string | ||
errorDict []errorInfo | ||
} | ||
|
||
type sortByCodespace []*moduleInfo | ||
|
||
func (b sortByCodespace) Len() int { return len(b) } | ||
func (b sortByCodespace) Less(i, j int) bool { return b[i].codespace < b[j].codespace } | ||
func (b sortByCodespace) Swap(i, j int) { b[i], b[j] = b[j], b[i] } | ||
|
||
func NewErrorDocumentGenerator(p string) *ErrorDocumentGenerator { | ||
return &ErrorDocumentGenerator{ | ||
targetPath: p, | ||
errorDocument: make(map[string][]*moduleInfo), | ||
} | ||
} | ||
|
||
func (edg *ErrorDocumentGenerator) listUpErrorsGoFiles(startPath, errorsFileName string) error { | ||
err := filepath.Walk(startPath, func(path string, info os.FileInfo, err error) error { | ||
if err != nil { | ||
return err | ||
} | ||
if !info.IsDir() && info.Name() == errorsFileName { | ||
edg.errorsFiles = append(edg.errorsFiles, path) | ||
} | ||
return nil | ||
}) | ||
if err != nil { | ||
return err | ||
} | ||
return nil | ||
} | ||
|
||
func (edg *ErrorDocumentGenerator) extractModuleName() error { | ||
for _, filepath := range edg.errorsFiles { | ||
var moduleName string | ||
startIndex := strings.Index(filepath, "/x/") + len("/x/") | ||
endIndex := strings.Index(filepath[startIndex:], "/") | ||
if startIndex != -1 && endIndex != -1 { | ||
moduleName = filepath[startIndex : startIndex+endIndex] | ||
} | ||
if moduleName == "" { | ||
return errors.New("failed to get module name for " + filepath) | ||
} | ||
edg.errorDocument[moduleName] = append(edg.errorDocument[moduleName], &moduleInfo{ | ||
filepath: filepath, | ||
codespace: "", | ||
constDict: make(map[string]string), | ||
errorDict: []errorInfo{}, | ||
}) | ||
} | ||
// sort by key and codespace | ||
for moduleName := range edg.errorDocument { | ||
edg.modules = append(edg.modules, moduleName) | ||
sort.Sort(sortByCodespace(edg.errorDocument[moduleName])) | ||
} | ||
sort.Strings(edg.modules) | ||
return nil | ||
} | ||
|
||
func (edg ErrorDocumentGenerator) outputCategory(file *os.File) { | ||
file.WriteString("<!-- TOC -->\n") | ||
file.WriteString("# Category\n") | ||
columnTemplate := " * [%s](#%s)\n" | ||
for _, moduleName := range edg.modules { | ||
file.WriteString(fmt.Sprintf(columnTemplate, cases.Title(language.Und).String(moduleName), moduleName)) | ||
} | ||
file.WriteString("<!-- TOC -->\n") | ||
} | ||
|
||
func (edg *ErrorDocumentGenerator) generateContent() error { | ||
// generate errors in each module | ||
for _, moduleName := range edg.modules { | ||
mods := edg.errorDocument[moduleName] | ||
for _, mod := range mods { | ||
if err := mod.parseErrorsFile(); err != nil { | ||
return err | ||
} | ||
if err := mod.parseKeysFile(); err != nil { | ||
return err | ||
} | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
func (edg ErrorDocumentGenerator) outputContent(file *os.File) error { | ||
extraInfoTemplate := " * [%s](%s)\n" | ||
for _, moduleName := range edg.modules { | ||
// module name | ||
file.WriteString("\n") | ||
file.WriteString("## " + cases.Title(language.Und).String(moduleName) + "\n") | ||
// table header | ||
file.WriteString("\n") | ||
file.WriteString("|Error Name|Codespace|Code|Description|\n") | ||
file.WriteString("|:-|:-|:-|:-|\n") | ||
// table contents | ||
mods := edg.errorDocument[moduleName] | ||
for _, mod := range mods { | ||
for _, errInfo := range mod.errorDict { | ||
// assign value to field "codespace" | ||
if s, err := errInfo.toString(mod.codespace); err != nil { | ||
return err | ||
} else { | ||
file.WriteString(s) | ||
} | ||
} | ||
} | ||
// extract infomation | ||
file.WriteString("\n>You can also find detailed information in the following Errors.go files:\n") | ||
for _, mod := range mods { | ||
relPath, err := filepath.Rel(edg.targetPath, mod.filepath) | ||
if err != nil { | ||
return err | ||
} | ||
file.WriteString(fmt.Sprintf(extraInfoTemplate, relPath, relPath)) | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
func (edg ErrorDocumentGenerator) AutoGenerate() error { | ||
// get all errors.go in x folder | ||
errorsFileName := "errors.go" | ||
err := edg.listUpErrorsGoFiles(edg.targetPath, errorsFileName) | ||
if len(edg.errorsFiles) == 0 || err != nil { | ||
return errors.New("not find target files in x folder") | ||
} | ||
// get each module name and bind it to paths (one module may have multiple errors.go) | ||
if err := edg.extractModuleName(); err != nil { | ||
return err | ||
} | ||
// generate content | ||
if err := edg.generateContent(); err != nil { | ||
return err | ||
} | ||
// prepare the file for writing | ||
filepath := edg.targetPath + "/ERRORS.md" | ||
file, err := os.Create(filepath) | ||
if err != nil { | ||
return err | ||
} | ||
defer file.Close() | ||
// output category | ||
edg.outputCategory(file) | ||
// output content | ||
if err := edg.outputContent(file); err != nil { | ||
return err | ||
} | ||
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
package generator | ||
|
||
import ( | ||
"bufio" | ||
"errors" | ||
"fmt" | ||
"os" | ||
"regexp" | ||
"strings" | ||
) | ||
|
||
type errorInfo struct { | ||
errorName string | ||
codespace string | ||
code string | ||
description string | ||
} | ||
|
||
func (ei errorInfo) toString(cs string) (string, error) { | ||
errorInfoTemplate := "|%s|%s|%s|%s|\n" | ||
if ei.codespace == "ModuleName" { | ||
if cs == "" { | ||
return "", errors.New("failed to find moduleName") | ||
} | ||
ei.codespace = cs | ||
} | ||
return fmt.Sprintf(errorInfoTemplate, ei.errorName, ei.codespace, ei.code, ei.description), nil | ||
} | ||
|
||
func (ei *errorInfo) getError(line string, constDict map[string]string) error { | ||
parts := strings.SplitN(line, "=", 2) | ||
ei.errorName = strings.TrimSpace(parts[0]) | ||
errBody := strings.TrimSpace(parts[1]) | ||
// error info is like as sdkerrors.Register(...) | ||
pattern := regexp.MustCompile(`sdkerrors\.Register\((.*)\)`) | ||
match := pattern.FindStringSubmatch(errBody) | ||
if len(match) == 2 { | ||
parts := strings.SplitN(match[1], ",", 3) | ||
if len(parts) == 3 { | ||
ei.codespace = strings.TrimSpace(parts[0]) | ||
ei.code = strings.TrimSpace(parts[1]) | ||
ei.description = strings.Trim(strings.TrimSpace(parts[2]), `"`) | ||
if constValue, found := constDict[ei.codespace]; found { | ||
ei.codespace = constValue | ||
} | ||
return nil | ||
} | ||
return errors.New("failed to get error info in: " + line) | ||
} | ||
return errors.New("failed to parse error info in: " + line) | ||
} | ||
|
||
func getConst(line string) (string, string, error) { | ||
line = strings.Replace(line, "const", "", 1) | ||
parts := strings.Split(line, "=") | ||
if len(parts) == 2 { | ||
i := strings.TrimSpace(parts[0]) | ||
val := strings.Trim(strings.TrimSpace(parts[1]), `"`) | ||
return i, val, nil | ||
} | ||
return "", "", errors.New("failed to get the value in: " + line) | ||
} | ||
|
||
func (mi *moduleInfo) parseErrorsFile() error { | ||
// var errorDict []errorInfo | ||
// constDict := make(map[string]string) | ||
file, err := os.Open(mi.filepath) | ||
if err != nil { | ||
return err | ||
} | ||
defer file.Close() | ||
|
||
scanner := bufio.NewScanner(file) | ||
for scanner.Scan() { | ||
line := scanner.Text() | ||
if strings.Contains(line, "=") { | ||
// get const | ||
if !strings.Contains(line, "sdkerrors.Register") { | ||
identifier, value, err := getConst(line) | ||
if err != nil { | ||
return err | ||
} | ||
mi.constDict[identifier] = value | ||
} else { | ||
// get error | ||
var errInfo errorInfo | ||
if err := errInfo.getError(line, mi.constDict); err != nil { | ||
return err | ||
} | ||
mi.errorDict = append(mi.errorDict, errInfo) | ||
} | ||
} | ||
} | ||
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
package generator | ||
|
||
import ( | ||
"bufio" | ||
"errors" | ||
"os" | ||
"strings" | ||
) | ||
|
||
func getCodeSpace(line string) (string, string, error) { | ||
line = strings.Replace(line, "const", "", 1) | ||
parts := strings.Split(line, "=") | ||
if len(parts) == 2 { | ||
i := strings.TrimSpace(parts[0]) | ||
val := strings.Trim(strings.TrimSpace(parts[1]), `"`) | ||
return i, val, nil | ||
} | ||
return "", "", errors.New("failed to get the value in: " + line) | ||
} | ||
|
||
func (mi *moduleInfo) parseKeysFile() error { | ||
// find keys.go or key.go | ||
possibleFileNames := []string{"keys.go", "key.go"} | ||
var keyFilePath string | ||
for _, fileName := range possibleFileNames { | ||
paramPath := strings.Replace(mi.filepath, "errors.go", fileName, 1) | ||
if _, err := os.Stat(paramPath); err == nil { | ||
keyFilePath = paramPath | ||
break | ||
} | ||
} | ||
// if keys.go or key.go is exist | ||
if keyFilePath != "" { | ||
file, err := os.Open(keyFilePath) | ||
if err != nil { | ||
return errors.New(keyFilePath + " cannot be opened") | ||
} | ||
defer file.Close() | ||
scanner := bufio.NewScanner(file) | ||
for scanner.Scan() { | ||
line := scanner.Text() | ||
// get module name | ||
if strings.Contains(line, "ModuleName = ") { | ||
_, val, err := getCodeSpace(line) | ||
if err != nil { | ||
return err | ||
} | ||
mi.codespace = val | ||
} | ||
} | ||
} | ||
|
||
return nil | ||
} |
Oops, something went wrong.