Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to set template delimiters #1499

Merged
merged 10 commits into from
Apr 17, 2023
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ dist
testdata/simple*/docs
testdata/quotes/docs
testdata/quotes/quotes.so
testdata/delims/docs
testdata/delims/delims.so
example/basic/docs/*
example/celler/docs/*
cover.out
Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ OPTIONS:
--overridesFile value File to read global type overrides from. (default: ".swaggo")
--parseGoList Parse dependency via 'go list' (default: true)
--tags value, -t value A comma-separated list of tags to filter the APIs for which the documentation is generated.Special case if the tag is prefixed with the '!' character then the APIs with that tag will be excluded
--templateDelims value, --td value Provide custom delimeters for Go template generation. The format is leftDelim,rightDelim. For example: "[[,]]"
--collectionFormat value, --cf value Set default collection format (default: "csv")
--help, -h show help (default: false)
```
Expand Down Expand Up @@ -908,6 +909,18 @@ By default `swag` command generates Swagger specification in three different fil

If you would like to limit a set of file types which should be generated you can use `--outputTypes` (short `-ot`) flag. Default value is `go,json,yaml` - output types separated with comma. To limit output only to `go` and `yaml` files, you would write `go,yaml`. With complete command that would be `swag init --outputTypes go,yaml`.

### Change the default Go Template action delimiters
[#980](https:/swaggo/swag/issues/980)
[#1177](https:/swaggo/swag/issues/1177)

If your swagger annotations or struct fields contain "{{" or "}}", the template generation will most likely fail, as these are the default delimiters for [go templates](https://pkg.go.dev/text/template#Template.Delims).

To make the generation work properly, you can change the default delimiters with `-td`. For example:
```console
swag init -g http/api.go -td "[[,]]"
```
The new delimiter is a string with the format "`<left delimiter>`,`<right delimiter>`".

## About the Project
This project was inspired by [yvasiyarov/swagger](https:/yvasiyarov/swagger) but we simplified the usage and added support a variety of [web frameworks](#supported-web-frameworks). Gopher image source is [tenntenn/gopher-stickers](https:/tenntenn/gopher-stickers). It has licenses [creative commons licensing](http://creativecommons.org/licenses/by/3.0/deed.en).
## Contributors
Expand Down
21 changes: 21 additions & 0 deletions cmd/swag/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const (
quietFlag = "quiet"
tagsFlag = "tags"
parseExtensionFlag = "parseExtension"
templateDelimsFlag = "templateDelims"
packageName = "packageName"
collectionFormatFlag = "collectionFormat"
)
Expand Down Expand Up @@ -143,6 +144,12 @@ var initFlags = []cli.Flag{
Value: "",
Usage: "A comma-separated list of tags to filter the APIs for which the documentation is generated.Special case if the tag is prefixed with the '!' character then the APIs with that tag will be excluded",
},
&cli.StringFlag{
Name: templateDelimsFlag,
Aliases: []string{"td"},
Value: "",
Usage: "Provide custom delimeters for Go template generation. The format is leftDelim,rightDelim. For example: \"[[,]]\"",
},
&cli.StringFlag{
Name: packageName,
Value: "",
Expand All @@ -165,6 +172,18 @@ func initAction(ctx *cli.Context) error {
return fmt.Errorf("not supported %s propertyStrategy", strategy)
}

leftDelim, rightDelim := "{{", "}}"

if ctx.IsSet(templateDelimsFlag) {
delims := strings.Split(ctx.String(templateDelimsFlag), ",")
if len(delims) != 2 {
return fmt.Errorf("exactly two template delimeters must be provided, comma separated")
} else if delims[0] == delims[1] {
return fmt.Errorf("template delimiters must be different")
}
leftDelim, rightDelim = strings.TrimSpace(delims[0]), strings.TrimSpace(delims[1])
}

outputTypes := strings.Split(ctx.String(outputTypesFlag), ",")
if len(outputTypes) == 0 {
return fmt.Errorf("no output types specified")
Expand Down Expand Up @@ -199,6 +218,8 @@ func initAction(ctx *cli.Context) error {
OverridesFile: ctx.String(overridesFileFlag),
ParseGoList: ctx.Bool(parseGoListFlag),
Tags: ctx.String(tagsFlag),
LeftTemplateDelim: leftDelim,
RightTemplateDelim: rightDelim,
PackageName: ctx.String(packageName),
Debugger: logger,
CollectionFormat: collectionFormat,
Expand Down
76 changes: 48 additions & 28 deletions gen/gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,12 @@ type Config struct {
// 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

Expand All @@ -150,6 +156,14 @@ func (g *Gen) Build(config *Config) error {
}
}

if config.LeftTemplateDelim == "" {
config.LeftTemplateDelim = "{{"
}

if config.RightTemplateDelim == "" {
config.RightTemplateDelim = "}}"
}

var overrides map[string]string

if config.OverridesFile != "" {
Expand Down Expand Up @@ -377,7 +391,7 @@ func (g *Gen) writeGoDoc(packageName string, output io.Writer, swagger *spec.Swa
generator, err := template.New("swagger_info").Funcs(template.FuncMap{
"printDoc": func(v string) string {
// Add schemes
v = "{\n \"schemes\": {{ marshal .Schemes }}," + v[1:]
v = "{\n \"schemes\": " + config.LeftTemplateDelim + " marshal .Schemes " + config.RightTemplateDelim + "," + v[1:]
// Sanitize backticks
return strings.Replace(v, "`", "`+\"`\"+`", -1)
},
Expand All @@ -396,16 +410,16 @@ func (g *Gen) writeGoDoc(packageName string, output io.Writer, swagger *spec.Swa
Info: &spec.Info{
VendorExtensible: swagger.Info.VendorExtensible,
InfoProps: spec.InfoProps{
Description: "{{escape .Description}}",
Title: "{{.Title}}",
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: "{{.Version}}",
Version: config.LeftTemplateDelim + ".Version" + config.RightTemplateDelim,
},
},
Host: "{{.Host}}",
BasePath: "{{.BasePath}}",
Host: config.LeftTemplateDelim + ".Host" + config.RightTemplateDelim,
BasePath: config.LeftTemplateDelim + ".BasePath" + config.RightTemplateDelim,
Paths: swagger.Paths,
Definitions: swagger.Definitions,
Parameters: swagger.Parameters,
Expand All @@ -426,29 +440,33 @@ func (g *Gen) writeGoDoc(packageName string, output io.Writer, swagger *spec.Swa
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
InstanceName string
Schemes []string
GeneratedTime bool
Timestamp time.Time
Doc string
Host string
PackageName string
BasePath string
Title string
Description string
Version 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,
InstanceName: config.InstanceName,
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,
InstanceName: config.InstanceName,
LeftTemplateDelim: config.LeftTemplateDelim,
RightTemplateDelim: config.RightTemplateDelim,
})
if err != nil {
return err
Expand Down Expand Up @@ -480,6 +498,8 @@ var SwaggerInfo{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }}
Description: {{ printf "%q" .Description}},
InfoInstanceName: {{ printf "%q" .InstanceName }},
SwaggerTemplate: docTemplate{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }},
LeftDelim: {{ printf "%q" .LeftTemplateDelim}},
RightDelim: {{ printf "%q" .RightTemplateDelim}},
}

func init() {
Expand Down
61 changes: 61 additions & 0 deletions gen/gen_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,67 @@ func TestGen_BuildDescriptionWithQuotes(t *testing.T) {
assert.JSONEq(t, string(expectedJSON), jsonOutput)
}

func TestGen_BuildDocCustomDelims(t *testing.T) {
config := &Config{
SearchDir: "../testdata/delims",
MainAPIFile: "./main.go",
OutputDir: "../testdata/delims/docs",
OutputTypes: outputTypes,
MarkdownFilesDir: "../testdata/delims",
InstanceName: "CustomDelims",
LeftTemplateDelim: "{%",
RightTemplateDelim: "%}",
}

require.NoError(t, New().Build(config))

expectedFiles := []string{
filepath.Join(config.OutputDir, "CustomDelims_docs.go"),
filepath.Join(config.OutputDir, "CustomDelims_swagger.json"),
filepath.Join(config.OutputDir, "CustomDelims_swagger.yaml"),
}
for _, expectedFile := range expectedFiles {
if _, err := os.Stat(expectedFile); os.IsNotExist(err) {
require.NoError(t, err)
}
}

cmd := exec.Command("go", "build", "-buildmode=plugin", "github.com/swaggo/swag/testdata/delims")

cmd.Dir = config.SearchDir

output, err := cmd.CombinedOutput()
if err != nil {
require.NoError(t, err, string(output))
}

p, err := plugin.Open(filepath.Join(config.SearchDir, "delims.so"))
if err != nil {
require.NoError(t, err)
}

defer os.Remove("delims.so")

readDoc, err := p.Lookup("ReadDoc")
if err != nil {
require.NoError(t, err)
}

jsonOutput := readDoc.(func() string)()

var jsonDoc interface{}
if err := json.Unmarshal([]byte(jsonOutput), &jsonDoc); err != nil {
require.NoError(t, err)
}

expectedJSON, err := os.ReadFile(filepath.Join(config.SearchDir, "expected.json"))
if err != nil {
require.NoError(t, err)
}

assert.JSONEq(t, string(expectedJSON), jsonOutput)
}

func TestGen_jsonIndent(t *testing.T) {
config := &Config{
SearchDir: searchDir,
Expand Down
14 changes: 11 additions & 3 deletions spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@ type Spec struct {
Description string
InfoInstanceName string
SwaggerTemplate string
LeftDelim string
RightDelim string
}

// ReadDoc parses SwaggerTemplate into swagger document.
func (i *Spec) ReadDoc() string {
i.Description = strings.ReplaceAll(i.Description, "\n", "\\n")

tpl, err := template.New("swagger_info").Funcs(template.FuncMap{
tpl := template.New("swagger_info").Funcs(template.FuncMap{
"marshal": func(v interface{}) string {
a, _ := json.Marshal(v)

Expand All @@ -37,13 +39,19 @@ func (i *Spec) ReadDoc() string {

return strings.ReplaceAll(str, "\\\\\"", "\\\\\\\"")
},
}).Parse(i.SwaggerTemplate)
})

if i.LeftDelim != "" && i.RightDelim != "" {
tpl = tpl.Delims(i.LeftDelim, i.RightDelim)
}

parsed, err := tpl.Parse(i.SwaggerTemplate)
if err != nil {
return i.SwaggerTemplate
}

var doc bytes.Buffer
if err = tpl.Execute(&doc, i); err != nil {
if err = parsed.Execute(&doc, i); err != nil {
return i.SwaggerTemplate
}

Expand Down
35 changes: 35 additions & 0 deletions spec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ func TestSpec_ReadDoc(t *testing.T) {
Description string
InfoInstanceName string
SwaggerTemplate string
LeftDelim string
RightDelim string
}

tests := []struct {
Expand Down Expand Up @@ -132,6 +134,37 @@ func TestSpec_ReadDoc(t *testing.T) {
},
want: "{{ .Schemesa }}",
},
{
name: "TestReadDocCustomDelims",
fields: fields{
Version: "1.0",
Host: "localhost:8080",
BasePath: "/",
InfoInstanceName: "TestInstanceName",
SwaggerTemplate: `{
"swagger": "2.0",
"info": {
"description": "{%escape .Description%}",
"title": "{%.Title%}",
"version": "{%.Version%}"
},
"host": "{%.Host%}",
"basePath": "{%.BasePath%}",
}`,
LeftDelim: "{%",
RightDelim: "%}",
},
want: "{" +
"\n\t\t\t\"swagger\": \"2.0\"," +
"\n\t\t\t\"info\": {" +
"\n\t\t\t\t\"description\": \"\",\n\t\t\t\t\"" +
"title\": \"\"," +
"\n\t\t\t\t\"version\": \"1.0\"" +
"\n\t\t\t}," +
"\n\t\t\t\"host\": \"localhost:8080\"," +
"\n\t\t\t\"basePath\": \"/\"," +
"\n\t\t}",
},
}

for _, tt := range tests {
Expand All @@ -145,6 +178,8 @@ func TestSpec_ReadDoc(t *testing.T) {
Description: tt.fields.Description,
InfoInstanceName: tt.fields.InfoInstanceName,
SwaggerTemplate: tt.fields.SwaggerTemplate,
LeftDelim: tt.fields.LeftDelim,
RightDelim: tt.fields.RightDelim,
}

assert.Equal(t, tt.want, doc.ReadDoc())
Expand Down
11 changes: 11 additions & 0 deletions testdata/delims/api/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package api

// MyFunc godoc
// @Description My Function
// @Success 200 {object} MyStruct
// @Router /myfunc [get]
func MyFunc() {}

type MyStruct struct {
URLTemplate string `json:"urltemplate" example:"http://example.org/{{ path }}" swaggertype:"string"`
}
Loading