Skip to content
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
24 changes: 18 additions & 6 deletions compilers/openapi/internal/schema/allof.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,6 @@ func lowerAllOf(c lowering.Ctx, ts *compile.Types, depth int, s *oas3.Schema, po
m := &ir.Model{TypeCommon: common, Constraints: cons}
comp := readAllOf(c, s, pointer) // the one flattened list; the readers below take it, not s
diags = append(diags, diagDisjointConjuncts(c, comp, composedShape)...)
if allOfDecides(c, s) {
v := voicingOf(c, s)
comp.speaks, comp.rebased = v.speaks, v.rebased
}
fields := newModelFields(c, ts, m)
diags = append(diags, fillAllOf(c, ts, depth, m, fields, comp)...)
diags = append(diags, fillModelProperties(c, ts, depth, fields, s, pointer)...)
Expand Down Expand Up @@ -167,8 +163,8 @@ type allOfComposition struct {
disjoint []string
// speaks marks each branch that folds at the position (voicesAmong), and
// rebased each whose folding keywords write a relative $ref it reads against
// another base, so it does not. They are nil for a composition distributed
// across a union, whose variants each keep every branch.
// another base, so it does not. They are nil where the allOf is not what the
// position lowers (allOfDecides).
speaks []bool
rebased []bool
}
Expand All @@ -191,6 +187,22 @@ func allOfBranches(s *oas3.Schema, pointer jsontext.Pointer) allOfComposition {
return allOfComposition{pointer: pointer, branches: out}
}

// readAllOf is allOfBranches of the schema written at pointer, with what its
// conjuncts jointly admit read and, where the allOf is what the position lowers,
// which branches fold there (voicingOf), for a lowering that composes it. That
// gate is positionVoices', so a branch gives up a keyword only where a reader
// of the position takes it up; both composers pass it, and the report of an
// allOf the election passed over does not.
func readAllOf(c lowering.Ctx, s *oas3.Schema, pointer jsontext.Pointer) allOfComposition {
comp := allOfBranches(s, pointer)
comp.disjoint = disjointConjuncts(c, s, comp)
if allOfDecides(c, s) {
v := voicingOf(c, s)
comp.speaks, comp.rebased = v.speaks, v.rebased
}
return comp
}

// allOfRefBranches yields the $ref branches of s's allOf after flattening,
// schemas only: the view an ancestor walk needs, which reads no pointer.
func allOfRefBranches(s *oas3.Schema) iter.Seq[*oas3.JSONSchema[oas3.Referenceable]] {
Expand Down
9 changes: 0 additions & 9 deletions compilers/openapi/internal/schema/allof_disjoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
package schema

import (
"encoding/json/jsontext"
"strings"

"github.com/speakeasy-api/openapi/jsonschema/oas3"
Expand Down Expand Up @@ -231,11 +230,3 @@ func diagDisjointConjuncts(c lowering.Ctx, comp allOfComposition, shape string)

// composedShape is the shape a composition lowers to, for diagDisjointConjuncts.
const composedShape = "a composition"

// readAllOf is allOfBranches of the schema written at pointer, with what its
// conjuncts jointly admit read, for a lowering that composes it.
func readAllOf(c lowering.Ctx, s *oas3.Schema, pointer jsontext.Pointer) allOfComposition {
comp := allOfBranches(s, pointer)
comp.disjoint = disjointConjuncts(c, s, comp)
return comp
}
9 changes: 6 additions & 3 deletions compilers/openapi/internal/schema/allof_residue_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,9 +249,10 @@ func TestAllOf_EachInlineBranchKeyedSeparately(t *testing.T) {

// TestAllOf_BranchResidueRidesEveryDistributedVariant covers fillAllOf's second
// caller: buildComposedVariant re-runs the same merge once per union variant, so
// the residue has to land on every variant the composition rides on. The
// diagnostic is announced once even so — the branch is one source construct
// however many variants carry it.
// the residue, the whole branch, has to land on every variant the composition
// rides on, while what the branch folds reaches the Union. The diagnostic is
// announced once even so — the branch is one source construct however many
// variants carry it.
func TestAllOf_BranchResidueRidesEveryDistributedVariant(t *testing.T) {
t.Parallel()
spec := openapitest.ComponentSpec(` Base: {type: object, properties: {id: {type: string}}}
Expand All @@ -263,6 +264,7 @@ func TestAllOf_BranchResidueRidesEveryDistributedVariant(t *testing.T) {
- type: object
properties: {c: {type: string}}
description: BranchDoc
patternProperties: {'^x': {type: string}}
oneOf:
- {$ref: '#/components/schemas/A'}
- {$ref: '#/components/schemas/B'}
Expand All @@ -280,6 +282,7 @@ func TestAllOf_BranchResidueRidesEveryDistributedVariant(t *testing.T) {
require.True(t, ok, "variant %d carries the branch residue; got %+v", i, variant.Unmodeled)
assert.Contains(t, string(entry.Value), `"description":"BranchDoc"`, "variant %d", i)
}
assert.Equal(t, "BranchDoc", u.Docs.Description)
assert.Equal(t, 1, openapitest.CountDiagsAt(diags, diag.DegradedConstruct, ir.SeverityInfo),
"one branch, one diagnostic, however many variants carry it; got %+v", diags)
}
Expand Down
15 changes: 11 additions & 4 deletions compilers/openapi/internal/schema/nulls_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -394,10 +394,17 @@ func TestNullableWidening_ABranchWidensTheAllOfHoldingIt(t *testing.T) {
assert.Equal(t, "d", propertyOf(t, doc, "Holder", "p").Docs.Description, name)
}

besideUnion := openapitest.HolderSpec("3.0.3",
"{oneOf: [{type: string}, {type: integer}], allOf: [{$ref: '#/components/schemas/Plain'}, {nullable: true}]}", "")
doc, _ = compileNullable(t, besideUnion, false)
assert.False(t, holderP(t, doc).Nullable, "nor does one a union conjoins with")
for name, tc := range map[string]struct{ union, code string }{
"degraded": {"oneOf: [{$ref: '#/components/schemas/Pet'}], anyOf: [{$ref: '#/components/schemas/Toy'}]", diag.DegradedConstruct},
"distributed": {"oneOf: [{$ref: '#/components/schemas/Pet'}, {$ref: '#/components/schemas/Toy'}]", diag.CompositionLowering},
} {
besideUnion := openapitest.HolderSpec("3.0.3", "{"+tc.union+", allOf: [{$ref: '#/components/schemas/Pet'}, {nullable: true}]}",
" Toy: {type: object, properties: {kind: {type: string}}}\n")
doc, diags = compileNullable(t, besideUnion, false)
require.True(t, openapitest.HasDiagCodeAt(diags, tc.code, "/components/schemas/Holder/properties/p"), name)
assert.True(t, holderP(t, doc).Nullable, "%s: so does one beside a union, as beside the allOf", name)
assert.NotEmpty(t, holderP(t, doc).Inferred, name)
}

properties := openapitest.HolderSpec("3.0.3",
"{allOf: [{$ref: '#/components/schemas/Pet'}, {nullable: true, properties: {a: {type: string}}}]}", "")
Expand Down
2 changes: 1 addition & 1 deletion compilers/openapi/internal/schema/refinement.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ type decision struct {
// the bindings, never the registry, so a decision turns on no lowering order, and
// each position's many readers share one reading.
func refinementOf(c lowering.Ctx, s *oas3.Schema, pointer jsontext.Pointer) (refinement, bool) {
if !allOfConjoins(s) {
if !allOfMayRefine(s) {
return refinement{}, false
}
d := lowering.Remembered(c, refinementAt{s, pointer}, func() decision { return decide(c, s, pointer) })
Expand Down
14 changes: 8 additions & 6 deletions compilers/openapi/internal/schema/union.go
Original file line number Diff line number Diff line change
Expand Up @@ -616,7 +616,8 @@ const refSiteUnionWhy = "oneOf/anyOf beside a $ref conjoin with it rather than c
func lowerDistributedUnion(c lowering.Ctx, ts *compile.Types, depth int, s *oas3.Schema, pointer jsontext.Pointer) (ir.TypeID, []ir.Diagnostic) {
var diags []ir.Diagnostic
id := internNode(c, ts, pointer, func(common ir.TypeCommon) ir.TypeDef {
body := composedBody{schema: s, comp: readAllOf(c, s, pointer), id: common.ID}
body := composedBody{schema: s, comp: readAllOf(c, s, pointer),
voices: positionVoices(c, ts, s, pointer, ir.TypeRef{}), id: common.ID}
diags = append(diags, diagDisjointConjuncts(c, body.comp, composedShape)...)
def, unionDiags := buildUnion(c, ts, s, common, pointer,
func(b *oas3.JSONSchema[oas3.Referenceable], vptr jsontext.Pointer) ir.TypeRef {
Expand All @@ -638,7 +639,8 @@ func lowerDistributedUnion(c lowering.Ctx, ts *compile.Types, depth int, s *oas3
// declares one schema, not one per branch.
type composedBody struct {
schema *oas3.Schema
comp allOfComposition // schema's flattened allOf, built once for every variant
comp allOfComposition // schema's flattened allOf, built once for every variant
voices []annotation.Voice // what comp's folding branches say of the position, read once too
id ir.TypeID
}

Expand Down Expand Up @@ -693,11 +695,11 @@ func composedVariantRef(c lowering.Ctx, id ir.TypeID, body *oas3.Schema, branch
// declared, named after the enclosing schema, not after whichever branch built
// them first.
//
// The enclosing schema's value constraints are read as lowerAllOf reads them
// (GitHub #407). Each variant carries a copy, since a bound on the schema
// constrains both sides of the disjunction.
// The enclosing schema's value constraints, its folding branches' among them,
// are read as lowerAllOf reads them (GitHub #407). Each variant carries a copy,
// since a bound on the schema constrains both sides of the disjunction.
func buildComposedVariant(c lowering.Ctx, ts *compile.Types, depth int, body composedBody, branch ir.TypeID, common ir.TypeCommon) (ir.TypeDef, []ir.Diagnostic) {
cons, diags := ConstraintsOf(c, &common.Unmodeled, body.schema, nil, body.comp.pointer)
cons, diags := ConstraintsOf(c, &common.Unmodeled, body.schema, body.voices, body.comp.pointer)
m := &ir.Model{TypeCommon: common, Constraints: cons}
fields := newModelFields(c, ts, m)
diags = append(diags, fillAllOf(c, ts, depth, m, fields, body.comp)...)
Expand Down
2 changes: 1 addition & 1 deletion compilers/openapi/internal/schema/use.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ func throughRef(s *oas3.Schema) *oas3.JSONSchema[oas3.Referenceable] {
if slices.Contains(dispatchOf(s).skipped, "allOf") {
return refs[0]
}
if !allOfConjoins(s) || annotation.DeclaresAny(s, objectKeywords) || !refinableBranches(allOfBranches(s, "")) {
if !allOfMayRefine(s) || annotation.DeclaresAny(s, objectKeywords) || !refinableBranches(allOfBranches(s, "")) {
return nil
}
return refs[0]
Expand Down
25 changes: 17 additions & 8 deletions compilers/openapi/internal/schema/voices.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,21 +88,30 @@ func foldValue(s *oas3.Schema, entry string) foldRendering {
return r
}

// allOfConjoins reports whether s's allOf conjoins with what its position
// lowers: it is no keyword beside a $ref, a $dynamicRef or a union, whose
// branches keep what they write on each variant
// (docs/compilers/openapi.md#union-beside-structure).
func allOfConjoins(s *oas3.Schema) bool {
return len(s.GetAllOf()) > 0 && s.Ref == nil && !declaresUnion(s) && !declaresDynamicRef(s)
// allOfBesideNoRef reports whether s writes an allOf with no $ref beside it,
// which would make the position a $ref site, whose alias keeps the allOf
// verbatim (docs/compilers/openapi.md#allof-beside-ref).
func allOfBesideNoRef(s *oas3.Schema) bool {
return len(s.GetAllOf()) > 0 && s.Ref == nil
}

// allOfMayRefine reports whether s's allOf may refine: no $ref, union or
// $dynamicRef stands beside it, the last two keeping it the composition it would
// otherwise be (docs/compilers/openapi.md#allof-refinement).
func allOfMayRefine(s *oas3.Schema) bool {
return allOfBesideNoRef(s) && !declaresUnion(s) && !declaresDynamicRef(s)
}

// allOfDecides reports whether s's allOf is what its position lowers: one the
// election of competing keywords made the value, or one refining, whose rival
// there, an enum or a const beside it, is one of its conjuncts. An allOf that
// value set beats otherwise stays verbatim
// (docs/compilers/openapi.md#competing-keywords).
// (docs/compilers/openapi.md#competing-keywords). A union beside the composing
// allOf leaves it deciding, composed into each variant the union distributes
// across or into the Model keeping the union verbatim, and so does a $dynamicRef
// it keeps from expanding.
func allOfDecides(c lowering.Ctx, s *oas3.Schema) bool {
if !allOfConjoins(s) {
if !allOfBesideNoRef(s) {
return false
}
if dispatchOf(s).won == "allOf" {
Expand Down
Loading
Loading