541 lines
14 KiB
Go
541 lines
14 KiB
Go
package gen
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"go/format"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
"text/template"
|
|
"time"
|
|
|
|
"git.ipao.vip/rogeecn/atomctl/pkg/swag"
|
|
"github.com/go-openapi/spec"
|
|
"golang.org/x/text/cases"
|
|
"golang.org/x/text/language"
|
|
"sigs.k8s.io/yaml"
|
|
)
|
|
|
|
var open = os.Open
|
|
|
|
// DefaultOverridesFile is the location swagger will look for type overrides.
|
|
const DefaultOverridesFile = ".swaggo"
|
|
|
|
type genTypeWriter func(*Config, *spec.Swagger) error
|
|
|
|
// Gen presents a generate tool for swag.
|
|
type Gen struct {
|
|
json func(data interface{}) ([]byte, error)
|
|
jsonIndent func(data interface{}) ([]byte, error)
|
|
jsonToYAML func(data []byte) ([]byte, error)
|
|
outputTypeMap map[string]genTypeWriter
|
|
debug Debugger
|
|
}
|
|
|
|
// Debugger is the interface that wraps the basic Printf method.
|
|
type Debugger interface {
|
|
Printf(format string, v ...interface{})
|
|
}
|
|
|
|
// New creates a new Gen.
|
|
func New() *Gen {
|
|
gen := Gen{
|
|
json: json.Marshal,
|
|
jsonIndent: func(data interface{}) ([]byte, error) {
|
|
return json.MarshalIndent(data, "", " ")
|
|
},
|
|
jsonToYAML: yaml.JSONToYAML,
|
|
debug: log.New(os.Stdout, "", log.LstdFlags),
|
|
}
|
|
|
|
gen.outputTypeMap = map[string]genTypeWriter{
|
|
"go": gen.writeDocSwagger,
|
|
"json": gen.writeJSONSwagger,
|
|
"yaml": gen.writeYAMLSwagger,
|
|
"yml": gen.writeYAMLSwagger,
|
|
}
|
|
|
|
return &gen
|
|
}
|
|
|
|
// Config presents Gen configurations.
|
|
type Config struct {
|
|
Debugger swag.Debugger
|
|
|
|
// SearchDir the swag would parse,comma separated if multiple
|
|
SearchDir string
|
|
|
|
// excludes dirs and files in SearchDir,comma separated
|
|
Excludes string
|
|
|
|
// outputs only specific extension
|
|
ParseExtension string
|
|
|
|
// OutputDir represents the output directory for all the generated files
|
|
OutputDir string
|
|
|
|
// OutputTypes define types of files which should be generated
|
|
OutputTypes []string
|
|
|
|
// MainAPIFile the Go file path in which 'swagger general API Info' is written
|
|
MainAPIFile string
|
|
|
|
// PropNamingStrategy represents property naming strategy like snake case,camel case,pascal case
|
|
PropNamingStrategy string
|
|
|
|
// MarkdownFilesDir used to find markdown files, which can be used for tag descriptions
|
|
MarkdownFilesDir string
|
|
|
|
// CodeExampleFilesDir used to find code example files, which can be used for x-codeSamples
|
|
CodeExampleFilesDir string
|
|
|
|
// InstanceName is used to get distinct names for different swagger documents in the
|
|
// same project. The default value is "swagger".
|
|
InstanceName string
|
|
|
|
// ParseDepth dependency parse depth
|
|
ParseDepth int
|
|
|
|
// ParseVendor whether swag should be parse vendor folder
|
|
ParseVendor bool
|
|
|
|
// ParseDependencies whether swag should be parse outside dependency folder: 0 none, 1 models, 2 operations, 3 all
|
|
ParseDependency int
|
|
|
|
// ParseInternal whether swag should parse internal packages
|
|
ParseInternal bool
|
|
|
|
// Strict whether swag should error or warn when it detects cases which are most likely user errors
|
|
Strict bool
|
|
|
|
// GeneratedTime whether swag should generate the timestamp at the top of docs.go
|
|
GeneratedTime bool
|
|
|
|
// RequiredByDefault set validation required for all fields by default
|
|
RequiredByDefault bool
|
|
|
|
// OverridesFile defines global type overrides.
|
|
OverridesFile string
|
|
|
|
// ParseGoList whether swag use go list to parse dependency
|
|
ParseGoList bool
|
|
|
|
// include only tags mentioned when searching, comma separated
|
|
Tags string
|
|
|
|
// LeftTemplateDelim defines the left delimiter for the template generation
|
|
LeftTemplateDelim string
|
|
|
|
// RightTemplateDelim defines the right delimiter for the template generation
|
|
RightTemplateDelim string
|
|
|
|
// PackageName defines package name of generated `docs.go`
|
|
PackageName string
|
|
|
|
// CollectionFormat set default collection format
|
|
CollectionFormat string
|
|
|
|
// Parse only packages whose import path match the given prefix, comma separated
|
|
PackagePrefix string
|
|
|
|
// State set host state
|
|
State string
|
|
|
|
// ParseFuncBody whether swag should parse api info inside of funcs
|
|
ParseFuncBody bool
|
|
}
|
|
|
|
// Build builds swagger json file for given searchDir and mainAPIFile. Returns json.
|
|
func (g *Gen) Build(config *Config) error {
|
|
if config.Debugger != nil {
|
|
g.debug = config.Debugger
|
|
}
|
|
if config.InstanceName == "" {
|
|
config.InstanceName = swag.Name
|
|
}
|
|
|
|
searchDirs := strings.Split(config.SearchDir, ",")
|
|
for _, searchDir := range searchDirs {
|
|
if _, err := os.Stat(searchDir); os.IsNotExist(err) {
|
|
return fmt.Errorf("dir: %s does not exist", searchDir)
|
|
}
|
|
}
|
|
|
|
if config.LeftTemplateDelim == "" {
|
|
config.LeftTemplateDelim = "{{"
|
|
}
|
|
|
|
if config.RightTemplateDelim == "" {
|
|
config.RightTemplateDelim = "}}"
|
|
}
|
|
|
|
var overrides map[string]string
|
|
|
|
if config.OverridesFile != "" {
|
|
overridesFile, err := open(config.OverridesFile)
|
|
if err != nil {
|
|
// Don't bother reporting if the default file is missing; assume there are no overrides
|
|
if !(config.OverridesFile == DefaultOverridesFile && os.IsNotExist(err)) {
|
|
return fmt.Errorf("could not open overrides file: %w", err)
|
|
}
|
|
} else {
|
|
g.debug.Printf("Using overrides from %s", config.OverridesFile)
|
|
|
|
overrides, err = parseOverrides(overridesFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
g.debug.Printf("Generate swagger docs....")
|
|
|
|
p := swag.New(
|
|
swag.SetParseDependency(config.ParseDependency),
|
|
swag.SetMarkdownFileDirectory(config.MarkdownFilesDir),
|
|
swag.SetDebugger(config.Debugger),
|
|
swag.SetExcludedDirsAndFiles(config.Excludes),
|
|
swag.SetParseExtension(config.ParseExtension),
|
|
swag.SetCodeExamplesDirectory(config.CodeExampleFilesDir),
|
|
swag.SetStrict(config.Strict),
|
|
swag.SetOverrides(overrides),
|
|
swag.ParseUsingGoList(config.ParseGoList),
|
|
swag.SetTags(config.Tags),
|
|
swag.SetCollectionFormat(config.CollectionFormat),
|
|
swag.SetPackagePrefix(config.PackagePrefix),
|
|
)
|
|
|
|
p.PropNamingStrategy = config.PropNamingStrategy
|
|
p.ParseVendor = config.ParseVendor
|
|
p.ParseInternal = config.ParseInternal
|
|
p.RequiredByDefault = config.RequiredByDefault
|
|
p.HostState = config.State
|
|
p.ParseFuncBody = config.ParseFuncBody
|
|
|
|
if err := p.ParseAPIMultiSearchDir(searchDirs, config.MainAPIFile, config.ParseDepth); err != nil {
|
|
return err
|
|
}
|
|
|
|
swagger := p.GetSwagger()
|
|
|
|
if err := os.MkdirAll(config.OutputDir, os.ModePerm); err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, outputType := range config.OutputTypes {
|
|
outputType = strings.ToLower(strings.TrimSpace(outputType))
|
|
if typeWriter, ok := g.outputTypeMap[outputType]; ok {
|
|
if err := typeWriter(config, swagger); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
log.Printf("output type '%s' not supported", outputType)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (g *Gen) writeDocSwagger(config *Config, swagger *spec.Swagger) error {
|
|
filename := "docs.go"
|
|
|
|
if config.State != "" {
|
|
filename = config.State + "_" + filename
|
|
}
|
|
|
|
if config.InstanceName != swag.Name {
|
|
filename = config.InstanceName + "_" + filename
|
|
}
|
|
|
|
docFileName := path.Join(config.OutputDir, filename)
|
|
|
|
absOutputDir, err := filepath.Abs(config.OutputDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var packageName string
|
|
if len(config.PackageName) > 0 {
|
|
packageName = config.PackageName
|
|
} else {
|
|
packageName = filepath.Base(absOutputDir)
|
|
packageName = strings.ReplaceAll(packageName, "-", "_")
|
|
}
|
|
|
|
docs, err := os.Create(docFileName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer docs.Close()
|
|
|
|
// Write doc
|
|
err = g.writeGoDoc(packageName, docs, swagger, config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
g.debug.Printf("create docs.go at %+v", docFileName)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (g *Gen) writeJSONSwagger(config *Config, swagger *spec.Swagger) error {
|
|
filename := "swagger.json"
|
|
|
|
if config.State != "" {
|
|
filename = config.State + "_" + filename
|
|
}
|
|
|
|
if config.InstanceName != swag.Name {
|
|
filename = config.InstanceName + "_" + filename
|
|
}
|
|
|
|
jsonFileName := path.Join(config.OutputDir, filename)
|
|
|
|
b, err := g.jsonIndent(swagger)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = g.writeFile(b, jsonFileName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
g.debug.Printf("create swagger.json at %+v", jsonFileName)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (g *Gen) writeYAMLSwagger(config *Config, swagger *spec.Swagger) error {
|
|
filename := "swagger.yaml"
|
|
|
|
if config.State != "" {
|
|
filename = config.State + "_" + filename
|
|
}
|
|
|
|
if config.InstanceName != swag.Name {
|
|
filename = config.InstanceName + "_" + filename
|
|
}
|
|
|
|
yamlFileName := path.Join(config.OutputDir, filename)
|
|
|
|
b, err := g.json(swagger)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
y, err := g.jsonToYAML(b)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot covert json to yaml error: %s", err)
|
|
}
|
|
|
|
err = g.writeFile(y, yamlFileName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
g.debug.Printf("create swagger.yaml at %+v", yamlFileName)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (g *Gen) writeFile(b []byte, file string) error {
|
|
f, err := os.Create(file)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer f.Close()
|
|
|
|
_, err = f.Write(b)
|
|
|
|
return err
|
|
}
|
|
|
|
func (g *Gen) formatSource(src []byte) []byte {
|
|
code, err := format.Source(src)
|
|
if err != nil {
|
|
code = src // Formatter failed, return original code.
|
|
}
|
|
|
|
return code
|
|
}
|
|
|
|
// Read and parse the overrides file.
|
|
func parseOverrides(r io.Reader) (map[string]string, error) {
|
|
overrides := make(map[string]string)
|
|
scanner := bufio.NewScanner(r)
|
|
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
|
|
// Skip comments
|
|
if len(line) > 1 && line[0:2] == "//" {
|
|
continue
|
|
}
|
|
|
|
parts := strings.Fields(line)
|
|
|
|
switch len(parts) {
|
|
case 0:
|
|
// only whitespace
|
|
continue
|
|
case 2:
|
|
// either a skip or malformed
|
|
if parts[0] != "skip" {
|
|
return nil, fmt.Errorf("could not parse override: '%s'", line)
|
|
}
|
|
|
|
overrides[parts[1]] = ""
|
|
case 3:
|
|
// either a replace or malformed
|
|
if parts[0] != "replace" {
|
|
return nil, fmt.Errorf("could not parse override: '%s'", line)
|
|
}
|
|
|
|
overrides[parts[1]] = parts[2]
|
|
default:
|
|
return nil, fmt.Errorf("could not parse override: '%s'", line)
|
|
}
|
|
}
|
|
|
|
if err := scanner.Err(); err != nil {
|
|
return nil, fmt.Errorf("error reading overrides file: %w", err)
|
|
}
|
|
|
|
return overrides, nil
|
|
}
|
|
|
|
func (g *Gen) writeGoDoc(packageName string, output io.Writer, swagger *spec.Swagger, config *Config) error {
|
|
generator, err := template.New("swagger_info").Funcs(template.FuncMap{
|
|
"printDoc": func(v string) string {
|
|
// Add schemes
|
|
v = "{\n \"schemes\": " + config.LeftTemplateDelim + " marshal .Schemes " + config.RightTemplateDelim + "," + v[1:]
|
|
// Sanitize backticks
|
|
return strings.Replace(v, "`", "`+\"`\"+`", -1)
|
|
},
|
|
}).Parse(packageTemplate)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
swaggerSpec := &spec.Swagger{
|
|
VendorExtensible: swagger.VendorExtensible,
|
|
SwaggerProps: spec.SwaggerProps{
|
|
ID: swagger.ID,
|
|
Consumes: swagger.Consumes,
|
|
Produces: swagger.Produces,
|
|
Swagger: swagger.Swagger,
|
|
Info: &spec.Info{
|
|
VendorExtensible: swagger.Info.VendorExtensible,
|
|
InfoProps: spec.InfoProps{
|
|
Description: config.LeftTemplateDelim + "escape .Description" + config.RightTemplateDelim,
|
|
Title: config.LeftTemplateDelim + ".Title" + config.RightTemplateDelim,
|
|
TermsOfService: swagger.Info.TermsOfService,
|
|
Contact: swagger.Info.Contact,
|
|
License: swagger.Info.License,
|
|
Version: config.LeftTemplateDelim + ".Version" + config.RightTemplateDelim,
|
|
},
|
|
},
|
|
Host: config.LeftTemplateDelim + ".Host" + config.RightTemplateDelim,
|
|
BasePath: config.LeftTemplateDelim + ".BasePath" + config.RightTemplateDelim,
|
|
Paths: swagger.Paths,
|
|
Definitions: swagger.Definitions,
|
|
Parameters: swagger.Parameters,
|
|
Responses: swagger.Responses,
|
|
SecurityDefinitions: swagger.SecurityDefinitions,
|
|
Security: swagger.Security,
|
|
Tags: swagger.Tags,
|
|
ExternalDocs: swagger.ExternalDocs,
|
|
},
|
|
}
|
|
|
|
// crafted docs.json
|
|
buf, err := g.jsonIndent(swaggerSpec)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
state := ""
|
|
if len(config.State) > 0 {
|
|
state = cases.Title(language.English).String(strings.ToLower(config.State))
|
|
}
|
|
|
|
buffer := &bytes.Buffer{}
|
|
|
|
err = generator.Execute(buffer, struct {
|
|
Timestamp time.Time
|
|
Doc string
|
|
Host string
|
|
PackageName string
|
|
BasePath string
|
|
Title string
|
|
Description string
|
|
Version string
|
|
State string
|
|
InstanceName string
|
|
Schemes []string
|
|
GeneratedTime bool
|
|
LeftTemplateDelim string
|
|
RightTemplateDelim string
|
|
}{
|
|
Timestamp: time.Now(),
|
|
GeneratedTime: config.GeneratedTime,
|
|
Doc: string(buf),
|
|
Host: swagger.Host,
|
|
PackageName: packageName,
|
|
BasePath: swagger.BasePath,
|
|
Schemes: swagger.Schemes,
|
|
Title: swagger.Info.Title,
|
|
Description: swagger.Info.Description,
|
|
Version: swagger.Info.Version,
|
|
State: state,
|
|
InstanceName: config.InstanceName,
|
|
LeftTemplateDelim: config.LeftTemplateDelim,
|
|
RightTemplateDelim: config.RightTemplateDelim,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
code := g.formatSource(buffer.Bytes())
|
|
|
|
// write
|
|
_, err = output.Write(code)
|
|
|
|
return err
|
|
}
|
|
|
|
var packageTemplate = `// Package {{.PackageName}} Code generated by swaggo/swag{{ if .GeneratedTime }} at {{ .Timestamp }}{{ end }}. DO NOT EDIT
|
|
package {{.PackageName}}
|
|
|
|
import "github.com/swaggo/swag"
|
|
|
|
const docTemplate{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }}{{ .State }} = ` + "`{{ printDoc .Doc}}`" + `
|
|
|
|
// Swagger{{ .State }}Info{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }} holds exported Swagger Info so clients can modify it
|
|
var Swagger{{ .State }}Info{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }} = &swag.Spec{
|
|
Version: {{ printf "%q" .Version}},
|
|
Host: {{ printf "%q" .Host}},
|
|
BasePath: {{ printf "%q" .BasePath}},
|
|
Schemes: []string{ {{ range $index, $schema := .Schemes}}{{if gt $index 0}},{{end}}{{printf "%q" $schema}}{{end}} },
|
|
Title: {{ printf "%q" .Title}},
|
|
Description: {{ printf "%q" .Description}},
|
|
InfoInstanceName: {{ printf "%q" .InstanceName }},
|
|
SwaggerTemplate: docTemplate{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }}{{ .State }},
|
|
LeftDelim: {{ printf "%q" .LeftTemplateDelim}},
|
|
RightDelim: {{ printf "%q" .RightTemplateDelim}},
|
|
}
|
|
|
|
func init() {
|
|
swag.Register(Swagger{{ .State }}Info{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }}.InstanceName(), Swagger{{ .State }}Info{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }})
|
|
}
|
|
`
|