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
22 changes: 17 additions & 5 deletions src/Microsoft.OpenApi/Models/OpenApiDocument.cs
Original file line number Diff line number Diff line change
Expand Up @@ -575,9 +575,9 @@ private static string ConvertByteArrayToString(byte[] hash)

string uriLocation;
var id = reference.Id;
if (!string.IsNullOrEmpty(id) && id!.Contains("/")) // this means its a URL reference
if (!string.IsNullOrEmpty(id) && id!.Contains('/')) // this means its a URL reference
{
uriLocation = id;
uriLocation = id!;
}
else
{
Expand Down Expand Up @@ -609,12 +609,24 @@ private static string ConvertByteArrayToString(byte[] hash)
: BaseUri + relativePath;
}

if (reference.Type is ReferenceType.Schema && uriLocation.Contains('#'))
var absoluteUri =
#if NETSTANDARD2_1 || NETCOREAPP || NET5_0_OR_GREATER
uriLocation.StartsWith('#')
#else
uriLocation.StartsWith("#", StringComparison.OrdinalIgnoreCase)
#endif
switch
{
true => new Uri(BaseUri, uriLocation).AbsoluteUri,
false => new Uri(uriLocation).AbsoluteUri
};

if (reference.Type is ReferenceType.Schema && absoluteUri.Contains('#'))
{
return Workspace?.ResolveJsonSchemaReference(new Uri(uriLocation).AbsoluteUri);
return Workspace?.ResolveJsonSchemaReference(absoluteUri);
}

return Workspace?.ResolveReference<IOpenApiReferenceable>(new Uri(uriLocation).AbsoluteUri);
return Workspace?.ResolveReference<IOpenApiReferenceable>(absoluteUri);
}

private static bool IsSubComponent(string reference)
Expand Down
39 changes: 20 additions & 19 deletions src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs
Original file line number Diff line number Diff line change
Expand Up @@ -340,29 +340,30 @@ public bool Contains(string location)
* #/components/schemas/human/allOf/0
*/

if (string.IsNullOrEmpty(location)) return default;
if (string.IsNullOrEmpty(location) || ToLocationUrl(location) is not Uri uri) return default;

var uri = ToLocationUrl(location);
string[] pathSegments;
#if NETSTANDARD2_1 || NETCOREAPP || NET5_0_OR_GREATER
if (!location.Contains("#/components/schemas/", StringComparison.OrdinalIgnoreCase))
#else
if (!location.Contains("#/components/schemas/"))
#endif
throw new ArgumentException($"Invalid schema reference location: {location}. It should contain '#/components/schemas/'");

if (uri is not null)
{
pathSegments = uri.Fragment.Split(['/'], StringSplitOptions.RemoveEmptyEntries);
var 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
// 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);
}
}
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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Text.Json.Nodes;
Expand Down Expand Up @@ -297,6 +298,99 @@ public async Task ShouldResolveRelativeSubReference()
Assert.Equal(JsonSchemaType.String, seq2Property.Items.Items.Type);
}
[Fact]
public async Task ShouldResolveRelativeSubReferenceUsingParsingContext()
{
// Arrange
var filePath = Path.Combine(SampleFolderPath, "relativeSubschemaReference.json");
using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read);
var jsonNode = await JsonNode.ParseAsync(fs);
var schemaJsonNode = jsonNode["components"]?["schemas"]?["Foo"];
Assert.NotNull(schemaJsonNode);
var diagnostic = new OpenApiDiagnostic();
var parsingContext = new ParsingContext(diagnostic);
parsingContext.StartObject("components");
parsingContext.StartObject("schemas");
parsingContext.StartObject("Foo");
var document = new OpenApiDocument();

// Act
var fooComponentSchema = parsingContext.ParseFragment<OpenApiSchema>(schemaJsonNode, OpenApiSpecVersion.OpenApi3_1, document);
document.AddComponent("Foo", fooComponentSchema);
var seq1Property = fooComponentSchema.Properties["seq1"];
Assert.NotNull(seq1Property);
var seq2Property = fooComponentSchema.Properties["seq2"];
Assert.NotNull(seq2Property);
Assert.Equal(JsonSchemaType.Array, seq2Property.Items.Type);
Assert.Equal(JsonSchemaType.String, seq2Property.Items.Items.Type);
}
[Fact]
public void ShouldFailToResolveRelativeSubReferenceFromTheObjectModel()
{
var document = new OpenApiDocument
{
Info = new OpenApiInfo { Title = "Test API", Version = "1.0.0" },
};
document.Components = new OpenApiComponents
{
Schemas = new Dictionary<string, IOpenApiSchema>
{
["Foo"] = new OpenApiSchema
{
Properties = new Dictionary<string, IOpenApiSchema>
{
["seq1"] = new OpenApiSchema { Type = JsonSchemaType.Array | JsonSchemaType.Null, Items = new OpenApiSchema { Type = JsonSchemaType.Array, Items = new OpenApiSchema { Type = JsonSchemaType.String } } },
["seq2"] = new OpenApiSchema { Type = JsonSchemaType.Array | JsonSchemaType.Null, Items = new OpenApiSchemaReference("#/properties/seq1/items", document) }
}
}
}
};
document.RegisterComponents();

var fooComponentSchema = document.Components.Schemas["Foo"];
var seq1Property = fooComponentSchema.Properties["seq1"];
Assert.NotNull(seq1Property);
var seq2Property = fooComponentSchema.Properties["seq2"];
Assert.NotNull(seq2Property);
Assert.Throws<ArgumentException>(() => seq2Property.Items.Type);
// it's impossible to resolve relative references from the object model only because we don't have a way to get to
// the parent object to build the full path for the reference.


// #/properties/seq1/items
// #/components/schemas/Foo/properties/seq1/items
}
[Fact]
public void ShouldResolveAbsoluteSubReferenceFromTheObjectModel()
{
var document = new OpenApiDocument
{
Info = new OpenApiInfo { Title = "Test API", Version = "1.0.0" },
};
document.Components = new OpenApiComponents
{
Schemas = new Dictionary<string, IOpenApiSchema>
{
["Foo"] = new OpenApiSchema
{
Properties = new Dictionary<string, IOpenApiSchema>
{
["seq1"] = new OpenApiSchema { Type = JsonSchemaType.Array | JsonSchemaType.Null, Items = new OpenApiSchema { Type = JsonSchemaType.Array, Items = new OpenApiSchema { Type = JsonSchemaType.String } } },
["seq2"] = new OpenApiSchema { Type = JsonSchemaType.Array | JsonSchemaType.Null, Items = new OpenApiSchemaReference("#/components/schemas/Foo/properties/seq1/items", document) }
}
}
}
};
document.RegisterComponents();

var fooComponentSchema = document.Components.Schemas["Foo"];
var seq1Property = fooComponentSchema.Properties["seq1"];
Assert.NotNull(seq1Property);
var seq2Property = fooComponentSchema.Properties["seq2"];
Assert.NotNull(seq2Property);
Assert.Equal(JsonSchemaType.Array, seq2Property.Items.Type);
Assert.Equal(JsonSchemaType.String, seq2Property.Items.Items.Type);
}
[Fact]
public async Task ShouldResolveRecursiveRelativeSubReference()
{
// Arrange
Expand Down