This commit is contained in:
2024-12-29 23:29:33 -05:00
parent c2df6028b3
commit cad40565b0
11 changed files with 689 additions and 0 deletions

View File

@@ -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)
}

73
docs/routes.md Normal file
View File

@@ -0,0 +1,73 @@
## Routes
<details>
<summary>`/`</summary>
- [RequestID]()
- [Logger]()
- [Recoverer]()
- [URLFormat]()
- [git.dubyatp.xyz/chat-api-server/api.Start.SetContentType.func5]()
- **/**
- _GET_
- [Start.func1]()
</details>
<details>
<summary>`/messages`</summary>
- [RequestID]()
- [Logger]()
- [Recoverer]()
- [URLFormat]()
- [git.dubyatp.xyz/chat-api-server/api.Start.SetContentType.func5]()
- **/messages**
- **/**
- _GET_
- [ListMessages]()
</details>
<details>
<summary>`/messages/{messageID}`</summary>
- [RequestID]()
- [Logger]()
- [Recoverer]()
- [URLFormat]()
- [git.dubyatp.xyz/chat-api-server/api.Start.SetContentType.func5]()
- **/messages**
- **/{messageID}**
- [MessageCtx]()
- **/**
- _GET_
- [GetMessage]()
</details>
<details>
<summary>`/panic`</summary>
- [RequestID]()
- [Logger]()
- [Recoverer]()
- [URLFormat]()
- [git.dubyatp.xyz/chat-api-server/api.Start.SetContentType.func5]()
- **/panic**
- _GET_
- [Start.func3]()
</details>
<details>
<summary>`/ping`</summary>
- [RequestID]()
- [Logger]()
- [Recoverer]()
- [URLFormat]()
- [git.dubyatp.xyz/chat-api-server/api.Start.SetContentType.func5]()
- **/ping**
- _GET_
- [Start.func2]()
</details>
Total # of routes: 5

1
go.mod
View File

@@ -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

6
go.sum
View File

@@ -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=

20
vendor/github.com/go-chi/docgen/LICENSE generated vendored Normal file
View File

@@ -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.

126
vendor/github.com/go-chi/docgen/builder.go generated vendored Normal file
View File

@@ -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
}

64
vendor/github.com/go-chi/docgen/docgen.go generated vendored Normal file
View File

@@ -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)
}

116
vendor/github.com/go-chi/docgen/funcinfo.go generated vendored Normal file
View File

@@ -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 ""
}

218
vendor/github.com/go-chi/docgen/markdown.go generated vendored Normal file
View File

@@ -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("<details>\n"))
md.buf.WriteString(fmt.Sprintf("<summary>`%s`</summary>\n", normalizer(pat)))
md.buf.WriteString(fmt.Sprintf("\n"))
printRouter(0, dr)
md.buf.WriteString(fmt.Sprintf("\n"))
md.buf.WriteString(fmt.Sprintf("</details>\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
}

50
vendor/github.com/go-chi/docgen/util.go generated vendored Normal file
View File

@@ -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
}

3
vendor/modules.txt vendored
View File

@@ -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