Skip to content
Open
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
7 changes: 7 additions & 0 deletions pkg/codegen/gencontext.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ type genContext struct {
// changing that would rename nested types across every spec.
nameHint []string

// unionBranch marks the schema being generated as a branch of a oneOf or
// anyOf, which names that branch after its path. An array branch hands
// its inline-enum items a path of their own then, so the items' enum does
// not take the branch's name (issue #2640).
unionBranch bool

// rootPosition marks the top of a components/schemas entry, where
// GenerateTypesForSchemas defines the type under a name renameSchema
// chooses rather than one derived from the path. A composition there
Expand Down Expand Up @@ -165,6 +171,7 @@ func newRootGenContext(nameHint []string) genContext {
func (ctx genContext) at(nameHint []string) genContext {
ctx.nameHint = slices.Clone(nameHint)
ctx.rootPosition = false
ctx.unionBranch = false
return ctx
}

Expand Down
22 changes: 21 additions & 1 deletion pkg/codegen/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -1726,7 +1726,27 @@ func oapiSchemaToGoType(ctx genContext, schema *openapi3.Schema, path []string,
// For arrays, we'll get the type of the Items and throw a
// [] in front of it.
itemHint := append(slices.Clone(path), "Item")
arrayType, err := generateGoSchema(ctx.at(itemHint), schema.Items, path)
itemPath := path
if ctx.unionBranch && schema.Items != nil && schema.Items.Ref == "" && schema.Items.Value != nil {
items := schema.Items.Value
extensions := combinedSchemaExtensions(schema.Items)
if _, named := extensions[extGoTypeName]; !named {
_, overridden := extensions[extPropGoType]
var oneOfEnum []enumViaOneOfValue
if !overridden && items.AllOf == nil {
oneOfEnum, _, _ = detectEnumViaOneOf(items)
}
itemType := schemaPrimaryType(items.Type)
isConst := globalState.is31 && items.Const != nil && !overridden && items.AllOf == nil &&
itemType.Slice() != nil && !itemType.Is("object") && !itemType.Is("array") && !isMultiTypeUnion(itemType)
if len(items.Enum) > 0 || isConst || oneOfEnum != nil {
// The branch and its inline enum would otherwise share a name.
itemPath = itemHint
}
}
}
itemCtx := ctx.at(itemHint)
arrayType, err := generateGoSchema(itemCtx, schema.Items, itemPath)
if err != nil {
return fmt.Errorf("error generating type for array: %w", err)
}
Expand Down
4 changes: 3 additions & 1 deletion pkg/codegen/union.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,9 @@ func generateUnions(ctx genContext, outSchema *Schema, schema *openapi3.Schema,
// that name in outSchema's additional types unless it generated as that named
// type already, along with the types it brings.
func generateUnionElement(ctx genContext, outSchema *Schema, element *openapi3.SchemaRef, elementPath []string) (Schema, error) {
elementSchema, err := generateGoSchema(ctx.at(elementPath), element, elementPath)
elementCtx := ctx.at(elementPath)
elementCtx.unionBranch = element.Ref == ""
elementSchema, err := generateGoSchema(elementCtx, element, elementPath)
if err != nil {
return Schema{}, err
}
Expand Down
145 changes: 145 additions & 0 deletions pkg/codegen/union_array_enum_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package codegen

import (
"regexp"
"testing"

"github.com/getkin/kin-openapi/openapi3"
"github.com/stretchr/testify/require"
)

// A oneOf/anyOf branch that is an array of an inline enum must not give the
// items' enum the same type name as the branch (issue #2640).
func TestUnionArrayBranchOfInlineEnum(t *testing.T) {
const spec = `
openapi: 3.0.3
info: {title: t, version: "1"}
paths:
/items:
post:
requestBody:
content:
application/json:
schema:
type: object
properties:
roles:
oneOf:
- {type: string}
- {type: array, items: {type: string, enum: [admin, viewer]}}
responses:
"200":
description: ok
content:
application/json:
schema:
$ref: '#/components/schemas/Item'
components:
schemas:
Item:
type: object
properties:
roles:
anyOf:
- {type: string}
- {type: array, items: {type: string, enum: [admin, viewer]}}
`
swagger, err := openapi3.NewLoader().LoadFromData([]byte(spec))
require.NoError(t, err)

code, err := Generate(swagger, Configuration{
PackageName: "api",
Generate: GenerateOptions{Models: true},
})
require.NoError(t, err)

for _, name := range []string{"PostItemsJSONBodyRoles1", "ItemRoles1"} {
require.Regexp(t, regexp.MustCompile(`type `+name+` = ?\[\]`+name+`Item\n`), code)
require.Regexp(t, regexp.MustCompile(`type `+name+`Item string\n`), code)
require.Len(t, regexp.MustCompile(`(?m)^type `+name+` `).FindAllString(code, -1), 1)
}
}

func TestUnionObjectBranchArrayPropertyInlineEnumName(t *testing.T) {
const spec = `
openapi: 3.0.3
info: {title: t, version: "1"}
paths: {}
components:
schemas:
Choice:
oneOf:
- type: object
properties:
roles: {type: array, items: {type: string, enum: [admin, viewer]}}
- {type: string}
`
swagger, err := openapi3.NewLoader().LoadFromData([]byte(spec))
require.NoError(t, err)
code, err := Generate(swagger, Configuration{
PackageName: "api",
Generate: GenerateOptions{Models: true},
OutputOptions: OutputOptions{SkipPrune: true},
})
require.NoError(t, err)
require.Regexp(t, `(?m)^type Choice0Roles string$`, code)
require.Regexp(t, `Roles \*\[\]Choice0Roles `, code)
require.NotContains(t, code, "Choice0RolesItem")
}

func TestUnionArrayBranchExplicitEnumNameLegacyConstants(t *testing.T) {
const spec = `
openapi: 3.0.3
info: {title: t, version: "1"}
paths: {}
components:
schemas:
Choice:
oneOf:
- {type: string}
- {type: array, items: {type: string, enum: [admin, viewer], x-go-type-name: Role}}
`
swagger, err := openapi3.NewLoader().LoadFromData([]byte(spec))
require.NoError(t, err)
code, err := Generate(swagger, Configuration{
PackageName: "api",
Generate: GenerateOptions{Models: true},
OutputOptions: OutputOptions{SkipPrune: true},
Compatibility: CompatibilityOptions{OldEnumConflicts: true},
})
require.NoError(t, err)
require.Regexp(t, `(?m)^\s*Choice1Admin\s+Role\s*=`, code)
require.Regexp(t, `(?m)^\s*Choice1Viewer\s+Role\s*=`, code)
require.NotContains(t, code, "Choice1ItemAdmin")
}

func TestUnionArrayBranchOfOpenAPI31InlineEnums(t *testing.T) {
for _, tc := range []struct{ name, items string }{
{"const", "type: string, const: admin"},
{"oneOf", "oneOf: [{title: Admin, const: admin}, {title: Viewer, const: viewer}]"},
} {
t.Run(tc.name, func(t *testing.T) {
spec := `
openapi: 3.1.0
info: {title: t, version: "1"}
paths: {}
components:
schemas:
Choice:
oneOf:
- {type: string}
- {type: array, items: {` + tc.items + `}}
`
swagger, err := openapi3.NewLoader().LoadFromData([]byte(spec))
require.NoError(t, err)
code, err := Generate(swagger, Configuration{
PackageName: "api",
Generate: GenerateOptions{Models: true},
OutputOptions: OutputOptions{SkipPrune: true},
})
require.NoError(t, err)
require.Regexp(t, `(?m)^type Choice1 = ?\[\]Choice1Item$`, code)
require.Regexp(t, `(?m)^type Choice1Item string$`, code)
})
}
}
Loading