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
6 changes: 6 additions & 0 deletions datamodel/document_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ type DocumentConfiguration struct {
// FileFilter should be used to limit the scope of the rolodex.
AllowFileReferences bool

// SkipExternalRefResolution will skip resolving external $ref references (those not starting with #).
// When enabled, external references will be left as-is during model building. Schema proxies will
// report IsReference()=true and GetReference() will return the ref string, but Schema() will return nil.
// This is useful for code generators that handle external refs via import mappings.
SkipExternalRefResolution bool

// AllowRemoteReferences will allow the index to lookup remote references. This is disabled by default.
//
// This behavior is now driven by the inclusion of a BaseURL. If a BaseURL is set, then the
Expand Down
34 changes: 25 additions & 9 deletions datamodel/low/base/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package base

import (
"context"
"errors"
"fmt"
"hash/maphash"
"sort"
Expand Down Expand Up @@ -1403,14 +1404,16 @@ func buildPropertyMap(ctx context.Context, parent *Schema, root *yaml.Node, idx
refString := ""
var refNode *yaml.Node
if h, _, l := utils.IsNodeRefValue(prop); h {
ref, fIdx, _, fctx := low.LocateRefNodeWithContext(foundCtx, prop, foundIdx)
ref, fIdx, err, fctx := low.LocateRefNodeWithContext(foundCtx, prop, foundIdx)
if ref != nil {

refNode = prop
prop = ref
refString = l
foundCtx = fctx
foundIdx = fIdx
} else if errors.Is(err, low.ErrExternalRefSkipped) {
refString = l
refNode = prop
} else {
return nil, fmt.Errorf("schema properties build failed: cannot find reference %s, line %d, col %d",
prop.Content[1].Value, prop.Content[1].Line, prop.Content[1].Column)
Expand Down Expand Up @@ -1554,12 +1557,14 @@ func buildSchema(ctx context.Context, schemas chan schemaProxyBuildResult, label
h := false
if h, _, refLocation = utils.IsNodeRefValue(valueNode); h {
isRef = true
ref, fIdx, _, fctx := low.LocateRefNodeWithContext(foundCtx, valueNode, foundIdx)
ref, fIdx, err, fctx := low.LocateRefNodeWithContext(foundCtx, valueNode, foundIdx)
if ref != nil {
refNode = valueNode
valueNode = ref
foundCtx = fctx
foundIdx = fIdx
} else if err == low.ErrExternalRefSkipped {
refNode = valueNode
} else {
errors <- fmt.Errorf("build schema failed: reference cannot be found: %s, line %d, col %d",
valueNode.Content[1].Value, valueNode.Content[1].Line, valueNode.Content[1].Column)
Expand Down Expand Up @@ -1587,16 +1592,17 @@ func buildSchema(ctx context.Context, schemas chan schemaProxyBuildResult, label
foundCtx = ctx
if h, _, refLocation = utils.IsNodeRefValue(vn); h {
isRef = true
ref, fIdx, _, fctx := low.LocateRefNodeWithContext(foundCtx, vn, foundIdx)
ref, fIdx, err, fctx := low.LocateRefNodeWithContext(foundCtx, vn, foundIdx)
if ref != nil {
refNode = vn
vn = ref
foundCtx = fctx
foundIdx = fIdx
} else if err == low.ErrExternalRefSkipped {
refNode = vn
} else {
err := fmt.Errorf("build schema failed: reference cannot be found: %s, line %d, col %d",
errors <- fmt.Errorf("build schema failed: reference cannot be found: %s, line %d, col %d",
vn.Content[1].Value, vn.Content[1].Line, vn.Content[1].Column)
errors <- err
return
}
}
Expand Down Expand Up @@ -1633,14 +1639,22 @@ func ExtractSchema(ctx context.Context, root *yaml.Node, idx *index.SpecIndex) (

foundIndex := idx
foundCtx := ctx
if rf, rl, _ := utils.IsNodeRefValue(root); rf {
if rf, rl, rv := utils.IsNodeRefValue(root); rf {
// locate reference in index.
ref, fIdx, _, nCtx := low.LocateRefNodeWithContext(ctx, root, idx)
ref, fIdx, err, nCtx := low.LocateRefNodeWithContext(ctx, root, idx)
if ref != nil {
schNode = ref
schLabel = rl
foundCtx = nCtx
foundIndex = fIdx
} else if errors.Is(err, low.ErrExternalRefSkipped) {
refLocation = rv
schema := &SchemaProxy{kn: root, vn: root, idx: idx, ctx: ctx}
_ = schema.Build(ctx, root, root, idx)
n := &low.NodeReference[*SchemaProxy]{Value: schema, KeyNode: root, ValueNode: root}
n.SetReference(refLocation, root)
schema.SetReference(refLocation, root)
return n, nil
} else {
v := root.Content[1].Value
if root.Content[1].Value == "" {
Expand All @@ -1654,14 +1668,16 @@ func ExtractSchema(ctx context.Context, root *yaml.Node, idx *index.SpecIndex) (
if schNode != nil {
h := false
if h, _, refLocation = utils.IsNodeRefValue(schNode); h {
ref, fIdx, _, nCtx := low.LocateRefNodeWithContext(foundCtx, schNode, foundIndex)
ref, fIdx, lerr, nCtx := low.LocateRefNodeWithContext(foundCtx, schNode, foundIndex)
if ref != nil {
refNode = schNode
schNode = ref
if fIdx != nil {
foundIndex = fIdx
}
foundCtx = nCtx
} else if errors.Is(lerr, low.ErrExternalRefSkipped) {
refNode = schNode
} else {
v := schNode.Content[1].Value
if schNode.Content[1].Value == "" {
Expand Down
6 changes: 6 additions & 0 deletions datamodel/low/base/schema_proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,12 @@ func (sp *SchemaProxy) Schema() *Schema {
return sp.rendered
}

// If this proxy represents an unresolved external ref, return nil without error.
if sp.IsReference() && sp.idx != nil && sp.idx.GetConfig() != nil &&
sp.idx.GetConfig().SkipExternalRefResolution && utils.IsExternalRef(sp.GetReference()) {
return nil
}

// handle property merging for references with sibling properties
buildNode := sp.vn
if sp.idx != nil && sp.idx.GetConfig() != nil {
Expand Down
47 changes: 47 additions & 0 deletions datamodel/low/base/schema_proxy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -700,3 +700,50 @@ type: array`), &node)
result := sp.attemptPropertyMerging(node.Content[0], config)
assert.Nil(t, result) // when merge fails, nil is returned
}

func TestSchemaProxy_SkipExternalRef_ReturnsNil(t *testing.T) {
yml := `components:
schemas:
Local:
type: object`

var idxNode yaml.Node
_ = yaml.Unmarshal([]byte(yml), &idxNode)
cfg := index.CreateClosedAPIIndexConfig()
cfg.SkipExternalRefResolution = true
idx := index.NewSpecIndexWithConfig(&idxNode, cfg)

sp := &SchemaProxy{idx: idx}
sp.SetReference("./models/Pet.yaml#/Pet", nil)
_ = sp.Build(context.Background(), nil, &yaml.Node{Kind: yaml.MappingNode}, idx)

// Schema() should return nil without setting a build error
result := sp.Schema()
assert.Nil(t, result)
assert.Nil(t, sp.GetBuildError())
assert.True(t, sp.IsReference())
assert.Equal(t, "./models/Pet.yaml#/Pet", sp.GetReference())
}

func TestSchemaProxy_SkipExternalRef_LocalRefNotBlocked(t *testing.T) {
yml := `components:
schemas:
Local:
type: object
properties:
name:
type: string`

var idxNode yaml.Node
_ = yaml.Unmarshal([]byte(yml), &idxNode)
cfg := index.CreateClosedAPIIndexConfig()
cfg.SkipExternalRefResolution = true
idx := index.NewSpecIndexWithConfig(&idxNode, cfg)

// local ref - should NOT be skipped by the guard
sp := &SchemaProxy{idx: idx}
sp.SetReference("#/components/schemas/Local", nil)

assert.True(t, sp.IsReference())
assert.Equal(t, "#/components/schemas/Local", sp.GetReference())
}
163 changes: 163 additions & 0 deletions datamodel/low/base/schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1400,6 +1400,7 @@ func TestExtractSchema_OneOfRef(t *testing.T) {
}

func TestSchema_Hash_Equal(t *testing.T) {
low.ClearHashCache()
left := `schema:
$schema: https://athing.com
multipleOf: 1
Expand Down Expand Up @@ -3043,3 +3044,165 @@ contentSchema:
// Different contentSchema types should produce different hashes
assert.NotEqual(t, hash1, hash2)
}

func TestBuildPropertyMap_SkipExternalRef(t *testing.T) {
// Schema with a property that has an external $ref
schemaYml := `type: object
properties:
local:
type: string
external:
$ref: './models/Pet.yaml#/Pet'`

var schemaNode yaml.Node
_ = yaml.Unmarshal([]byte(schemaYml), &schemaNode)

cfg := index.CreateClosedAPIIndexConfig()
cfg.SkipExternalRefResolution = true
idx := index.NewSpecIndexWithConfig(&schemaNode, cfg)

var schema Schema
_ = low.BuildModel(schemaNode.Content[0], &schema)
err := schema.Build(context.Background(), schemaNode.Content[0], idx)
assert.Nil(t, err) // parent builds successfully

// Check properties
assert.NotNil(t, schema.Properties.Value)
found := false
for k, v := range schema.Properties.Value.FromOldest() {
if k.Value == "external" {
found = true
proxy := v.Value
assert.True(t, proxy.IsReference())
assert.Equal(t, "./models/Pet.yaml#/Pet", proxy.GetReference())
// Schema() should return nil for unresolved external ref
assert.Nil(t, proxy.Schema())
assert.Nil(t, proxy.GetBuildError())
}
}
assert.True(t, found, "expected to find 'external' property")
}

func TestBuildSchema_AllOf_SkipExternalRef(t *testing.T) {
schemaYml := `allOf:
- $ref: './models/Base.yaml#/Base'
- type: object
properties:
name:
type: string`

var schemaNode yaml.Node
_ = yaml.Unmarshal([]byte(schemaYml), &schemaNode)
cfg := index.CreateClosedAPIIndexConfig()
cfg.SkipExternalRefResolution = true
idx := index.NewSpecIndexWithConfig(&schemaNode, cfg)

var schema Schema
_ = low.BuildModel(schemaNode.Content[0], &schema)
err := schema.Build(context.Background(), schemaNode.Content[0], idx)
assert.Nil(t, err)

assert.NotNil(t, schema.AllOf.Value)
assert.Len(t, schema.AllOf.Value, 2)

// First allOf item should be the external ref
first := schema.AllOf.Value[0].Value
assert.True(t, first.IsReference())
assert.Equal(t, "./models/Base.yaml#/Base", first.GetReference())
assert.Nil(t, first.Schema())
assert.Nil(t, first.GetBuildError())
}

func TestBuildSchema_OneOf_SkipExternalRef(t *testing.T) {
schemaYml := `oneOf:
- $ref: 'https://example.com/Cat.yaml'
- type: object
properties:
bark:
type: boolean`

var schemaNode yaml.Node
_ = yaml.Unmarshal([]byte(schemaYml), &schemaNode)
cfg := index.CreateClosedAPIIndexConfig()
cfg.SkipExternalRefResolution = true
idx := index.NewSpecIndexWithConfig(&schemaNode, cfg)

var schema Schema
_ = low.BuildModel(schemaNode.Content[0], &schema)
err := schema.Build(context.Background(), schemaNode.Content[0], idx)
assert.Nil(t, err)

assert.NotNil(t, schema.OneOf.Value)
assert.Len(t, schema.OneOf.Value, 2)

first := schema.OneOf.Value[0].Value
assert.True(t, first.IsReference())
assert.Equal(t, "https://example.com/Cat.yaml", first.GetReference())
assert.Nil(t, first.Schema())
assert.Nil(t, first.GetBuildError())
}

func TestBuildSchema_AllOfMap_SkipExternalRef(t *testing.T) {
// allOf as a single map $ref (not an array) exercises the map branch of buildSchema (Site B)
schemaYml := `allOf:
$ref: './models/Base.yaml#/Base'`

var schemaNode yaml.Node
_ = yaml.Unmarshal([]byte(schemaYml), &schemaNode)
cfg := index.CreateClosedAPIIndexConfig()
cfg.SkipExternalRefResolution = true
idx := index.NewSpecIndexWithConfig(&schemaNode, cfg)

var schema Schema
_ = low.BuildModel(schemaNode.Content[0], &schema)
err := schema.Build(context.Background(), schemaNode.Content[0], idx)
assert.Nil(t, err)

assert.NotNil(t, schema.AllOf.Value)
assert.Len(t, schema.AllOf.Value, 1)

first := schema.AllOf.Value[0].Value
assert.True(t, first.IsReference())
assert.Equal(t, "./models/Base.yaml#/Base", first.GetReference())
assert.Nil(t, first.Schema())
assert.Nil(t, first.GetBuildError())
}

func TestExtractSchema_RootRef_SkipExternalRef(t *testing.T) {
yml := `$ref: './models/Pet.yaml#/Pet'`

var root yaml.Node
_ = yaml.Unmarshal([]byte(yml), &root)

cfg := index.CreateClosedAPIIndexConfig()
cfg.SkipExternalRefResolution = true
idx := index.NewSpecIndexWithConfig(&root, cfg)

result, err := ExtractSchema(context.Background(), root.Content[0], idx)
assert.Nil(t, err)
assert.NotNil(t, result)
assert.True(t, result.Value.IsReference())
assert.Equal(t, "./models/Pet.yaml#/Pet", result.Value.GetReference())
assert.Nil(t, result.Value.Schema())
assert.Nil(t, result.Value.GetBuildError())
}

func TestExtractSchema_SchemaKeyRef_SkipExternalRef(t *testing.T) {
yml := `schema:
$ref: './models/Pet.yaml#/Pet'`

var root yaml.Node
_ = yaml.Unmarshal([]byte(yml), &root)

cfg := index.CreateClosedAPIIndexConfig()
cfg.SkipExternalRefResolution = true
idx := index.NewSpecIndexWithConfig(&root, cfg)

result, err := ExtractSchema(context.Background(), root.Content[0], idx)
assert.Nil(t, err)
assert.NotNil(t, result)
assert.True(t, result.Value.IsReference())
assert.Equal(t, "./models/Pet.yaml#/Pet", result.Value.GetReference())
assert.Nil(t, result.Value.Schema())
assert.Nil(t, result.Value.GetBuildError())
}
Loading