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
45 changes: 44 additions & 1 deletion src/Microsoft.OpenApi/Models/OpenApiDocument.cs
Original file line number Diff line number Diff line change
Expand Up @@ -581,17 +581,60 @@ private static string ConvertByteArrayToString(byte[] hash)
}
else
{
string relativePath = $"#{OpenApiConstants.ComponentsSegment}{reference.Type.GetDisplayName()}/{id}";
string relativePath;
var referenceV3 = !string.IsNullOrEmpty(reference.ReferenceV3) ? reference.ReferenceV3! : string.Empty;

if (!string.IsNullOrEmpty(referenceV3) && IsSubComponent(referenceV3))
{
// Enables setting the complete JSON path for nested subschemas e.g. #/components/schemas/person/properties/address
if (useExternal)
{
var relPathSegment = referenceV3.Split(['#'], StringSplitOptions.RemoveEmptyEntries)[1];
relativePath = $"#{relPathSegment}";
}
else
{
relativePath = 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.Contains('#'))
{
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(['/'], StringSplitOptions.RemoveEmptyEntries);

// Expect exactly 3 segments for root-level schema: ["components", "schemas", "person"]
// Anything longer means it's a subcomponent.
return segments.Length > 3;
}

return false;
}

/// <summary>
/// Reads the stream input and parses it into an Open API document.
/// </summary>
Expand Down
56 changes: 54 additions & 2 deletions src/Microsoft.OpenApi/Models/OpenApiReference.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT license.

using System;
using System.Linq;
using System.Text.Json.Nodes;
using Microsoft.OpenApi.Reader;

Expand Down Expand Up @@ -69,13 +70,19 @@ public class OpenApiReference : IOpenApiSerializable, IOpenApiDescribedElement,
/// </summary>
public OpenApiDocument? HostDocument { get => hostDocument; init => hostDocument = value; }

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();
Expand All @@ -96,7 +103,14 @@ public string? ReferenceV3
return Id;
}

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

Expand All @@ -122,7 +136,7 @@ public string? ReferenceV2
return Id;
}

return "#/" + GetReferenceTypeNameAsV2(Type) + "/" + Id;
return $"#/{GetReferenceTypeNameAsV2(Type)}/{Id}";
}
}

Expand Down Expand Up @@ -295,5 +309,43 @@ internal void SetSummaryAndDescriptionFromMapNode(MapNode mapNode)
Summary = summary;
}
}

internal void SetJsonPointerPath(string pointer, string nodeLocation)
{
// Relative reference to internal JSON schema node/resource (e.g. "#/properties/b")
if (pointer.StartsWith("#/", StringComparison.OrdinalIgnoreCase) && !pointer.Contains("/components/schemas"))
{
ReferenceV3 = ResolveRelativePointer(nodeLocation, pointer);
}

// Absolute reference or anchor (e.g. "#/components/schemas/..." or full URL)
else if ((pointer.Contains('#') || pointer.StartsWith("http", StringComparison.OrdinalIgnoreCase))
Comment thread
MaggieKimani1 marked this conversation as resolved.
&& !string.Equals(ReferenceV3, pointer, StringComparison.OrdinalIgnoreCase))
{
ReferenceV3 = pointer;
}
}

private static string ResolveRelativePointer(string nodeLocation, string relativeRef)
{
// Convert nodeLocation to path segments
var segments = nodeLocation.TrimStart('#').Split(['/'], StringSplitOptions.RemoveEmptyEntries).ToList();

// Convert relativeRef to dynamic segments
var relativeSegments = relativeRef.TrimStart('#').Split(['/'], StringSplitOptions.RemoveEmptyEntries);

// Locate the first occurrence of relativeRef segments in the full path
for (int i = 0; i <= segments.Count - relativeSegments.Length; i++)
{
if (relativeSegments.SequenceEqual(segments.Skip(i).Take(relativeSegments.Length)))
{
// Trim to include just the matching segment chain
segments = [.. segments.Take(i + relativeSegments.Length)];
break;
}
}

return $"#/{string.Join("/", segments)}";
}
}
}
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 @@ -153,6 +153,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
11 changes: 10 additions & 1 deletion src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -361,14 +361,17 @@ public static IOpenApiSchema LoadSchema(ParseNode node, OpenApiDocument hostDocu
var mapNode = node.CheckMapNode(OpenApiConstants.Schema);

var pointer = mapNode.GetReferencePointer();
var identifier = mapNode.GetJsonSchemaIdentifier();
var nodeLocation = node.Context.GetLocation();

if (pointer != null)
{
var reference = GetReferenceIdAndExternalResource(pointer);
var result = new OpenApiSchemaReference(reference.Item1, hostDocument, reference.Item2);
result.Reference.SetSummaryAndDescriptionFromMapNode(mapNode);
result.Reference.SetJsonPointerPath(pointer, nodeLocation);
return result;
}
}

var schema = new OpenApiSchema();

Expand Down Expand Up @@ -398,6 +401,12 @@ public static IOpenApiSchema LoadSchema(ParseNode node, OpenApiDocument hostDocu
schema.Extensions.Remove(OpenApiConstants.NullableExtension);
}

if (!string.IsNullOrEmpty(identifier) && 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;

namespace Microsoft.OpenApi
{
Expand Down Expand Up @@ -326,6 +327,90 @@ public bool Contains(string location)
return default;
}

/// <summary>
/// Recursively resolves a schema from a URI fragment.
/// </summary>
/// <param name="location"></param>
/// <returns></returns>
internal 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 IOpenApiSchema targetSchema)
{
// traverse remaining segments after fetching the base schema
var remainingSegments = pathSegments.Skip(4).ToArray();
return ResolveSubSchema(targetSchema, remainingSegments);
}
}

return default;
}

internal 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:
Comment thread
MaggieKimani1 marked this conversation as resolved.
var propName = pathSegments[0];
if (schema.Properties != null && schema.Properties.TryGetValue(propName, out var propSchema))
return ResolveSubSchema(propSchema, [.. pathSegments.Skip(1)]);
break;
Comment thread
MaggieKimani1 marked this conversation as resolved.
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'
Loading