diff --git a/api/api.go b/api/api.go index 3bf83e5..458811d 100644 --- a/api/api.go +++ b/api/api.go @@ -2,13 +2,17 @@ package api import ( "flag" + "fmt" "net/http" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" + "github.com/go-chi/docgen" "github.com/go-chi/render" ) +var routes = flag.Bool("routes", false, "Generate API route documentation") + func Start() { flag.Parse() @@ -40,5 +44,13 @@ func Start() { }) }) + if *routes { + fmt.Println(docgen.MarkdownRoutesDoc(r, docgen.MarkdownOpts{ + ProjectPath: "git.dubyatp.xyz/chat-api-server", + Intro: "Welcome to the chat API server. This is a simple API for sending and receiving messages.", + })) + return + } + http.ListenAndServe(":3000", r) } diff --git a/docs/routes.md b/docs/routes.md new file mode 100644 index 0000000..8437a99 --- /dev/null +++ b/docs/routes.md @@ -0,0 +1,73 @@ +## Routes + +
+`/` + +- [RequestID]() +- [Logger]() +- [Recoverer]() +- [URLFormat]() +- [git.dubyatp.xyz/chat-api-server/api.Start.SetContentType.func5]() +- **/** + - _GET_ + - [Start.func1]() + +
+
+`/messages` + +- [RequestID]() +- [Logger]() +- [Recoverer]() +- [URLFormat]() +- [git.dubyatp.xyz/chat-api-server/api.Start.SetContentType.func5]() +- **/messages** + - **/** + - _GET_ + - [ListMessages]() + +
+
+`/messages/{messageID}` + +- [RequestID]() +- [Logger]() +- [Recoverer]() +- [URLFormat]() +- [git.dubyatp.xyz/chat-api-server/api.Start.SetContentType.func5]() +- **/messages** + - **/{messageID}** + - [MessageCtx]() + - **/** + - _GET_ + - [GetMessage]() + +
+
+`/panic` + +- [RequestID]() +- [Logger]() +- [Recoverer]() +- [URLFormat]() +- [git.dubyatp.xyz/chat-api-server/api.Start.SetContentType.func5]() +- **/panic** + - _GET_ + - [Start.func3]() + +
+
+`/ping` + +- [RequestID]() +- [Logger]() +- [Recoverer]() +- [URLFormat]() +- [git.dubyatp.xyz/chat-api-server/api.Start.SetContentType.func5]() +- **/ping** + - _GET_ + - [Start.func2]() + +
+ +Total # of routes: 5 \ No newline at end of file diff --git a/go.mod b/go.mod index 1a368c1..6e245d6 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.23 require ( github.com/go-chi/chi/v5 v5.2.0 github.com/go-chi/render v1.0.3 + github.com/go-chi/docgen v1.3.0 ) require github.com/ajg/form v1.5.1 // indirect diff --git a/go.sum b/go.sum index 9810ce7..a7d0ea6 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,12 @@ github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= +github.com/go-chi/chi/v5 v5.0.1/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/chi/v5 v5.2.0 h1:Aj1EtB0qR2Rdo2dG4O94RIU35w2lvQSj6BRA4+qwFL0= github.com/go-chi/chi/v5 v5.2.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/docgen v1.3.0 h1:dmDJ2I+EJfCTrxfgxQDwfR/OpZLTRFKe7EKB8v7yuxI= +github.com/go-chi/docgen v1.3.0/go.mod h1:G9W0G551cs2BFMSn/cnGwX+JBHEloAgo17MBhyrnhPI= +github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns= github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/vendor/github.com/go-chi/docgen/LICENSE b/vendor/github.com/go-chi/docgen/LICENSE new file mode 100644 index 0000000..4344db7 --- /dev/null +++ b/vendor/github.com/go-chi/docgen/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2016-Present https://github.com/go-chi authors + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/go-chi/docgen/builder.go b/vendor/github.com/go-chi/docgen/builder.go new file mode 100644 index 0000000..9012109 --- /dev/null +++ b/vendor/github.com/go-chi/docgen/builder.go @@ -0,0 +1,126 @@ +package docgen + +import ( + "errors" + "fmt" + "net/http" + "path/filepath" + "strings" + + "github.com/go-chi/chi/v5" +) + +func BuildDoc(r chi.Routes) (Doc, error) { + d := Doc{} + + goPath := getGoPath() + if goPath == "" { + return d, errors.New("docgen: unable to determine your $GOPATH") + } + + // Walk and generate the router docs + d.Router = buildDocRouter(r) + return d, nil +} + +func buildDocRouter(r chi.Routes) DocRouter { + rts := r + dr := DocRouter{Middlewares: []DocMiddleware{}} + drts := DocRoutes{} + dr.Routes = drts + + for _, mw := range rts.Middlewares() { + dmw := DocMiddleware{ + FuncInfo: buildFuncInfo(mw), + } + dr.Middlewares = append(dr.Middlewares, dmw) + } + + for _, rt := range rts.Routes() { + drt := DocRoute{Pattern: rt.Pattern, Handlers: DocHandlers{}} + + if rt.SubRoutes != nil { + subRoutes := rt.SubRoutes + subDrts := buildDocRouter(subRoutes) + drt.Router = &subDrts + + } else { + hall := rt.Handlers["*"] + for method, h := range rt.Handlers { + if method != "*" && hall != nil && fmt.Sprintf("%v", hall) == fmt.Sprintf("%v", h) { + continue + } + + dh := DocHandler{Method: method, Middlewares: []DocMiddleware{}} + + var endpoint http.Handler + chain, _ := h.(*chi.ChainHandler) + + if chain != nil { + for _, mw := range chain.Middlewares { + dh.Middlewares = append(dh.Middlewares, DocMiddleware{ + FuncInfo: buildFuncInfo(mw), + }) + } + endpoint = chain.Endpoint + } else { + endpoint = h + } + + dh.FuncInfo = buildFuncInfo(endpoint) + + drt.Handlers[method] = dh + } + } + + drts[rt.Pattern] = drt + } + + return dr +} + +func buildFuncInfo(i interface{}) FuncInfo { + fi := FuncInfo{} + frame := getCallerFrame(i) + goPathSrc := filepath.Join(getGoPath(), "src") + + if frame == nil { + fi.Unresolvable = true + return fi + } + + pkgName := getPkgName(frame.File) + if pkgName == "chi" { + fi.Unresolvable = true + } + funcPath := frame.Func.Name() + + idx := strings.Index(funcPath, "/"+pkgName) + if idx > 0 { + fi.Pkg = funcPath[:idx+1+len(pkgName)] + fi.Func = funcPath[idx+2+len(pkgName):] + } else { + fi.Func = funcPath + } + + if strings.Index(fi.Func, ".func") > 0 { + fi.Anonymous = true + } + + fi.File = frame.File + fi.Line = frame.Line + if filepath.HasPrefix(fi.File, goPathSrc) { + fi.File = fi.File[len(goPathSrc)+1:] + } + + // Check if file info is unresolvable + if !strings.Contains(funcPath, pkgName) { + fi.Unresolvable = true + } + + if !fi.Unresolvable { + fi.Comment = getFuncComment(frame.File, frame.Line) + } + + return fi +} diff --git a/vendor/github.com/go-chi/docgen/docgen.go b/vendor/github.com/go-chi/docgen/docgen.go new file mode 100644 index 0000000..94f8439 --- /dev/null +++ b/vendor/github.com/go-chi/docgen/docgen.go @@ -0,0 +1,64 @@ +package docgen + +import ( + "encoding/json" + "fmt" + + "github.com/go-chi/chi/v5" +) + +type Doc struct { + Router DocRouter `json:"router"` +} + +type DocRouter struct { + Middlewares []DocMiddleware `json:"middlewares"` + Routes DocRoutes `json:"routes"` +} + +type DocMiddleware struct { + FuncInfo +} + +type DocRoute struct { + Pattern string `json:"-"` + Handlers DocHandlers `json:"handlers,omitempty"` + Router *DocRouter `json:"router,omitempty"` +} + +type DocRoutes map[string]DocRoute // Pattern : DocRoute + +type DocHandler struct { + Middlewares []DocMiddleware `json:"middlewares"` + Method string `json:"method"` + FuncInfo +} + +type DocHandlers map[string]DocHandler // Method : DocHandler + +func PrintRoutes(r chi.Routes) { + var printRoutes func(parentPattern string, r chi.Routes) + printRoutes = func(parentPattern string, r chi.Routes) { + rts := r.Routes() + for _, rt := range rts { + if rt.SubRoutes == nil { + fmt.Println(parentPattern + rt.Pattern) + } else { + pat := rt.Pattern + + subRoutes := rt.SubRoutes + printRoutes(parentPattern+pat, subRoutes) + } + } + } + printRoutes("", r) +} + +func JSONRoutesDoc(r chi.Routes) string { + doc, _ := BuildDoc(r) + v, err := json.MarshalIndent(doc, "", " ") + if err != nil { + panic(err) + } + return string(v) +} diff --git a/vendor/github.com/go-chi/docgen/funcinfo.go b/vendor/github.com/go-chi/docgen/funcinfo.go new file mode 100644 index 0000000..6013f10 --- /dev/null +++ b/vendor/github.com/go-chi/docgen/funcinfo.go @@ -0,0 +1,116 @@ +package docgen + +import ( + "go/parser" + "go/token" + "path/filepath" + "reflect" + "runtime" + "strings" +) + +type FuncInfo struct { + Pkg string `json:"pkg"` + Func string `json:"func"` + Comment string `json:"comment"` + File string `json:"file,omitempty"` + Line int `json:"line,omitempty"` + Anonymous bool `json:"anonymous,omitempty"` + Unresolvable bool `json:"unresolvable,omitempty"` +} + +func GetFuncInfo(i interface{}) FuncInfo { + fi := FuncInfo{} + frame := getCallerFrame(i) + goPathSrc := filepath.Join(getGoPath(), "src") + + if frame == nil { + fi.Unresolvable = true + return fi + } + + pkgName := getPkgName(frame.File) + if pkgName == "chi" { + fi.Unresolvable = true + } + funcPath := frame.Func.Name() + + idx := strings.Index(funcPath, "/"+pkgName) + if idx > 0 { + fi.Pkg = funcPath[:idx+1+len(pkgName)] + fi.Func = funcPath[idx+2+len(pkgName):] + } else { + fi.Func = funcPath + } + + if strings.Index(fi.Func, ".func") > 0 { + fi.Anonymous = true + } + + fi.File = frame.File + fi.Line = frame.Line + if filepath.HasPrefix(fi.File, goPathSrc) { + fi.File = fi.File[len(goPathSrc)+1:] + } + + // Check if file info is unresolvable + if strings.Index(funcPath, pkgName) < 0 { + fi.Unresolvable = true + } + + if !fi.Unresolvable { + fi.Comment = getFuncComment(frame.File, frame.Line) + } + + return fi +} + +func getCallerFrame(i interface{}) *runtime.Frame { + value := reflect.ValueOf(i) + if value.Kind() != reflect.Func { + return nil + } + pc := value.Pointer() + frames := runtime.CallersFrames([]uintptr{pc}) + if frames == nil { + return nil + } + frame, _ := frames.Next() + if frame.Entry == 0 { + return nil + } + return &frame +} + +func getPkgName(file string) string { + fset := token.NewFileSet() + astFile, err := parser.ParseFile(fset, file, nil, parser.PackageClauseOnly) + if err != nil { + return "" + } + if astFile.Name == nil { + return "" + } + return astFile.Name.Name +} + +func getFuncComment(file string, line int) string { + fset := token.NewFileSet() + + astFile, err := parser.ParseFile(fset, file, nil, parser.ParseComments) + if err != nil { + return "" + } + + if len(astFile.Comments) == 0 { + return "" + } + + for _, cmt := range astFile.Comments { + if fset.Position(cmt.End()).Line+1 == line { + return cmt.Text() + } + } + + return "" +} diff --git a/vendor/github.com/go-chi/docgen/markdown.go b/vendor/github.com/go-chi/docgen/markdown.go new file mode 100644 index 0000000..37e5aa8 --- /dev/null +++ b/vendor/github.com/go-chi/docgen/markdown.go @@ -0,0 +1,218 @@ +package docgen + +import ( + "bytes" + "errors" + "fmt" + "sort" + "strings" + + "github.com/go-chi/chi/v5" +) + +type MarkdownDoc struct { + Opts MarkdownOpts + Router chi.Router + Doc Doc + Routes map[string]DocRouter // Pattern : DocRouter + + buf *bytes.Buffer +} + +type MarkdownOpts struct { + // ProjectPath is the base Go import path of the project + ProjectPath string + + // Intro text included at the top of the generated markdown file. + Intro string + + // ForceRelativeLinks to be relative even if they're not on github + ForceRelativeLinks bool + + // URLMap allows specifying a map of package import paths to their link sources + // Used for mapping vendored dependencies to their upstream sources + // For example: + // map[string]string{"github.com/my/package/vendor/go-chi/chi/": "https://github.com/go-chi/chi/blob/master/"} + URLMap map[string]string +} + +func MarkdownRoutesDoc(r chi.Router, opts MarkdownOpts) string { + md := &MarkdownDoc{Router: r, Opts: opts} + if err := md.Generate(); err != nil { + return fmt.Sprintf("ERROR: %s\n", err.Error()) + } + return md.String() +} + +func (md *MarkdownDoc) String() string { + return md.buf.String() +} + +func (md *MarkdownDoc) Generate() error { + if md.Router == nil { + return errors.New("docgen: router is nil") + } + + doc, err := BuildDoc(md.Router) + if err != nil { + return err + } + + md.Doc = doc + md.buf = &bytes.Buffer{} + md.Routes = make(map[string]DocRouter) + + md.WriteIntro() + md.WriteRoutes() + + return nil +} + +func (md *MarkdownDoc) WriteIntro() { + pkgName := md.Opts.ProjectPath + md.buf.WriteString(fmt.Sprintf("# %s\n\n", pkgName)) + + intro := md.Opts.Intro + md.buf.WriteString(fmt.Sprintf("%s\n\n", intro)) +} + +func (md *MarkdownDoc) WriteRoutes() { + md.buf.WriteString(fmt.Sprintf("## Routes\n\n")) + + var buildRoutesMap func(parentPattern string, ar, nr, dr *DocRouter) + buildRoutesMap = func(parentPattern string, ar, nr, dr *DocRouter) { + + nr.Middlewares = append(nr.Middlewares, dr.Middlewares...) + + for pat, rt := range dr.Routes { + pattern := parentPattern + pat + + nr.Routes = DocRoutes{} + + if rt.Router != nil { + nnr := &DocRouter{} + nr.Routes[pat] = DocRoute{ + Pattern: pat, + Handlers: rt.Handlers, + Router: nnr, + } + buildRoutesMap(pattern, ar, nnr, rt.Router) + + } else if len(rt.Handlers) > 0 { + nr.Routes[pat] = DocRoute{ + Pattern: pat, + Handlers: rt.Handlers, + Router: nil, + } + + // Remove the trailing slash if the handler is a subroute for "/" + routeKey := pattern + if pat == "/" && len(routeKey) > 1 { + routeKey = routeKey[:len(routeKey)-1] + } + md.Routes[routeKey] = copyDocRouter(*ar) + + } else { + panic("not possible") + } + } + + } + + // Build a route tree that consists of the full route pattern + // and the part of the tree for just that specific route, stored + // in routes map on the markdown struct. This is the structure we + // are going to render to markdown. + dr := md.Doc.Router + ar := DocRouter{} + buildRoutesMap("", &ar, &ar, &dr) + + // Generate the markdown to render the above structure + var printRouter func(depth int, dr DocRouter) + printRouter = func(depth int, dr DocRouter) { + + tabs := "" + for i := 0; i < depth; i++ { + tabs += "\t" + } + + // Middlewares + for _, mw := range dr.Middlewares { + md.buf.WriteString(fmt.Sprintf("%s- [%s](%s)\n", tabs, mw.Func, md.githubSourceURL(mw.File, mw.Line))) + } + + // Routes + for _, rt := range dr.Routes { + md.buf.WriteString(fmt.Sprintf("%s- **%s**\n", tabs, normalizer(rt.Pattern))) + + if rt.Router != nil { + printRouter(depth+1, *rt.Router) + } else { + for meth, dh := range rt.Handlers { + md.buf.WriteString(fmt.Sprintf("%s\t- _%s_\n", tabs, meth)) + + // Handler middlewares + for _, mw := range dh.Middlewares { + md.buf.WriteString(fmt.Sprintf("%s\t\t- [%s](%s)\n", tabs, mw.Func, md.githubSourceURL(mw.File, mw.Line))) + } + + // Handler endpoint + md.buf.WriteString(fmt.Sprintf("%s\t\t- [%s](%s)\n", tabs, dh.Func, md.githubSourceURL(dh.File, dh.Line))) + } + } + } + } + + routePaths := []string{} + for pat := range md.Routes { + routePaths = append(routePaths, pat) + } + sort.Strings(routePaths) + + for _, pat := range routePaths { + dr := md.Routes[pat] + md.buf.WriteString(fmt.Sprintf("
\n")) + md.buf.WriteString(fmt.Sprintf("`%s`\n", normalizer(pat))) + md.buf.WriteString(fmt.Sprintf("\n")) + printRouter(0, dr) + md.buf.WriteString(fmt.Sprintf("\n")) + md.buf.WriteString(fmt.Sprintf("
\n")) + } + + md.buf.WriteString(fmt.Sprintf("\n")) + md.buf.WriteString(fmt.Sprintf("Total # of routes: %d\n", len(md.Routes))) + + // TODO: total number of handlers.. +} + +func (md *MarkdownDoc) githubSourceURL(file string, line int) string { + // Currently, we only automatically link to source for github projects + if strings.Index(file, "github.com/") != 0 && !md.Opts.ForceRelativeLinks { + return "" + } + if md.Opts.ProjectPath == "" { + return "" + } + for pkg, url := range md.Opts.URLMap { + if idx := strings.Index(file, pkg); idx >= 0 { + pos := idx + len(pkg) + url = strings.TrimRight(url, "/") + filepath := strings.TrimLeft(file[pos:], "/") + return fmt.Sprintf("%s/%s#L%d", url, filepath, line) + } + } + if idx := strings.Index(file, md.Opts.ProjectPath); idx >= 0 { + // relative + pos := idx + len(md.Opts.ProjectPath) + return fmt.Sprintf("%s#L%d", file[pos:], line) + } + // absolute + return fmt.Sprintf("https://%s#L%d", file, line) +} + +func normalizer(s string) string { + if strings.Contains(s, "/*") { + return strings.Replace(s, "/*", "", -1) + } + return s +} diff --git a/vendor/github.com/go-chi/docgen/util.go b/vendor/github.com/go-chi/docgen/util.go new file mode 100644 index 0000000..1ac14de --- /dev/null +++ b/vendor/github.com/go-chi/docgen/util.go @@ -0,0 +1,50 @@ +package docgen + +import ( + "go/build" + "os" +) + +func copyDocRouter(dr DocRouter) DocRouter { + var cloneRouter func(dr DocRouter) DocRouter + var cloneRoutes func(drt DocRoutes) DocRoutes + + cloneRoutes = func(drts DocRoutes) DocRoutes { + rts := DocRoutes{} + + for pat, drt := range drts { + rt := DocRoute{Pattern: drt.Pattern} + if len(drt.Handlers) > 0 { + rt.Handlers = DocHandlers{} + for meth, dh := range drt.Handlers { + rt.Handlers[meth] = dh + } + } + if drt.Router != nil { + rr := cloneRouter(*drt.Router) + rt.Router = &rr + } + rts[pat] = rt + } + + return rts + } + + cloneRouter = func(dr DocRouter) DocRouter { + cr := DocRouter{} + cr.Middlewares = make([]DocMiddleware, len(dr.Middlewares)) + copy(cr.Middlewares, dr.Middlewares) + cr.Routes = cloneRoutes(dr.Routes) + return cr + } + + return cloneRouter(dr) +} + +func getGoPath() string { + goPath := os.Getenv("GOPATH") + if goPath == "" { + goPath = build.Default.GOPATH + } + return goPath +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 5857ddd..5f2bae7 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -5,6 +5,9 @@ github.com/ajg/form ## explicit; go 1.14 github.com/go-chi/chi/v5 github.com/go-chi/chi/v5/middleware +# github.com/go-chi/docgen v1.3.0 +## explicit; go 1.15 +github.com/go-chi/docgen # github.com/go-chi/render v1.0.3 ## explicit; go 1.16 github.com/go-chi/render