Skip to content

Commit

Permalink
Add support for the attributes in struct fields
Browse files Browse the repository at this point in the history
This commit adds support for the following attributes in the struct
fields:

- "minimum": for numeric fields, via `minimum:"value"` struct tag
- "maximum": for numeric fields, via `maximum:"value"` struct tag
- "minLength": for string fields, via `minLength:"value"` struct tag
- "maxLength": for string fields, via `maxLength:"value"` struct tag
- "enums": via `enums:"value1,value2,value3"` struct tag
- "default": via `default:"value"` struct tag

All these tags except "default" are also supported in "array" fields.
  • Loading branch information
Nikolai Obedin committed Nov 6, 2018
1 parent c0507b2 commit 1095ca5
Show file tree
Hide file tree
Showing 5 changed files with 126 additions and 18 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,16 @@ Make it AND condition
// @Param default query string false "string default" default(A)
```
It also works for the struct fields:
```go
type Foo struct {
Bar string `minLength:"4" maxLength:"16"`
Baz int `minimum:"10" maximum:"20" default:"15"`
Qux []string `enums:"foo,bar,baz"`
}
```
### Available
Field Name | Type | Description
Expand Down
96 changes: 83 additions & 13 deletions parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,12 @@ type structField struct {
isRequired bool
crossPkg string
exampleValue interface{}
maximum *float64
minimum *float64
maxLength *int64
minLength *int64
enums []interface{}
defaultValue interface{}
}

func (parser *Parser) parseStruct(pkgName string, field *ast.Field) (properties map[string]spec.Schema) {
Expand Down Expand Up @@ -542,7 +548,13 @@ func (parser *Parser) parseStruct(pkgName string, field *ast.Field) (properties
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{structField.arrayType},
Type: []string{structField.arrayType},
Maximum: structField.maximum,
Minimum: structField.minimum,
MaxLength: structField.maxLength,
MinLength: structField.minLength,
Enum: structField.enums,
Default: structField.defaultValue,
},
},
},
Expand All @@ -563,6 +575,12 @@ func (parser *Parser) parseStruct(pkgName string, field *ast.Field) (properties
Description: desc,
Format: structField.formatType,
Required: required,
Maximum: structField.maximum,
Minimum: structField.minimum,
MaxLength: structField.maxLength,
MinLength: structField.minLength,
Enum: structField.enums,
Default: structField.defaultValue,
},
SwaggerSchemaProps: spec.SwaggerSchemaProps{
Example: structField.exampleValue,
Expand Down Expand Up @@ -590,6 +608,12 @@ func (parser *Parser) parseStruct(pkgName string, field *ast.Field) (properties
Format: structField.formatType,
Properties: props,
Required: nestRequired,
Maximum: structField.maximum,
Minimum: structField.minimum,
MaxLength: structField.maxLength,
MinLength: structField.minLength,
Enum: structField.enums,
Default: structField.defaultValue,
},
SwaggerSchemaProps: spec.SwaggerSchemaProps{
Example: structField.exampleValue,
Expand Down Expand Up @@ -655,8 +679,8 @@ func (parser *Parser) parseField(field *ast.Field) *structField {
return structField
}
// `json:"tag"` -> json:"tag"
structTag := strings.Replace(field.Tag.Value, "`", "", -1)
jsonTag := reflect.StructTag(structTag).Get("json")
structTag := reflect.StructTag(strings.Replace(field.Tag.Value, "`", "", -1))
jsonTag := structTag.Get("json")
// json:"tag,hoge"
if strings.Contains(jsonTag, ",") {
// json:",hoge"
Expand All @@ -672,8 +696,7 @@ func (parser *Parser) parseField(field *ast.Field) *structField {
structField.name = jsonTag
}

typeTag := reflect.StructTag(structTag).Get("swaggertype")
if typeTag != "" {
if typeTag := structTag.Get("swaggertype"); typeTag != "" {
parts := strings.Split(typeTag, ",")
if 0 < len(parts) && len(parts) <= 2 {
newSchemaType := parts[0]
Expand All @@ -688,35 +711,82 @@ func (parser *Parser) parseField(field *ast.Field) *structField {
structField.arrayType = newArrayType
}
}
exampleTag := reflect.StructTag(structTag).Get("example")
if exampleTag != "" {
if exampleTag := structTag.Get("example"); exampleTag != "" {
structField.exampleValue = defineTypeOfExample(structField.schemaType, structField.arrayType, exampleTag)
}
formatTag := reflect.StructTag(structTag).Get("format")
if formatTag != "" {
if formatTag := structTag.Get("format"); formatTag != "" {
structField.formatType = formatTag
}
bindingTag := reflect.StructTag(structTag).Get("binding")
if bindingTag != "" {
if bindingTag := structTag.Get("binding"); bindingTag != "" {
for _, val := range strings.Split(bindingTag, ",") {
if val == "required" {
structField.isRequired = true
break
}
}
}
validateTag := reflect.StructTag(structTag).Get("validate")
if validateTag != "" {
if validateTag := structTag.Get("validate"); validateTag != "" {
for _, val := range strings.Split(validateTag, ",") {
if val == "required" {
structField.isRequired = true
break
}
}
}
if enumsTag := structTag.Get("enums"); enumsTag != "" {
enumType := structField.schemaType
if structField.schemaType == "array" {
enumType = structField.arrayType
}

for _, e := range strings.Split(enumsTag, ",") {
structField.enums = append(structField.enums, defineType(enumType, e))
}
}
if defaultTag := structTag.Get("default"); defaultTag != "" {
structField.defaultValue = defineType(structField.schemaType, defaultTag)
}

if IsNumericType(structField.schemaType) || IsNumericType(structField.arrayType) {
structField.maximum = getFloatTag(structTag, "maximum")
structField.minimum = getFloatTag(structTag, "minimum")
}
if structField.schemaType == "string" || structField.arrayType == "string" {
structField.maxLength = getIntTag(structTag, "maxLength")
structField.minLength = getIntTag(structTag, "minLength")
}

return structField
}

func getFloatTag(structTag reflect.StructTag, tagName string) *float64 {
strValue := structTag.Get(tagName)
if strValue == "" {
return nil
}

value, err := strconv.ParseFloat(strValue, 64)
if err != nil {
panic(fmt.Errorf("can't parse numeric value of %q tag: %v", tagName, err))
}

return &value
}

func getIntTag(structTag reflect.StructTag, tagName string) *int64 {
strValue := structTag.Get(tagName)
if strValue == "" {
return nil
}

value, err := strconv.ParseInt(strValue, 10, 64)
if err != nil {
panic(fmt.Errorf("can't parse numeric value of %q tag: %v", tagName, err))
}

return &value
}

func toSnakeCase(in string) string {
runes := []rune(in)
length := len(runes)
Expand Down
24 changes: 23 additions & 1 deletion parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,8 @@ func TestParseSimpleApi(t *testing.T) {
},
"name": {
"type": "string",
"maxLength": 16,
"minLength": 4,
"example": "detail_category_name"
},
"photo_urls": {
Expand All @@ -546,6 +548,19 @@ func TestParseSimpleApi(t *testing.T) {
"decimal": {
"type": "number"
},
"enum_array": {
"type": "array",
"items": {
"type": "integer",
"enum": [
1,
2,
3,
5,
7
]
}
},
"id": {
"type": "integer",
"format": "int64",
Expand All @@ -563,6 +578,7 @@ func TestParseSimpleApi(t *testing.T) {
},
"is_alive": {
"type": "boolean",
"default": true,
"example": true
},
"name": {
Expand Down Expand Up @@ -593,10 +609,16 @@ func TestParseSimpleApi(t *testing.T) {
},
"price": {
"type": "number",
"maximum": 1000,
"minimum": 1,
"example": 3.25
},
"status": {
"type": "string"
"type": "string",
"enum": [
"healthy",
"ill"
]
},
"tags": {
"type": "array",
Expand Down
5 changes: 5 additions & 0 deletions schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ func IsPrimitiveType(typeName string) bool {
}
}

// IsNumericType determines whether the swagger type name is a numeric type
func IsNumericType(typeName string) bool {
return typeName == "integer" || typeName == "number"
}

// TransToValidSchemeType indicates type will transfer golang basic type to swagger supported type.
func TransToValidSchemeType(typeName string) string {
switch typeName {
Expand Down
9 changes: 5 additions & 4 deletions testdata/simple/web/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ type Pet struct {
PhotoUrls []string `json:"photo_urls" example:"http://test/image/1.jpg,http://test/image/2.jpg" format:"url"`
SmallCategory struct {
ID int `json:"id" example:"1"`
Name string `json:"name" example:"detail_category_name" binding:"required"`
Name string `json:"name" example:"detail_category_name" binding:"required" minLength:"4" maxLength:"16"`
PhotoUrls []string `json:"photo_urls" example:"http://test/image/1.jpg,http://test/image/2.jpg"`
} `json:"small_category"`
} `json:"category"`
Expand All @@ -25,14 +25,15 @@ type Pet struct {
Tags []Tag `json:"tags"`
Pets *[]Pet2 `json:"pets"`
Pets2 []*Pet2 `json:"pets2"`
Status string `json:"status"`
Price float32 `json:"price" example:"3.25"`
IsAlive bool `json:"is_alive" example:"true"`
Status string `json:"status" enums:"healthy,ill"`
Price float32 `json:"price" example:"3.25" minimum:"1.0" maximum:"1000"`
IsAlive bool `json:"is_alive" example:"true" default:"true"`
Data interface{} `json:"data"`
Hidden string `json:"-"`
UUID uuid.UUID `json:"uuid"`
Decimal decimal.Decimal `json:"decimal"`
IntArray []int `json:"int_array" example:"1,2"`
EnumArray []int `json:"enum_array" enums:"1,2,3,5,7"`
}

type Tag struct {
Expand Down

0 comments on commit 1095ca5

Please sign in to comment.