Skip to content

Commit

Permalink
Support openapiV3 oneOf for fields/responses (#1671)
Browse files Browse the repository at this point in the history
* fix: drive new schema from ref and field tags

* feat: support openapi-v3 oneOf tag

* feat: support openapi-v3 oneOf for response
  • Loading branch information
KKKIIO committed Nov 7, 2023
1 parent 37dac67 commit 8f63cde
Show file tree
Hide file tree
Showing 10 changed files with 302 additions and 64 deletions.
67 changes: 67 additions & 0 deletions field_parser_v3_test.go

Large diffs are not rendered by default.

25 changes: 21 additions & 4 deletions field_parserv3.go
Expand Up @@ -83,13 +83,15 @@ func (sf *structFieldV3) setMax(valValue string) {

type tagBaseFieldParserV3 struct {
p *Parser
file *ast.File
field *ast.Field
tag reflect.StructTag
}

func newTagBaseFieldParserV3(p *Parser, field *ast.Field) FieldParserV3 {
func newTagBaseFieldParserV3(p *Parser, file *ast.File, field *ast.Field) FieldParserV3 {
fieldParser := tagBaseFieldParserV3{
p: p,
file: file,
field: field,
tag: "",
}
Expand Down Expand Up @@ -134,9 +136,10 @@ func (ps *tagBaseFieldParserV3) ComplementSchema(schema *spec.RefOrSpec[spec.Sch
if err != nil {
return err
}
// if !reflect.ValueOf(newSchema).IsZero() {
// *schema = *(newSchema.WithAllOf(*schema.Spec))
// }
if !reflect.ValueOf(newSchema).IsZero() {
newSchema.AllOf = []*spec.RefOrSpec[spec.Schema]{{Spec: schema.Spec}}
*schema = spec.RefOrSpec[spec.Schema]{Spec: &newSchema}
}
return nil
}

Expand Down Expand Up @@ -339,6 +342,19 @@ func (ps *tagBaseFieldParserV3) complementSchema(schema *spec.Schema, types []st
}
}

var oneOfSchemas []*spec.RefOrSpec[spec.Schema]
oneOfTagValue := ps.tag.Get(oneOfTag)
if oneOfTagValue != "" {
oneOfTypes := strings.Split((oneOfTagValue), ",")
for _, oneOfType := range oneOfTypes {
oneOfSchema, err := ps.p.getTypeSchemaV3(oneOfType, ps.file, true)
if err != nil {
return fmt.Errorf("can't find oneOf type %q: %v", oneOfType, err)
}
oneOfSchemas = append(oneOfSchemas, oneOfSchema)
}
}

elemSchema := schema

if field.schemaType == ARRAY {
Expand All @@ -362,6 +378,7 @@ func (ps *tagBaseFieldParserV3) complementSchema(schema *spec.Schema, types []st
elemSchema.MinLength = field.minLength
elemSchema.Enum = field.enums
elemSchema.Pattern = field.pattern
elemSchema.OneOf = oneOfSchemas

return nil
}
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Expand Up @@ -9,7 +9,7 @@ require (
github.com/pkg/errors v0.9.1
github.com/stretchr/testify v1.8.2
github.com/sv-tools/openapi v0.2.1
golang.org/x/tools v0.8.0
golang.org/x/tools v0.13.0
sigs.k8s.io/yaml v1.3.0
)

Expand All @@ -30,7 +30,7 @@ require (
github.com/mailru/easyjson v0.7.7 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/urfave/cli/v2 v2.25.1
golang.org/x/sys v0.7.0 // indirect
golang.org/x/sys v0.12.0 // indirect
gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.1 // indirect
)
10 changes: 5 additions & 5 deletions go.sum
Expand Up @@ -64,11 +64,11 @@ github.com/urfave/cli/v2 v2.25.1 h1:zw8dSP7ghX0Gmm8vugrs6q9Ku0wzweqPyshy+syu9Gw=
github.com/urfave/cli/v2 v2.25.1/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y=
golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4=
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down
1 change: 1 addition & 0 deletions operation.go
Expand Up @@ -423,6 +423,7 @@ const (
extensionsTag = "extensions"
collectionFormatTag = "collectionFormat"
patternTag = "pattern"
oneOfTag = "oneOf"
)

var regexAttributes = map[string]*regexp.Regexp{
Expand Down
128 changes: 79 additions & 49 deletions operationv3.go
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"go/ast"
"log"
"maps"

Check failure on line 8 in operationv3.go

View workflow job for this annotation

GitHub Actions / test (1.18.x, ubuntu-latest)

package maps is not in GOROOT (/opt/hostedtoolcache/go/1.18.10/x64/src/maps)

Check failure on line 8 in operationv3.go

View workflow job for this annotation

GitHub Actions / test (1.19.x, ubuntu-latest)

package maps is not in GOROOT (/opt/hostedtoolcache/go/1.19.13/x64/src/maps)

Check failure on line 8 in operationv3.go

View workflow job for this annotation

GitHub Actions / test (1.20.x, ubuntu-latest)

package maps is not in GOROOT (/opt/hostedtoolcache/go/1.20.10/x64/src/maps)
"net/http"
"strconv"
"strings"
Expand Down Expand Up @@ -926,22 +927,15 @@ func (o *OperationV3) ParseResponseComment(commentLine string, astFile *ast.File

for _, codeStr := range strings.Split(matches[1], ",") {
if strings.EqualFold(codeStr, defaultTag) {
response := o.DefaultResponse()
response.Description = description

mimeType := "application/json" // TODO: set correct mimeType
setResponseSchema(response, mimeType, schema)

continue
}

code, err := strconv.Atoi(codeStr)
if err != nil {
return fmt.Errorf("can not parse response comment \"%s\"", commentLine)
}

if description == "" {
description = http.StatusText(code)
codeStr = ""
} else {
code, err := strconv.Atoi(codeStr)
if err != nil {
return fmt.Errorf("can not parse response comment \"%s\"", commentLine)
}
if description == "" {
description = http.StatusText(code)
}
}

response := spec.NewResponseSpec()
Expand Down Expand Up @@ -979,15 +973,12 @@ func (o *OperationV3) ParseEmptyResponseComment(commentLine string) error {

for _, codeStr := range strings.Split(matches[1], ",") {
if strings.EqualFold(codeStr, defaultTag) {
response := o.DefaultResponse()
response.Description = description

continue
}

_, err := strconv.Atoi(codeStr)
if err != nil {
return fmt.Errorf("can not parse response comment \"%s\"", commentLine)
codeStr = ""
} else {
_, err := strconv.Atoi(codeStr)
if err != nil {
return fmt.Errorf("can not parse response comment \"%s\"", commentLine)
}
}

o.AddResponse(codeStr, newResponseWithDescription(description))
Expand All @@ -996,21 +987,10 @@ func (o *OperationV3) ParseEmptyResponseComment(commentLine string) error {
return nil
}

// DefaultResponse return the default response member pointer.
func (o *OperationV3) DefaultResponse() *spec.Response {
if o.Responses.Spec.Default == nil {
o.Responses.Spec.Default = spec.NewResponseSpec()
o.Responses.Spec.Default.Spec.Spec.Headers = make(map[string]*spec.RefOrSpec[spec.Extendable[spec.Header]])
}

if o.Responses.Spec.Default.Spec.Spec.Content == nil {
o.Responses.Spec.Default.Spec.Spec.Content = make(map[string]*spec.Extendable[spec.MediaType])
}

return o.Responses.Spec.Default.Spec.Spec
}

// AddResponse add a response for a code.
// If the code is already exist, it will merge with the old one:
// 1. The description will be replaced by the new one if the new one is not empty.
// 2. The content schema will be merged using `oneOf` if the new one is not empty.
func (o *OperationV3) AddResponse(code string, response *spec.RefOrSpec[spec.Extendable[spec.Response]]) {
if response.Spec.Spec.Headers == nil {
response.Spec.Spec.Headers = make(map[string]*spec.RefOrSpec[spec.Extendable[spec.Header]])
Expand All @@ -1020,24 +1000,74 @@ func (o *OperationV3) AddResponse(code string, response *spec.RefOrSpec[spec.Ext
o.Responses.Spec.Response = make(map[string]*spec.RefOrSpec[spec.Extendable[spec.Response]])
}

o.Responses.Spec.Response[code] = response
res := response
var prev *spec.RefOrSpec[spec.Extendable[spec.Response]]
if code != "" {
prev = o.Responses.Spec.Response[code]
} else {
prev = o.Responses.Spec.Default
}
if prev != nil { // merge into prev
res = prev
if response.Spec.Spec.Description != "" {
prev.Spec.Spec.Description = response.Spec.Spec.Description
}
if len(response.Spec.Spec.Content) > 0 {
// responses should only have one content type
singleKey := ""
for k := range response.Spec.Spec.Content {
singleKey = k
break
}
if prevMediaType := prev.Spec.Spec.Content[singleKey]; prevMediaType == nil {
prev.Spec.Spec.Content = response.Spec.Spec.Content
} else {
newMediaType := response.Spec.Spec.Content[singleKey]
if len(newMediaType.Extensions) > 0 {
if prevMediaType.Extensions == nil {
prevMediaType.Extensions = make(map[string]interface{})
}
maps.Copy(prevMediaType.Extensions, newMediaType.Extensions)
}
if len(newMediaType.Spec.Examples) > 0 {
if prevMediaType.Spec.Examples == nil {
prevMediaType.Spec.Examples = make(map[string]*spec.RefOrSpec[spec.Extendable[spec.Example]])
}
maps.Copy(prevMediaType.Spec.Examples, newMediaType.Spec.Examples)
}
if prevSchema := prevMediaType.Spec.Schema; prevSchema.Ref != nil || prevSchema.Spec.OneOf == nil {
oneOfSchema := spec.NewSchemaSpec()
oneOfSchema.Spec.OneOf = []*spec.RefOrSpec[spec.Schema]{prevSchema, newMediaType.Spec.Schema}
prevMediaType.Spec.Schema = oneOfSchema
} else {
prevSchema.Spec.OneOf = append(prevSchema.Spec.OneOf, newMediaType.Spec.Schema)
}
}
}
}

if code != "" {
o.Responses.Spec.Response[code] = res
} else {
o.Responses.Spec.Default = res
}
}

// ParseEmptyResponseOnly parse only comment out status code ,eg: @Success 200.
func (o *OperationV3) ParseEmptyResponseOnly(commentLine string) error {
for _, codeStr := range strings.Split(commentLine, ",") {
var description string
if strings.EqualFold(codeStr, defaultTag) {
_ = o.DefaultResponse()

continue
}

code, err := strconv.Atoi(codeStr)
if err != nil {
return fmt.Errorf("can not parse response comment \"%s\"", commentLine)
codeStr = ""
} else {
code, err := strconv.Atoi(codeStr)
if err != nil {
return fmt.Errorf("can not parse response comment \"%s\"", commentLine)
}
description = http.StatusText(code)
}

o.AddResponse(codeStr, newResponseWithDescription(http.StatusText(code)))
o.AddResponse(codeStr, newResponseWithDescription(description))
}

return nil
Expand Down
6 changes: 3 additions & 3 deletions parserv3.go
Expand Up @@ -15,8 +15,8 @@ import (
"github.com/sv-tools/openapi/spec"
)

// FieldParserFactoryV3 func(ps *Parser, field *ast.Field) FieldParserV3 create FieldParser.
type FieldParserFactoryV3 func(ps *Parser, field *ast.Field) FieldParserV3
// FieldParserFactoryV3 create FieldParser.
type FieldParserFactoryV3 func(ps *Parser, file *ast.File, field *ast.Field) FieldParserV3

// FieldParserV3 parse struct field.
type FieldParserV3 interface {
Expand Down Expand Up @@ -903,7 +903,7 @@ func (p *Parser) parseStructFieldV3(file *ast.File, field *ast.Field) (map[strin
}
}

ps := p.fieldParserFactoryV3(p, field)
ps := p.fieldParserFactoryV3(p, file, field)

if ps.ShouldSkip() {
return nil, nil, nil
Expand Down

0 comments on commit 8f63cde

Please sign in to comment.