Skip to content

Commit

Permalink
feat: support markdown description (#377)
Browse files Browse the repository at this point in the history
  • Loading branch information
Nerzal authored and easonlin404 committed Jun 17, 2019
1 parent 6ee61d7 commit 6170d1b
Show file tree
Hide file tree
Showing 12 changed files with 168 additions and 25 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ dist
testdata/simple*/docs
cover.out


# Test binary, build with `go test -c`
*.test

Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,20 @@ $ swag init
| BasePath | The base path on which the API is served. | // @BasePath /api/v1 |
| schemes | The transfer protocol for the operation that separated by spaces. | // @schemes http https |
### Using markdown descriptions
When a short string in your documentation is insufficient, or you need images, code examples and things like that you may want to use markdown descriptions. In order to use markdown descriptions use the following annotations.
| annotation | description | example |
|-------------|--------------------------------------------|---------------------------------|
| title | **Required.** The title of the application.| // @title Swagger Example API |
| version | **Required.** Provides the version of the application API.| // @version 1.0 |
| description.markdown | A short description of the application. Parsed from the api.md file. This is an alternative to @description |// @description.markdown No value needed, this parses the description from api.md |
| tag.name | Name of a tag.| // @tag.name This is the name of the tag |
| tag.description.markdown | Description of the tag this is an alternative to tag.description. The description will be read from a file named like tagname.md | // @tag.description.markdown |
## API Operation
**Example**
Expand Down
24 changes: 19 additions & 5 deletions cmd/swag/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ import (
"github.com/urfave/cli"
)

const searchDirFlag = "dir"
const generalInfoFlag = "generalInfo"
const propertyStrategyFlag = "propertyStrategy"
const outputFlag = "output"
const parseVendorFlag = "parseVendor"
const markdownFilesDirFlag = "markdownFiles"

func main() {
app := cli.NewApp()
app.Version = swag.Version
Expand All @@ -20,11 +27,12 @@ func main() {
Aliases: []string{"i"},
Usage: "Create docs.go",
Action: func(c *cli.Context) error {
searchDir := c.String("dir")
mainAPIFile := c.String("generalInfo")
strategy := c.String("propertyStrategy")
outputDir := c.String("output")
parseVendor := c.Bool("parseVendor")
searchDir := c.String(searchDirFlag)
mainAPIFile := c.String(generalInfoFlag)
strategy := c.String(propertyStrategyFlag)
outputDir := c.String(outputFlag)
parseVendor := c.Bool(parseVendorFlag)
markdownFilesDir := c.String(markdownFilesDirFlag)

switch strategy {
case swag.CamelCase, swag.SnakeCase, swag.PascalCase:
Expand All @@ -38,6 +46,7 @@ func main() {
PropNamingStrategy: strategy,
OutputDir: outputDir,
ParseVendor: parseVendor,
MarkdownFilesDir: markdownFilesDir,
})
},
Flags: []cli.Flag{
Expand Down Expand Up @@ -65,6 +74,11 @@ func main() {
Name: "parseVendor",
Usage: "Parse go files in 'vendor' folder, disabled by default",
},
cli.StringFlag{
Name: "markdownFiles, md",
Value: "",
Usage: "Parse folder containing markdown files to use as description, disabled by default",
},
},
},
}
Expand Down
5 changes: 4 additions & 1 deletion gen/gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ type Config struct {

//ParseVendor whether swag should be parse vendor folder
ParseVendor bool

// MarkdownFilesDir used to find markdownfiles, which can be used for tag descriptions
MarkdownFilesDir string
}

// Build builds swagger json file for gived searchDir and mainAPIFile. Returns json
Expand All @@ -48,7 +51,7 @@ func (g *Gen) Build(config *Config) error {
}

log.Println("Generate swagger docs....")
p := swag.New()
p := swag.New(swag.SetMarkdownFileDirectory(config.MarkdownFilesDir))
p.PropNamingStrategy = config.PropNamingStrategy
p.ParseVendor = config.ParseVendor

Expand Down
68 changes: 67 additions & 1 deletion parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"go/ast"
goparser "go/parser"
"go/token"
"io/ioutil"
"net/http"
"os"
"path"
Expand Down Expand Up @@ -54,10 +55,13 @@ type Parser struct {

// structStack stores full names of the structures that were already parsed or are being parsed now
structStack []string

// markdownFileDir holds the path to the folder, where markdown files are stored
markdownFileDir string
}

// New creates a new Parser with default properties.
func New() *Parser {
func New(options ...func(*Parser)) *Parser {
parser := &Parser{
swagger: &spec.Swagger{
SwaggerProps: spec.SwaggerProps{
Expand All @@ -78,9 +82,21 @@ func New() *Parser {
CustomPrimitiveTypes: make(map[string]string),
registerTypes: make(map[string]*ast.TypeSpec),
}

for _, option := range options {
option(parser)
}

return parser
}

// SetMarkdownFileDirectory sets the directory to search for markdownfiles
func SetMarkdownFileDirectory(directoryPath string) func(*Parser) {
return func(p *Parser) {
p.markdownFileDir = directoryPath
}
}

// ParseAPI parses general api info for gived searchDir and mainAPIFile
func (parser *Parser) ParseAPI(searchDir string, mainAPIFile string) error {
Println("Generate general API Info")
Expand Down Expand Up @@ -146,6 +162,18 @@ func (parser *Parser) ParseGeneralAPIInfo(mainAPIFile string) error {
} else if multilineBlock {
parser.swagger.Info.Description += "\n" + strings.TrimSpace(commentLine[len(attribute):])
}
case "@description.markdown":
filePath, err := getMarkdownFileForTag("api", parser.markdownFileDir)
if err != nil {
return err
}

commentInfo, err := ioutil.ReadFile(parser.markdownFileDir + "/" + filePath)
if err != nil {
return errors.New("Failed to find matching markdown file for api description: " + "api" + " error: " + err.Error())
}

parser.swagger.Info.Description = string(commentInfo)
case "@termsofservice":
parser.swagger.Info.TermsOfService = strings.TrimSpace(commentLine[len(attribute):])
case "@contact.name":
Expand Down Expand Up @@ -176,6 +204,20 @@ func (parser *Parser) ParseGeneralAPIInfo(mainAPIFile string) error {
tag := parser.swagger.Tags[len(parser.swagger.Tags)-1]
tag.TagProps.Description = commentInfo
replaceLastTag(parser.swagger.Tags, tag)
case "@tag.description.markdown":
tag := parser.swagger.Tags[len(parser.swagger.Tags)-1]
filePath, err := getMarkdownFileForTag(tag.TagProps.Name, parser.markdownFileDir)
if err != nil {
return err
}

commentInfo, err := ioutil.ReadFile(parser.markdownFileDir + "/" + filePath)
if err != nil {
return errors.New("Failed to find matching markdown file for tag: " + tag.TagProps.Name + " error: " + err.Error())
}

tag.TagProps.Description = string(commentInfo)
replaceLastTag(parser.swagger.Tags, tag)
case "@tag.docs.url":
commentInfo := strings.TrimSpace(commentLine[len(attribute):])
tag := parser.swagger.Tags[len(parser.swagger.Tags)-1]
Expand Down Expand Up @@ -360,6 +402,30 @@ func (parser *Parser) ParseGeneralAPIInfo(mainAPIFile string) error {
return nil
}

func getMarkdownFileForTag(tagName string, dirPath string) (string, error) {
filesInfos, err := ioutil.ReadDir(dirPath)
if err != nil {
return "", err
}

for _, fileInfo := range filesInfos {
if fileInfo.IsDir() {
continue
}
fileName := fileInfo.Name()

if !strings.Contains(fileName, ".md") {
continue
}

if strings.Contains(fileName, tagName) {
return fileName, nil
}
}

return "", errors.New("Unable to find Markdown file in the given directory")
}

func getScopeScheme(scope string) (string, error) {
scopeValue := scope[strings.Index(scope, "@scope."):]
if scopeValue == "" {
Expand Down
55 changes: 43 additions & 12 deletions parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2522,32 +2522,63 @@ func TestSkipMustParseVendor(t *testing.T) {
func TestApiParseTag(t *testing.T) {
searchDir := "testdata/tags"
mainAPIFile := "main.go"
p := New()
p := New(SetMarkdownFileDirectory(searchDir))
p.PropNamingStrategy = PascalCase
err := p.ParseAPI(searchDir, mainAPIFile)
assert.NoError(t, err)

if len(p.swagger.Tags) != 2 {
t.Log("Number of tags did not match")
t.Fail()
if len(p.swagger.Tags) != 3 {
t.Error("Number of tags did not match")
}

dogs := p.swagger.Tags[0]
if dogs.TagProps.Name != "dogs" || dogs.TagProps.Description != "Dogs are cool" {
t.Log("Failed to parse dogs name or description")
t.Fail()
t.Error("Failed to parse dogs name or description")
}

cats := p.swagger.Tags[1]
if cats.TagProps.Name != "cats" || cats.TagProps.Description != "Cats are the devil" {
t.Log("Failed to parse cats name or description")
t.Fail()
t.Error("Failed to parse cats name or description")
}

if cats.TagProps.ExternalDocs.URL != "https://google.de" || cats.TagProps.ExternalDocs.Description != "google is super useful to find out that cats are evil!" {
t.Log("URL: ", cats.TagProps.ExternalDocs.URL)
t.Log("Description: ", cats.TagProps.ExternalDocs.Description)
t.Log("Failed to parse cats external documentation")
t.Fail()
t.Error("URL: ", cats.TagProps.ExternalDocs.URL)
t.Error("Description: ", cats.TagProps.ExternalDocs.Description)
t.Error("Failed to parse cats external documentation")
}
}

func TestParseTagMarkdownDescription(t *testing.T) {
searchDir := "testdata/tags"
mainAPIFile := "main.go"
p := New(SetMarkdownFileDirectory(searchDir))
p.PropNamingStrategy = PascalCase
err := p.ParseAPI(searchDir, mainAPIFile)
if err != nil {
t.Error("Failed to parse api description: " + err.Error())
}

if len(p.swagger.Tags) != 3 {
t.Error("Number of tags did not match")
}

apes := p.swagger.Tags[2]
if apes.TagProps.Description == "" {
t.Error("Failed to parse tag description markdown file")
}
}

func TestParseApiMarkdownDescription(t *testing.T) {
searchDir := "testdata/tags"
mainAPIFile := "main.go"
p := New(SetMarkdownFileDirectory(searchDir))
p.PropNamingStrategy = PascalCase
err := p.ParseAPI(searchDir, mainAPIFile)
if err != nil {
t.Error("Failed to parse api description: " + err.Error())
}

if p.swagger.Info.Description == "" {
t.Error("Failed to parse api description: " + err.Error())
}
}
11 changes: 7 additions & 4 deletions property_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,23 +274,26 @@ func TestGetPropertyNameInterface(t *testing.T) {
func TestParseTag(t *testing.T) {
searchDir := "testdata/tags"
mainAPIFile := "main.go"
p := New()
p := New(SetMarkdownFileDirectory(searchDir))
p.PropNamingStrategy = PascalCase
err := p.ParseAPI(searchDir, mainAPIFile)
assert.NoError(t, err)

if len(p.swagger.Tags) != 2 {
if len(p.swagger.Tags) != 3 {
t.Log(len(p.swagger.Tags))
t.Log("Number of tags did not match")
t.Fail()
t.FailNow()
}

dogs := p.swagger.Tags[0]
if dogs.TagProps.Name != "dogs" || dogs.TagProps.Description != "Dogs are cool" {
t.Log("Failed to parse dogs name or description")
t.FailNow()
}

cats := p.swagger.Tags[1]
if cats.TagProps.Name != "cats" || dogs.TagProps.Description != "Cats are the devil" {
if cats.TagProps.Name != "cats" || cats.TagProps.Description != "Cats are the devil" {
t.Log("Failed to parse cats name or description")
t.FailNow()
}
}
2 changes: 1 addition & 1 deletion testdata/simple2/docs/docs.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// GENERATED BY THE COMMAND ABOVE; DO NOT EDIT
// This file was generated by swaggo/swag at
// 2019-06-12 13:12:22.266776997 +0200 CEST m=+0.119881542
// 2019-06-12 14:11:08.278195965 +0200 CEST m=+0.065173340

package docs

Expand Down
2 changes: 1 addition & 1 deletion testdata/simple3/docs/docs.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// GENERATED BY THE COMMAND ABOVE; DO NOT EDIT
// This file was generated by swaggo/swag at
// 2019-06-12 13:12:22.317087441 +0200 CEST m=+0.170192113
// 2019-06-12 14:11:08.291157752 +0200 CEST m=+0.078135144

package docs

Expand Down
3 changes: 3 additions & 0 deletions testdata/tags/apes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## Apes

Apes are very cool!
5 changes: 5 additions & 0 deletions testdata/tags/api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
## CoolApi Title

### Cool API SubTitle

We love markdown!
3 changes: 3 additions & 0 deletions testdata/tags/main.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package main

// @description.markdown
// @tag.name dogs
// @tag.description Dogs are cool
// @tag.name cats
// @tag.description Cats are the devil
// @tag.docs.url https://google.de
// @tag.docs.description google is super useful to find out that cats are evil!
// @tag.name apes
// @tag.description.markdown
func main() {}

0 comments on commit 6170d1b

Please sign in to comment.