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

Overriding deep-level fields in response object #672

Merged
merged 8 commits into from
Apr 20, 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
17 changes: 12 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -510,13 +510,14 @@ type Account struct {

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

type Order struct { //in `proto` package
...
Id uint `json:"id"`
Data interface{} `json:"data"`
}
```

Expand All @@ -531,7 +532,13 @@ type Order struct { //in `proto` package
```go
@success 200 {object} jsonresult.JSONResult{data1=string,data2=[]string,data3=proto.Order,data4=[]proto.Order} "desc"
```

- overriding deep-level fields
```go
type DeepObject struct { //in `proto` package
...
}
@success 200 {object} jsonresult.JSONResult{data1=proto.Order{data=proto.DeepObject},data2=[]proto.Order{data=[]proto.DeepObject}} "desc"
```
### Add a headers in response

```go
Expand Down
242 changes: 137 additions & 105 deletions operation.go
Original file line number Diff line number Diff line change
Expand Up @@ -632,54 +632,152 @@ func findTypeDef(importPath, typeName string) (*ast.TypeSpec, error) {

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

type nestedField struct {
Name string
Type string
IsArray bool
Ref spec.Ref
}
//RepsonseType{data1=Type1,data2=Type2}
var combinedPattern = regexp.MustCompile(`^([\w\-\.\/\[\]]+)\{(.*)\}$`)

func (nested *nestedField) getSchema() *spec.Schema {
if IsPrimitiveType(nested.Type) {
return &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{nested.Type}}}
func (operation *Operation) parseResponseObjectSchema(refType string, astFile *ast.File) (*spec.Schema, error) {
switch {
case refType == "interface{}":
return &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{"object"}}}, nil
case IsGolangPrimitiveType(refType):
refType = TransToValidSchemeType(refType)
return &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{refType}}}, nil
case IsPrimitiveType(refType):
return &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{refType}}}, nil
case strings.HasPrefix(refType, "[]"):
schema, err := operation.parseResponseObjectSchema(refType[2:], astFile)
if err != nil {
return nil, err
}
return &spec.Schema{SchemaProps: spec.SchemaProps{
Type: []string{"array"},
Items: &spec.SchemaOrArray{Schema: schema}},
}, nil
case strings.HasPrefix(refType, "map["):
//ignore key type
idx := strings.Index(refType, "]")
if idx < 0 {
return nil, fmt.Errorf("invalid type: %s", refType)
}
refType = refType[idx+1:]
var valueSchema spec.SchemaOrBool
if refType == "interface{}" {
valueSchema.Allows = true
} else {
schema, err := operation.parseResponseObjectSchema(refType, astFile)
if err != nil {
return &spec.Schema{}, err
}
valueSchema.Schema = schema
}
return &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
AdditionalProperties: &valueSchema,
},
}, nil
case strings.Contains(refType, "{"):
return operation.parseResponseCombinedObjectSchema(refType, astFile)
default:
if operation.parser != nil { // checking refType has existing in 'TypeDefinitions'
refNewType, typeSpec, err := operation.registerSchemaType(refType, astFile)
if err != nil {
return nil, err
}
refType = TypeDocName(refNewType, typeSpec)
}
return &spec.Schema{SchemaProps: spec.SchemaProps{Ref: spec.Ref{
Ref: jsonreference.MustCreateRef("#/definitions/" + refType),
}}}, nil
}

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

//RepsonseType{data1=Type1,data2=Type2}
var nestedPattern = regexp.MustCompile(`^([\w\-\.\/]+)\{(.*)\}$`)

func (operation *Operation) tryExtractNestedFields(specStr string, astFile *ast.File) (refType string, nestedFields []*nestedField, err error) {
matches := nestedPattern.FindStringSubmatch(specStr)
func (operation *Operation) parseResponseCombinedObjectSchema(refType string, astFile *ast.File) (*spec.Schema, error) {
matches := combinedPattern.FindStringSubmatch(refType)
if len(matches) != 3 {
return specStr, nil, nil
return nil, fmt.Errorf("invalid type: %s", refType)
}
refType = matches[1]
fields := strings.Split(matches[2], ",")
for _, field := range fields {
if matches := strings.Split(field, "="); len(matches) == 2 {
nested := &nestedField{Name: matches[0], Type: matches[1], IsArray: strings.HasPrefix(matches[1], "[]")}
if nested.IsArray {
nested.Type = nested.Type[2:]
schema, err := operation.parseResponseObjectSchema(refType, astFile)
if err != nil {
return nil, err
}

parseFields := func(s string) []string {
n := 0
return strings.FieldsFunc(s, func(r rune) bool {
if r == '{' {
n++
return false
} else if r == '}' {
n--
return false
}
nested.Type = TransToValidSchemeType(nested.Type)
if !IsPrimitiveType(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
}
return r == ',' && n == 0
})
}

nested.Ref = spec.Ref{
Ref: jsonreference.MustCreateRef("#/definitions/" + TypeDocName(refType, typeSpec)),
}
fields := parseFields(matches[2])
props := map[string]spec.Schema{}
for _, field := range fields {
if matches := strings.SplitN(field, "=", 2); len(matches) == 2 {
if strings.HasPrefix(matches[1], "[]") {
itemSchema, err := operation.parseResponseObjectSchema(matches[1][2:], astFile)
if err != nil {
return nil, err
}
props[matches[0]] = spec.Schema{SchemaProps: spec.SchemaProps{
Type: []string{"array"},
Items: &spec.SchemaOrArray{Schema: itemSchema}},
}
} else {
schema, err := operation.parseResponseObjectSchema(matches[1], astFile)
if err != nil {
return nil, err
}
props[matches[0]] = *schema
}
nestedFields = append(nestedFields, nested)
}
}
return

if len(props) == 0 {
return schema, nil
}
return &spec.Schema{
SchemaProps: spec.SchemaProps{
AllOf: []spec.Schema{
*schema,
{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: props,
},
},
},
},
}, nil
}

func (operation *Operation) parseResponseSchema(schemaType, refType string, astFile *ast.File) (*spec.Schema, error) {
switch schemaType {
case "object":
if !strings.HasPrefix(refType, "[]") {
return operation.parseResponseObjectSchema(refType, astFile)
}
refType = refType[2:]
fallthrough
case "array":
schema, err := operation.parseResponseObjectSchema(refType, astFile)
if err != nil {
return nil, err
}
return &spec.Schema{SchemaProps: spec.SchemaProps{
Type: []string{"array"},
Items: &spec.SchemaOrArray{Schema: schema}},
}, nil
default:
return &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{schemaType}}}, nil
}
}

// ParseResponseComment parses comment for given `response` comment string.
Expand All @@ -694,87 +792,20 @@ func (operation *Operation) ParseResponseComment(commentLine string, astFile *as
return err
}

response := spec.Response{}

code, _ := strconv.Atoi(matches[1])

responseDescription := strings.Trim(matches[4], "\"")
if responseDescription == "" {
responseDescription = http.StatusText(code)
}
response.Description = responseDescription

schemaType := strings.Trim(matches[2], "{}")
refType := matches[3]

refType, nestedFields, err := operation.tryExtractNestedFields(refType, astFile)
schema, err := operation.parseResponseSchema(schemaType, refType, astFile)
if err != nil {
return err
}

var typeSpec *ast.TypeSpec
if !IsGolangPrimitiveType(refType) {
if operation.parser != nil { // checking refType has existing in 'TypeDefinitions'
var err error
if refType, typeSpec, err = operation.registerSchemaType(refType, astFile); err != nil {
return err
}
}
}

// so we have to know all type in app
response.Schema = &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{schemaType}}}

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

if nestedFields == nil {
response.Schema.Ref = ref
} else {
props := make(map[string]spec.Schema)
for _, nested := range nestedFields {
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}
}

} else if schemaType == "array" {
refType = TransToValidSchemeType(refType)
if IsPrimitiveType(refType) {
response.Schema.Items = &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: spec.StringOrArray{refType},
},
},
}
} else {
response.Schema.Items = &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Ref: spec.Ref{Ref: jsonreference.MustCreateRef("#/definitions/" + TypeDocName(refType, typeSpec))},
},
},
}
}
}

if operation.Responses == nil {
operation.Responses = &spec.Responses{
ResponsesProps: spec.ResponsesProps{
Expand All @@ -783,8 +814,9 @@ func (operation *Operation) ParseResponseComment(commentLine string, astFile *as
}
}

operation.Responses.StatusCodeResponses[code] = response

operation.Responses.StatusCodeResponses[code] = spec.Response{
ResponseProps: spec.ResponseProps{Schema: schema, Description: responseDescription},
}
return nil
}

Expand Down
Loading