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
23 changes: 19 additions & 4 deletions compilers/openapi/internal/schema/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -1443,20 +1443,35 @@ func componentSchemaAt(c lowering.Ctx, pointer string) *oas3.Schema {
func declaresResourceIDAbove(c lowering.Ctx, pointer string) bool {
view := nodeview.New()
cur := nodeview.DocumentRoot(nodeview.Deref(c.Doc.GetRootNode()))
for seg := range strings.SplitSeq(pointer, "/") {
for _, token := range pointerTokens(pointer) {
if cur == nil {
return false
}
if view.ChildByToken(cur, "$id") != nil {
return true
}
if seg != "" { // every pointer starts with the empty segment
cur = nodeview.Deref(view.ChildByToken(cur, ids.UnescapeSegment(seg)))
}
cur = nodeview.Deref(view.ChildByToken(cur, ids.UnescapeSegment(token)))
}
return cur != nil && view.ChildByToken(cur, "$id") != nil
}

// pointerTokens returns the RFC 6901 reference tokens of pointer, still escaped.
// Splitting on '/' yields one leading empty segment that is the split's artifact
// rather than a token, and only that one is: a later empty segment names the key
// "", which is how a component schema named "" is addressed
// (/components/schemas/). Dropping every empty segment stopped the walk above
// such a position and read the $id of its parent instead of its own.
//
// A string with no leading '/' has no tokens at all: the empty pointer names the
// whole document, and a relative pointer names no position in it.
func pointerTokens(pointer string) []string {
rest, found := strings.CutPrefix(pointer, "/")
if !found {
return nil
}
return strings.Split(rest, "/")
}

// dynamicFragment returns the plain fragment name a $dynamicRef addresses. Only
// the same-document `#name` spelling resolves here: a URI part names another
// resource (Milestone 1 interns only same-file targets) and a `#/…` pointer
Expand Down
36 changes: 36 additions & 0 deletions compilers/openapi/internal/schema/schema_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,42 @@ func TestDeclaresResourceIDAbove_WithoutARawTree(t *testing.T) {
"a document with no raw tree declares no resource anywhere")
}

// TestDeclaresResourceIDAbove_EmptySegmentIsATokenNotAnArtifact pins which empty
// segments the path walk may drop. Splitting a pointer on '/' produces one
// leading empty segment that no reference token stands behind; every later one
// is the token naming the key "", which is how a schema literally named "" is
// addressed. Dropping those stopped the walk above the position and read the
// $id of the components/schemas map instead of the schema's own.
func TestDeclaresResourceIDAbove_EmptySegmentIsATokenNotAnArtifact(t *testing.T) {
t.Parallel()
l, diags := loweredFor(t, `openapi: 3.1.0
info: {title: T, version: "1"}
paths: {}
components:
schemas:
"":
$id: https://example.com/empty
properties: {x: {type: string}}
Sibling: {type: string}
`)
requireNoErrorDiags(t, diags)

for name, tc := range map[string]struct {
pointer string
want bool
}{
"the position is named by a trailing empty token": {"/components/schemas/", true},
"an interior empty token still descends": {"/components/schemas//properties/x", true},
"a sibling sits above no $id at all": {"/components/schemas/Sibling", false},
"the empty pointer names the document root": {"", false},
} {
t.Run(name, func(t *testing.T) {
t.Parallel()
assert.Equal(t, tc.want, declaresResourceIDAbove(l.ctx, tc.pointer))
})
}
}

// TestDynamicAnchors_WalksEveryNodeShape drives the raw-tree walk over the
// shapes a YAML document can present, rather than only the mappings a schema
// happens to be written as. The walk reads the raw tree because oas3.Schema has
Expand Down
11 changes: 11 additions & 0 deletions compilers/openapi/internal/schema/schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3021,6 +3021,17 @@ func TestDynamicRef_IrreducibleIsKeptAndSaysWhy(t *testing.T) {
schemas: anchor + " A: {$id: 'https://example.com/a', $dynamicRef: '#m'}\n",
wantWhy: `an $id at or above "/components/schemas/A"`,
},
{
// The pointer of a schema named "" ends in an empty reference token,
// which the path walk once dropped as if it were the artifact of
// splitting a pointer on '/' — reading the components/schemas map for
// the $id instead of the schema itself, and expanding across a
// boundary it should have stopped at.
name: `a schema named "" is still in a resource of its own`,
schemas: anchor + " \"\": {$id: 'https://example.com/empty', $dynamicRef: '#m'}\n",
wantWhy: `an $id at or above "/components/schemas/"`,
at: "t/anon/components/schemas/",
},
{
name: "an enclosing schema starts the resource",
schemas: anchor +
Expand Down
Loading