Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
a5dfcf7
fix(compilers/openapi): read a union alone, distribute inline branches
OmarAlJarrah Nov 10, 2026
d4e8c08
fix(compilers/openapi): read a union on an allOf branch as a conjunct
OmarAlJarrah Nov 10, 2026
dcf19f4
fix(compilers/openapi)!: distribute a union written as an allOf branch
OmarAlJarrah Nov 10, 2026
4565ba8
docs: record that an inline union branch now distributes
OmarAlJarrah Nov 10, 2026
5dea18e
test(compilers/openapi): pin a const and a tuple beside a union alone
OmarAlJarrah Nov 10, 2026
e8f3563
fix(compilers/openapi): keep a subtype a discriminator routes to whole
OmarAlJarrah Nov 10, 2026
52d4eea
perf(compilers/openapi): ask nothing of a union a schema does not write
OmarAlJarrah Nov 10, 2026
bbb76ce
docs: put each union sentence back beside what it leans on
OmarAlJarrah Nov 10, 2026
370a9ff
docs(compilers/openapi): cite the issues routed subtypes wait on
OmarAlJarrah Nov 10, 2026
b0ca051
docs(compilers/openapi): say a discriminator routes a subtype, not a tag
OmarAlJarrah Nov 10, 2026
bc2d127
fix(compilers/openapi): compare a union branch keyword by keyword
OmarAlJarrah Nov 10, 2026
63639ce
test(compilers/openapi): pin four guards the union readings rely on
OmarAlJarrah Nov 10, 2026
6d82d5a
Merge branch 'main' into fix/openapi-union-allof-branches
OmarAlJarrah Nov 10, 2026
88c4e61
test(compilers/openapi): pin the guards that keep a union whole
OmarAlJarrah Nov 10, 2026
67c2f71
refactor(compilers/openapi): drop unionOf, which only built a literal
OmarAlJarrah Nov 10, 2026
9de8980
test(compilers/openapi): pin a union branch written as the principal
OmarAlJarrah Nov 10, 2026
cf3a016
Merge branch 'main' into fix/openapi-union-allof-branches
OmarAlJarrah Nov 10, 2026
b077a59
fix(compilers/openapi): fold beside a union on an allOf branch too
OmarAlJarrah Nov 10, 2026
32607e8
fix(compilers/openapi): distribute a branch union beside a $dynamicRef
OmarAlJarrah Nov 10, 2026
9145921
docs(ir): record the Unmodeled keys 0.39.0 stops writing
OmarAlJarrah Nov 10, 2026
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
222 changes: 183 additions & 39 deletions compilers/openapi/e2e/conformance_composition_test.go

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions compilers/openapi/e2e/conformance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ func conformanceCases() []conformanceCase {
{"allof-required-only", assertAllOfRequiredOnly},
{"required-members", assertRequiredMembers},
{"allof-oneof-cooccurrence", assertAllOfOneOfCooccurrence},
{"allof-union-branch", assertAllOfUnionBranch},
{"allof-inline-residue", assertAllOfInlineResidue},
{"allof-nested-inline", assertAllOfNestedInline},
{"allof-ref-branch-siblings", assertAllOfRefBranchSiblings},
Expand All @@ -176,6 +177,8 @@ func conformanceCases() []conformanceCase {
{"unhomed-keywords", assertUnhomedKeywords},
{"union-beside-ref", assertUnionBesideRef},
{"union-restated-branches", assertUnionRestatedBranches},
{"union-restated-type", assertUnionRestatedType},
{"union-inline-branches", assertUnionInlineBranches},
{"codeclared-keywords", assertCoDeclaredKeywords},
{"codeclared-schema-content", assertCoDeclaredSchemaContent},
{"content-several-entries", assertContentSeveralEntries},
Expand Down
2 changes: 1 addition & 1 deletion compilers/openapi/internal/lowering/promotion.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ func DefaultExtensionPromotions() map[ExtensionTarget][]ExtensionSource {
// which is what makes "the extension survives its own promotion" structural
// instead of a rule each caller has to remember: nothing here can consume an
// entry. It takes them at the top level, so a key under a scope or inside a
// verbatim entry reaches no field (GitHub #661, #1220, #1221).
// verbatim entry reaches no field (GitHub #594, #1220, #1221).
type Promoter struct {
targets map[ExtensionTarget][]ExtensionSource
// keys is every extension key targets reads, distinct and sorted.
Expand Down
57 changes: 39 additions & 18 deletions compilers/openapi/internal/operation/multipart.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,13 +286,29 @@ func partPropID(prop *ir.Property, d ids.Document, wire string, schemaPtr jsonte
// partMembers returns, by wire name, the declaration the flattening of the type
// body denotes takes for each of its members
// (docs/ir/models.md#the-flattening), which is the one an encoding key names:
// what a reader of the body's flattened shape looks up. A body standing for no
// model has none.
// what a reader of the body's flattened shape looks up. A body standing for a
// union, as one a composition is distributed across does, has its variants'
// members, the first variant declaring each naming it (docs/ir/payloads.md). A
// body standing for no model has none.
func partMembers(reg ir.TypeRegistry, body ir.TypeID) map[string]*ir.Property {
flat := ir.FlattenedProperties(reg, body)
members := make(map[string]*ir.Property, len(flat.Properties))
for _, fp := range flat.Properties {
members[fp.Property.EffectiveWireName()] = fp.Property
holders := []ir.TypeID{body}
node := func(id ir.TypeID) (ir.TypeDef, bool) {
td, ok := reg[id]
return td, ok
}
if u, ok := bodyNode(node, body).(*ir.Union); ok && u != nil {
holders = holders[:0]
for _, v := range u.Variants {
holders = append(holders, v.Type.Target)
}
}
members := map[string]*ir.Property{}
for _, holder := range holders {
for _, fp := range ir.FlattenedProperties(reg, holder).Properties {
if name := fp.Property.EffectiveWireName(); members[name] == nil {
members[name] = fp.Property
}
}
}
return members
}
Expand Down Expand Up @@ -488,25 +504,30 @@ const maxBodyAliasHops = 64
// It reports ok=false for a body that stands for no model at all — a primitive,
// an enum, an opaque scalar — where no pointer would name a property either.
func bodyModel(node func(ir.TypeID) (ir.TypeDef, bool), body ir.TypeID) (ir.Provenance, bool) {
m, ok := bodyNode(node, body).(*ir.Model)
if !ok || m == nil {
return ir.Provenance{}, false
}
return m.Provenance, true
}

// bodyNode returns the node body stands for past the alias scalars a
// $ref-with-siblings position hoists, nil where the chain reaches no node or
// runs past maxBodyAliasHops.
func bodyNode(node func(ir.TypeID) (ir.TypeDef, bool), body ir.TypeID) ir.TypeDef {
id := body
for range maxBodyAliasHops {
td, found := node(id)
if !found {
return ir.Provenance{}, false
return nil
}
switch t := td.(type) {
case *ir.Model:
return t.Provenance, true
case *ir.Scalar:
if t.Base == nil {
return ir.Provenance{}, false
}
id = t.Base.Target
default:
return ir.Provenance{}, false
s, alias := td.(*ir.Scalar)
if !alias || s == nil || s.Base == nil {
return td
}
id = s.Base.Target
}
return ir.Provenance{}, false
return nil
}

// bodySchemaPointer returns the document and pointer partEncodings keys
Expand Down
24 changes: 24 additions & 0 deletions compilers/openapi/internal/operation/multipart_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,30 @@ func TestPartMembers_DeadEnds(t *testing.T) {
assert.Equal(t, file.ID, members["file"].ID)
}

// TestPartMembers_AUnionBodyHasItsVariantsMembers pins a body standing for a
// union, as one a composition is distributed across does, through an alias
// too: its members are its variants', the first variant declaring a name naming
// it, so a key names the body's own property on every variant.
func TestPartMembers_AUnionBodyHasItsVariantsMembers(t *testing.T) {
t.Parallel()
shared := ir.Property{ID: "p/body/file", WireName: "file"}
first := ir.Property{ID: "p/one/x", WireName: "x"}
reg := ir.TypeRegistry{
"t/one": &ir.Model{ID: "t/one", Properties: []ir.Property{shared, first}},
"t/two": &ir.Model{ID: "t/two", Properties: []ir.Property{shared, {ID: "p/two/x", WireName: "x"}}},
"t/u": &ir.Union{ID: "t/u", Variants: []ir.Variant{{Type: ir.TypeRef{Target: "t/one"}}, {Type: ir.TypeRef{Target: "t/two"}}}},
"t/alias": &ir.Scalar{ID: "t/alias", Base: &ir.TypeRef{Target: "t/u"}},
"t/nil": (*ir.Union)(nil),
}
for _, body := range []ir.TypeID{"t/u", "t/alias"} {
members := partMembers(reg, body)
require.Len(t, members, 2, "%s", body)
assert.Equal(t, shared.ID, members["file"].ID, "%s", body)
assert.Equal(t, first.ID, members["x"].ID, "the first variant declaring x names it: %s", body)
}
assert.Empty(t, partMembers(reg, "t/nil"), "a typed nil union stands for no members")
}

// TestBodyParts_RefHopBound covers the walk's $ref hop bound: past it the walk
// stops and says so.
func TestBodyParts_RefHopBound(t *testing.T) {
Expand Down
8 changes: 6 additions & 2 deletions compilers/openapi/internal/schema/admission.go
Original file line number Diff line number Diff line change
Expand Up @@ -604,12 +604,16 @@ func (w *admission) restates(held admitted, from, js *oas3.JSONSchema[oas3.Refer
// same JSON, read against one base where it writes a relative $ref: under an
// $id naming another URI, the same spelling resolves elsewhere. A copy in
// another document is read apart: its $refs resolve against that document, and
// its dialect may differ (docs/compilers/openapi.md#openapi-versions).
// its dialect may differ (docs/compilers/openapi.md#openapi-versions). A schema
// placed nowhere, a nil one among them, is written alike with none.
func (w *admission) sameText(a, b *oas3.JSONSchema[oas3.Referenceable]) bool {
at, placed := w.c.Bindings().Site(a)
bt, placedB := w.c.Bindings().Site(b)
if !placed || !placedB || at.Doc != bt.Doc {
return false
}
text := w.text(a)
if !placed || !placedB || at.Doc != bt.Doc || text == nil || !bytes.Equal(text, w.text(b)) {
if text == nil || !bytes.Equal(text, w.text(b)) {
return false
}
// The texts are remembered, so comparing them first leaves a base, which
Expand Down
15 changes: 13 additions & 2 deletions compilers/openapi/internal/schema/allof.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func lowerAllOf(c lowering.Ctx, ts *compile.Types, depth int, s *oas3.Schema, po
if d != nil {
m.Discriminator = d
}
tag, tagDiags := subtypeDiscriminatorValue(c, s, common.ID, pointer)
tag, _, tagDiags := subtypeDiscriminatorValue(c, s, common.ID, pointer)
m.DiscriminatorValue = tag
diags = append(diags, tagDiags...)
return objectNode(m)
Expand Down Expand Up @@ -167,6 +167,10 @@ type allOfComposition struct {
// position lowers (allOfDecides).
speaks []bool
rebased []bool
// distributes is the pointer of the branch whose union the position
// distributes across its variants (branchUnion), whose merge consumes it,
// and empty where no branch's is.
distributes jsontext.Pointer
}

// branchSpeaks reports whether the branch at index i folds at the position.
Expand Down Expand Up @@ -402,8 +406,10 @@ func preserveUnmergedBranch(c lowering.Ctx, m *ir.Model, comp allOfComposition,
return nil // boolean branch; applyFalseBranches handles it.
}
speaks := comp.branchSpeaks(i)
distributed := comp.distributes != "" && e.pointer == comp.distributes
residue := unmergedBranchKeys(bs, func(keyword string) bool {
return mergeConsumes(bs, keyword, e.wrapper) || (speaks && folds(c, bs, keyword, boundHomes{value: true}))
return mergeConsumes(bs, keyword, e.wrapper) || (speaks && folds(c, bs, keyword, boundHomes{value: true})) ||
(distributed && declaresUnionKeyword(keyword))
})
if len(residue) == 0 {
return nil
Expand Down Expand Up @@ -518,6 +524,11 @@ func mergeConsumes(bs *oas3.Schema, key string, flattened bool) bool {
}
}

// declaresUnionKeyword reports whether keyword is oneOf or anyOf.
func declaresUnionKeyword(keyword string) bool {
return keyword == "oneOf" || keyword == "anyOf"
}

// declaresObjectOnly reports whether the branch's declared type is exactly
// `object`, which the composed ir.Model already restates so the merge loses
// nothing by reading it. A set — `[object, "null"]` included — says more than
Expand Down
2 changes: 1 addition & 1 deletion compilers/openapi/internal/schema/allof_conflicts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1490,7 +1490,7 @@ func TestAllOf_AnUntypedReadPassingOverAKeywordKeepsTheConflict(t *testing.T) {
ref("NotStr"), ref("ObjOneOf"), ref("ObjAnyOf"), ref("IfObj"), ref("AliasOneOf"),
"{$ref: '#/components/schemas/Empty', type: object}",
"{$ref: '#/components/schemas/Empty', allOf: [{type: object}]}",
"{enum: [{a: x}]}", "{const: {a: x}}", "{oneOf: [{type: object}, {type: array}]}",
"{enum: [{a: x}]}", "{const: {a: x}}", "{oneOf: [{type: object}, {type: array}], anyOf: [{type: object}]}",
"{anyOf: [{type: object}]}", "{not: {type: string}}", "{$dynamicRef: '#/components/schemas/Props'}",
"{$recursiveRef: '#'}", "false", ref("Missing"), ref("Refuses"),
} {
Expand Down
10 changes: 9 additions & 1 deletion compilers/openapi/internal/schema/branchshape.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ func (r *bodyReading) declared() declarationSet {
// a reading its budget cut short says nothing, so it counts as one that does,
// even where the body's own keywords would settle it (GitHub #1423).
func unionBranchesDeclareShape(c lowering.Ctx, s *oas3.Schema) bool {
return setsDeclareShape(c, s, s.GetOneOf(), s.GetAnyOf())
}

// setsDeclareShape is unionBranchesDeclareShape for the branches of sets, a
// union on an allOf branch of s among them, judged against s as the body.
func setsDeclareShape(c lowering.Ctx, s *oas3.Schema, sets ...[]*oas3.JSONSchema[oas3.Referenceable]) bool {
var r *bodyReading
body := func() *bodyReading {
if r == nil {
Expand All @@ -85,7 +91,9 @@ func unionBranchesDeclareShape(c lowering.Ctx, s *oas3.Schema) bool {
declares := func(b *oas3.JSONSchema[oas3.Referenceable]) bool {
return isRefBranch(b) || branchDeclaresShape(body, b)
}
found := slices.ContainsFunc(s.GetOneOf(), declares) || slices.ContainsFunc(s.GetAnyOf(), declares)
found := slices.ContainsFunc(sets, func(set []*oas3.JSONSchema[oas3.Referenceable]) bool {
return slices.ContainsFunc(set, declares)
})
return found || r != nil && r.w.incomplete()
}

Expand Down
6 changes: 3 additions & 3 deletions compilers/openapi/internal/schema/census.go
Original file line number Diff line number Diff line change
Expand Up @@ -329,13 +329,13 @@ func referenceOutcomeKeywords(c lowering.Ctx, s *oas3.Schema, pointer jsontext.P
if _, _, expanded := dynamicExpansion(c, s, pointer); expanded {
return unhomedKeywords(s, nil, nil), expandedShape
}
if hasUnionSiblings(s) {
if hasUnionSiblings(c, s) {
return nil, ""
}
if _, _, collapses := nullUnionCollapse(s, ""); !collapses {
return nil, ""
}
return unhomedKeywords(s, nil, nil), collapsedShape
return unhomedKeywords(s, nil, branchesSay(c, s)), collapsedShape
}

// preserveUnhomedKeywords keeps verbatim the census keywords and value
Expand All @@ -357,7 +357,7 @@ func preserveUnhomedKeywords(c lowering.Ctx, ts *compile.Types, s *oas3.Schema,
return id, diags
}
owns, _ := ts.Lookup(c.IDs().Coordinate(pointer))
unhomed := unhomedKeywords(s, td, d.skipped)
unhomed := unhomedKeywords(s, td, slices.Concat(d.skipped, d.said))
if owns == id && !constraintsHome(td) {
// No alias will be hoisted over a node the position already owns, so the
// bounds it wrote reach no Constraints field anywhere.
Expand Down
20 changes: 10 additions & 10 deletions compilers/openapi/internal/schema/discriminator.go
Original file line number Diff line number Diff line change
Expand Up @@ -330,39 +330,39 @@ func propIDByName(m *ir.Model, name string) (ir.PropID, bool) {
}

// subtypeDiscriminatorValue returns the wire tag this allOf subtype carries in
// its discriminator hierarchy, or "" when no ancestor anchors one
// its hierarchy, false where none routes to it, "" being a tag
// (docs/ir/models.md): the mapping key naming the subtype, else its schema name
// as declared, not as its escaped pointer token spells it (GitHub #505). An
// inline subtype has no name, so only a mapping entry can tag it (GitHub #517).
// inline subtype has no name, so only a mapping entry tags it (GitHub #517).
//
// Every discriminated ancestor is asked, since an intermediate may declare none
// Every discriminated ancestor is asked, as an intermediate may declare none
// (GitHub #305). Of several mapping keys for the subtype the smallest in byte
// order is elected, as a mapping is unordered (GitHub #410); the base's
// order is elected, a mapping being unordered (GitHub #410); the base's
// Discriminator keeps them all, and an info diagnostic reports it.
func subtypeDiscriminatorValue(c lowering.Ctx, s *oas3.Schema, id ir.TypeID, pointer jsontext.Pointer) (string, []ir.Diagnostic) {
func subtypeDiscriminatorValue(c lowering.Ctx, s *oas3.Schema, id ir.TypeID, pointer jsontext.Pointer) (string, bool, []ir.Diagnostic) {
ds := ancestorDiscriminators(c.Bindings(), s)
if len(ds) == 0 {
return "", nil
return "", false, nil
}
for _, d := range ds {
tags := mappingTagsFor(c, d, id)
if len(tags) == 0 {
continue
}
if len(tags) == 1 {
return tags[0], nil
return tags[0], true, nil
}
return tags[0], []ir.Diagnostic{c.DiagAt(ir.SeverityInfo, diag.DegradedConstruct, pointer,
return tags[0], true, []ir.Diagnostic{c.DiagAt(ir.SeverityInfo, diag.DegradedConstruct, pointer,
"the discriminator mapping names this schema under %d keys (%s); "+
"discriminatorValue holds the smallest in byte order, and the base's mapping keeps them all",
len(tags), strings.Join(tags, ", "))}
}
// Read the state, not only the name: a top-level entry of another kind, such
// as components/x-shared/Cat, has none to read (GitHub #572).
if name, state := ids.ComponentSchema(pointer); state == ids.NamedComponent {
return name, nil
return name, true, nil
}
return "", nil
return "", false, nil
}

// mappingTagsFor returns every key d's mapping spells for the type id, sorted
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,6 @@ func TestDiscriminatorBesideUnion_MovesWhenARouteIsNoSubtype(t *testing.T) {
{"lowers to an alias over its $ref", "3.1.0", "{propertyName: kind, mapping: {cat: '#/components/schemas/Cat'}}",
" Cat: {$ref: '#/components/schemas/Other', allOf: [{$ref: '#/components/schemas/Pet'}]}\n" +
" Other: {type: object}\n" + subDog, `"mapping"`},
{"lowers to a distributed union", "3.1.0", "{propertyName: kind, mapping: {cat: '#/components/schemas/Cat'}}",
" Cat: " + viaPet + ", oneOf: [{$ref: '#/components/schemas/House'}, {$ref: '#/components/schemas/Wild'}]}\n" +
" House: {type: object}\n Wild: {type: object}\n" + subDog, `"mapping"`},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
Expand Down Expand Up @@ -147,8 +144,10 @@ func TestDiscriminatorBesideUnion_KeepsARoutingMapAModel(t *testing.T) {

// TestDiscriminatorBesideUnion_StaysWhenEveryRouteIsASubtype pins the case the
// model keeps its discriminator beside a kept union: every route is a subtype,
// at any distance and however it is spelled. A branch no name reaches, inline
// or a position, is no route, so it does not move the discriminator either.
// at any distance and however it is spelled, a route writing a union beside
// its allOf among them, which the route keeps a subtype. A branch no name
// reaches, inline or a position, is no route, so it does not move the
// discriminator either.
func TestDiscriminatorBesideUnion_StaysWhenEveryRouteIsASubtype(t *testing.T) {
t.Parallel()
const grand = " Cat: {allOf: [{$ref: '#/components/schemas/Mid'}], type: object}\n" +
Expand All @@ -168,6 +167,11 @@ func TestDiscriminatorBesideUnion_StaysWhenEveryRouteIsASubtype(t *testing.T) {
{"grandchild", "3.1.0", petWith("{propertyName: kind, mapping: {cat: '#/components/schemas/Cat'}}"), grand},
{"default", "3.2.0", petWith("{propertyName: kind, defaultMapping: '#/components/schemas/Dog'}"), subtypes},
{"branches no name reaches", "3.1.0", unnamed, cat + " Holder: {type: object, properties: {pet: {type: object}}}\n"},
{"a union beside its allOf, which a route keeps undistributed", "3.1.0",
petWith("{propertyName: kind, mapping: {cat: '#/components/schemas/Cat'}}"),
" Cat: {allOf: [{$ref: '#/components/schemas/Pet'}], " +
"oneOf: [{$ref: '#/components/schemas/House'}, {$ref: '#/components/schemas/Wild'}]}\n" +
" House: {type: object}\n Wild: {type: object}\n" + subDog},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
Expand Down Expand Up @@ -515,7 +519,7 @@ var degradedShapes = []struct {
name string
branches string
}{
{"inline branch", " oneOf: [{$ref: '#/components/schemas/Cat'}, {type: object, properties: {kind: {type: string}}}]\n"},
{"null branch", " oneOf: [{$ref: '#/components/schemas/Cat'}, {type: 'null'}]\n"},
{"oneOf beside anyOf", " oneOf: [{$ref: '#/components/schemas/Cat'}]\n anyOf: [{$ref: '#/components/schemas/Dog'}]\n"},
{"validation-only branches", " oneOf: [{required: [kind]}, {required: [x]}]\n"},
}
Expand Down
22 changes: 18 additions & 4 deletions compilers/openapi/internal/schema/drafts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,16 +112,30 @@ func TestRefSite_AKeywordBesideADraftsRefIsWarnedOf(t *testing.T) {

// TestUnion_ABranchWritingAListOfItemsRestatesNothing pins that a union branch
// writing `items` as a list, a tuple in a draft, restates no element the body
// declares, so the union is kept as a constraint beside the body rather than
// read as validation the body already says.
// declares, so the union is distributed across the body rather than read as
// validation the body already says.
func TestUnion_ABranchWritingAListOfItemsRestatesNothing(t *testing.T) {
t.Parallel()
doc, diags := parseFull(t, openapitest.ComponentSpec(" U:\n "+d4+"\n type: object\n"+
" properties: {p: {type: array, items: {type: string}}}\n"+
" oneOf: [{properties: {p: {items: [{type: integer}]}}}, {properties: {p: {maxItems: 1}}}]\n"))
openapitest.RequireNoErrorDiags(t, diags)
u := doc.Types[componentID("U")].Common().Unmodeled
assert.Equal(t, ir.ReasonDegradedLowering, u["openapi:oneOf"].Reason)
assert.Equal(t, readDistributed, unionReading(doc, "U"))
}

// TestUnion_AListOfItemsBesideTheBranchesIsNotSaid pins that a body writing
// `items` as a list, a tuple in a draft, is said by no branch writing one
// element schema, so the union keeps the body rather than lowering alone and
// dropping the tuple (docs/compilers/openapi.md#union-beside-structure).
func TestUnion_AListOfItemsBesideTheBranchesIsNotSaid(t *testing.T) {
t.Parallel()
doc, diags := parseFull(t, openapitest.ComponentSpec(" U:\n "+d4+"\n type: array\n"+
" items: [{type: string}, {type: integer}]\n"+
" anyOf: [{type: array, items: {type: string, format: uuid}}, {type: array, items: {type: string, format: date}}]\n"))
openapitest.RequireNoErrorDiags(t, diags)
assert.Equal(t, string(ir.ReasonDegradedLowering), unionReading(doc, "U"))
_, tuple := typeByName(doc, "U").(*ir.Tuple)
assert.True(t, tuple, "the body keeps its tuple; got %T", typeByName(doc, "U"))
}

// TestAllOf_ABranchUnderADraftsIDFoldsNothing pins that draft 4's `id` gives an
Expand Down
Loading
Loading