Skip to content

Commit

Permalink
api: autogenerate swagger Router tags
Browse files Browse the repository at this point in the history
this uses go:generate magic, and is included in CI as well
to ensure it's never out-of-sync
  • Loading branch information
altergui committed Mar 9, 2023
1 parent e5f82f9 commit eb9f6c8
Show file tree
Hide file tree
Showing 4 changed files with 251 additions and 5 deletions.
18 changes: 17 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions api/api.go
Original file line number Diff line number Diff line change
@@ -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/[email protected] fmt

import (
Expand Down
233 changes: 233 additions & 0 deletions api/autoswag/main.go
Original file line number Diff line number Diff line change
@@ -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()
}
4 changes: 0 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down

0 comments on commit eb9f6c8

Please sign in to comment.