diff --git a/cmd/apidoc/main.go b/cmd/apidoc/main.go index 20f4de7..df2db79 100644 --- a/cmd/apidoc/main.go +++ b/cmd/apidoc/main.go @@ -11,12 +11,10 @@ import ( func main() { var searchDir string var outputDir string - var mainFile string var isSingle bool var isHelp bool flag.StringVar(&searchDir, "dir", ".", "--dir") flag.StringVar(&outputDir, "output", "./docs/", "--output") - flag.StringVar(&mainFile, "main", "main.go", "--main") flag.BoolVar(&isSingle, "single", false, "--single") flag.BoolVar(&isHelp, "help", false, "--help") flag.Parse() @@ -24,20 +22,18 @@ func main() { fmt.Println(`apidoc is a tool for Go to generate apis markdown docs. Usage: - apidoc --dir= --output= --main= --single + apidoc --dir= --output= --single Flags: --dir: search apis dir, default . --output: generate markdown files dir, default ./docs/ - --main: the path of main go file, default main.go --single: generate single markdown file, default multi group files`) return } g := gen.New(&gen.Config{ - SearchDir: searchDir, - MainFile: mainFile, - OutputDir: outputDir, - IsGenGroupFile: !isSingle, + SearchDir: searchDir, + OutputDir: outputDir, + IsGenSingleFile: isSingle, }) if err := g.Build(); err != nil { log.Fatal(err) diff --git a/examples/docs/README.md b/examples/docs/README.md index 8475939..f992153 100644 --- a/examples/docs/README.md +++ b/examples/docs/README.md @@ -22,13 +22,13 @@ version: _@1.0.1_ 2.5. [获取地址列表](./apis-address.md#5-获取地址列表)(Deprecated) -3. [资料管理](./apis-profile.md) +3. [菜单管理](./apis-menu.md) - 3.1. [获取用户资料](./apis-profile.md#1-获取用户资料) + 3.1. [获取菜单节点](./apis-menu.md#1-获取菜单节点) -4. [菜单管理](./apis-menu.md) +4. [资料管理](./apis-profile.md) - 4.1. [获取菜单节点](./apis-menu.md#1-获取菜单节点) + 4.1. [获取用户资料](./apis-profile.md#1-获取用户资料) 5. [测试示例](./apis-demo.md) diff --git a/examples/docs/apis-demo.md b/examples/docs/apis-demo.md index f7d018d..6235e5b 100644 --- a/examples/docs/apis-demo.md +++ b/examples/docs/apis-demo.md @@ -14,6 +14,8 @@ ### 1. struct数组 +version: _1.0.2.1_ + ```text GET /user/demo/struct_array ``` @@ -212,9 +214,9 @@ __Response__: { //object(common.Response), 通用返回结果 "code": 0, //int, 返回状态码 "data": { //object(handler.DemoTime) - "time_1": "2022-05-16T11:38:50.59873+08:00", //object(time.Time), example1 + "time_1": "2022-05-16T16:47:48.741899+08:00", //object(time.Time), example1 "time_2": "2022-05-14 15:04:05", //object(time.Time), example2 - "time_3": "2022-05-16T11:38:50.598876+08:00" //object(time.Time) + "time_3": "2022-05-16T16:47:48.742123+08:00" //object(time.Time) }, "msg": "success" //string, 返回消息 } diff --git a/examples/svc-user/handler/account.go b/examples/svc-user/handler/account.go index ac021a9..ee364b0 100644 --- a/examples/svc-user/handler/account.go +++ b/examples/svc-user/handler/account.go @@ -35,6 +35,7 @@ type RegisterResponse struct { //@response 200 common.Response{code=10011,msg="password format error"} "密码格式错误" //@author alovn //@desc 用户注册接口说明 +//@order -1 func (h *AccountHandler) Register(w http.ResponseWriter, r *http.Request) { res := common.NewResponse(200, "注册成功", &RegisterResponse{ Username: "abc", diff --git a/examples/svc-user/handler/demo.go b/examples/svc-user/handler/demo.go index 5a2d3d0..53e7924 100644 --- a/examples/svc-user/handler/demo.go +++ b/examples/svc-user/handler/demo.go @@ -37,6 +37,7 @@ type DemoMap map[string]DemoData //@title struct数组 //@group demo //@response 200 []DemoData "demo struct array" +//@version 1.0.2.1 func (h *DemoHandler) StructArray(w http.ResponseWriter, r *http.Request) { } diff --git a/examples/svc-user/main.go b/examples/svc-user/main.go index 0d0f10d..f62d468 100644 --- a/examples/svc-user/main.go +++ b/examples/svc-user/main.go @@ -53,6 +53,7 @@ func main() { //@group demo //@title 测试示例 //@desc 其它一些示例演示 + //@order 6 { demo := handler.NewDemoHandler() mux.HandleFunc("/user/demo/struct_array", demo.StructArray) diff --git a/gen/gen.go b/gen/gen.go index 8874563..1927936 100644 --- a/gen/gen.go +++ b/gen/gen.go @@ -5,6 +5,8 @@ import ( "fmt" "os" "path/filepath" + "sort" + "strings" "text/template" "github.com/alovn/apidoc" @@ -21,11 +23,10 @@ func New(c *Config) *Gen { } type Config struct { - SearchDir string - OutputDir string - TemplateFile string - MainFile string - IsGenGroupFile bool + SearchDir string + OutputDir string + TemplateFile string + IsGenSingleFile bool } func (g *Gen) Build() error { @@ -33,23 +34,39 @@ func (g *Gen) Build() error { return errors.New("error config") } p := apidoc.New() - if err := p.Parse(g.c.SearchDir, g.c.MainFile); err != nil { + if err := p.Parse(g.c.SearchDir); err != nil { return err } doc := p.GetApiDoc() - if doc.Title == "" && doc.Service == "" && len(doc.Apis) == 0 { - fmt.Println("can't find apis") + if doc.Service == "" { + fmt.Println("apidoc @service is not set") + return nil + } + if doc.Title == "" { + fmt.Println("apidoc @title is not set") + return nil + } + if doc.TotalCount == 0 { + fmt.Println("apis count is 0") return nil } - if len(doc.Apis) > 0 { + if len(doc.UngroupedApis) > 0 { doc.Groups = append(doc.Groups, &apidoc.ApiGroupSpec{ Group: "ungrouped", Title: "ungrouped", Description: "Ungrouped apis", - Apis: doc.Apis, + Apis: doc.UngroupedApis, }) + doc.UngroupedApis = doc.UngroupedApis[:0] } + sort.Slice(doc.Groups, func(i, j int) bool { + a, b := doc.Groups[i], doc.Groups[j] + if a.Order == b.Order { + return strings.Compare(a.Group, b.Group) < 0 + } + return a.Order < b.Order + }) if err := os.MkdirAll(g.c.OutputDir, os.ModePerm); err != nil { return err @@ -60,16 +77,44 @@ func (g *Gen) Build() error { return a + b }, } + sortApis := func(apis []*apidoc.ApiSpec) { + less := func(i, j int) bool { + a, b := apis[i], apis[j] + return a.Order < b.Order + } + sort.Slice(apis, less) + } - if g.c.IsGenGroupFile { + if g.c.IsGenSingleFile { + t := template.New("apis-single").Funcs(funcMap) + t, err := t.Parse(singleApisTemplate) + if err != nil { + return err + } + f, err := os.Create(filepath.Join(g.c.OutputDir, "README.md")) + if err != nil { + return err + } + defer f.Close() + for _, g := range doc.Groups { + apis := g.Apis + sortApis(apis) + } + + if err = t.Execute(f, doc); err != nil { + return err + } + fmt.Println("generated: README.md") + } else { //group - t := template.New("group").Funcs(funcMap) + t := template.New("group-apis").Funcs(funcMap) t, err := t.Parse(groupApisTemplate) if err != nil { return err } for _, v := range doc.Groups { group := v + sortApis(group.Apis) fileName := fmt.Sprintf("apis-%s.md", group.Group) f, err := os.Create(filepath.Join(g.c.OutputDir, fileName)) if err != nil { @@ -83,7 +128,7 @@ func (g *Gen) Build() error { } //readme - t = template.New("apis").Funcs(funcMap) + t = template.New("group-readme").Funcs(funcMap) t, err = t.Parse(groupReadmeTemplate) if err != nil { return err @@ -97,26 +142,7 @@ func (g *Gen) Build() error { return err } fmt.Println("generated: README.md") - - return nil - } - - t := template.New("apis-single").Funcs(funcMap) - t, err := t.Parse(singleApisTemplate) - if err != nil { - return err - } - f, err := os.Create(filepath.Join(g.c.OutputDir, "README.md")) - if err != nil { - return err - } - defer f.Close() - for _, g := range doc.Groups { - doc.Apis = append(g.Apis, doc.Apis...) - } - if err = t.Execute(f, doc); err != nil { - return err } - fmt.Println("generated: README.md") + fmt.Println("apis total count:", doc.TotalCount) return nil } diff --git a/gen/template/group_apis.tpl b/gen/template/group_apis.tpl index 72c608c..18567ae 100644 --- a/gen/template/group_apis.tpl +++ b/gen/template/group_apis.tpl @@ -27,6 +27,11 @@ ___Deprecated___ author: _{{$v.Author}}_ {{- end}} +{{- if $v.Version}} + +version: _{{$v.Version}}_ +{{- end}} + ```text {{$v.HTTPMethod}} {{$v.FullURL}} ``` diff --git a/gen/template/single.tpl b/gen/template/single.tpl index 8a6966c..843e578 100644 --- a/gen/template/single.tpl +++ b/gen/template/single.tpl @@ -37,6 +37,11 @@ ___Deprecated___ author: _{{$v.Author}}_ {{- end}} +{{- if $v.Version}} + +version: _{{$v.Version}}_ +{{- end}} + ```text {{$v.HTTPMethod}} {{$v.FullURL}} ``` diff --git a/operation.go b/operation.go index d5739fe..7c60f88 100644 --- a/operation.go +++ b/operation.go @@ -74,6 +74,12 @@ func (operation *Operation) ParseComment(comment string, astFile *ast.File) erro return operation.ParseParametersComment(strings.TrimPrefix(lowerAttribute, "@"), lineRemainder, astFile) case deprecatedAttr, "deprecated:": operation.Deprecated = true + case orderAttr: + if i, err := strconv.Atoi(lineRemainder); err == nil { + operation.Order = i + } + case versionAttr: + operation.Version = lineRemainder } return nil } @@ -105,7 +111,6 @@ func (operation *Operation) ParseRouterComment(commentLine string) error { operation.HTTPMethod = httpMethod operation.Api = matches[2] - operation.FullURL = fmt.Sprintf("%s/%s", strings.TrimSuffix(operation.parser.doc.BaseURL, "/"), strings.TrimPrefix(operation.Api, "/")) return nil } diff --git a/parser.go b/parser.go index db1d6c1..ce349e9 100644 --- a/parser.go +++ b/parser.go @@ -11,6 +11,7 @@ import ( "os" "os/exec" "path/filepath" + "strconv" "strings" ) @@ -33,6 +34,7 @@ const ( formatAttr = "@format" deprecatedAttr = "@deprecated" authorAttr = "@author" + orderAttr = "@order" //for sort //doc baseURLAttr = "@baseurl" @@ -77,7 +79,7 @@ func SetExcludedDirsAndFiles(excludes string) func(*Parser) { } } -func (parser *Parser) Parse(searchDir string, mainFile string) error { +func (parser *Parser) Parse(searchDir string) error { packageDir, err := getPkgName(searchDir) if err != nil { return err @@ -85,13 +87,6 @@ func (parser *Parser) Parse(searchDir string, mainFile string) error { if err = parser.getAllGoFileInfo(packageDir, searchDir); err != nil { return err } - mainPath, err := filepath.Abs(filepath.Join(searchDir, mainFile)) - if err != nil { - return err - } - if err = parser.parseApiDocInfo(mainPath); err != nil { - return err - } if err = parser.packages.ParseTypes(); err != nil { return err } @@ -105,60 +100,62 @@ func (parser *Parser) GetApiDoc() *ApiDocSpec { return parser.doc } -func (parser *Parser) parseApiDocInfo(mainPath string) error { - fileTree, err := goparser.ParseFile(token.NewFileSet(), mainPath, nil, goparser.ParseComments) - if err != nil { - return fmt.Errorf("cannot parse source files %s: %s", mainPath, err) - } - for _, comment := range fileTree.Comments { +func (parser *Parser) parseApiInfos(fileName string, astFile *ast.File) error { + //parse group + for _, comment := range astFile.Comments { comments := strings.Split(comment.Text(), "\n") - if !isApiDocComment(comments) { - continue - } - if isApiGroupComment(comments) { - if err := parser.parseApiGroupInfo(comments); err != nil { + if isApiDocGroupComment(comments) { + if err := parser.parseApiDocGroupInfo(comments); err != nil { return err } continue } - - err = parseApiDocInfo(parser, comments) - if err != nil { - return err - } } - return nil -} - -func (parser *Parser) parseApiInfos(fileName string, astFile *ast.File) error { for _, astDescription := range astFile.Decls { - astDeclaration, ok := astDescription.(*ast.FuncDecl) - if ok && astDeclaration.Doc != nil && astDeclaration.Doc.List != nil { - if astDeclaration.Name.Name == "main" { - continue - } - operation := NewOperation(parser) - for _, comment := range astDeclaration.Doc.List { - err := operation.ParseComment(comment.Text, astFile) - if err != nil { - return fmt.Errorf("ParseComment error in file %s :%+v", fileName, err) + switch astDeclaration := astDescription.(type) { + case *ast.FuncDecl: + if astDeclaration.Doc != nil && astDeclaration.Doc.List != nil { + comments := strings.Split(astDeclaration.Doc.Text(), "\n") + if astDeclaration.Name.Name == "main" { //parse service + if isApiDocServiceComment(comments) { + if err := parser.parseApiDocServiceInfo(comments); err != nil { + return err + } + continue + } } - } - if operation.ApiSpec.Group == "" { - parser.doc.Apis = append(parser.doc.Apis, &operation.ApiSpec) - } else { - if g, ok := parser.groups[operation.ApiSpec.Group]; ok { - g.Apis = append(g.Apis, &operation.ApiSpec) + if isApiDocGroupComment(comments) { //parse group, if in func decl + if err := parser.parseApiDocGroupInfo(comments); err != nil { + return err + } + continue + } + //parse apis + operation := NewOperation(parser) + for _, comment := range comments { + err := operation.ParseComment(comment, astFile) + if err != nil { + return fmt.Errorf("ParseComment error in file %s :%+v", fileName, err) + } + } + operation.ApiSpec.doc = parser.doc //ptr + if operation.ApiSpec.Group == "" { + parser.doc.UngroupedApis = append(parser.doc.UngroupedApis, &operation.ApiSpec) } else { - group := ApiGroupSpec{ - Group: operation.ApiSpec.Group, - Title: operation.ApiSpec.Group, - Description: "", + if g, ok := parser.groups[operation.ApiSpec.Group]; ok { + g.Apis = append(g.Apis, &operation.ApiSpec) + } else { + group := ApiGroupSpec{ + Group: operation.ApiSpec.Group, + Title: operation.ApiSpec.Group, + Description: "", + } + group.Apis = append(group.Apis, &operation.ApiSpec) + parser.groups[operation.ApiSpec.Group] = &group + parser.doc.Groups = append(parser.doc.Groups, &group) } - group.Apis = append(group.Apis, &operation.ApiSpec) - parser.groups[operation.ApiSpec.Group] = &group - parser.doc.Groups = append(parser.doc.Groups, &group) } + parser.doc.TotalCount += 1 } } } @@ -166,7 +163,7 @@ func (parser *Parser) parseApiInfos(fileName string, astFile *ast.File) error { return nil } -func (parser *Parser) parseApiGroupInfo(comments []string) error { +func (parser *Parser) parseApiDocGroupInfo(comments []string) error { previousAttribute := "" var group ApiGroupSpec for line := 0; line < len(comments); line++ { @@ -188,6 +185,10 @@ func (parser *Parser) parseApiGroupInfo(comments []string) error { continue } group.Description = value + case orderAttr: + if i, err := strconv.Atoi(value); err == nil { + group.Order = i + } } } if group.Group == "" { @@ -197,6 +198,7 @@ func (parser *Parser) parseApiGroupInfo(comments []string) error { g.Group = group.Group g.Title = group.Title g.Description = group.Description + g.Order = group.Order } else { parser.groups[group.Group] = &group parser.doc.Groups = append(parser.doc.Groups, &group) @@ -204,7 +206,10 @@ func (parser *Parser) parseApiGroupInfo(comments []string) error { return nil } -func parseApiDocInfo(parser *Parser, comments []string) error { +func (parser *Parser) parseApiDocServiceInfo(comments []string) error { + if parser.doc.Service != "" { + return errors.New("error: service has been set, multiple service?") + } previousAttribute := "" for line := 0; line < len(comments); line++ { commentLine := comments[line] @@ -234,7 +239,7 @@ func parseApiDocInfo(parser *Parser, comments []string) error { return nil } -func isApiDocComment(comments []string) bool { +func isApiDocServiceComment(comments []string) bool { for _, commentLine := range comments { attribute := strings.ToLower(strings.Split(commentLine, " ")[0]) switch attribute { @@ -244,21 +249,21 @@ func isApiDocComment(comments []string) bool { return false } } - - return true + return false } -func isApiGroupComment(comments []string) bool { +func isApiDocGroupComment(comments []string) bool { + isGroup := false for _, commentLine := range comments { attribute := strings.ToLower(strings.Split(commentLine, " ")[0]) switch attribute { - case apiAttr, successAttr, failureAttr, responseAttr: + case serviceAttr, apiAttr, successAttr, failureAttr, responseAttr: return false case groupAttr: - return true + isGroup = true } } - return false + return isGroup } func getPkgName(searchDir string) (string, error) { diff --git a/spec.go b/spec.go index ed043b7..082f219 100644 --- a/spec.go +++ b/spec.go @@ -11,14 +11,14 @@ import ( ) type ApiDocSpec struct { - Service string - Title string - Version string - Description string - Scheme string - BaseURL string - Groups []*ApiGroupSpec - Apis []*ApiSpec + Service string + Title string + Version string + Description string + BaseURL string + Groups []*ApiGroupSpec + UngroupedApis []*ApiSpec + TotalCount int } type ApiGroupSpec struct { @@ -26,13 +26,14 @@ type ApiGroupSpec struct { Title string Description string Apis []*ApiSpec + Order int //sort } type ApiSpec struct { + doc *ApiDocSpec Title string HTTPMethod string Api string - FullURL string Version string Accept string //json,xml,form Format string //json,xml @@ -42,6 +43,11 @@ type ApiSpec struct { Group string Responses []*ApiResponseSpec Requests ApiRequestSpec + Order int //sort +} + +func (a *ApiSpec) FullURL() string { + return fmt.Sprintf("%s/%s", strings.TrimSuffix(a.doc.BaseURL, "/"), strings.TrimPrefix(a.Api, "/")) } type ApiRequestSpec struct {