Skip to content

Commit

Permalink
docs: create ERRORS.md for x/module (Finschia#1059)
Browse files Browse the repository at this point in the history
* 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
170210 authored Aug 9, 2023
1 parent 2d437b0 commit ba75f8e
Show file tree
Hide file tree
Showing 11 changed files with 697 additions and 0 deletions.
37 changes: 37 additions & 0 deletions .github/workflows/check-generated.yml
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,4 @@ Ref: https://keepachangelog.com/en/1.0.0/

### Document Updates
* (readme) [\#997](https://github.com/finschia/finschia-sdk/pull/997) fix swagger url
* (docs) [\#1059](https://github.com/Finschia/finschia-sdk/pull/1059) create ERRORS.md for x/module
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,10 @@ libsodium:
fi
.PHONY: libsodium

error-doc-gen:
cd ./tools/error_doc && go run ./
.PHONY: error-doc-gen

###############################################################################
### release ###
###############################################################################
Expand Down
175 changes: 175 additions & 0 deletions tools/error_doc/generator/error_docs_generator.go
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
}
95 changes: 95 additions & 0 deletions tools/error_doc/generator/errors_file_parser.go
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
}
54 changes: 54 additions & 0 deletions tools/error_doc/generator/keys_file_parser.go
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
}
5 changes: 5 additions & 0 deletions tools/error_doc/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module github.com/Finschia/finschia-sdk/tools/error_doc

go 1.20

require golang.org/x/text v0.11.0
2 changes: 2 additions & 0 deletions tools/error_doc/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
Loading

0 comments on commit ba75f8e

Please sign in to comment.