diff --git a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs index c5ac5f759..afd8da8d2 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs @@ -587,7 +587,26 @@ private static string ConvertByteArrayToString(byte[] hash) } else { - string relativePath = $"#{OpenApiConstants.ComponentsSegment}{reference.Type.GetDisplayName()}/{id}"; + string relativePath; + + if (!string.IsNullOrEmpty(reference.ReferenceV3) && IsSubComponent(reference.ReferenceV3!)) + { + // Enables setting the complete JSON path for nested subschemas e.g. #/components/schemas/person/properties/address + if (useExternal) + { + var relPathSegment = reference.ReferenceV3!.Split('#')[1]; + relativePath = $"#{relPathSegment}"; + } + else + { + relativePath = reference.ReferenceV3!; + } + } + else + { + relativePath = $"#{OpenApiConstants.ComponentsSegment}{reference.Type.GetDisplayName()}/{id}"; + } + Uri? externalResourceUri = useExternal ? Workspace?.GetDocumentId(reference.ExternalResource) : null; uriLocation = useExternal && externalResourceUri is not null @@ -595,9 +614,32 @@ private static string ConvertByteArrayToString(byte[] hash) : BaseUri + relativePath; } + if (reference.Type is ReferenceType.Schema && !uriLocation.StartsWith("http", StringComparison.OrdinalIgnoreCase)) + { + return Workspace?.ResolveJsonSchemaReference(new Uri(uriLocation).AbsoluteUri); + } + return Workspace?.ResolveReference(new Uri(uriLocation).AbsoluteUri); } + private static bool IsSubComponent(string reference) + { + // Normalize fragment part only + var parts = reference.Split('#'); + var fragment = parts.Length > 1 ? parts[1] : string.Empty; + + if (fragment.StartsWith("/components/schemas/", StringComparison.OrdinalIgnoreCase)) + { + var segments = fragment.Split('/'); + + // Expect exactly 4 segments for root-level schema: ["", "components", "schemas", "person"] + // Anything longer means it's a subcomponent. + return segments.Length > 4; + } + + return false; + } + /// /// Reads the stream input and parses it into an Open API document. /// diff --git a/src/Microsoft.OpenApi/Models/OpenApiReference.cs b/src/Microsoft.OpenApi/Models/OpenApiReference.cs index 0e05ec89d..0385ac151 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiReference.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiReference.cs @@ -73,6 +73,7 @@ public class OpenApiReference : IOpenApiSerializable, IOpenApiDescribedElement, /// public OpenApiDocument? HostDocument { get => hostDocument; init => hostDocument = value; } + private string? _referenceV3; /// /// Gets the full reference string for v3.0. /// @@ -80,9 +81,14 @@ public string? ReferenceV3 { get { + if (!string.IsNullOrEmpty(_referenceV3)) + { + return _referenceV3; + } + if (IsExternal) { - return GetExternalReferenceV3(); + return _referenceV3 = GetExternalReferenceV3(); } if (Type == ReferenceType.Tag) @@ -100,7 +106,14 @@ public string? ReferenceV3 return Id; } - return "#/components/" + Type.GetDisplayName() + "/" + Id; + return _referenceV3 = "#/components/" + Type.GetDisplayName() + "/" + Id; + } + set + { + if (value is not null) + { + _referenceV3 = value; + } } } @@ -299,5 +312,15 @@ internal void SetSummaryAndDescriptionFromMapNode(MapNode mapNode) Summary = summary; } } + + internal void SetJsonPointerPath(string pointer) + { + // Eg of an internal subcomponent's JSONPath: #/components/schemas/person/properties/address + if ((pointer.Contains("#") || pointer.StartsWith("http", StringComparison.OrdinalIgnoreCase)) + && !string.IsNullOrEmpty(ReferenceV3) && !ReferenceV3!.Equals(pointer, StringComparison.OrdinalIgnoreCase)) + { + ReferenceV3 = pointer; + } + } } } diff --git a/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs b/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs index ae7adefa6..9dd081b17 100644 --- a/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs +++ b/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs @@ -156,6 +156,16 @@ public override string GetRaw() return refNode?.GetScalarValue(); } + public string? GetJsonSchemaIdentifier() + { + if (!_node.TryGetPropertyValue("$id", out JsonNode? idNode)) + { + return null; + } + + return idNode?.GetScalarValue(); + } + public string? GetSummaryValue() { if (!_node.TryGetPropertyValue("summary", out JsonNode? summaryNode)) diff --git a/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs b/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs index 71265aa8c..02f4f14c1 100644 --- a/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs @@ -363,14 +363,16 @@ public static IOpenApiSchema LoadSchema(ParseNode node, OpenApiDocument hostDocu var mapNode = node.CheckMapNode(OpenApiConstants.Schema); var pointer = mapNode.GetReferencePointer(); + var identifier = mapNode.GetJsonSchemaIdentifier(); if (pointer != null) { var reference = GetReferenceIdAndExternalResource(pointer); var result = new OpenApiSchemaReference(reference.Item1, hostDocument, reference.Item2); result.Reference.SetSummaryAndDescriptionFromMapNode(mapNode); + result.Reference.SetJsonPointerPath(pointer); return result; - } + } var schema = new OpenApiSchema(); @@ -397,6 +399,12 @@ public static IOpenApiSchema LoadSchema(ParseNode node, OpenApiDocument hostDocu schema.Extensions.Remove(OpenApiConstants.NullableExtension); } + if (identifier is not null && hostDocument.Workspace is not null) + { + // register the schema in our registry using the identifier's URL + hostDocument.Workspace.RegisterComponentForDocument(hostDocument, schema, identifier); + } + return schema; } } diff --git a/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs b/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs index 5519696d9..697a4cda9 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using Microsoft.OpenApi.Extensions; using Microsoft.OpenApi.Interfaces; using Microsoft.OpenApi.Models; @@ -330,6 +331,90 @@ public bool Contains(string location) return default; } + /// + /// Recursively resolves a schema from a URI fragment. + /// + /// + /// + public IOpenApiSchema? ResolveJsonSchemaReference(string location) + { + /* Enables resolving references for nested subschemas + * Examples: + * #/components/schemas/person/properties/address" + * #/components/schemas/human/allOf/0 + */ + + if (string.IsNullOrEmpty(location)) return default; + + var uri = ToLocationUrl(location); + string[] pathSegments; + + if (uri is not null) + { + pathSegments = uri.Fragment.Split(['/'], StringSplitOptions.RemoveEmptyEntries); + + // Build the base path for the root schema: "#/components/schemas/person" + var fragment = OpenApiConstants.ComponentsSegment + ReferenceType.Schema.GetDisplayName() + ComponentSegmentSeparator + pathSegments[3]; + var uriBuilder = new UriBuilder(uri) + { + Fragment = fragment + }; // to avoid escaping the # character in the resulting Uri + + if (_IOpenApiReferenceableRegistry.TryGetValue(uriBuilder.Uri, out var schema) && schema is OpenApiSchema targetSchema) + { + // traverse remaining segments after fetching the base schema + var remainingSegments = pathSegments.Skip(4).ToArray(); + return ResolveSubSchema(targetSchema, remainingSegments); + } + } + + return default; + } + + private static IOpenApiSchema? ResolveSubSchema(IOpenApiSchema schema, string[] pathSegments) + { + // Traverse schema object to resolve subschemas + if (pathSegments.Length == 0) + { + return schema; + } + var currentSegment = pathSegments[0]; + pathSegments = [.. pathSegments.Skip(1)]; // skip one segment for the next recursive call + + switch (currentSegment) + { + case OpenApiConstants.Properties: + var propName = pathSegments[0]; + if (schema.Properties != null && schema.Properties.TryGetValue(propName, out var propSchema)) + return ResolveSubSchema(propSchema, [.. pathSegments.Skip(1)]); + break; + case OpenApiConstants.Items: + return schema.Items is OpenApiSchema itemsSchema ? ResolveSubSchema(itemsSchema, pathSegments) : null; + + case OpenApiConstants.AdditionalProperties: + return schema.AdditionalProperties is OpenApiSchema additionalSchema ? ResolveSubSchema(additionalSchema, pathSegments) : null; + case OpenApiConstants.AllOf: + case OpenApiConstants.AnyOf: + case OpenApiConstants.OneOf: + if (!int.TryParse(pathSegments[0], out var index)) return null; + + var list = currentSegment switch + { + OpenApiConstants.AllOf => schema.AllOf, + OpenApiConstants.AnyOf => schema.AnyOf, + OpenApiConstants.OneOf => schema.OneOf, + _ => null + }; + + // recurse into the indexed subschema if valid + if (list != null && index < list.Count) + return ResolveSubSchema(list[index], [.. pathSegments.Skip(1)]); + break; + } + + return null; + } + private Uri? ToLocationUrl(string location) { if (BaseUrl is not null) diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/OAS-schemas.yaml b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/OAS-schemas.yaml new file mode 100644 index 000000000..dcaabab1c --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/OAS-schemas.yaml @@ -0,0 +1,18 @@ +openapi: 3.1.0 +info: + title: OpenAPI document containing reusable components + version: 1.0.0 +components: + schemas: + person: + type: object + properties: + name: + type: string + address: + type: object + properties: + street: + type: string + city: + type: string diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/componentExternalReference.yaml b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/componentExternalReference.yaml new file mode 100644 index 000000000..407927cee --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/componentExternalReference.yaml @@ -0,0 +1,13 @@ +openapi: 3.1.0 +info: + title: Example of reference object in a component object + version: 1.0.0 +paths: + /item: + get: + security: + - customapikey: [] +components: + securitySchemes: + customapikey: + $ref: ./customApiKey.yaml#/components/securityschemes/customapikey \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/customApiKey.yaml b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/customApiKey.yaml new file mode 100644 index 000000000..4c2ab5107 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/customApiKey.yaml @@ -0,0 +1,11 @@ +openapi: 3.1.0 +info: + title: Example of reference object pointing to a parameter + version: 1.0.0 +paths: {} +components: + securitySchemes: + customapikey: + type: apiKey + name: x-api-key + in: header \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/examples.yaml b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/examples.yaml new file mode 100644 index 000000000..831bccdf2 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/examples.yaml @@ -0,0 +1,11 @@ +# file for examples (examples.yaml) +openapi: 3.1.0 +info: + title: OpenAPI document containing examples for reuse + version: 1.0.0 +components: + examples: + item-list: + value: + - name: thing + description: a thing \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/externalComponentSubschemaReference.yaml b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/externalComponentSubschemaReference.yaml new file mode 100644 index 000000000..3627afcdb --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/externalComponentSubschemaReference.yaml @@ -0,0 +1,14 @@ +openapi: 3.1.0 +info: + title: Reference to an external OpenApi document component + version: 1.0.0 +paths: + /person/{id}: + get: + responses: + 200: + description: ok + content: + application/json: + schema: + $ref: 'OAS-schemas.yaml#/components/schemas/person/properties/address' \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/inlineExternalReference.yaml b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/inlineExternalReference.yaml new file mode 100644 index 000000000..d05adb724 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/inlineExternalReference.yaml @@ -0,0 +1,15 @@ +openapi: 3.1.0 +info: + title: Example of reference object pointing to an example object in an OpenAPI document + version: 1.0.0 +paths: + /items: + get: + responses: + '200': + description: sample description + content: + application/json: + examples: + item-list: + $ref: './examples.yaml#/components/examples/item-list' \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/inlineLocalReference.yaml b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/inlineLocalReference.yaml new file mode 100644 index 000000000..25241d100 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/inlineLocalReference.yaml @@ -0,0 +1,14 @@ +openapi: 3.1.0 +info: + title: Example of reference object pointing to a parameter + version: 1.0.0 +paths: + /item: + get: + parameters: + - $ref: '#/components/parameters/size' +components: + parameters: + size: + schema: + type: number \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/internalComponentReferenceUsingId.yaml b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/internalComponentReferenceUsingId.yaml new file mode 100644 index 000000000..ed2e4f91c --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/internalComponentReferenceUsingId.yaml @@ -0,0 +1,29 @@ +openapi: 3.1.0 +info: + title: Reference an internal component using id + version: 1.0.0 +paths: + /person/{id}: + get: + responses: + 200: + description: ok + content: + application/json: + schema: + $ref: 'https://schemas.acme.org/person' +components: + schemas: + person: + $id: 'https://schemas.acme.org/person' + type: object + properties: + name: + type: string + address: + type: object + properties: + street: + type: string + city: + type: string \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/internalComponentsSubschemaReference.yaml b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/internalComponentsSubschemaReference.yaml new file mode 100644 index 000000000..fef558ab7 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/internalComponentsSubschemaReference.yaml @@ -0,0 +1,55 @@ +openapi: 3.1.0 +info: + title: Reference to an internal component + version: 1.0.0 +paths: + /person/{id}: + get: + responses: + 200: + description: ok + content: + application/json: + schema: + $ref: '#/components/schemas/person' + /person/{id}/address: + get: + responses: + 200: + description: ok + content: + application/json: + schema: + $ref: '#/components/schemas/person/properties/address' + /human: + get: + responses: + 200: + description: ok + content: + application/json: + schema: + $ref: '#/components/schemas/human/allOf/0' +components: + schemas: + human: + allOf: + - $ref: '#/components/schemas/person/items' + - type: object + properties: + name: + type: string + person: + type: object + properties: + name: + type: string + address: + type: object + properties: + street: + type: string + city: + type: string + items: + type: integer \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/localReferenceToJsonSchemaResource.yaml b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/localReferenceToJsonSchemaResource.yaml new file mode 100644 index 000000000..bef88ee90 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/localReferenceToJsonSchemaResource.yaml @@ -0,0 +1,23 @@ +openapi: 3.1.0 +info: + title: OpenAPI document containing examples for reuse + version: 1.0.0 +components: + schemas: + a: + type: + - object + - 'null' + properties: + b: + type: + - object + - 'null' + properties: + c: + type: + - object + - 'null' + properties: + b: + $ref: '#/properties/b' \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/rootComponentSchemaReference.yaml b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/rootComponentSchemaReference.yaml new file mode 100644 index 000000000..789b1abb2 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/rootComponentSchemaReference.yaml @@ -0,0 +1,24 @@ +openapi: 3.1.0 +info: + title: Reference at the root of a component schema + version: 1.0.0 +paths: + /items: + get: + responses: + 200: + description: ok + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/specialitem' +components: + schemas: + specialitem: # Use the item type but provide a different title for the type + title: Special Item + $ref: "#/components/schemas/item" + item: + title: Item + type: object \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/rootInlineSchemaReference.yaml b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/rootInlineSchemaReference.yaml new file mode 100644 index 000000000..cd2843bb2 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/rootInlineSchemaReference.yaml @@ -0,0 +1,18 @@ +openapi: 3.1.0 +info: + title: Reference in at the root of an inline schema + version: 1.0.0 +paths: + /item: + get: + responses: + 200: + description: ok + content: + application/json: + schema: + $ref: '#/components/schemas/item' +components: + schemas: + item: + type: object \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/subschemaComponentSchemaReference.yaml b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/subschemaComponentSchemaReference.yaml new file mode 100644 index 000000000..80dbc2f34 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/subschemaComponentSchemaReference.yaml @@ -0,0 +1,22 @@ +openapi: 3.1.0 +info: + title: Reference in a subschema of an component schema + version: 1.0.0 +paths: + /items: + get: + responses: + 200: + description: ok + content: + application/json: + schema: + $ref: '#/components/schemas/items' +components: + schemas: + items: + type: array + items: + $ref: '#/components/schemas/item' + item: + type: object \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/subschemaInlineSchemaReference.yaml b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/subschemaInlineSchemaReference.yaml new file mode 100644 index 000000000..cb72b0c66 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/subschemaInlineSchemaReference.yaml @@ -0,0 +1,20 @@ +openapi: 3.1.0 +info: + title: Reference in at the root of an inline schema + version: 1.0.0 +paths: + /items: + get: + responses: + 200: + description: ok + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/item' +components: + schemas: + item: + type: object \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/RelativeReferenceTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/RelativeReferenceTests.cs new file mode 100644 index 000000000..053094f50 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/RelativeReferenceTests.cs @@ -0,0 +1,206 @@ +using System.IO; +using System.Net.Http; +using System.Text.Json.Nodes; +using System.Threading.Tasks; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Reader; +using Microsoft.OpenApi.Writers; +using Xunit; + +namespace Microsoft.OpenApi.Readers.Tests.V31Tests +{ + public class RelativeReferenceTests + { + private const string SampleFolderPath = "V31Tests/ReferenceSamples"; + + [Fact] + public async Task ParseInlineLocalReferenceWorks() + { + // Arrange + var filePath = Path.Combine(SampleFolderPath, "inlineLocalReference.yaml"); + + // Act + var actual = (await OpenApiDocument.LoadAsync(filePath, SettingsFixture.ReaderSettings)).Document; + var schemaType = actual.Paths["/item"].Operations[HttpMethod.Get].Parameters[0].Schema.Type; + + // Assert + Assert.Equal(JsonSchemaType.Number, schemaType); + } + + [Fact] + public async Task ParseInlineExternalReferenceWorks() + { + // Arrange + var expected = new JsonArray + { + new JsonObject + { + ["name"] = "thing", + ["description"] = "a thing" + } + }; + + var path = Path.Combine(Directory.GetCurrentDirectory(), SampleFolderPath); + + var settings = new OpenApiReaderSettings + { + LoadExternalRefs = true, + BaseUrl = new(path), + }; + settings.AddYamlReader(); + + // Act + var actual = (await OpenApiDocument.LoadAsync(Path.Combine(SampleFolderPath, "inlineExternalReference.yaml"), settings)).Document; + var exampleValue = actual.Paths["/items"].Operations[HttpMethod.Get].Responses["200"].Content["application/json"].Examples["item-list"].Value; + + // Assert + Assert.NotNull(exampleValue); + Assert.IsType(exampleValue); + Assert.Equal(expected.ToJsonString(), exampleValue.ToJsonString()); + } + + [Fact] + public async Task ParseComponentExternalReferenceWorks() + { + // Arrange + var path = Path.Combine(Directory.GetCurrentDirectory(), SampleFolderPath); + var settings = new OpenApiReaderSettings + { + LoadExternalRefs = true, + BaseUrl = new(path), + }; + settings.AddYamlReader(); + + // Act + var actual = (await OpenApiDocument.LoadAsync(Path.Combine(SampleFolderPath, "componentExternalReference.yaml"), settings)).Document; + var securitySchemeValue = actual.Components.SecuritySchemes["customapikey"]; + + // Assert + Assert.Equal("x-api-key", securitySchemeValue.Name); + } + + [Fact] + public async Task ParseRootInlineJsonSchemaReferenceWorks() + { + // Arrange + var filePath = Path.Combine(SampleFolderPath, "rootInlineSchemaReference.yaml"); + + // Act + var actual = (await OpenApiDocument.LoadAsync(filePath, SettingsFixture.ReaderSettings)).Document; + var schema = actual.Paths["/item"].Operations[HttpMethod.Get].Responses["200"].Content["application/json"].Schema; + + // Assert + Assert.Equal(JsonSchemaType.Object, schema.Type); + } + + [Fact] + public async Task ParseSubschemaInlineJsonSchemaReferenceWorks() + { + // Arrange + var filePath = Path.Combine(SampleFolderPath, "subschemaInlineSchemaReference.yaml"); + + // Act + var actual = (await OpenApiDocument.LoadAsync(filePath, SettingsFixture.ReaderSettings)).Document; + var schema = actual.Paths["/items"].Operations[HttpMethod.Get].Responses["200"].Content["application/json"].Schema.Items; + + // Assert + Assert.Equal(JsonSchemaType.Object, schema.Type); + } + + [Fact] + public async Task ParseRootComponentJsonSchemaReferenceWorks() + { + // Arrange + var filePath = Path.Combine(SampleFolderPath, "rootComponentSchemaReference.yaml"); + + // Act + var actual = (await OpenApiDocument.LoadAsync(filePath, SettingsFixture.ReaderSettings)).Document; + var schema = actual.Components.Schemas["specialitem"]; + + // Assert + Assert.Equal(JsonSchemaType.Object, schema.Type); + Assert.Equal("Item", schema.Title); + } + + [Fact] + public async Task ParseSubschemaComponentJsonSchemaReferenceWorks() + { + // Arrange + var filePath = Path.Combine(SampleFolderPath, "subschemaComponentSchemaReference.yaml"); + + // Act + var actual = (await OpenApiDocument.LoadAsync(filePath, SettingsFixture.ReaderSettings)).Document; + var schema = actual.Components.Schemas["items"].Items; + + // Assert + Assert.Equal(JsonSchemaType.Object, schema.Type); + } + + [Fact] + public async Task ParseInternalComponentSubschemaJsonSchemaReferenceWorks() + { + // Arrange + var filePath = Path.Combine(SampleFolderPath, "internalComponentsSubschemaReference.yaml"); + + // Act + var actual = (await OpenApiDocument.LoadAsync(filePath, SettingsFixture.ReaderSettings)).Document; + var addressSchema = actual.Paths["/person/{id}/address"].Operations[HttpMethod.Get].Responses["200"].Content["application/json"].Schema; + var itemsSchema = actual.Paths["/human"].Operations[HttpMethod.Get].Responses["200"].Content["application/json"].Schema; + + // Assert + Assert.Equal(JsonSchemaType.Object, addressSchema.Type); + Assert.Equal(JsonSchemaType.Integer, itemsSchema.Type); + } + + [Fact] + public async Task ParseExternalComponentSubschemaJsonSchemaReferenceWorks() + { + // Arrange + var path = Path.Combine(Directory.GetCurrentDirectory(), SampleFolderPath); + var settings = new OpenApiReaderSettings + { + LoadExternalRefs = true, + BaseUrl = new(path), + }; + settings.AddYamlReader(); + + // Act + var actual = (await OpenApiDocument.LoadAsync(Path.Combine(SampleFolderPath, "externalComponentSubschemaReference.yaml"), settings)).Document; + var schema = actual.Paths["/person/{id}"].Operations[HttpMethod.Get].Responses["200"].Content["application/json"].Schema; + + // Assert + Assert.Equal(JsonSchemaType.Object, schema.Type); + } + + [Fact] + public async Task ParseReferenceToInternalComponentUsingDollarIdWorks() + { + // Arrange + var filePath = Path.Combine(SampleFolderPath, "internalComponentReferenceUsingId.yaml"); + + // Act + var actual = (await OpenApiDocument.LoadAsync(filePath, SettingsFixture.ReaderSettings)).Document; + var schema = actual.Paths["/person/{id}"].Operations[HttpMethod.Get].Responses["200"].Content["application/json"].Schema; + + // Assert + Assert.Equal(JsonSchemaType.Object, schema.Type); + } + + [Fact] + public async Task ParseLocalReferenceToJsonSchemaResourceWorks() + { + // Arrange + var filePath = Path.Combine(SampleFolderPath, "localReferenceToJsonSchemaResource.yaml"); + var stringWriter = new StringWriter(); + var writer = new OpenApiYamlWriter(stringWriter); + + // Act + var actual = (await OpenApiDocument.LoadAsync(filePath, SettingsFixture.ReaderSettings)).Document; + var schema = actual.Components.Schemas["a"].Properties["b"].Properties["c"].Properties["b"]; + schema.SerializeAsV31(writer); + + // Assert + Assert.Equal(JsonSchemaType.Object | JsonSchemaType.Null, schema.Type); + } + } +} diff --git a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt index 1e3c88ec0..55bc8dc0c 100644 --- a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt +++ b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt @@ -965,7 +965,7 @@ namespace Microsoft.OpenApi.Models public bool IsFragment { get; init; } public bool IsLocal { get; } public string? ReferenceV2 { get; } - public string? ReferenceV3 { get; } + public string? ReferenceV3 { get; set; } public string? Summary { get; set; } public Microsoft.OpenApi.Models.ReferenceType Type { get; init; } public void SerializeAsV2(Microsoft.OpenApi.Writers.IOpenApiWriter writer) { } @@ -1650,6 +1650,7 @@ namespace Microsoft.OpenApi.Services public System.Uri? GetDocumentId(string? key) { } public bool RegisterComponentForDocument(Microsoft.OpenApi.Models.OpenApiDocument openApiDocument, T componentToRegister, string id) { } public void RegisterComponents(Microsoft.OpenApi.Models.OpenApiDocument document) { } + public Microsoft.OpenApi.Models.Interfaces.IOpenApiSchema? ResolveJsonSchemaReference(string location) { } public T? ResolveReference(string location) { } } public class OperationSearch : Microsoft.OpenApi.Services.OpenApiVisitorBase