Skip to content

Commit

Permalink
Merge pull request #651 from whunmr/nested_schema
Browse files Browse the repository at this point in the history
Feat: Support response model composition, post-binding a interface{} field of struct
  • Loading branch information
sdghchj committed Mar 23, 2020
2 parents 2f74ff2 + 187ec48 commit 8e21f4c
Show file tree
Hide file tree
Showing 3 changed files with 275 additions and 2 deletions.
27 changes: 27 additions & 0 deletions README.md
Expand Up @@ -26,6 +26,7 @@ Swag converts Go annotations to Swagger Documentation 2.0. We've created a varie
- [Examples](#examples)
- [Descriptions over multiple lines](#descriptions-over-multiple-lines)
- [User defined structure with an array type](#user-defined-structure-with-an-array-type)
- [Model composition in response](#model-composition-in-response)
- [Add a headers in response](#add-a-headers-in-response)
- [Use multiple path params](#use-multiple-path-params)
- [Example value of struct](#example-value-of-struct)
Expand Down Expand Up @@ -500,6 +501,32 @@ type Account struct {
Name string `json:"name" example:"account name"`
}
```

### Model composition in response
```go
@success 200 {object} jsonresult.JSONResult{data=proto.Order} "desc"
```

```go
type JSONResult struct {
Code int `json:"code" `
Message string `json:"message"`
Data interface{} `json:"data"`
}

type Order struct { //in `proto` package
...
}
```

- also support array of objects and primitive types as nested response
```go
@success 200 {object} jsonresult.JSONResult{data=[]proto.Order} "desc"
@success 200 {object} jsonresult.JSONResult{data=string} "desc"
@success 200 {object} jsonresult.JSONResult{data=[]string} "desc"
```


### Add a headers in response

```go
Expand Down
78 changes: 76 additions & 2 deletions operation.go
Expand Up @@ -630,7 +630,69 @@ func findTypeDef(importPath, typeName string) (*ast.TypeSpec, error) {
return nil, fmt.Errorf("type spec not found")
}

var responsePattern = regexp.MustCompile(`([\d]+)[\s]+([\w\{\}]+)[\s]+([\w\-\.\/]+)[^"]*(.*)?`)
var responsePattern = regexp.MustCompile(`([\d]+)[\s]+([\w\{\}]+)[\s]+([\w\-\.\/\{\}=\[\]]+)[^"]*(.*)?`)

type nestedField struct {
Name string
Type string
IsArray bool
Ref spec.Ref
}

func (nested *nestedField) getSchema() *spec.Schema {
if IsGolangPrimitiveType(nested.Type) {
return &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{nested.Type}}}
}

return &spec.Schema{SchemaProps: spec.SchemaProps{Ref: nested.Ref}}
}

func (nested *nestedField) fillNestedSchema(response *spec.Response, ref spec.Ref) {
props := make(map[string]spec.Schema, 0)
if nested.IsArray {
props[nested.Name] = spec.Schema{SchemaProps: spec.SchemaProps{
Type: []string{"array"},
Items: &spec.SchemaOrArray{Schema: nested.getSchema()},
}}
} else {
props[nested.Name] = *nested.getSchema()
}
nestedSpec := spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: props,
},
}
response.Schema.AllOf = []spec.Schema{{SchemaProps: spec.SchemaProps{Ref: ref}}, nestedSpec}
}

var nestedObjectPattern = regexp.MustCompile(`^([\w\-\.\/]+)\{(.*)=([^\[\]]*)\}$`)
var nestedArrayPattern = regexp.MustCompile(`^([\w\-\.\/]+)\{(.*)=\[\]([^\[\]]*)\}$`)

func (operation *Operation) tryExtractNestedField(specStr string, astFile *ast.File) (refType string, nested *nestedField, err error) {
if matches := nestedObjectPattern.FindStringSubmatch(specStr); len(matches) == 4 {
refType, nested = matches[1], &nestedField{Name: matches[2], Type: matches[3], IsArray: false}
} else if matches := nestedArrayPattern.FindStringSubmatch(specStr); len(matches) == 4 {
refType, nested = matches[1], &nestedField{Name: matches[2], Type: matches[3], IsArray: true}
} else {
return specStr, nil, nil
}

if !IsGolangPrimitiveType(nested.Type) {
if operation.parser != nil { // checking refType has existing in 'TypeDefinitions'
refType, typeSpec, err := operation.registerSchemaType(nested.Type, astFile)
if err != nil {
return specStr, nil, err
}

nested.Ref = spec.Ref{
Ref: jsonreference.MustCreateRef("#/definitions/" + TypeDocName(refType, typeSpec)),
}
}
}

return
}

// ParseResponseComment parses comment for given `response` comment string.
func (operation *Operation) ParseResponseComment(commentLine string, astFile *ast.File) error {
Expand All @@ -657,6 +719,11 @@ func (operation *Operation) ParseResponseComment(commentLine string, astFile *as
schemaType := strings.Trim(matches[2], "{}")
refType := matches[3]

refType, nested, err := operation.tryExtractNestedField(refType, astFile)
if err != nil {
return err
}

var typeSpec *ast.TypeSpec
if !IsGolangPrimitiveType(refType) {
if operation.parser != nil { // checking refType has existing in 'TypeDefinitions'
Expand All @@ -672,9 +739,16 @@ func (operation *Operation) ParseResponseComment(commentLine string, astFile *as

if schemaType == "object" {
response.Schema.SchemaProps = spec.SchemaProps{}
response.Schema.Ref = spec.Ref{
ref := spec.Ref{
Ref: jsonreference.MustCreateRef("#/definitions/" + TypeDocName(refType, typeSpec)),
}

if nested == nil {
response.Schema.Ref = ref
} else {
nested.fillNestedSchema(&response, ref)
}

} else if schemaType == "array" {
refType = TransToValidSchemeType(refType)
if IsPrimitiveType(refType) {
Expand Down
172 changes: 172 additions & 0 deletions operation_test.go
Expand Up @@ -188,6 +188,178 @@ func TestParseResponseCommentWithObjectType(t *testing.T) {
assert.Equal(t, expected, string(b))
}

func TestParseResponseCommentWithNestedPrimitiveType(t *testing.T) {
comment := `@Success 200 {object} model.CommonHeader{data=string} "Error message, if code != 200`
operation := NewOperation()
operation.parser = New()

operation.parser.TypeDefinitions["model"] = make(map[string]*ast.TypeSpec)
operation.parser.TypeDefinitions["model"]["CommonHeader"] = &ast.TypeSpec{}

err := operation.ParseComment(comment, nil)
assert.NoError(t, err)

response := operation.Responses.StatusCodeResponses[200]
assert.Equal(t, `Error message, if code != 200`, response.Description)

b, _ := json.MarshalIndent(operation, "", " ")

expected := `{
"responses": {
"200": {
"description": "Error message, if code != 200",
"schema": {
"allOf": [
{
"$ref": "#/definitions/model.CommonHeader"
},
{
"type": "object",
"properties": {
"data": {
"type": "string"
}
}
}
]
}
}
}
}`
assert.Equal(t, expected, string(b))
}

func TestParseResponseCommentWithNestedPrimitiveArrayType(t *testing.T) {
comment := `@Success 200 {object} model.CommonHeader{data=[]string} "Error message, if code != 200`
operation := NewOperation()
operation.parser = New()

operation.parser.TypeDefinitions["model"] = make(map[string]*ast.TypeSpec)
operation.parser.TypeDefinitions["model"]["CommonHeader"] = &ast.TypeSpec{}

err := operation.ParseComment(comment, nil)
assert.NoError(t, err)

response := operation.Responses.StatusCodeResponses[200]
assert.Equal(t, `Error message, if code != 200`, response.Description)

b, _ := json.MarshalIndent(operation, "", " ")

expected := `{
"responses": {
"200": {
"description": "Error message, if code != 200",
"schema": {
"allOf": [
{
"$ref": "#/definitions/model.CommonHeader"
},
{
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
]
}
}
}
}`
assert.Equal(t, expected, string(b))
}

func TestParseResponseCommentWithNestedObjectType(t *testing.T) {
comment := `@Success 200 {object} model.CommonHeader{data=model.Payload} "Error message, if code != 200`
operation := NewOperation()
operation.parser = New()

operation.parser.TypeDefinitions["model"] = make(map[string]*ast.TypeSpec)
operation.parser.TypeDefinitions["model"]["CommonHeader"] = &ast.TypeSpec{}
operation.parser.TypeDefinitions["model"]["Payload"] = &ast.TypeSpec{}

err := operation.ParseComment(comment, nil)
assert.NoError(t, err)

response := operation.Responses.StatusCodeResponses[200]
assert.Equal(t, `Error message, if code != 200`, response.Description)

b, _ := json.MarshalIndent(operation, "", " ")

expected := `{
"responses": {
"200": {
"description": "Error message, if code != 200",
"schema": {
"allOf": [
{
"$ref": "#/definitions/model.CommonHeader"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/model.Payload"
}
}
}
]
}
}
}
}`
assert.Equal(t, expected, string(b))
}

func TestParseResponseCommentWithNestedArrayObjectType(t *testing.T) {
comment := `@Success 200 {object} model.CommonHeader{data=[]model.Payload} "Error message, if code != 200`
operation := NewOperation()
operation.parser = New()

operation.parser.TypeDefinitions["model"] = make(map[string]*ast.TypeSpec)
operation.parser.TypeDefinitions["model"]["CommonHeader"] = &ast.TypeSpec{}
operation.parser.TypeDefinitions["model"]["Payload"] = &ast.TypeSpec{}

err := operation.ParseComment(comment, nil)
assert.NoError(t, err)

response := operation.Responses.StatusCodeResponses[200]
assert.Equal(t, `Error message, if code != 200`, response.Description)

b, _ := json.MarshalIndent(operation, "", " ")

expected := `{
"responses": {
"200": {
"description": "Error message, if code != 200",
"schema": {
"allOf": [
{
"$ref": "#/definitions/model.CommonHeader"
},
{
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/model.Payload"
}
}
}
}
]
}
}
}
}`
assert.Equal(t, expected, string(b))
}

func TestParseResponseCommentWithObjectTypeInSameFile(t *testing.T) {
comment := `@Success 200 {object} testOwner "Error message, if code != 200"`
operation := NewOperation()
Expand Down

0 comments on commit 8e21f4c

Please sign in to comment.