diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1500ae65b..a670c4854 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -25,12 +25,28 @@ jobs: go-version: '1.19.5' cache: true - name: Tidy go cache - run: go mod tidy + run: | + go mod tidy + if [[ $(git status --porcelain) ]]; then + git diff + echo + echo "go mod tidy made these changes, please run 'go mod tidy' and include those changes in a commit" + exit 1 + fi - name: Run gofmt # Run gofmt first, as it's quick and issues are common. run: diff -u <(echo -n) <(gofmt -s -d .) - name: Run go vet run: go vet ./... + - name: Run go generate + run: | + go generate ./... + if [[ $(git status --porcelain) ]]; then + git diff + echo + echo "go generate made these changes, please run 'go generate ./...' and include those changes in a commit" + exit 1 + fi - name: Download staticcheck # staticcheck provides a github action, use it (https://staticcheck.io/docs/running-staticcheck/ci/github-actions/) # or use golangci-lint (github action) with staticcheck as enabled linter diff --git a/api/api.go b/api/api.go index 60415579b..74e5e44fd 100644 --- a/api/api.go +++ b/api/api.go @@ -1,5 +1,6 @@ package api +//go:generate go run go.vocdoni.io/dvote/api/autoswag //go:generate go run github.com/swaggo/swag/cmd/swag@v1.8.10 fmt import ( diff --git a/api/autoswag/main.go b/api/autoswag/main.go new file mode 100644 index 000000000..67b4d8223 --- /dev/null +++ b/api/autoswag/main.go @@ -0,0 +1,233 @@ +// This code is tailor-made for api package and very brittle, +// can't do much about it due to the very nature of it. +// It will find func names that start with "enable" (hardcoded) +// then inside look for RegisterMethod calls (based on func signature) +// parse the URL, method and handler func of those RegisterMethod +// and then look for those handler funcs (in the same file) +// to replace (or add) @Router tags in the handler func doc comment + +package main + +import ( + "bytes" + "fmt" + "go/ast" + "go/format" + "go/parser" + "go/token" + "io" + "log" + "os" + "strings" +) + +type PathMethod struct { + path, method string +} + +func main() { + if len(os.Args) > 1 { + err := ParseFile(os.Args[1]) + if err != nil { + fmt.Println(err) + } + return + } + + // else parse the whole directory + log.SetOutput(io.Discard) // without debug logs + cwd, err := os.Getwd() + if err != nil { + panic(err) + } + files, err := os.ReadDir(cwd) + if err != nil { + panic(err) + } + for _, f := range files { + if !f.IsDir() { + err := ParseFile(f.Name()) + if err != nil { + fmt.Println(err) + } + } + } +} + +// ParseFile rewrites (in place) the given file +func ParseFile(file string) error { + // Parse the Go file + fset := token.NewFileSet() + parsedFile, err := parser.ParseFile(fset, file, nil, parser.ParseComments) + if err != nil { + return err + } + fmap := ParseRegisterMethodCalls(parsedFile) + UpdateRouterTags(fset, parsedFile, fmap) + + // Print the updated file + var buf bytes.Buffer + if err := format.Node(&buf, fset, parsedFile); err != nil { + panic(err) + } + + fd, err := os.Create(file) + if err != nil { + panic(err) + } + if _, err := fd.Write(buf.Bytes()); err != nil { + panic(err) + } + return nil +} + +// ParseRegisterMethodCalls returns a map of func names +// with the URL and method that are registered as handlers for +func ParseRegisterMethodCalls(parsedFile *ast.File) map[string][]PathMethod { + fmap := make(map[string][]PathMethod) + // Find the `RegisterMethod` calls in the file + for _, decl := range parsedFile.Decls { + fn, ok := decl.(*ast.FuncDecl) + if !ok { + continue + } + + if !strings.HasPrefix(fn.Name.Name, "enable") { + continue + } + + // Extract the path and method from the `RegisterMethod` call + path := "" + method := "" + fname := "" + for _, stmt := range fn.Body.List { + ifstmt, ok := stmt.(*ast.IfStmt) + if !ok { + continue + } + assign, ok := ifstmt.Init.(*ast.AssignStmt) + if !ok { + continue + } + call, ok := assign.Rhs[0].(*ast.CallExpr) + if !ok { + continue + } + if len(call.Args) != 4 { + continue + } + + url, ok := call.Args[0].(*ast.BasicLit) + if !ok { + continue + } + if url.Kind != token.STRING { + continue + } + path = strings.Trim(url.Value, "\"") + log.Printf("url %s ", url.Value) + + httpmethod, ok := call.Args[1].(*ast.BasicLit) + if !ok { + continue + } + if httpmethod.Kind != token.STRING { + continue + } + method = strings.ToLower(strings.Trim(httpmethod.Value, "\"")) + log.Printf("[%s] ", httpmethod.Value) + + fn, ok := call.Args[3].(*ast.SelectorExpr) + if !ok { + continue + } + log.Printf("-> %s\n", fn.Sel.Name) + fname = fn.Sel.Name + + // Add the method to the list if a path and method were found + if path != "" && method != "" && fname != "" { + fmap[fname] = append(fmap[fname], PathMethod{path: path, method: method}) + } + } + } + return fmap +} + +// UpdateRouterTags updates the @Router tags of the funcs passed in fmap +func UpdateRouterTags(fset *token.FileSet, parsedFile *ast.File, fmap map[string][]PathMethod) { + cmap := ast.NewCommentMap(fset, parsedFile, parsedFile.Comments) + + ast.Inspect(parsedFile, func(n ast.Node) bool { + switch x := n.(type) { + case *ast.FuncDecl: + if pms, found := fmap[x.Name.Name]; found { + list := []*ast.Comment{} + for _, line := range cmap[x][0].List { + if strings.Contains(line.Text, "@Router") { + continue + } + line.Slash = token.Pos(int(x.Pos()) - 1) + list = append(list, line) + } + + for _, pm := range pms { + line := &ast.Comment{ + Text: fmt.Sprintf(`// @Router %s [%s]`, pm.path, pm.method), + Slash: token.Pos(int(x.Pos() - 1)), + } + list = append(list, line) + } + + cmap[x] = []*ast.CommentGroup{ + { + List: list, + }, + } + } + } + return true + }) + parsedFile.Comments = cmap.Filter(parsedFile).Comments() +} + +// InitComments replaces the whole func doc with a hardcoded template (this is currently unused) +func InitComments(fset *token.FileSet, parsedFile *ast.File, fmap map[string][]PathMethod) { + cmap := ast.NewCommentMap(fset, parsedFile, parsedFile.Comments) + + ast.Inspect(parsedFile, func(n ast.Node) bool { + switch x := n.(type) { + case *ast.FuncDecl: + if _, found := fmap[x.Name.Name]; found { + list := []*ast.Comment{} + list = append(list, &ast.Comment{ + Text: fmt.Sprintf("// %s", x.Name.Name), + Slash: token.Pos(int(x.Pos() - 1)), + }) + list = append(list, &ast.Comment{ + Text: "//", + Slash: token.Pos(int(x.Pos() - 1)), + }) + list = append(list, &ast.Comment{ + Text: "// @Summary TODO", + Slash: token.Pos(int(x.Pos() - 1)), + }) + list = append(list, &ast.Comment{ + Text: "// @Description TODO", + Slash: token.Pos(int(x.Pos() - 1)), + }) + list = append(list, &ast.Comment{ + Text: "// @Success 200 {object} object", + Slash: token.Pos(int(x.Pos() - 1)), + }) + + cmap[x] = []*ast.CommentGroup{ + { + List: list, + }, + } + } + } + return true + }) + parsedFile.Comments = cmap.Filter(parsedFile).Comments() +} diff --git a/go.sum b/go.sum index 810b2f5a6..ea6003c0e 100644 --- a/go.sum +++ b/go.sum @@ -2705,10 +2705,6 @@ go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8= go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= -go.vocdoni.io/proto v1.14.0 h1:nNLrmifiMU7af97yt6sWFamBEL8dlodKE9lSmNTfFK4= -go.vocdoni.io/proto v1.14.0/go.mod h1:oi/WtiBFJ6QwNDv2aUQYwOnUKzYuS/fBqXF8xDNwcGo= -go.vocdoni.io/proto v1.14.1-0.20230228232005-64de430e285c h1:ruOPQ4wJFVBoaO0h8ZeBWR8J3vMNqfribyuETCXBf+E= -go.vocdoni.io/proto v1.14.1-0.20230228232005-64de430e285c/go.mod h1:oi/WtiBFJ6QwNDv2aUQYwOnUKzYuS/fBqXF8xDNwcGo= go.vocdoni.io/proto v1.14.1-0.20230228233108-b62c64058ac7 h1:nO306nPS/ZFxlSRJmMXL1V8GCWOQILATNy/jNooIJOc= go.vocdoni.io/proto v1.14.1-0.20230228233108-b62c64058ac7/go.mod h1:oi/WtiBFJ6QwNDv2aUQYwOnUKzYuS/fBqXF8xDNwcGo= go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=