Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resolve external document dereference to OpenApiDocument using $ref to $id #1826

Merged
merged 10 commits into from
Oct 3, 2024
2 changes: 1 addition & 1 deletion src/Microsoft.OpenApi/Models/OpenApiDocument.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
public class OpenApiDocument : IOpenApiSerializable, IOpenApiExtensible
{
/// <summary>
/// Related workspace containing OpenApiDocuments that are referenced in this document
/// Related workspace containing components that are referenced in a document
/// </summary>
public OpenApiWorkspace Workspace { get; set; }

Expand Down Expand Up @@ -128,7 +128,7 @@

writer.WriteStartObject();

// openApi;

Check warning on line 131 in src/Microsoft.OpenApi/Models/OpenApiDocument.cs

View workflow job for this annotation

GitHub Actions / Build

Remove this commented out code. (https://rules.sonarsource.com/csharp/RSPEC-125)
writer.WriteProperty(OpenApiConstants.OpenApi, "3.1.0");

// jsonSchemaDialect
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ internal async Task<OpenApiDiagnostic> LoadAsync(OpenApiReference reference,
{
_workspace.AddDocumentId(reference.ExternalResource, document.BaseUri);
var version = diagnostic?.SpecificationVersion ?? OpenApiSpecVersion.OpenApi3_0;
_workspace.RegisterComponents(document, version);
_workspace.RegisterComponents(document);
document.Workspace = _workspace;

// Collect remote references by walking document
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ public static OpenApiDocument LoadOpenApi(RootNode rootNode)
FixRequestBodyReferences(openApiDoc);

// Register components
openApiDoc.Workspace.RegisterComponents(openApiDoc, OpenApiSpecVersion.OpenApi2_0);
openApiDoc.Workspace.RegisterComponents(openApiDoc);

return openApiDoc;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public static OpenApiDocument LoadOpenApi(RootNode rootNode)
ParseMap(openApiNode, openApiDoc, _openApiFixedFields, _openApiPatternFields, openApiDoc);

// Register components
openApiDoc.Workspace.RegisterComponents(openApiDoc, OpenApiSpecVersion.OpenApi3_0);
openApiDoc.Workspace.RegisterComponents(openApiDoc);

return openApiDoc;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public static OpenApiDocument LoadOpenApi(RootNode rootNode)
ParseMap(openApiNode, openApiDoc, _openApiFixedFields, _openApiPatternFields, openApiDoc);

// Register components
openApiDoc.Workspace.RegisterComponents(openApiDoc, OpenApiSpecVersion.OpenApi3_1);
openApiDoc.Workspace.RegisterComponents(openApiDoc);

return openApiDoc;
}
Expand Down
8 changes: 5 additions & 3 deletions src/Microsoft.OpenApi/Reader/V31/OpenApiV31Deserializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -157,9 +157,11 @@ private static (string, string) GetReferenceIdAndExternalResource(string pointer
string refId = !pointer.Contains('#') ? pointer : refSegments.Last();

var isExternalResource = !refSegments.First().StartsWith("#");
string externalResource = isExternalResource
? $"{refSegments.First()}/{refSegments[1].TrimEnd('#')}"
: null;
string externalResource = null;
if (isExternalResource && pointer.Contains('#'))
{
externalResource = $"{refSegments.First()}/{refSegments[1].TrimEnd('#')}";
}

return (refId, externalResource);
}
Expand Down

This file was deleted.

85 changes: 85 additions & 0 deletions src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.OpenApi.Extensions;
using Microsoft.OpenApi.Interfaces;
using Microsoft.OpenApi.Models;

Expand Down Expand Up @@ -54,6 +55,90 @@ public int ComponentsCount()
return _IOpenApiReferenceableRegistry.Count + _artifactsRegistry.Count;
}

/// <summary>
/// Registers a document's components into the workspace
/// </summary>
/// <param name="document"></param>
public void RegisterComponents(OpenApiDocument document)
{
if (document?.Components == null) return;

string baseUri = document.BaseUri + OpenApiConstants.ComponentsSegment;
string location;

// Register Schema
foreach (var item in document.Components.Schemas)
{
location = item.Value.Id ?? baseUri + ReferenceType.Schema.GetDisplayName() + "/" + item.Key;

RegisterComponent(location, item.Value);
}

// Register Parameters
foreach (var item in document.Components.Parameters)
{
location = baseUri + ReferenceType.Parameter.GetDisplayName() + "/" + item.Key;
RegisterComponent(location, item.Value);
}

// Register Responses
foreach (var item in document.Components.Responses)
{
location = baseUri + ReferenceType.Response.GetDisplayName() + "/" + item.Key;
RegisterComponent(location, item.Value);
}

// Register RequestBodies
foreach (var item in document.Components.RequestBodies)
{
location = baseUri + ReferenceType.RequestBody.GetDisplayName() + "/" + item.Key;
RegisterComponent(location, item.Value);
}

// Register Links
foreach (var item in document.Components.Links)
{
location = baseUri + ReferenceType.Link.GetDisplayName() + "/" + item.Key;
RegisterComponent(location, item.Value);
}

// Register Callbacks
foreach (var item in document.Components.Callbacks)
{
location = baseUri + ReferenceType.Callback.GetDisplayName() + "/" + item.Key;
RegisterComponent(location, item.Value);
}

// Register PathItems
foreach (var item in document.Components.PathItems)
{
location = baseUri + ReferenceType.PathItem.GetDisplayName() + "/" + item.Key;
RegisterComponent(location, item.Value);
}

// Register Examples
foreach (var item in document.Components.Examples)
{
location = baseUri + ReferenceType.Example.GetDisplayName() + "/" + item.Key;
RegisterComponent(location, item.Value);
}

// Register Headers
foreach (var item in document.Components.Headers)
{
location = baseUri + ReferenceType.Header.GetDisplayName() + "/" + item.Key;
RegisterComponent(location, item.Value);
}

// Register SecuritySchemes
foreach (var item in document.Components.SecuritySchemes)
{
location = baseUri + ReferenceType.SecurityScheme.GetDisplayName() + "/" + item.Key;
RegisterComponent(location, item.Value);
}
}


/// <summary>
/// Registers a component in the component registry.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.OpenApi.Extensions;
using Microsoft.OpenApi.Interfaces;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Models.References;
using Microsoft.OpenApi.Reader;
using Microsoft.OpenApi.Tests;
using Microsoft.OpenApi.Writers;
using Microsoft.OpenApi.Services;
using Xunit;
using System.Linq;

namespace Microsoft.OpenApi.Readers.Tests.V31Tests
{
Expand Down Expand Up @@ -392,7 +394,7 @@ public void ParseDocumentsWithReusablePathItemInWebhooksSucceeds()
new OpenApiDiagnostic() { SpecificationVersion = OpenApiSpecVersion.OpenApi3_1 });

var outputWriter = new StringWriter(CultureInfo.InvariantCulture);
var writer = new OpenApiJsonWriter(outputWriter, new() { InlineLocalReferences = true } );
var writer = new OpenApiJsonWriter(outputWriter, new() { InlineLocalReferences = true });
actual.OpenApiDocument.SerializeAsV31(writer);
var serialized = outputWriter.ToString();
}
Expand Down Expand Up @@ -445,7 +447,7 @@ public void ParseDocumentWithPatternPropertiesInSchemaWorks()
}
}
};

// Serialization
var mediaType = result.OpenApiDocument.Paths["/example"].Operations[OperationType.Get].Responses["200"].Content["application/json"];

Expand All @@ -461,7 +463,7 @@ public void ParseDocumentWithPatternPropertiesInSchemaWorks()
type: string
prop3:
type: string";

var actualMediaType = mediaType.SerializeAsYaml(OpenApiSpecVersion.OpenApi3_1);

// Assert
Expand All @@ -484,5 +486,49 @@ public void ParseDocumentWithReferenceByIdGetsResolved()
Assert.Equal("object", requestBodySchema.Type);
Assert.Equal("string", parameterSchema.Type);
}

[Fact]
public async Task ExternalDocumentDereferenceToOpenApiDocumentUsingJsonPointerWorks()
{
// Arrange
var path = Path.Combine(Directory.GetCurrentDirectory(), SampleFolderPath);

var settings = new OpenApiReaderSettings
{
LoadExternalRefs = true,
BaseUrl = new(path),
};

// Act
var result = await OpenApiDocument.LoadAsync(Path.Combine(SampleFolderPath, "externalRefByJsonPointer.yaml"), settings);
var responseSchema = result.OpenApiDocument.Paths["/resource"].Operations[OperationType.Get].Responses["200"].Content["application/json"].Schema;

// Assert
result.OpenApiDocument.Workspace.Contains("./externalResource.yaml");
responseSchema.Properties.Count.Should().Be(2); // reference has been resolved
}

[Fact]
public async Task ParseExternalDocumentDereferenceToOpenApiDocumentByIdWorks()
{
// Arrange
var path = Path.Combine(Directory.GetCurrentDirectory(), SampleFolderPath);

var settings = new OpenApiReaderSettings
{
LoadExternalRefs = true,
BaseUrl = new(path),
};

// Act
var result = await OpenApiDocument.LoadAsync(Path.Combine(SampleFolderPath, "externalRefById.yaml"), settings);
var doc2 = OpenApiDocument.Load(Path.Combine(SampleFolderPath, "externalResource.yaml")).OpenApiDocument;

var requestBodySchema = result.OpenApiDocument.Paths["/resource"].Operations[OperationType.Get].Parameters.First().Schema;
result.OpenApiDocument.Workspace.RegisterComponents(doc2);

// Assert
requestBodySchema.Properties.Count.Should().Be(2); // reference has been resolved
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
openapi: 3.1.0
info:
title: ReferenceById
version: 1.0.0
paths:
/resource:
get:
parameters:
- name: id
in: query
required: true
schema:
$ref: 'https://example.com/schemas/user.json'
components: {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
openapi: 3.1.0
info:
title: ReferenceById
version: 1.0.0
paths:
/resource:
get:
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: './externalResource.yaml#/components/schemas/todo'
components: {}
Loading
Loading