Skip to content

Commit 8f63cde

Browse files
authored
Support openapiV3 oneOf for fields/responses (#1671)
* fix: drive new schema from ref and field tags * feat: support openapi-v3 oneOf tag * feat: support openapi-v3 oneOf for response
1 parent 37dac67 commit 8f63cde

File tree

10 files changed

+302
-64
lines changed

10 files changed

+302
-64
lines changed

field_parser_v3_test.go

Lines changed: 67 additions & 0 deletions
Large diffs are not rendered by default.

field_parserv3.go

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,13 +83,15 @@ func (sf *structFieldV3) setMax(valValue string) {
8383

8484
type tagBaseFieldParserV3 struct {
8585
p *Parser
86+
file *ast.File
8687
field *ast.Field
8788
tag reflect.StructTag
8889
}
8990

90-
func newTagBaseFieldParserV3(p *Parser, field *ast.Field) FieldParserV3 {
91+
func newTagBaseFieldParserV3(p *Parser, file *ast.File, field *ast.Field) FieldParserV3 {
9192
fieldParser := tagBaseFieldParserV3{
9293
p: p,
94+
file: file,
9395
field: field,
9496
tag: "",
9597
}
@@ -134,9 +136,10 @@ func (ps *tagBaseFieldParserV3) ComplementSchema(schema *spec.RefOrSpec[spec.Sch
134136
if err != nil {
135137
return err
136138
}
137-
// if !reflect.ValueOf(newSchema).IsZero() {
138-
// *schema = *(newSchema.WithAllOf(*schema.Spec))
139-
// }
139+
if !reflect.ValueOf(newSchema).IsZero() {
140+
newSchema.AllOf = []*spec.RefOrSpec[spec.Schema]{{Spec: schema.Spec}}
141+
*schema = spec.RefOrSpec[spec.Schema]{Spec: &newSchema}
142+
}
140143
return nil
141144
}
142145

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

345+
var oneOfSchemas []*spec.RefOrSpec[spec.Schema]
346+
oneOfTagValue := ps.tag.Get(oneOfTag)
347+
if oneOfTagValue != "" {
348+
oneOfTypes := strings.Split((oneOfTagValue), ",")
349+
for _, oneOfType := range oneOfTypes {
350+
oneOfSchema, err := ps.p.getTypeSchemaV3(oneOfType, ps.file, true)
351+
if err != nil {
352+
return fmt.Errorf("can't find oneOf type %q: %v", oneOfType, err)
353+
}
354+
oneOfSchemas = append(oneOfSchemas, oneOfSchema)
355+
}
356+
}
357+
342358
elemSchema := schema
343359

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

366383
return nil
367384
}

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ require (
99
github.com/pkg/errors v0.9.1
1010
github.com/stretchr/testify v1.8.2
1111
github.com/sv-tools/openapi v0.2.1
12-
golang.org/x/tools v0.8.0
12+
golang.org/x/tools v0.13.0
1313
sigs.k8s.io/yaml v1.3.0
1414
)
1515

@@ -30,7 +30,7 @@ require (
3030
github.com/mailru/easyjson v0.7.7 // indirect
3131
github.com/pmezard/go-difflib v1.0.0 // indirect
3232
github.com/urfave/cli/v2 v2.25.1
33-
golang.org/x/sys v0.7.0 // indirect
33+
golang.org/x/sys v0.12.0 // indirect
3434
gopkg.in/yaml.v2 v2.4.0
3535
gopkg.in/yaml.v3 v3.0.1 // indirect
3636
)

go.sum

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,11 @@ github.com/urfave/cli/v2 v2.25.1 h1:zw8dSP7ghX0Gmm8vugrs6q9Ku0wzweqPyshy+syu9Gw=
6464
github.com/urfave/cli/v2 v2.25.1/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
6565
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
6666
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
67-
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
68-
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
69-
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
70-
golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y=
71-
golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4=
67+
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
68+
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
69+
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
70+
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
71+
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
7272
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
7373
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
7474
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

operation.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,7 @@ const (
423423
extensionsTag = "extensions"
424424
collectionFormatTag = "collectionFormat"
425425
patternTag = "pattern"
426+
oneOfTag = "oneOf"
426427
)
427428

428429
var regexAttributes = map[string]*regexp.Regexp{

operationv3.go

Lines changed: 79 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"go/ast"
77
"log"
8+
"maps"
89
"net/http"
910
"strconv"
1011
"strings"
@@ -926,22 +927,15 @@ func (o *OperationV3) ParseResponseComment(commentLine string, astFile *ast.File
926927

927928
for _, codeStr := range strings.Split(matches[1], ",") {
928929
if strings.EqualFold(codeStr, defaultTag) {
929-
response := o.DefaultResponse()
930-
response.Description = description
931-
932-
mimeType := "application/json" // TODO: set correct mimeType
933-
setResponseSchema(response, mimeType, schema)
934-
935-
continue
936-
}
937-
938-
code, err := strconv.Atoi(codeStr)
939-
if err != nil {
940-
return fmt.Errorf("can not parse response comment \"%s\"", commentLine)
941-
}
942-
943-
if description == "" {
944-
description = http.StatusText(code)
930+
codeStr = ""
931+
} else {
932+
code, err := strconv.Atoi(codeStr)
933+
if err != nil {
934+
return fmt.Errorf("can not parse response comment \"%s\"", commentLine)
935+
}
936+
if description == "" {
937+
description = http.StatusText(code)
938+
}
945939
}
946940

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

980974
for _, codeStr := range strings.Split(matches[1], ",") {
981975
if strings.EqualFold(codeStr, defaultTag) {
982-
response := o.DefaultResponse()
983-
response.Description = description
984-
985-
continue
986-
}
987-
988-
_, err := strconv.Atoi(codeStr)
989-
if err != nil {
990-
return fmt.Errorf("can not parse response comment \"%s\"", commentLine)
976+
codeStr = ""
977+
} else {
978+
_, err := strconv.Atoi(codeStr)
979+
if err != nil {
980+
return fmt.Errorf("can not parse response comment \"%s\"", commentLine)
981+
}
991982
}
992983

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

999-
// DefaultResponse return the default response member pointer.
1000-
func (o *OperationV3) DefaultResponse() *spec.Response {
1001-
if o.Responses.Spec.Default == nil {
1002-
o.Responses.Spec.Default = spec.NewResponseSpec()
1003-
o.Responses.Spec.Default.Spec.Spec.Headers = make(map[string]*spec.RefOrSpec[spec.Extendable[spec.Header]])
1004-
}
1005-
1006-
if o.Responses.Spec.Default.Spec.Spec.Content == nil {
1007-
o.Responses.Spec.Default.Spec.Spec.Content = make(map[string]*spec.Extendable[spec.MediaType])
1008-
}
1009-
1010-
return o.Responses.Spec.Default.Spec.Spec
1011-
}
1012-
1013990
// AddResponse add a response for a code.
991+
// If the code is already exist, it will merge with the old one:
992+
// 1. The description will be replaced by the new one if the new one is not empty.
993+
// 2. The content schema will be merged using `oneOf` if the new one is not empty.
1014994
func (o *OperationV3) AddResponse(code string, response *spec.RefOrSpec[spec.Extendable[spec.Response]]) {
1015995
if response.Spec.Spec.Headers == nil {
1016996
response.Spec.Spec.Headers = make(map[string]*spec.RefOrSpec[spec.Extendable[spec.Header]])
@@ -1020,24 +1000,74 @@ func (o *OperationV3) AddResponse(code string, response *spec.RefOrSpec[spec.Ext
10201000
o.Responses.Spec.Response = make(map[string]*spec.RefOrSpec[spec.Extendable[spec.Response]])
10211001
}
10221002

1023-
o.Responses.Spec.Response[code] = response
1003+
res := response
1004+
var prev *spec.RefOrSpec[spec.Extendable[spec.Response]]
1005+
if code != "" {
1006+
prev = o.Responses.Spec.Response[code]
1007+
} else {
1008+
prev = o.Responses.Spec.Default
1009+
}
1010+
if prev != nil { // merge into prev
1011+
res = prev
1012+
if response.Spec.Spec.Description != "" {
1013+
prev.Spec.Spec.Description = response.Spec.Spec.Description
1014+
}
1015+
if len(response.Spec.Spec.Content) > 0 {
1016+
// responses should only have one content type
1017+
singleKey := ""
1018+
for k := range response.Spec.Spec.Content {
1019+
singleKey = k
1020+
break
1021+
}
1022+
if prevMediaType := prev.Spec.Spec.Content[singleKey]; prevMediaType == nil {
1023+
prev.Spec.Spec.Content = response.Spec.Spec.Content
1024+
} else {
1025+
newMediaType := response.Spec.Spec.Content[singleKey]
1026+
if len(newMediaType.Extensions) > 0 {
1027+
if prevMediaType.Extensions == nil {
1028+
prevMediaType.Extensions = make(map[string]interface{})
1029+
}
1030+
maps.Copy(prevMediaType.Extensions, newMediaType.Extensions)
1031+
}
1032+
if len(newMediaType.Spec.Examples) > 0 {
1033+
if prevMediaType.Spec.Examples == nil {
1034+
prevMediaType.Spec.Examples = make(map[string]*spec.RefOrSpec[spec.Extendable[spec.Example]])
1035+
}
1036+
maps.Copy(prevMediaType.Spec.Examples, newMediaType.Spec.Examples)
1037+
}
1038+
if prevSchema := prevMediaType.Spec.Schema; prevSchema.Ref != nil || prevSchema.Spec.OneOf == nil {
1039+
oneOfSchema := spec.NewSchemaSpec()
1040+
oneOfSchema.Spec.OneOf = []*spec.RefOrSpec[spec.Schema]{prevSchema, newMediaType.Spec.Schema}
1041+
prevMediaType.Spec.Schema = oneOfSchema
1042+
} else {
1043+
prevSchema.Spec.OneOf = append(prevSchema.Spec.OneOf, newMediaType.Spec.Schema)
1044+
}
1045+
}
1046+
}
1047+
}
1048+
1049+
if code != "" {
1050+
o.Responses.Spec.Response[code] = res
1051+
} else {
1052+
o.Responses.Spec.Default = res
1053+
}
10241054
}
10251055

10261056
// ParseEmptyResponseOnly parse only comment out status code ,eg: @Success 200.
10271057
func (o *OperationV3) ParseEmptyResponseOnly(commentLine string) error {
10281058
for _, codeStr := range strings.Split(commentLine, ",") {
1059+
var description string
10291060
if strings.EqualFold(codeStr, defaultTag) {
1030-
_ = o.DefaultResponse()
1031-
1032-
continue
1033-
}
1034-
1035-
code, err := strconv.Atoi(codeStr)
1036-
if err != nil {
1037-
return fmt.Errorf("can not parse response comment \"%s\"", commentLine)
1061+
codeStr = ""
1062+
} else {
1063+
code, err := strconv.Atoi(codeStr)
1064+
if err != nil {
1065+
return fmt.Errorf("can not parse response comment \"%s\"", commentLine)
1066+
}
1067+
description = http.StatusText(code)
10381068
}
10391069

1040-
o.AddResponse(codeStr, newResponseWithDescription(http.StatusText(code)))
1070+
o.AddResponse(codeStr, newResponseWithDescription(description))
10411071
}
10421072

10431073
return nil

parserv3.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ import (
1515
"github.com/sv-tools/openapi/spec"
1616
)
1717

18-
// FieldParserFactoryV3 func(ps *Parser, field *ast.Field) FieldParserV3 create FieldParser.
19-
type FieldParserFactoryV3 func(ps *Parser, field *ast.Field) FieldParserV3
18+
// FieldParserFactoryV3 create FieldParser.
19+
type FieldParserFactoryV3 func(ps *Parser, file *ast.File, field *ast.Field) FieldParserV3
2020

2121
// FieldParserV3 parse struct field.
2222
type FieldParserV3 interface {
@@ -903,7 +903,7 @@ func (p *Parser) parseStructFieldV3(file *ast.File, field *ast.Field) (map[strin
903903
}
904904
}
905905

906-
ps := p.fieldParserFactoryV3(p, field)
906+
ps := p.fieldParserFactoryV3(p, file, field)
907907

908908
if ps.ShouldSkip() {
909909
return nil, nil, nil

0 commit comments

Comments
 (0)