Skip to content

Commit d4218f2

Browse files
authored
Struct fields supported for header and path param types (#1740)
* Support object data types for header params Add initial struct test for header names and validation. * Add form and query struct test for operations * Operation param add path struct model support and tests wip: fix merge
1 parent 76695ca commit d4218f2

File tree

5 files changed

+207
-24
lines changed

5 files changed

+207
-24
lines changed

field_parser.go

+14-2
Original file line numberDiff line numberDiff line change
@@ -96,13 +96,25 @@ func (ps *tagBaseFieldParser) FieldName() (string, error) {
9696
}
9797
}
9898

99-
func (ps *tagBaseFieldParser) FormName() string {
99+
func (ps *tagBaseFieldParser) firstTagValue(tag string) string {
100100
if ps.field.Tag != nil {
101-
return strings.TrimRight(strings.TrimSpace(strings.Split(ps.tag.Get(formTag), ",")[0]), "[]")
101+
return strings.TrimRight(strings.TrimSpace(strings.Split(ps.tag.Get(tag), ",")[0]), "[]")
102102
}
103103
return ""
104104
}
105105

106+
func (ps *tagBaseFieldParser) FormName() string {
107+
return ps.firstTagValue(formTag)
108+
}
109+
110+
func (ps *tagBaseFieldParser) HeaderName() string {
111+
return ps.firstTagValue(headerTag)
112+
}
113+
114+
func (ps *tagBaseFieldParser) PathName() string {
115+
return ps.firstTagValue(uriTag)
116+
}
117+
106118
func toSnakeCase(in string) string {
107119
var (
108120
runes = []rune(in)

operation.go

+13-17
Original file line numberDiff line numberDiff line change
@@ -286,16 +286,7 @@ func (operation *Operation) ParseParamComment(commentLine string, astFile *ast.F
286286
param := createParameter(paramType, description, name, objectType, refType, required, enums, operation.parser.collectionFormatInQuery)
287287

288288
switch paramType {
289-
case "path", "header":
290-
switch objectType {
291-
case ARRAY:
292-
if !IsPrimitiveType(refType) {
293-
return fmt.Errorf("%s is not supported array type for %s", refType, paramType)
294-
}
295-
case OBJECT:
296-
return fmt.Errorf("%s is not supported type for %s", refType, paramType)
297-
}
298-
case "query", "formData":
289+
case "path", "header", "query", "formData":
299290
switch objectType {
300291
case ARRAY:
301292
if !IsPrimitiveType(refType) && !(refType == "file" && paramType == "formData") {
@@ -324,11 +315,14 @@ func (operation *Operation) ParseParamComment(commentLine string, astFile *ast.F
324315
}
325316
}
326317

327-
var formName = name
328-
if item.Schema.Extensions != nil {
329-
if nameVal, ok := item.Schema.Extensions[formTag]; ok {
330-
formName = nameVal.(string)
331-
}
318+
nameOverrideType := paramType
319+
// query also uses formData tags
320+
if paramType == "query" {
321+
nameOverrideType = "formData"
322+
}
323+
// load overridden type specific name from extensions if exists
324+
if nameVal, ok := item.Schema.Extensions[nameOverrideType]; ok {
325+
name = nameVal.(string)
332326
}
333327

334328
switch {
@@ -346,10 +340,10 @@ func (operation *Operation) ParseParamComment(commentLine string, astFile *ast.F
346340
if !IsSimplePrimitiveType(itemSchema.Type[0]) {
347341
continue
348342
}
349-
param = createParameter(paramType, prop.Description, formName, prop.Type[0], itemSchema.Type[0], findInSlice(schema.Required, name), itemSchema.Enum, operation.parser.collectionFormatInQuery)
343+
param = createParameter(paramType, prop.Description, name, prop.Type[0], itemSchema.Type[0], findInSlice(schema.Required, item.Name), itemSchema.Enum, operation.parser.collectionFormatInQuery)
350344

351345
case IsSimplePrimitiveType(prop.Type[0]):
352-
param = createParameter(paramType, prop.Description, formName, PRIMITIVE, prop.Type[0], findInSlice(schema.Required, name), nil, operation.parser.collectionFormatInQuery)
346+
param = createParameter(paramType, prop.Description, name, PRIMITIVE, prop.Type[0], findInSlice(schema.Required, item.Name), nil, operation.parser.collectionFormatInQuery)
353347
default:
354348
operation.parser.debug.Printf("skip field [%s] in %s is not supported type for %s", name, refType, paramType)
355349
continue
@@ -406,6 +400,8 @@ func (operation *Operation) ParseParamComment(commentLine string, astFile *ast.F
406400
const (
407401
formTag = "form"
408402
jsonTag = "json"
403+
uriTag = "uri"
404+
headerTag = "header"
409405
bindingTag = "binding"
410406
defaultTag = "default"
411407
enumsTag = "enums"

operation_test.go

+148-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package swag
22

33
import (
44
"encoding/json"
5+
"fmt"
56
"go/ast"
67
goparser "go/parser"
78
"go/token"
@@ -1177,11 +1178,17 @@ func TestOperation_ParseParamComment(t *testing.T) {
11771178
t.Parallel()
11781179
for _, paramType := range []string{"header", "path", "query", "formData"} {
11791180
t.Run(paramType, func(t *testing.T) {
1181+
// unknown object returns error
11801182
assert.Error(t, NewOperation(nil).ParseComment(`@Param some_object `+paramType+` main.Object true "Some Object"`, nil))
1183+
1184+
// verify objects are supported here
1185+
o := NewOperation(nil)
1186+
o.parser.addTestType("main.TestObject")
1187+
err := o.ParseComment(`@Param some_object `+paramType+` main.TestObject true "Some Object"`, nil)
1188+
assert.NoError(t, err)
11811189
})
11821190
}
11831191
})
1184-
11851192
}
11861193

11871194
// Test ParseParamComment Query Params
@@ -2067,6 +2074,146 @@ func TestParseParamCommentByExtensions(t *testing.T) {
20672074
assert.Equal(t, expected, string(b))
20682075
}
20692076

2077+
func TestParseParamStructCodeExample(t *testing.T) {
2078+
t.Parallel()
2079+
2080+
fset := token.NewFileSet()
2081+
ast, err := goparser.ParseFile(fset, "operation_test.go", `package swag
2082+
import structs "github.com/swaggo/swag/testdata/param_structs"
2083+
`, goparser.ParseComments)
2084+
assert.NoError(t, err)
2085+
2086+
parser := New()
2087+
err = parser.parseFile("github.com/swaggo/swag/testdata/param_structs", "testdata/param_structs/structs.go", nil, ParseModels)
2088+
assert.NoError(t, err)
2089+
_, err = parser.packages.ParseTypes()
2090+
assert.NoError(t, err)
2091+
2092+
validateParameters := func(operation *Operation, params ...spec.Parameter) {
2093+
assert.Equal(t, len(params), len(operation.Parameters))
2094+
2095+
for _, param := range params {
2096+
found := false
2097+
for _, p := range operation.Parameters {
2098+
if p.Name == param.Name {
2099+
assert.Equal(t, param.ParamProps, p.ParamProps)
2100+
assert.Equal(t, param.CommonValidations, p.CommonValidations)
2101+
assert.Equal(t, param.SimpleSchema, p.SimpleSchema)
2102+
found = true
2103+
break
2104+
}
2105+
}
2106+
assert.True(t, found, "found parameter %s", param.Name)
2107+
}
2108+
}
2109+
2110+
// values used in validation checks
2111+
max := float64(10)
2112+
maxLen := int64(10)
2113+
min := float64(0)
2114+
2115+
// query and form behave the same
2116+
for _, param := range []string{"query", "formData"} {
2117+
t.Run(param+" struct", func(t *testing.T) {
2118+
operation := NewOperation(parser)
2119+
comment := fmt.Sprintf(`@Param model %s structs.FormModel true "query params"`, param)
2120+
err = operation.ParseComment(comment, ast)
2121+
assert.NoError(t, err)
2122+
2123+
validateParameters(operation,
2124+
spec.Parameter{
2125+
ParamProps: spec.ParamProps{
2126+
Name: "f",
2127+
Description: "",
2128+
In: param,
2129+
Required: true,
2130+
},
2131+
CommonValidations: spec.CommonValidations{
2132+
MaxLength: &maxLen,
2133+
},
2134+
SimpleSchema: spec.SimpleSchema{
2135+
Type: "string",
2136+
},
2137+
},
2138+
spec.Parameter{
2139+
ParamProps: spec.ParamProps{
2140+
Name: "b",
2141+
Description: "B is another field",
2142+
In: param,
2143+
},
2144+
SimpleSchema: spec.SimpleSchema{
2145+
Type: "boolean",
2146+
},
2147+
})
2148+
})
2149+
}
2150+
2151+
t.Run("header struct", func(t *testing.T) {
2152+
operation := NewOperation(parser)
2153+
comment := `@Param auth header structs.AuthHeader true "auth header"`
2154+
err = operation.ParseComment(comment, ast)
2155+
assert.NoError(t, err)
2156+
2157+
validateParameters(operation,
2158+
spec.Parameter{
2159+
ParamProps: spec.ParamProps{
2160+
Name: "X-Auth-Token",
2161+
Description: "Token is the auth token",
2162+
In: "header",
2163+
Required: true,
2164+
},
2165+
SimpleSchema: spec.SimpleSchema{
2166+
Type: "string",
2167+
},
2168+
}, spec.Parameter{
2169+
ParamProps: spec.ParamProps{
2170+
Name: "anotherHeader",
2171+
Description: "AnotherHeader is another header",
2172+
In: "header",
2173+
},
2174+
CommonValidations: spec.CommonValidations{
2175+
Maximum: &max,
2176+
Minimum: &min,
2177+
},
2178+
SimpleSchema: spec.SimpleSchema{
2179+
Type: "integer",
2180+
},
2181+
})
2182+
})
2183+
2184+
t.Run("path struct", func(t *testing.T) {
2185+
operation := NewOperation(parser)
2186+
comment := `@Param path path structs.PathModel true "path params"`
2187+
err = operation.ParseComment(comment, ast)
2188+
assert.NoError(t, err)
2189+
2190+
validateParameters(operation,
2191+
spec.Parameter{
2192+
ParamProps: spec.ParamProps{
2193+
Name: "id",
2194+
Description: "ID is the id",
2195+
In: "path",
2196+
Required: true,
2197+
},
2198+
SimpleSchema: spec.SimpleSchema{
2199+
Type: "integer",
2200+
},
2201+
}, spec.Parameter{
2202+
ParamProps: spec.ParamProps{
2203+
Name: "name",
2204+
Description: "",
2205+
In: "path",
2206+
},
2207+
CommonValidations: spec.CommonValidations{
2208+
MaxLength: &maxLen,
2209+
},
2210+
SimpleSchema: spec.SimpleSchema{
2211+
Type: "string",
2212+
},
2213+
})
2214+
})
2215+
}
2216+
20702217
func TestParseIdComment(t *testing.T) {
20712218
t.Parallel()
20722219

parser.go

+12-4
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,8 @@ type FieldParser interface {
189189
ShouldSkip() bool
190190
FieldName() (string, error)
191191
FormName() string
192+
HeaderName() string
193+
PathName() string
192194
CustomSchema() (*spec.Schema, error)
193195
ComplementSchema(schema *spec.Schema) error
194196
IsRequired() (bool, error)
@@ -1506,11 +1508,17 @@ func (parser *Parser) parseStructField(file *ast.File, field *ast.Field) (map[st
15061508
tagRequired = append(tagRequired, fieldName)
15071509
}
15081510

1511+
if schema.Extensions == nil {
1512+
schema.Extensions = make(spec.Extensions)
1513+
}
15091514
if formName := ps.FormName(); len(formName) > 0 {
1510-
if schema.Extensions == nil {
1511-
schema.Extensions = make(spec.Extensions)
1512-
}
1513-
schema.Extensions[formTag] = formName
1515+
schema.Extensions["formData"] = formName
1516+
}
1517+
if headerName := ps.HeaderName(); len(headerName) > 0 {
1518+
schema.Extensions["header"] = headerName
1519+
}
1520+
if pathName := ps.PathName(); len(pathName) > 0 {
1521+
schema.Extensions["path"] = pathName
15141522
}
15151523

15161524
return map[string]spec.Schema{fieldName: *schema}, tagRequired, nil

testdata/param_structs/structs.go

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package structs
2+
3+
type FormModel struct {
4+
Foo string `form:"f" binding:"required" validate:"max=10"`
5+
// B is another field
6+
B bool
7+
}
8+
9+
type AuthHeader struct {
10+
// Token is the auth token
11+
Token string `header:"X-Auth-Token" binding:"required"`
12+
// AnotherHeader is another header
13+
AnotherHeader int `validate:"gte=0,lte=10"`
14+
}
15+
16+
type PathModel struct {
17+
// ID is the id
18+
Identifier int `uri:"id" binding:"required"`
19+
Name string `validate:"max=10"`
20+
}

0 commit comments

Comments
 (0)