diff --git a/bundler/bundler_ref_rewrite_test.go b/bundler/bundler_ref_rewrite_test.go index 44206ec1d..3fb9a5bdd 100644 --- a/bundler/bundler_ref_rewrite_test.go +++ b/bundler/bundler_ref_rewrite_test.go @@ -12,6 +12,7 @@ import ( "testing" "testing/fstest" + "github.com/pb33f/libopenapi" "github.com/pb33f/libopenapi/datamodel" "github.com/pb33f/libopenapi/index" "github.com/stretchr/testify/assert" @@ -306,6 +307,117 @@ paths: assertNoFilePathRefs(t, result.Bytes) } +func TestBundleDocumentComposedWithOrigins_SchemaProxyGetReferenceUsesBundledRef(t *testing.T) { + tmpDir := t.TempDir() + + topSpec := `openapi: 3.1.0 +info: + title: Bundle Ref Getter Test + version: 1.0.0 +paths: + /: + get: + operationId: getRoot + responses: + '400': + $ref: "#/components/responses/BadRequest" + '500': + $ref: "./shared.yaml#/components/responses/InternalServerError" +components: + responses: + BadRequest: + $ref: "./shared.yaml#/components/responses/BadRequest" + schemas: + Error: + type: object + properties: + wrong: + type: string +` + + sharedSpec := `openapi: 3.1.0 +components: + responses: + BadRequest: + description: Bad Request + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + InternalServerError: + description: Internal Server Error + content: + application/json: + schema: + $ref: "#/components/schemas/InternalServerError" + schemas: + Error: + type: object + properties: + message: + type: string + InternalServerError: + type: object + properties: + message: + type: string +` + + topFile := filepath.Join(tmpDir, "top.yaml") + sharedFile := filepath.Join(tmpDir, "shared.yaml") + require.NoError(t, os.WriteFile(topFile, []byte(topSpec), 0644)) + require.NoError(t, os.WriteFile(sharedFile, []byte(sharedSpec), 0644)) + + config := datamodel.NewDocumentConfiguration() + config.BasePath = tmpDir + config.SpecFilePath = topFile + config.ExtractRefsSequentially = true + + spec, err := os.ReadFile(topFile) + require.NoError(t, err) + + doc, err := libopenapi.NewDocumentWithConfiguration(spec, config) + require.NoError(t, err) + + model, err := doc.BuildV3Model() + require.NoError(t, err) + + result, err := BundleDocumentComposedWithOrigins(&model.Model, nil) + require.NoError(t, err) + require.NotNil(t, result) + + bundledStr := string(result.Bytes) + assert.Contains(t, bundledStr, "#/components/schemas/Error__shared") + assert.Contains(t, bundledStr, "#/components/schemas/InternalServerError") + + op := model.Model.Paths.PathItems.GetOrZero("/").Get + require.NotNil(t, op) + require.NotNil(t, op.Responses) + + badRequest := op.Responses.Codes.GetOrZero("400") + require.NotNil(t, badRequest) + badRequestSchema := badRequest.Content.GetOrZero("application/json").Schema + require.NotNil(t, badRequestSchema) + + internalError := op.Responses.Codes.GetOrZero("500") + require.NotNil(t, internalError) + internalErrorSchema := internalError.Content.GetOrZero("application/json").Schema + require.NotNil(t, internalErrorSchema) + + assert.Equal(t, "#/components/schemas/Error__shared", badRequestSchema.GetReference()) + assert.Equal(t, "#/components/schemas/InternalServerError", internalErrorSchema.GetReference()) + + badRequestOrigin := result.Origins[badRequestSchema.GetReference()] + require.NotNil(t, badRequestOrigin) + assert.Equal(t, sharedFile, badRequestOrigin.OriginalFile) + assert.Equal(t, "#/components/schemas/Error", badRequestOrigin.OriginalRef) + + internalErrorOrigin := result.Origins[internalErrorSchema.GetReference()] + require.NotNil(t, internalErrorOrigin) + assert.Equal(t, sharedFile, internalErrorOrigin.OriginalFile) + assert.Equal(t, "#/components/schemas/InternalServerError", internalErrorOrigin.OriginalRef) +} + // TestBundlerComposedWithOrigins_AbsolutePathRefReuse ensures absolute-path refs // that point at inline-required content are replaced with the inlined node content. func TestBundlerComposedWithOrigins_AbsolutePathRefReuse(t *testing.T) { diff --git a/datamodel/high/base/schema_proxy.go b/datamodel/high/base/schema_proxy.go index 0240b9eee..84b4ce2e2 100644 --- a/datamodel/high/base/schema_proxy.go +++ b/datamodel/high/base/schema_proxy.go @@ -313,6 +313,11 @@ func (sp *SchemaProxy) GetReference() string { if sp.refStr != "" { return sp.refStr } + if refNode := sp.GetReferenceNode(); refNode != nil { + if refValNode := utils.GetRefValueNode(refNode); refValNode != nil { + return refValNode.Value + } + } return sp.schema.GetValue().GetReference() } diff --git a/datamodel/high/base/schema_proxy_test.go b/datamodel/high/base/schema_proxy_test.go index e23ec6a0e..c00d8859f 100644 --- a/datamodel/high/base/schema_proxy_test.go +++ b/datamodel/high/base/schema_proxy_test.go @@ -150,6 +150,26 @@ func TestSchemaProxy_GetReference(t *testing.T) { assert.Equal(t, refNode, sp.GetReferenceNode()) } +func TestSchemaProxy_GetReference_PrefersLiveRefNodeValue(t *testing.T) { + refNode := utils.CreateRefNode("#/components/schemas/MySchema") + + ref := low.Reference{} + ref.SetReference("#/components/schemas/MySchema", refNode) + + sp := &SchemaProxy{ + schema: &low.NodeReference[*lowbase.SchemaProxy]{ + Value: &lowbase.SchemaProxy{ + Reference: ref, + }, + }, + } + + refNode.Content[1].Value = "#/components/schemas/MySchema__shared" + + assert.Equal(t, "#/components/schemas/MySchema__shared", sp.GetReference()) + assert.Equal(t, refNode, sp.GetReferenceNode()) +} + func TestSchemaProxy_IsReference_Nil(t *testing.T) { var sp *SchemaProxy assert.False(t, sp.IsReference()) @@ -1526,6 +1546,50 @@ func TestCreateSchemaProxyRefWithSchema_InlinePreservedRef(t *testing.T) { require.GreaterOrEqual(t, len(node.Content), 4) // $ref key+val + description key+val } +func TestSchemaProxy_MarshalYAMLInline_CircularReference_MatchesAbsoluteBasePath(t *testing.T) { + const ymlComponents = `components: + schemas: + Ten: + type: object` + + var idxNode yaml.Node + err := yaml.Unmarshal([]byte(ymlComponents), &idxNode) + require.NoError(t, err) + + idx := index.NewSpecIndexWithConfig(&idxNode, index.CreateOpenAPIIndexConfig()) + idx.SetAbsolutePath(filepath.Join(t.TempDir(), "spec.yaml")) + idx.SetCircularReferences([]*index.CircularReferenceResult{ + { + LoopPoint: &index.Reference{ + Definition: "#/components/schemas/NotTen", + FullDefinition: idx.GetSpecAbsolutePath(), + }, + }, + }) + + refNode := utils.CreateRefNode("#/components/schemas/Ten") + + lowProxy := new(lowbase.SchemaProxy) + err = lowProxy.Build(context.Background(), nil, refNode, idx) + require.NoError(t, err) + + sp := NewSchemaProxy(&low.NodeReference[*lowbase.SchemaProxy]{ + Value: lowProxy, + ValueNode: refNode, + }) + + rendered, err := sp.MarshalYAMLInline() + require.Error(t, err) + require.NotNil(t, rendered) + assert.Contains(t, err.Error(), "circular reference") + + node, ok := rendered.(*yaml.Node) + require.True(t, ok) + require.Len(t, node.Content, 2) + assert.Equal(t, "$ref", node.Content[0].Value) + assert.Equal(t, "#/components/schemas/Ten", node.Content[1].Value) +} + func TestCreateSchemaProxyRefWithSchema_CircularRefSafe(t *testing.T) { // Verify inline rendering doesn't panic when rendered Schema has a non-nil low-level const ymlComponents = `components: