Skip to content

chore: implement test cases for types of References in an OpenApi document #2352

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

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
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
44 changes: 43 additions & 1 deletion src/Microsoft.OpenApi/Models/OpenApiDocument.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@
{
return;
}
_tags = value is HashSet<OpenApiTag> tags && tags.Comparer is OpenApiTagComparer ?

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

View workflow job for this annotation

GitHub Actions / Build

Change this condition so that it does not always evaluate to 'True'. (https://rules.sonarsource.com/csharp/RSPEC-2589)
tags :
new HashSet<OpenApiTag>(value, OpenApiTagComparer.Instance);
}
Expand Down Expand Up @@ -587,17 +587,59 @@
}
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
? externalResourceUri.AbsoluteUri + relativePath
: BaseUri + relativePath;
}

if (reference.Type is ReferenceType.Schema && !uriLocation.StartsWith("http", StringComparison.OrdinalIgnoreCase))
{
return Workspace?.ResolveJsonSchemaReference(new Uri(uriLocation).AbsoluteUri);
}

return Workspace?.ResolveReference<IOpenApiReferenceable>(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;
}

/// <summary>
/// Reads the stream input and parses it into an Open API document.
/// </summary>
Expand Down
27 changes: 25 additions & 2 deletions src/Microsoft.OpenApi/Models/OpenApiReference.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,18 +71,24 @@
/// <summary>
/// The OpenApiDocument that is hosting the OpenApiReference instance. This is used to enable dereferencing the reference.
/// </summary>
public OpenApiDocument? HostDocument { get => hostDocument; init => hostDocument = value; }

Check warning on line 74 in src/Microsoft.OpenApi/Models/OpenApiReference.cs

View workflow job for this annotation

GitHub Actions / Build

Make this an auto-implemented property and remove its backing field. (https://rules.sonarsource.com/csharp/RSPEC-2292)

private string? _referenceV3;
/// <summary>
/// Gets the full reference string for v3.0.
/// </summary>
public string? ReferenceV3
{
get
{
if (!string.IsNullOrEmpty(_referenceV3))
{
return _referenceV3;
}

if (IsExternal)
{
return GetExternalReferenceV3();
return _referenceV3 = GetExternalReferenceV3();
}

if (Type == ReferenceType.Tag)
Expand All @@ -94,13 +100,20 @@
{
return Id;
}
if (!string.IsNullOrEmpty(Id) && Id is not null && Id.StartsWith("http://", StringComparison.OrdinalIgnoreCase) ||

Check warning on line 103 in src/Microsoft.OpenApi/Models/OpenApiReference.cs

View workflow job for this annotation

GitHub Actions / Build

Change this condition so that it does not always evaluate to 'True'. (https://rules.sonarsource.com/csharp/RSPEC-2589)
!string.IsNullOrEmpty(Id) && Id is not null && Id.StartsWith("https://", StringComparison.OrdinalIgnoreCase))

Check warning on line 104 in src/Microsoft.OpenApi/Models/OpenApiReference.cs

View workflow job for this annotation

GitHub Actions / Build

Change this condition so that it does not always evaluate to 'True'. (https://rules.sonarsource.com/csharp/RSPEC-2589)
{
return Id;
}

return "#/components/" + Type.GetDisplayName() + "/" + Id;
return _referenceV3 = "#/components/" + Type.GetDisplayName() + "/" + Id;
}
set
{
if (value is not null)
{
_referenceV3 = value;
}
}
}

Expand Down Expand Up @@ -299,5 +312,15 @@
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;
}
}
}
}
10 changes: 10 additions & 0 deletions src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
{
internal static partial class OpenApiV31Deserializer
{
private static readonly FixedFieldMap<OpenApiSchema> _openApiSchemaFixedFields = new()

Check warning on line 19 in src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs

View workflow job for this annotation

GitHub Actions / Build

Refactor this field to reduce its Cognitive Complexity from 52 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)
{
{
"title",
Expand Down Expand Up @@ -203,7 +203,7 @@
{
var list = n.CreateSimpleList((n2, p) => n2.GetScalarValue(), doc);
JsonSchemaType combinedType = 0;
foreach(var type in list)

Check warning on line 206 in src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs

View workflow job for this annotation

GitHub Actions / Build

Loops should be simplified using the "Where" LINQ method (https://rules.sonarsource.com/csharp/RSPEC-3267)
{
if (type is not null)
{
Expand Down Expand Up @@ -363,14 +363,16 @@
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();

Expand All @@ -397,6 +399,12 @@
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;
}
}
Expand Down
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 System.Linq;
using Microsoft.OpenApi.Extensions;
using Microsoft.OpenApi.Interfaces;
using Microsoft.OpenApi.Models;
Expand All @@ -20,7 +21,7 @@
private readonly Dictionary<Uri, Stream> _artifactsRegistry = new();
private readonly Dictionary<Uri, IOpenApiReferenceable> _IOpenApiReferenceableRegistry = new(new UriWithFragmentEquailityComparer());

private class UriWithFragmentEquailityComparer : IEqualityComparer<Uri>

Check warning on line 24 in src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs

View workflow job for this annotation

GitHub Actions / Build

Private classes which are not derived in the current assembly should be marked as 'sealed'. (https://rules.sonarsource.com/csharp/RSPEC-3260)
{
public bool Equals(Uri? x, Uri? y)
{
Expand Down Expand Up @@ -84,7 +85,7 @@
/// Registers a document's components into the workspace
/// </summary>
/// <param name="document"></param>
public void RegisterComponents(OpenApiDocument document)

Check warning on line 88 in src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs

View workflow job for this annotation

GitHub Actions / Build

Refactor this method to reduce its Cognitive Complexity from 31 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)
{
if (document?.Components == null) return;

Expand Down Expand Up @@ -271,7 +272,7 @@
/// <param name="value"></param>
public void AddDocumentId(string? key, Uri? value)
{
if (!string.IsNullOrEmpty(key) && key is not null && value is not null && !_documentsIdRegistry.ContainsKey(key))

Check warning on line 275 in src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs

View workflow job for this annotation

GitHub Actions / Build

Change this condition so that it does not always evaluate to 'True'. (https://rules.sonarsource.com/csharp/RSPEC-2589)
{
_documentsIdRegistry[key] = value;
}
Expand Down Expand Up @@ -330,6 +331,90 @@
return default;
}

/// <summary>
/// Recursively resolves a schema from a URI fragment.
/// </summary>
/// <param name="location"></param>
/// <returns></returns>
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)

Check warning on line 374 in src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs

View workflow job for this annotation

GitHub Actions / Build

Refactor this method to reduce its Cognitive Complexity from 17 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)
{
// 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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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'
Original file line number Diff line number Diff line change
@@ -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'
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading