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

Support response model composition #651

Merged
merged 8 commits into from
Mar 23, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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