//go:build scripts // This program generates /internal/api/handlers.go. // It can be invoked by running go run -tags scripts scripts/handlers/gen_handlers.go // Supported args: // -print-only package handlers import ( "fmt" "go/ast" "go/parser" "go/token" "os" "path/filepath" "regexp" "sort" "strings" "text/template" "github.com/rs/zerolog/log" "allaboutapps.dev/aw/go-starter/scripts/internal/util" ) // https://blog.carlmjohnson.net/post/2016-11-27-how-to-use-go-generate/ var ( handlersPackage = "/internal/api/handlers" pathProjectRoot = util.GetProjectRootDir() pathHandlersRoot = pathProjectRoot + handlersPackage pathModFile = pathProjectRoot + "/go.mod" pathHandlersFile = pathHandlersRoot + "/handlers.go" // methodPrefixes defines all keywords that we search for in the handlers sub packages // * must be the naming of the route (Capitalized) methodPrefixes = []string{ // https://swagger.io/specification/v2/ fixed fields: GET, PUT, POST, DELETE, OPTIONS, HEAD, PATCH "Get", "Put", "Post", "Delete", "Options", "Head", "Patch", } // Match suffixes like 'Route', 'RouteV1', 'RouteV123' methodSuffixPattern = regexp.MustCompile(`Route(V[0-9]*)?$`) packageTemplate = template.Must(template.New("").Parse(`// Code generated by go run -tags scripts scripts/handlers/gen_handlers.go; DO NOT EDIT. package handlers import ( "allaboutapps.dev/aw/go-starter/internal/api" {{- range .SubPkgs }} "{{ . }}" {{- end }} "github.com/labstack/echo/v4" ) func AttachAllRoutes(s *api.Server) { // attach our routes s.Router.Routes = []*echo.Route{ {{- range .Funcs }} {{ .PackageName }}.{{ .FunctionName }}(s), {{- end }} } } `)) ) type ResolvedFunction struct { FunctionName string PackageName string PackageNameFQDN string } type TempateData struct { SubPkgs []string Funcs []ResolvedFunction } // get all functions in above handler packages // that match * func GenHandlers(printOnly bool) error { funcs := []ResolvedFunction{} baseModuleName, err := util.GetModuleName(pathModFile) if err != nil { log.Fatal().Err(err).Msg("Failed to get module name") } set := token.NewFileSet() err = filepath.Walk(pathHandlersRoot, func(path string, fileInfo os.FileInfo, err error) error { if err != nil { return fmt.Errorf("failed to access path %q: %w", path, err) } // ignore handler file to be generated // ignore directories // ignore non go files // ignore test go files if path == pathHandlersFile || fileInfo.IsDir() || !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "test.go") { return nil } gofile, err := parser.ParseFile(set, path, nil, 0) if err != nil { return fmt.Errorf("failed to parse package: %q: %w", path, err) } fileDir := filepath.Dir(path) packageNameFQDN := strings.Replace(fileDir, pathProjectRoot, baseModuleName, 1) for _, d := range gofile.Decls { if fn, isFn := d.(*ast.FuncDecl); isFn { fnName := fn.Name.String() for _, prefix := range methodPrefixes { if strings.HasPrefix(fnName, prefix) && methodSuffixPattern.MatchString(fnName) { funcs = append(funcs, ResolvedFunction{ FunctionName: fnName, PackageName: gofile.Name.Name, PackageNameFQDN: packageNameFQDN, }) } } } } // fmt.Println(packageNameFQDN, path) return nil }) if err != nil { return fmt.Errorf("failed to walk handlers root: %w", err) } // stable sort the functions // first PackageName then FunctionName sort.Slice(funcs, func(i, j int) bool { return funcs[i].PackageNameFQDN+funcs[i].FunctionName < funcs[j].PackageNameFQDN+funcs[j].FunctionName }) // only add subPkg if there was actually a route within it found subPkgs := []string{} for _, fun := range funcs { mustAppend := true for _, a := range subPkgs { if a == fun.PackageNameFQDN { mustAppend = false } } if mustAppend { subPkgs = append(subPkgs, fun.PackageNameFQDN) } } if printOnly { for _, function := range funcs { log.Info().Msgf("%s %s", function.PackageNameFQDN, function.FunctionName) } // bailout return nil } handlersFile, err := os.Create(pathHandlersFile) if err != nil { return fmt.Errorf("failed to create handlers file: %w", err) } defer handlersFile.Close() if err = packageTemplate.Execute(handlersFile, TempateData{ SubPkgs: subPkgs, Funcs: funcs, }); err != nil { return fmt.Errorf("failed to execute package template: %w", err) } return nil }