diff --git a/src/Microsoft.OpenApi/Models/BaseOpenApiReference.cs b/src/Microsoft.OpenApi/Models/BaseOpenApiReference.cs index 506161298..78295fc88 100644 --- a/src/Microsoft.OpenApi/Models/BaseOpenApiReference.cs +++ b/src/Microsoft.OpenApi/Models/BaseOpenApiReference.cs @@ -51,6 +51,16 @@ public class BaseOpenApiReference : IOpenApiSerializable /// public bool IsFragment { get; init; } + private bool? _isStandalone; + /// + /// Gets or sets a flag indicating whether this reference points directly to a standalone external resource rather than a component within it. + /// + internal bool IsStandalone + { + get => _isStandalone ?? (Type == ReferenceType.Schema && IsExternal && Id is not null && string.Equals(ExternalResource, Id, StringComparison.Ordinal) && !Id.Contains("#")); + set => _isStandalone = value; + } + private OpenApiDocument? hostDocument; /// /// The OpenApiDocument that is hosting the OpenApiReference instance. This is used to enable dereferencing the reference. @@ -148,6 +158,9 @@ public BaseOpenApiReference(BaseOpenApiReference reference) Type = reference.Type; Id = reference.Id; HostDocument = reference.HostDocument; + IsFragment = reference.IsFragment; + IsStandalone = reference.IsStandalone; + _referenceV3 = reference._referenceV3; } /// @@ -241,6 +254,11 @@ public virtual void SerializeAsV2(IOpenApiWriter writer) private string? GetExternalReferenceV3() { + if (IsStandalone) + { + return ExternalResource; + } + if (Id != null) { if (IsFragment) @@ -321,20 +339,24 @@ internal void SetJsonPointerPath(string pointer, ParsingContext context) { // Relative reference to internal JSON schema node/resource (e.g. "#/properties/b") #if NETSTANDARD2_1 || NETCOREAPP2_1_OR_GREATER || NET5_0_OR_GREATER - if (pointer.StartsWith("#/", StringComparison.OrdinalIgnoreCase) && !pointer.Contains("/components/schemas", StringComparison.OrdinalIgnoreCase)) + if (pointer.StartsWith("#/", StringComparison.OrdinalIgnoreCase) && !pointer.Contains("/components/", StringComparison.OrdinalIgnoreCase)) #else - if (pointer.StartsWith("#/", StringComparison.OrdinalIgnoreCase) && !pointer.ToLowerInvariant().Contains("/components/schemas")) + if (pointer.StartsWith("#/", StringComparison.OrdinalIgnoreCase) && !pointer.ToLowerInvariant().Contains("/components/")) #endif { ReferenceV3 = ResolveRelativePointer(context.GetLocation(), pointer); } - // Absolute reference or anchor (e.g. "#/components/schemas/..." or full URL) - else if ((pointer.Contains('#') || pointer.StartsWith("http", StringComparison.OrdinalIgnoreCase)) - && !string.Equals(ReferenceV3, pointer, StringComparison.OrdinalIgnoreCase)) + // Absolute reference, anchor, or relative pointer (e.g. "#/components/schemas/...", full URL, or "schemas/uuid.schema.json") + else if (!string.Equals(ReferenceV3, pointer, StringComparison.OrdinalIgnoreCase)) { ReferenceV3 = pointer; } + + if (IsExternal) + { + IsStandalone = !pointer.Contains('#'); + } } private static string ResolveRelativePointer(string nodeLocation, string relativeRef) diff --git a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs index de2bb9d16..4e6bcbff6 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. using System; @@ -631,14 +631,21 @@ private static string ConvertByteArrayToString(byte[] hash) string uriLocation; var id = reference.Id; - if (!string.IsNullOrEmpty(id) && id!.Contains('/')) // this means its a URL reference + var referenceV3 = !string.IsNullOrEmpty(reference.ReferenceV3) ? reference.ReferenceV3! : string.Empty; + var isStandaloneExternal = reference.IsExternal && + (reference.IsStandalone || (!string.IsNullOrEmpty(referenceV3) ? !referenceV3.Contains("#") : (reference.Type == ReferenceType.Schema && id is not null && !id.Contains("#") && id.Contains("/")))); + + if (isStandaloneExternal) + { + uriLocation = id ?? reference.ExternalResource ?? string.Empty; + } + else if (!string.IsNullOrEmpty(id) && id!.Contains("/")) { uriLocation = id!; } else { string relativePath; - var referenceV3 = !string.IsNullOrEmpty(reference.ReferenceV3) ? reference.ReferenceV3! : string.Empty; if (reference.Type is ReferenceType.Schema && useExternal && TryGetPlainNameFragment(referenceV3, out var plainNameFragment)) { @@ -669,19 +676,28 @@ private static string ConvertByteArrayToString(byte[] hash) : BaseUri + relativePath; } - var absoluteUri = -#if NETSTANDARD2_1 || NETCOREAPP || NET5_0_OR_GREATER - uriLocation.StartsWith('#') -#else - uriLocation.StartsWith("#", StringComparison.OrdinalIgnoreCase) -#endif - switch + Uri? resolvedUri = null; + if (Uri.TryCreate(uriLocation, UriKind.Absolute, out var absUri)) { - true => new Uri(BaseUri, uriLocation).AbsoluteUri, - false => new Uri(uriLocation).AbsoluteUri - }; + resolvedUri = absUri; + } + else if (useExternal && reference.ExternalResource is not null && Workspace?.GetDocumentId(reference.ExternalResource) is Uri extDocUri) + { + resolvedUri = extDocUri; + } + else if (BaseUri is not null && Uri.TryCreate(BaseUri, uriLocation, out var relCombinedUri)) + { + resolvedUri = relCombinedUri; + } + + if (resolvedUri is null) + { + return null; + } + + var absoluteUri = resolvedUri.AbsoluteUri; - if (reference.Type is ReferenceType.Schema && absoluteUri.Contains('#') && parentSchema is not null) + if (reference.Type is ReferenceType.Schema && absoluteUri.Contains("#") && parentSchema is not null) { return Workspace?.ResolveJsonSchemaReference(absoluteUri, parentSchema); } diff --git a/src/Microsoft.OpenApi/Reader/Services/OpenApiWorkspaceLoader.cs b/src/Microsoft.OpenApi/Reader/Services/OpenApiWorkspaceLoader.cs index e793aa985..a4bfcb9c8 100644 --- a/src/Microsoft.OpenApi/Reader/Services/OpenApiWorkspaceLoader.cs +++ b/src/Microsoft.OpenApi/Reader/Services/OpenApiWorkspaceLoader.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading; using System.Threading.Tasks; @@ -45,18 +45,25 @@ internal async Task LoadAsync(BaseOpenApiReference reference, // If not already in workspace, load it and process references if (item.ExternalResource is not null && !_workspace.Contains(item.ExternalResource)) { - var uri = new Uri(item.ExternalResource, UriKind.RelativeOrAbsolute); - var input = await _loader.LoadAsync(item.HostDocument!.BaseUri, uri, cancellationToken).ConfigureAwait(false); - var result = await OpenApiDocument.LoadAsync(input, format, _readerSettings, cancellationToken).ConfigureAwait(false); - // Merge diagnostics - if (result.Diagnostic != null) + try { - diagnostic.AppendDiagnostic(result.Diagnostic, item.ExternalResource); + var uri = new Uri(item.ExternalResource, UriKind.RelativeOrAbsolute); + var input = await _loader.LoadAsync(item.HostDocument!.BaseUri, uri, cancellationToken).ConfigureAwait(false); + var result = await OpenApiDocument.LoadAsync(input, format, _readerSettings, cancellationToken).ConfigureAwait(false); + // Merge diagnostics + if (result.Diagnostic != null) + { + diagnostic.AppendDiagnostic(result.Diagnostic, item.ExternalResource); + } + if (result.Document != null) + { + var loadDiagnostic = await LoadAsync(item, result.Document, format, diagnostic, cancellationToken).ConfigureAwait(false); + diagnostic = loadDiagnostic; + } } - if (result.Document != null) + catch (Exception ex) when (ex is not OperationCanceledException) { - var loadDiagnostic = await LoadAsync(item, result.Document, format, diagnostic, cancellationToken).ConfigureAwait(false); - diagnostic = loadDiagnostic; + diagnostic.Errors.Add(new OpenApiError(string.Empty, ex.Message)); } } } diff --git a/src/Microsoft.OpenApi/Reader/V3/OpenApiCallbackDeserializer.cs b/src/Microsoft.OpenApi/Reader/V3/OpenApiCallbackDeserializer.cs index 686968e4c..95b55ceae 100644 --- a/src/Microsoft.OpenApi/Reader/V3/OpenApiCallbackDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V3/OpenApiCallbackDeserializer.cs @@ -31,7 +31,9 @@ public static IOpenApiCallback LoadCallback(JsonNode node, OpenApiDocument hostD if (pointer != null) { var reference = GetReferenceIdAndExternalResource(pointer); - return new OpenApiCallbackReference(reference.Item1, hostDocument, reference.Item2); + var callbackReference = new OpenApiCallbackReference(reference.Item1, hostDocument, reference.Item2); + callbackReference.Reference.SetJsonPointerPath(pointer, context); + return callbackReference; } var domainObject = new OpenApiCallback(); diff --git a/src/Microsoft.OpenApi/Reader/V3/OpenApiExampleDeserializer.cs b/src/Microsoft.OpenApi/Reader/V3/OpenApiExampleDeserializer.cs index 03f587f58..ade0e7e06 100644 --- a/src/Microsoft.OpenApi/Reader/V3/OpenApiExampleDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V3/OpenApiExampleDeserializer.cs @@ -49,7 +49,9 @@ public static IOpenApiExample LoadExample(JsonNode node, OpenApiDocument hostDoc if (pointer != null) { var reference = GetReferenceIdAndExternalResource(pointer); - return new OpenApiExampleReference(reference.Item1, hostDocument, reference.Item2); + var exampleReference = new OpenApiExampleReference(reference.Item1, hostDocument, reference.Item2); + exampleReference.Reference.SetJsonPointerPath(pointer, context); + return exampleReference; } var example = new OpenApiExample(); diff --git a/src/Microsoft.OpenApi/Reader/V3/OpenApiHeaderDeserializer.cs b/src/Microsoft.OpenApi/Reader/V3/OpenApiHeaderDeserializer.cs index c82b47bd8..6a70bda45 100644 --- a/src/Microsoft.OpenApi/Reader/V3/OpenApiHeaderDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V3/OpenApiHeaderDeserializer.cs @@ -118,7 +118,9 @@ public static IOpenApiHeader LoadHeader(JsonNode node, OpenApiDocument hostDocum if (pointer != null) { var reference = GetReferenceIdAndExternalResource(pointer); - return new OpenApiHeaderReference(reference.Item1, hostDocument, reference.Item2); + var headerReference = new OpenApiHeaderReference(reference.Item1, hostDocument, reference.Item2); + headerReference.Reference.SetJsonPointerPath(pointer, context); + return headerReference; } var header = new OpenApiHeader(); diff --git a/src/Microsoft.OpenApi/Reader/V3/OpenApiLinkDeserializer.cs b/src/Microsoft.OpenApi/Reader/V3/OpenApiLinkDeserializer.cs index 37eb476b4..e47aa94d6 100644 --- a/src/Microsoft.OpenApi/Reader/V3/OpenApiLinkDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V3/OpenApiLinkDeserializer.cs @@ -52,7 +52,9 @@ public static IOpenApiLink LoadLink(JsonNode node, OpenApiDocument hostDocument, if (pointer != null) { var reference = GetReferenceIdAndExternalResource(pointer); - return new OpenApiLinkReference(reference.Item1, hostDocument, reference.Item2); + var linkReference = new OpenApiLinkReference(reference.Item1, hostDocument, reference.Item2); + linkReference.Reference.SetJsonPointerPath(pointer, context); + return linkReference; } ParseMap(jsonObject, link, _linkFixedFields, _linkPatternFields, hostDocument, context); diff --git a/src/Microsoft.OpenApi/Reader/V3/OpenApiParameterDeserializer.cs b/src/Microsoft.OpenApi/Reader/V3/OpenApiParameterDeserializer.cs index 435515658..d51befb53 100644 --- a/src/Microsoft.OpenApi/Reader/V3/OpenApiParameterDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V3/OpenApiParameterDeserializer.cs @@ -158,7 +158,9 @@ public static IOpenApiParameter LoadParameter(JsonNode node, OpenApiDocument hos if (pointer != null) { var reference = GetReferenceIdAndExternalResource(pointer); - return new OpenApiParameterReference(reference.Item1, hostDocument, reference.Item2); + var parameterReference = new OpenApiParameterReference(reference.Item1, hostDocument, reference.Item2); + parameterReference.Reference.SetJsonPointerPath(pointer, context); + return parameterReference; } var parameter = new OpenApiParameter(); diff --git a/src/Microsoft.OpenApi/Reader/V3/OpenApiPathItemDeserializer.cs b/src/Microsoft.OpenApi/Reader/V3/OpenApiPathItemDeserializer.cs index b24619835..7b3a1fce3 100644 --- a/src/Microsoft.OpenApi/Reader/V3/OpenApiPathItemDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V3/OpenApiPathItemDeserializer.cs @@ -54,7 +54,9 @@ public static IOpenApiPathItem LoadPathItem(JsonNode node, OpenApiDocument hostD if (pointer != null) { var reference = GetReferenceIdAndExternalResource(pointer); - return new OpenApiPathItemReference(reference.Item1, hostDocument, reference.Item2); + var pathItemReference = new OpenApiPathItemReference(reference.Item1, hostDocument, reference.Item2); + pathItemReference.Reference.SetJsonPointerPath(pointer, context); + return pathItemReference; } var pathItem = new OpenApiPathItem(); diff --git a/src/Microsoft.OpenApi/Reader/V3/OpenApiRequestBodyDeserializer.cs b/src/Microsoft.OpenApi/Reader/V3/OpenApiRequestBodyDeserializer.cs index 8aee2e9a8..bd54de6df 100644 --- a/src/Microsoft.OpenApi/Reader/V3/OpenApiRequestBodyDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V3/OpenApiRequestBodyDeserializer.cs @@ -51,7 +51,9 @@ public static IOpenApiRequestBody LoadRequestBody(JsonNode node, OpenApiDocument if (pointer != null) { var reference = GetReferenceIdAndExternalResource(pointer); - return new OpenApiRequestBodyReference(reference.Item1, hostDocument, reference.Item2); + var requestBodyReference = new OpenApiRequestBodyReference(reference.Item1, hostDocument, reference.Item2); + requestBodyReference.Reference.SetJsonPointerPath(pointer, context); + return requestBodyReference; } var requestBody = new OpenApiRequestBody(); diff --git a/src/Microsoft.OpenApi/Reader/V3/OpenApiResponseDeserializer.cs b/src/Microsoft.OpenApi/Reader/V3/OpenApiResponseDeserializer.cs index dba913db4..a5aacc271 100644 --- a/src/Microsoft.OpenApi/Reader/V3/OpenApiResponseDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V3/OpenApiResponseDeserializer.cs @@ -57,7 +57,9 @@ public static IOpenApiResponse LoadResponse(JsonNode node, OpenApiDocument hostD if (pointer != null) { var reference = GetReferenceIdAndExternalResource(pointer); - return new OpenApiResponseReference(reference.Item1, hostDocument, reference.Item2); + var responseReference = new OpenApiResponseReference(reference.Item1, hostDocument, reference.Item2); + responseReference.Reference.SetJsonPointerPath(pointer, context); + return responseReference; } var response = new OpenApiResponse(); diff --git a/src/Microsoft.OpenApi/Reader/V3/OpenApiSecuritySchemeDeserializer.cs b/src/Microsoft.OpenApi/Reader/V3/OpenApiSecuritySchemeDeserializer.cs index 3f9d8be83..322273be6 100644 --- a/src/Microsoft.OpenApi/Reader/V3/OpenApiSecuritySchemeDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V3/OpenApiSecuritySchemeDeserializer.cs @@ -106,7 +106,9 @@ public static IOpenApiSecurityScheme LoadSecurityScheme(JsonNode node, OpenApiDo if (pointer != null) { var reference = GetReferenceIdAndExternalResource(pointer); - return new OpenApiSecuritySchemeReference(reference.Item1, hostDocument, reference.Item2); + var securitySchemeReference = new OpenApiSecuritySchemeReference(reference.Item1, hostDocument, reference.Item2); + securitySchemeReference.Reference.SetJsonPointerPath(pointer, context); + return securitySchemeReference; } var securityScheme = new OpenApiSecurityScheme(); diff --git a/src/Microsoft.OpenApi/Reader/V31/OpenApiCallbackDeserializer.cs b/src/Microsoft.OpenApi/Reader/V31/OpenApiCallbackDeserializer.cs index f37f59d2b..5bbd656d1 100644 --- a/src/Microsoft.OpenApi/Reader/V31/OpenApiCallbackDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V31/OpenApiCallbackDeserializer.cs @@ -28,6 +28,7 @@ public static IOpenApiCallback LoadCallback(JsonNode node, OpenApiDocument hostD var reference = GetReferenceIdAndExternalResource(pointer); var callbackReference = new OpenApiCallbackReference(reference.Item1, hostDocument, reference.Item2); callbackReference.Reference.SetMetadataFromJsonObject(jsonObject); + callbackReference.Reference.SetJsonPointerPath(pointer, context); return callbackReference; } diff --git a/src/Microsoft.OpenApi/Reader/V31/OpenApiDiscriminatorDeserializer.cs b/src/Microsoft.OpenApi/Reader/V31/OpenApiDiscriminatorDeserializer.cs index 38452fda2..9ecd0897b 100644 --- a/src/Microsoft.OpenApi/Reader/V31/OpenApiDiscriminatorDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V31/OpenApiDiscriminatorDeserializer.cs @@ -56,8 +56,25 @@ public static OpenApiDiscriminator LoadDiscriminator(JsonNode node, OpenApiDocum public static OpenApiSchemaReference LoadMapping(JsonNode node, OpenApiDocument hostDocument, ParsingContext context) { var pointer = node.GetScalarValue() ?? throw new InvalidOperationException("Could not get a pointer reference"); + if (IsBareSchemaName(pointer)) + { + var schemaRef = new OpenApiSchemaReference(pointer, hostDocument); + schemaRef.Reference.SetJsonPointerPath(pointer, context); + return schemaRef; + } + var reference = GetReferenceIdAndExternalResource(pointer); - return new OpenApiSchemaReference(reference.Item1, hostDocument, reference.Item2); + var result = new OpenApiSchemaReference(reference.Item1, hostDocument, reference.Item2); + result.Reference.SetJsonPointerPath(pointer, context); + return result; + } + + private static bool IsBareSchemaName(string pointer) + { + return !pointer.StartsWith("#", StringComparison.OrdinalIgnoreCase) + && !pointer.Contains("/") + && !pointer.Contains("#") + && !pointer.Contains(":"); } } } diff --git a/src/Microsoft.OpenApi/Reader/V31/OpenApiExampleDeserializer.cs b/src/Microsoft.OpenApi/Reader/V31/OpenApiExampleDeserializer.cs index 8c6fef31e..bddf504ba 100644 --- a/src/Microsoft.OpenApi/Reader/V31/OpenApiExampleDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V31/OpenApiExampleDeserializer.cs @@ -56,6 +56,7 @@ public static IOpenApiExample LoadExample(JsonNode node, OpenApiDocument hostDoc var reference = GetReferenceIdAndExternalResource(pointer); var exampleReference = new OpenApiExampleReference(reference.Item1, hostDocument, reference.Item2); exampleReference.Reference.SetMetadataFromJsonObject(jsonObject); + exampleReference.Reference.SetJsonPointerPath(pointer, context); return exampleReference; } diff --git a/src/Microsoft.OpenApi/Reader/V31/OpenApiHeaderDeserializer.cs b/src/Microsoft.OpenApi/Reader/V31/OpenApiHeaderDeserializer.cs index 10eac3dea..ab56d7402 100644 --- a/src/Microsoft.OpenApi/Reader/V31/OpenApiHeaderDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V31/OpenApiHeaderDeserializer.cs @@ -123,6 +123,7 @@ public static IOpenApiHeader LoadHeader(JsonNode node, OpenApiDocument hostDocum var reference = GetReferenceIdAndExternalResource(pointer); var headerReference = new OpenApiHeaderReference(reference.Item1, hostDocument, reference.Item2); headerReference.Reference.SetMetadataFromJsonObject(jsonObject); + headerReference.Reference.SetJsonPointerPath(pointer, context); return headerReference; } diff --git a/src/Microsoft.OpenApi/Reader/V31/OpenApiLinkDeserializer.cs b/src/Microsoft.OpenApi/Reader/V31/OpenApiLinkDeserializer.cs index e9f07205d..3ac927a81 100644 --- a/src/Microsoft.OpenApi/Reader/V31/OpenApiLinkDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V31/OpenApiLinkDeserializer.cs @@ -60,6 +60,7 @@ public static IOpenApiLink LoadLink(JsonNode node, OpenApiDocument hostDocument, var reference = GetReferenceIdAndExternalResource(pointer); var linkReference = new OpenApiLinkReference(reference.Item1, hostDocument, reference.Item2); linkReference.Reference.SetMetadataFromJsonObject(jsonObject); + linkReference.Reference.SetJsonPointerPath(pointer, context); return linkReference; } diff --git a/src/Microsoft.OpenApi/Reader/V31/OpenApiParameterDeserializer.cs b/src/Microsoft.OpenApi/Reader/V31/OpenApiParameterDeserializer.cs index 177342fb4..deec75e4d 100644 --- a/src/Microsoft.OpenApi/Reader/V31/OpenApiParameterDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V31/OpenApiParameterDeserializer.cs @@ -166,6 +166,7 @@ public static IOpenApiParameter LoadParameter(JsonNode node, OpenApiDocument hos var reference = GetReferenceIdAndExternalResource(pointer); var parameterReference = new OpenApiParameterReference(reference.Item1, hostDocument, reference.Item2); parameterReference.Reference.SetMetadataFromJsonObject(jsonObject); + parameterReference.Reference.SetJsonPointerPath(pointer, context); return parameterReference; } diff --git a/src/Microsoft.OpenApi/Reader/V31/OpenApiPathItemDeserializer.cs b/src/Microsoft.OpenApi/Reader/V31/OpenApiPathItemDeserializer.cs index 101572f5b..dccb5adee 100644 --- a/src/Microsoft.OpenApi/Reader/V31/OpenApiPathItemDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V31/OpenApiPathItemDeserializer.cs @@ -58,6 +58,7 @@ public static IOpenApiPathItem LoadPathItem(JsonNode node, OpenApiDocument hostD var reference = GetReferenceIdAndExternalResource(pointer); var pathItemReference = new OpenApiPathItemReference(reference.Item1, hostDocument, reference.Item2); pathItemReference.Reference.SetMetadataFromJsonObject(jsonObject); + pathItemReference.Reference.SetJsonPointerPath(pointer, context); return pathItemReference; } diff --git a/src/Microsoft.OpenApi/Reader/V31/OpenApiRequestBodyDeserializer.cs b/src/Microsoft.OpenApi/Reader/V31/OpenApiRequestBodyDeserializer.cs index 1efb61a82..62675bdbd 100644 --- a/src/Microsoft.OpenApi/Reader/V31/OpenApiRequestBodyDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V31/OpenApiRequestBodyDeserializer.cs @@ -52,6 +52,7 @@ public static IOpenApiRequestBody LoadRequestBody(JsonNode node, OpenApiDocument var reference = GetReferenceIdAndExternalResource(pointer); var requestBodyReference = new OpenApiRequestBodyReference(reference.Item1, hostDocument, reference.Item2); requestBodyReference.Reference.SetMetadataFromJsonObject(jsonObject); + requestBodyReference.Reference.SetJsonPointerPath(pointer, context); return requestBodyReference; } diff --git a/src/Microsoft.OpenApi/Reader/V31/OpenApiResponseDeserializer.cs b/src/Microsoft.OpenApi/Reader/V31/OpenApiResponseDeserializer.cs index b8d64733d..a1992ca97 100644 --- a/src/Microsoft.OpenApi/Reader/V31/OpenApiResponseDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V31/OpenApiResponseDeserializer.cs @@ -63,6 +63,7 @@ public static IOpenApiResponse LoadResponse(JsonNode node, OpenApiDocument hostD var reference = GetReferenceIdAndExternalResource(pointer); var responseReference = new OpenApiResponseReference(reference.Item1, hostDocument, reference.Item2); responseReference.Reference.SetMetadataFromJsonObject(jsonObject); + responseReference.Reference.SetJsonPointerPath(pointer, context); return responseReference; } diff --git a/src/Microsoft.OpenApi/Reader/V31/OpenApiSecuritySchemeDeserializer.cs b/src/Microsoft.OpenApi/Reader/V31/OpenApiSecuritySchemeDeserializer.cs index fac4b87c1..b7095da47 100644 --- a/src/Microsoft.OpenApi/Reader/V31/OpenApiSecuritySchemeDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V31/OpenApiSecuritySchemeDeserializer.cs @@ -116,6 +116,7 @@ public static IOpenApiSecurityScheme LoadSecurityScheme(JsonNode node, OpenApiDo var reference = GetReferenceIdAndExternalResource(pointer); var securitySchemeReference = new OpenApiSecuritySchemeReference(reference.Item1, hostDocument, reference.Item2); securitySchemeReference.Reference.SetMetadataFromJsonObject(jsonObject); + securitySchemeReference.Reference.SetJsonPointerPath(pointer, context); return securitySchemeReference; } diff --git a/src/Microsoft.OpenApi/Reader/V31/OpenApiV31Deserializer.cs b/src/Microsoft.OpenApi/Reader/V31/OpenApiV31Deserializer.cs index 24c49b8ff..4aa9c0f52 100644 --- a/src/Microsoft.OpenApi/Reader/V31/OpenApiV31Deserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V31/OpenApiV31Deserializer.cs @@ -177,7 +177,7 @@ private static (string, string?) GetReferenceIdAndExternalResource(string pointe var isExternalResource = !refSegments[0].StartsWith("#", StringComparison.OrdinalIgnoreCase); string? externalResource = null; - if (isExternalResource && pointer.Contains('#')) + if (isExternalResource) { externalResource = pointer.Split('#')[0].TrimEnd('#'); } diff --git a/src/Microsoft.OpenApi/Reader/V32/OpenApiCallbackDeserializer.cs b/src/Microsoft.OpenApi/Reader/V32/OpenApiCallbackDeserializer.cs index 1c3b9dcab..83fa274bb 100644 --- a/src/Microsoft.OpenApi/Reader/V32/OpenApiCallbackDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V32/OpenApiCallbackDeserializer.cs @@ -28,6 +28,7 @@ public static IOpenApiCallback LoadCallback(JsonNode node, OpenApiDocument hostD var reference = GetReferenceIdAndExternalResource(pointer); var callbackReference = new OpenApiCallbackReference(reference.Item1, hostDocument, reference.Item2); callbackReference.Reference.SetMetadataFromJsonObject(jsonObject); + callbackReference.Reference.SetJsonPointerPath(pointer, context); return callbackReference; } diff --git a/src/Microsoft.OpenApi/Reader/V32/OpenApiDiscriminatorDeserializer.cs b/src/Microsoft.OpenApi/Reader/V32/OpenApiDiscriminatorDeserializer.cs index a3aec0c53..49abc3ad8 100644 --- a/src/Microsoft.OpenApi/Reader/V32/OpenApiDiscriminatorDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V32/OpenApiDiscriminatorDeserializer.cs @@ -51,8 +51,25 @@ public static OpenApiDiscriminator LoadDiscriminator(JsonNode node, OpenApiDocum public static OpenApiSchemaReference LoadMapping(JsonNode node, OpenApiDocument hostDocument, ParsingContext context) { var pointer = node.GetScalarValue() ?? throw new InvalidOperationException("Could not get a pointer reference"); + if (IsBareSchemaName(pointer)) + { + var schemaRef = new OpenApiSchemaReference(pointer, hostDocument); + schemaRef.Reference.SetJsonPointerPath(pointer, context); + return schemaRef; + } + var reference = GetReferenceIdAndExternalResource(pointer); - return new OpenApiSchemaReference(reference.Item1, hostDocument, reference.Item2); + var result = new OpenApiSchemaReference(reference.Item1, hostDocument, reference.Item2); + result.Reference.SetJsonPointerPath(pointer, context); + return result; + } + + private static bool IsBareSchemaName(string pointer) + { + return !pointer.StartsWith("#", StringComparison.OrdinalIgnoreCase) + && !pointer.Contains("/") + && !pointer.Contains("#") + && !pointer.Contains(":"); } } } diff --git a/src/Microsoft.OpenApi/Reader/V32/OpenApiExampleDeserializer.cs b/src/Microsoft.OpenApi/Reader/V32/OpenApiExampleDeserializer.cs index b83b25c65..8c038603d 100644 --- a/src/Microsoft.OpenApi/Reader/V32/OpenApiExampleDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V32/OpenApiExampleDeserializer.cs @@ -66,6 +66,7 @@ public static IOpenApiExample LoadExample(JsonNode node, OpenApiDocument hostDoc var reference = GetReferenceIdAndExternalResource(pointer); var exampleReference = new OpenApiExampleReference(reference.Item1, hostDocument, reference.Item2); exampleReference.Reference.SetMetadataFromJsonObject(jsonObject); + exampleReference.Reference.SetJsonPointerPath(pointer, context); return exampleReference; } diff --git a/src/Microsoft.OpenApi/Reader/V32/OpenApiHeaderDeserializer.cs b/src/Microsoft.OpenApi/Reader/V32/OpenApiHeaderDeserializer.cs index f7cd702d3..639c97f23 100644 --- a/src/Microsoft.OpenApi/Reader/V32/OpenApiHeaderDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V32/OpenApiHeaderDeserializer.cs @@ -123,6 +123,7 @@ public static IOpenApiHeader LoadHeader(JsonNode node, OpenApiDocument hostDocum var reference = GetReferenceIdAndExternalResource(pointer); var headerReference = new OpenApiHeaderReference(reference.Item1, hostDocument, reference.Item2); headerReference.Reference.SetMetadataFromJsonObject(jsonObject); + headerReference.Reference.SetJsonPointerPath(pointer, context); return headerReference; } diff --git a/src/Microsoft.OpenApi/Reader/V32/OpenApiLinkDeserializer.cs b/src/Microsoft.OpenApi/Reader/V32/OpenApiLinkDeserializer.cs index 5655d397d..912c8cbff 100644 --- a/src/Microsoft.OpenApi/Reader/V32/OpenApiLinkDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V32/OpenApiLinkDeserializer.cs @@ -60,6 +60,7 @@ public static IOpenApiLink LoadLink(JsonNode node, OpenApiDocument hostDocument, var reference = GetReferenceIdAndExternalResource(pointer); var linkReference = new OpenApiLinkReference(reference.Item1, hostDocument, reference.Item2); linkReference.Reference.SetMetadataFromJsonObject(jsonObject); + linkReference.Reference.SetJsonPointerPath(pointer, context); return linkReference; } diff --git a/src/Microsoft.OpenApi/Reader/V32/OpenApiMediaTypeDeserializer.cs b/src/Microsoft.OpenApi/Reader/V32/OpenApiMediaTypeDeserializer.cs index 7bff0f942..c09ec61b9 100644 --- a/src/Microsoft.OpenApi/Reader/V32/OpenApiMediaTypeDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V32/OpenApiMediaTypeDeserializer.cs @@ -97,6 +97,7 @@ public static IOpenApiMediaType LoadMediaType(JsonNode node, OpenApiDocument hos var reference = GetReferenceIdAndExternalResource(pointer); var mediaTypeReference = new OpenApiMediaTypeReference(reference.Item1, hostDocument, reference.Item2); mediaTypeReference.Reference.SetMetadataFromJsonObject(jsonObject); + mediaTypeReference.Reference.SetJsonPointerPath(pointer, context); return mediaTypeReference; } diff --git a/src/Microsoft.OpenApi/Reader/V32/OpenApiParameterDeserializer.cs b/src/Microsoft.OpenApi/Reader/V32/OpenApiParameterDeserializer.cs index 4a8300766..213376ae3 100644 --- a/src/Microsoft.OpenApi/Reader/V32/OpenApiParameterDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V32/OpenApiParameterDeserializer.cs @@ -166,6 +166,7 @@ public static IOpenApiParameter LoadParameter(JsonNode node, OpenApiDocument hos var reference = GetReferenceIdAndExternalResource(pointer); var parameterReference = new OpenApiParameterReference(reference.Item1, hostDocument, reference.Item2); parameterReference.Reference.SetMetadataFromJsonObject(jsonObject); + parameterReference.Reference.SetJsonPointerPath(pointer, context); return parameterReference; } diff --git a/src/Microsoft.OpenApi/Reader/V32/OpenApiPathItemDeserializer.cs b/src/Microsoft.OpenApi/Reader/V32/OpenApiPathItemDeserializer.cs index 3cc9bab03..b89dc80e0 100644 --- a/src/Microsoft.OpenApi/Reader/V32/OpenApiPathItemDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V32/OpenApiPathItemDeserializer.cs @@ -82,6 +82,7 @@ public static IOpenApiPathItem LoadPathItem(JsonNode node, OpenApiDocument hostD var reference = GetReferenceIdAndExternalResource(pointer); var pathItemReference = new OpenApiPathItemReference(reference.Item1, hostDocument, reference.Item2); pathItemReference.Reference.SetMetadataFromJsonObject(jsonObject); + pathItemReference.Reference.SetJsonPointerPath(pointer, context); return pathItemReference; } diff --git a/src/Microsoft.OpenApi/Reader/V32/OpenApiRequestBodyDeserializer.cs b/src/Microsoft.OpenApi/Reader/V32/OpenApiRequestBodyDeserializer.cs index a3ccbfcda..63ce237ef 100644 --- a/src/Microsoft.OpenApi/Reader/V32/OpenApiRequestBodyDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V32/OpenApiRequestBodyDeserializer.cs @@ -52,6 +52,7 @@ public static IOpenApiRequestBody LoadRequestBody(JsonNode node, OpenApiDocument var reference = GetReferenceIdAndExternalResource(pointer); var requestBodyReference = new OpenApiRequestBodyReference(reference.Item1, hostDocument, reference.Item2); requestBodyReference.Reference.SetMetadataFromJsonObject(jsonObject); + requestBodyReference.Reference.SetJsonPointerPath(pointer, context); return requestBodyReference; } diff --git a/src/Microsoft.OpenApi/Reader/V32/OpenApiResponseDeserializer.cs b/src/Microsoft.OpenApi/Reader/V32/OpenApiResponseDeserializer.cs index a0e36d017..c2f9f890d 100644 --- a/src/Microsoft.OpenApi/Reader/V32/OpenApiResponseDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V32/OpenApiResponseDeserializer.cs @@ -59,6 +59,7 @@ public static IOpenApiResponse LoadResponse(JsonNode node, OpenApiDocument hostD var reference = GetReferenceIdAndExternalResource(pointer); var responseReference = new OpenApiResponseReference(reference.Item1, hostDocument, reference.Item2); responseReference.Reference.SetMetadataFromJsonObject(jsonObject); + responseReference.Reference.SetJsonPointerPath(pointer, context); return responseReference; } diff --git a/src/Microsoft.OpenApi/Reader/V32/OpenApiSecuritySchemeDeserializer.cs b/src/Microsoft.OpenApi/Reader/V32/OpenApiSecuritySchemeDeserializer.cs index ca5ad4e48..745614d92 100644 --- a/src/Microsoft.OpenApi/Reader/V32/OpenApiSecuritySchemeDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V32/OpenApiSecuritySchemeDeserializer.cs @@ -114,6 +114,7 @@ public static IOpenApiSecurityScheme LoadSecurityScheme(JsonNode node, OpenApiDo var reference = GetReferenceIdAndExternalResource(pointer); var securitySchemeReference = new OpenApiSecuritySchemeReference(reference.Item1, hostDocument, reference.Item2); securitySchemeReference.Reference.SetMetadataFromJsonObject(jsonObject); + securitySchemeReference.Reference.SetJsonPointerPath(pointer, context); return securitySchemeReference; } diff --git a/src/Microsoft.OpenApi/Reader/V32/OpenApiV32Deserializer.cs b/src/Microsoft.OpenApi/Reader/V32/OpenApiV32Deserializer.cs index c989da889..eb211d5cf 100644 --- a/src/Microsoft.OpenApi/Reader/V32/OpenApiV32Deserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V32/OpenApiV32Deserializer.cs @@ -177,7 +177,7 @@ private static (string, string?) GetReferenceIdAndExternalResource(string pointe var isExternalResource = !refSegments[0].StartsWith("#", StringComparison.OrdinalIgnoreCase); string? externalResource = null; - if (isExternalResource && pointer.Contains('#')) + if (isExternalResource) { externalResource = pointer.Split('#')[0].TrimEnd('#'); } diff --git a/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs b/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs index 8872a8983..7c58d476a 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. using System; @@ -949,10 +949,16 @@ public bool Contains(string location) private Uri? ToLocationUrl(string location) { - if (BaseUrl is not null) + if (BaseUrl is not null && Uri.TryCreate(BaseUrl, location, out var relativeUri)) { - return new(BaseUrl, location); + return relativeUri; } + + if (Uri.TryCreate(location, UriKind.Absolute, out var absoluteUri)) + { + return absoluteUri; + } + return null; } } diff --git a/test/Microsoft.OpenApi.Readers.Tests/OpenApiWorkspaceTests/OpenApiWorkspaceStreamTests.cs b/test/Microsoft.OpenApi.Readers.Tests/OpenApiWorkspaceTests/OpenApiWorkspaceStreamTests.cs index 34f4f2d45..f7a9906b9 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/OpenApiWorkspaceTests/OpenApiWorkspaceStreamTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/OpenApiWorkspaceTests/OpenApiWorkspaceStreamTests.cs @@ -124,6 +124,46 @@ public async Task LoadDocumentWithExternalReferencesInSubDirectories() } }, petSchemaTarget); } + + [Fact] + public async Task LoadDocumentAsync_WhenLoaderThrowsOperationCanceledException_PropagatesCancellation() + { + var settings = new OpenApiReaderSettings + { + LoadExternalRefs = true, + CustomExternalLoader = new CancellingStreamLoader(), + BaseUrl = new("file://c:\\"), + }; + settings.AddYamlReader(); + + await Assert.ThrowsAnyAsync(async () => + await OpenApiDocument.LoadAsync("V3Tests/Samples/OpenApiWorkspace/TodoMain.yaml", settings, token: TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task LoadDocumentAsync_WhenCancellationTokenCanceled_PropagatesCancellation() + { + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + var settings = new OpenApiReaderSettings + { + LoadExternalRefs = true, + CustomExternalLoader = new ResourceLoader(), + BaseUrl = new("file://c:\\"), + }; + settings.AddYamlReader(); + + await Assert.ThrowsAnyAsync(async () => + await OpenApiDocument.LoadAsync("V3Tests/Samples/OpenApiWorkspace/TodoMain.yaml", settings, token: cts.Token)); + } + } + + public class CancellingStreamLoader : IStreamLoader + { + public Stream Load(Uri uri) => throw new OperationCanceledException(); + public Task LoadAsync(Uri baseUrl, Uri uri, CancellationToken cancellationToken = default) + => Task.FromException(new OperationCanceledException(cancellationToken)); } public class MockLoader : IStreamLoader diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiComponentsTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiComponentsTests.cs index 01f7d97a8..7c6c7ba6c 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiComponentsTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiComponentsTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. using Microsoft.OpenApi.Reader; @@ -14,7 +14,7 @@ public class OpenApiComponentsTests [InlineData("A:\\Dir\\File.json#/components/schemas/ExternalAbsWindowsPathModel", "ExternalAbsWindowsPathModel", "A:\\Dir\\File.json")] [InlineData("/Dir/File.json#/components/schemas/ExternalAbsUnixPathModel", "ExternalAbsUnixPathModel", "/Dir/File.json")] [InlineData("https://host.lan:1234/path/to/file/resource.json#/components/schemas/ExternalHttpsModel", "ExternalHttpsModel", "https://host.lan:1234/path/to/file/resource.json")] - [InlineData("File.json", "File.json", null)] + [InlineData("File.json", "File.json", "File.json")] public void ParseExternalSchemaReferenceShouldSucceed(string reference, string referenceId, string externalResource) { var input = $@"{{ diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiDiscriminatorTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiDiscriminatorTests.cs index 854afad9e..42dbbd429 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiDiscriminatorTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiDiscriminatorTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. using System.Collections.Generic; @@ -42,5 +42,170 @@ public async Task ParseBasicDiscriminatorShouldSucceed() DefaultMapping = new OpenApiSchemaReference("Animal", openApiDocument) }, discriminator); } + + [Fact] + public void ParseDiscriminatorWithBareSchemaNameMappingResolvesLocalTarget() + { + var openApi = """ + { + "openapi": "3.1.1", + "info": { "title": "Repro", "version": "1" }, + "paths": {}, + "components": { + "schemas": { + "Dog": { + "type": "object", + "properties": { "bark": { "type": "boolean" } } + }, + "Pet": { + "type": "object", + "required": [ "petType" ], + "properties": { + "petType": { "type": "string" } + }, + "discriminator": { + "propertyName": "petType", + "mapping": { + "dog": "Dog" + }, + "x-oas-default-mapping": "Dog" + } + } + } + } + } + """; + + var readResult = OpenApiDocument.Parse(openApi, "json"); + var doc = readResult.Document; + Assert.NotNull(doc); + Assert.Empty(readResult.Diagnostic.Errors); + + var petSchema = Assert.IsType(doc.Components.Schemas["Pet"]); + Assert.NotNull(petSchema.Discriminator); + + var dogMapping = petSchema.Discriminator.Mapping["dog"]; + Assert.False(dogMapping.Reference.IsExternal); + Assert.Null(dogMapping.Reference.ExternalResource); + Assert.Equal("Dog", dogMapping.Reference.Id); + + var dogSchema = Assert.IsType(doc.Components.Schemas["Dog"]); + Assert.Same(dogSchema, dogMapping.Target); + + Assert.NotNull(petSchema.Discriminator.DefaultMapping); + Assert.False(petSchema.Discriminator.DefaultMapping.Reference.IsExternal); + Assert.Same(dogSchema, petSchema.Discriminator.DefaultMapping.Target); + } + + [Theory] + [InlineData("Dog.json")] + [InlineData("Dog.yaml")] + [InlineData("Dog.yml")] + public void ParseDiscriminatorWithFileSuffixedBareSchemaNameResolvesLocalTarget(string schemaName) + { + var openApi = $$""" + { + "openapi": "3.1.1", + "info": { "title": "Repro", "version": "1" }, + "paths": {}, + "components": { + "schemas": { + "{{schemaName}}": { + "type": "object", + "properties": { "bark": { "type": "boolean" } } + }, + "Pet": { + "type": "object", + "required": [ "petType" ], + "properties": { + "petType": { "type": "string" } + }, + "discriminator": { + "propertyName": "petType", + "mapping": { + "dog": "{{schemaName}}" + }, + "x-oas-default-mapping": "{{schemaName}}" + } + } + } + } + } + """; + + var readResult = OpenApiDocument.Parse(openApi, "json"); + var doc = readResult.Document; + Assert.NotNull(doc); + Assert.Empty(readResult.Diagnostic.Errors); + + var petSchema = Assert.IsType(doc.Components.Schemas["Pet"]); + Assert.NotNull(petSchema.Discriminator); + + var dogMapping = petSchema.Discriminator.Mapping["dog"]; + Assert.False(dogMapping.Reference.IsExternal); + Assert.Null(dogMapping.Reference.ExternalResource); + Assert.Equal(schemaName, dogMapping.Reference.Id); + + var dogSchema = Assert.IsType(doc.Components.Schemas[schemaName]); + Assert.Same(dogSchema, dogMapping.Target); + + Assert.NotNull(petSchema.Discriminator.DefaultMapping); + Assert.False(petSchema.Discriminator.DefaultMapping.Reference.IsExternal); + Assert.Same(dogSchema, petSchema.Discriminator.DefaultMapping.Target); + } + + [Fact] + public void ParseDiscriminatorWithForwardDeclaredSchemaMappingResolvesLocalTarget() + { + var openApi = """ + { + "openapi": "3.1.1", + "info": { "title": "Repro", "version": "1" }, + "paths": {}, + "components": { + "schemas": { + "Pet": { + "type": "object", + "required": [ "petType" ], + "properties": { + "petType": { "type": "string" } + }, + "discriminator": { + "propertyName": "petType", + "mapping": { + "dog": "Dog.json" + }, + "x-oas-default-mapping": "Dog.json" + } + }, + "Dog.json": { + "type": "object", + "properties": { "bark": { "type": "boolean" } } + } + } + } + } + """; + + var readResult = OpenApiDocument.Parse(openApi, "json"); + var doc = readResult.Document; + Assert.NotNull(doc); + Assert.Empty(readResult.Diagnostic.Errors); + + var petSchema = Assert.IsType(doc.Components.Schemas["Pet"]); + Assert.NotNull(petSchema.Discriminator); + + var dogMapping = petSchema.Discriminator.Mapping["dog"]; + Assert.False(dogMapping.Reference.IsExternal); + Assert.Null(dogMapping.Reference.ExternalResource); + Assert.Equal("Dog.json", dogMapping.Reference.Id); + + var dogSchema = Assert.IsType(doc.Components.Schemas["Dog.json"]); + Assert.Same(dogSchema, dogMapping.Target); + + Assert.NotNull(petSchema.Discriminator.DefaultMapping); + Assert.False(petSchema.Discriminator.DefaultMapping.Reference.IsExternal); + Assert.Same(dogSchema, petSchema.Discriminator.DefaultMapping.Target); + } } } diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaReferenceDeserializerTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaReferenceDeserializerTests.cs index 782ae6e45..8010092c4 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaReferenceDeserializerTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaReferenceDeserializerTests.cs @@ -57,4 +57,31 @@ public void ShouldDeserializeSchemaReferenceAnnotations() Assert.Equal("example reference value", resultReference.Examples[0]?.ToString()); Assert.NotNull(resultReference.Target); } + + [Theory] + [InlineData("schemas/uuid.schema.json", "schemas/uuid.schema.json", "schemas/uuid.schema.json")] + [InlineData("./schemas/uuid.schema.json", "./schemas/uuid.schema.json", "./schemas/uuid.schema.json")] + [InlineData("uuid.schema.json", "uuid.schema.json", "uuid.schema.json")] + [InlineData("https://example.com/schemas/resource.json", "https://example.com/schemas/resource.json", "https://example.com/schemas/resource.json")] + public void ShouldDeserializeExternalSchemaReferenceWithoutFragment(string refPointer, string expectedId, string expectedExternalResource) + { + var json = $$""" + { + "$ref": "{{refPointer}}" + } + """; + + var hostDocument = new OpenApiDocument(); + var jsonNode = JsonNode.Parse(json); + + var result = OpenApiV31Deserializer.LoadSchema(jsonNode, hostDocument, new ParsingContext(new())); + + Assert.NotNull(result); + var resultReference = Assert.IsType(result); + + Assert.True(resultReference.Reference.IsExternal); + Assert.Equal(expectedId, resultReference.Reference.Id); + Assert.Equal(expectedExternalResource, resultReference.Reference.ExternalResource); + Assert.Equal(refPointer, resultReference.Reference.ReferenceV3); + } } diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/RelativeReferenceTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/RelativeReferenceTests.cs index d829c4d5a..10107301c 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/RelativeReferenceTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/RelativeReferenceTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Net.Http; @@ -747,5 +747,98 @@ public void ExitsEarlyOnCyclicalReferences() Assert.Equal("#/components/schemas/Category", parentSchemaRef.Reference.ReferenceV3); Assert.NotNull(parentSchemaRef.Target); } + + [Fact] + public void ParseDocumentWithRelativeReferenceWithoutFragmentDoesNotThrowUriFormatException() + { + var openApi = """ + { + "openapi": "3.1.1", + "info": { "title": "Repro", "version": "1" }, + "paths": {}, + "components": { + "schemas": { + "Request": { + "type": "object", + "properties": { "attemptId": { "$ref": "schemas/uuid.schema.json" } } + } + } + } + } + """; + + var readResult = OpenApiDocument.Parse(openApi, "json"); + var doc = readResult.Document; + var diagnostic = readResult.Diagnostic; + + Assert.NotNull(doc); + Assert.Empty(diagnostic.Errors); + + var requestSchema = Assert.IsType(doc.Components.Schemas["Request"]); + var attemptIdRef = Assert.IsType(requestSchema.Properties["attemptId"]); + Assert.True(attemptIdRef.Reference.IsExternal); + Assert.Equal("schemas/uuid.schema.json", attemptIdRef.Reference.ExternalResource); + Assert.Equal("schemas/uuid.schema.json", attemptIdRef.Reference.Id); + + Assert.Null(attemptIdRef.Target); + + using var writer = new StringWriter(); + doc.SerializeAsV31(new OpenApiJsonWriter(writer)); + var serialized = writer.ToString(); + Assert.Contains("\"$ref\": \"schemas/uuid.schema.json\"", serialized); + Assert.DoesNotContain("#/components/schemas/schemas/uuid.schema.json", serialized); + } + + [Fact] + public void ParseDocumentWithExternalNonSchemaReferenceCollidingWithDocumentNamePreservesComponentFragment() + { + var openApi = """ + { + "openapi": "3.1.1", + "info": { "title": "Repro", "version": "1" }, + "paths": { + "/items": { + "get": { + "parameters": [ + { "$ref": "common.json#/components/parameters/common.json" } + ], + "responses": { + "200": { "$ref": "common.json#/components/responses/common.json" } + } + } + } + } + } + """; + + var readResult = OpenApiDocument.Parse(openApi, "json"); + var doc = readResult.Document; + var diagnostic = readResult.Diagnostic; + + Assert.NotNull(doc); + Assert.Empty(diagnostic.Errors); + + var pathItem = doc.Paths["/items"]; + var operation = pathItem.Operations[HttpMethod.Get]; + + var paramRef = Assert.IsType(operation.Parameters[0]); + Assert.True(paramRef.Reference.IsExternal); + Assert.Equal("common.json", paramRef.Reference.ExternalResource); + Assert.Equal("common.json", paramRef.Reference.Id); + Assert.Equal("common.json#/components/parameters/common.json", paramRef.Reference.ReferenceV3); + + var responseRef = Assert.IsType(operation.Responses["200"]); + Assert.True(responseRef.Reference.IsExternal); + Assert.Equal("common.json", responseRef.Reference.ExternalResource); + Assert.Equal("common.json", responseRef.Reference.Id); + Assert.Equal("common.json#/components/responses/common.json", responseRef.Reference.ReferenceV3); + + using var writer = new StringWriter(); + doc.SerializeAsV31(new OpenApiJsonWriter(writer)); + var serialized = writer.ToString(); + Assert.Contains("\"$ref\": \"common.json#/components/parameters/common.json\"", serialized); + Assert.Contains("\"$ref\": \"common.json#/components/responses/common.json\"", serialized); + Assert.DoesNotContain("\"$ref\": \"common.json\"", serialized); + } } } diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiComponentsTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiComponentsTests.cs index da82e35e7..2128454f9 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiComponentsTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiComponentsTests.cs @@ -14,7 +14,7 @@ public class OpenApiComponentsTests [InlineData("A:\\Dir\\File.json#/components/schemas/ExternalAbsWindowsPathModel", "ExternalAbsWindowsPathModel", "A:\\Dir\\File.json")] [InlineData("/Dir/File.json#/components/schemas/ExternalAbsUnixPathModel", "ExternalAbsUnixPathModel", "/Dir/File.json")] [InlineData("https://host.lan:1234/path/to/file/resource.json#/components/schemas/ExternalHttpsModel", "ExternalHttpsModel", "https://host.lan:1234/path/to/file/resource.json")] - [InlineData("File.json", "File.json", null)] + [InlineData("File.json", "File.json", "File.json")] public void ParseExternalSchemaReferenceShouldSucceed(string reference, string referenceId, string externalResource) { var input = $@"{{ diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiDiscriminatorTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiDiscriminatorTests.cs index c571b0ce6..fffdc7c52 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiDiscriminatorTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiDiscriminatorTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. using System.Collections.Generic; @@ -42,5 +42,170 @@ public async Task ParseBasicDiscriminatorShouldSucceed() DefaultMapping = new OpenApiSchemaReference("Animal", openApiDocument) }, discriminator); } + + [Fact] + public void ParseDiscriminatorWithBareSchemaNameMappingResolvesLocalTarget() + { + var openApi = """ + { + "openapi": "3.2.0", + "info": { "title": "Repro", "version": "1" }, + "paths": {}, + "components": { + "schemas": { + "Dog": { + "type": "object", + "properties": { "bark": { "type": "boolean" } } + }, + "Pet": { + "type": "object", + "required": [ "petType" ], + "properties": { + "petType": { "type": "string" } + }, + "discriminator": { + "propertyName": "petType", + "mapping": { + "dog": "Dog" + }, + "defaultMapping": "Dog" + } + } + } + } + } + """; + + var readResult = OpenApiDocument.Parse(openApi, "json"); + var doc = readResult.Document; + Assert.NotNull(doc); + Assert.Empty(readResult.Diagnostic.Errors); + + var petSchema = Assert.IsType(doc.Components.Schemas["Pet"]); + Assert.NotNull(petSchema.Discriminator); + + var dogMapping = petSchema.Discriminator.Mapping["dog"]; + Assert.False(dogMapping.Reference.IsExternal); + Assert.Null(dogMapping.Reference.ExternalResource); + Assert.Equal("Dog", dogMapping.Reference.Id); + + var dogSchema = Assert.IsType(doc.Components.Schemas["Dog"]); + Assert.Same(dogSchema, dogMapping.Target); + + Assert.NotNull(petSchema.Discriminator.DefaultMapping); + Assert.False(petSchema.Discriminator.DefaultMapping.Reference.IsExternal); + Assert.Same(dogSchema, petSchema.Discriminator.DefaultMapping.Target); + } + + [Theory] + [InlineData("Dog.json")] + [InlineData("Dog.yaml")] + [InlineData("Dog.yml")] + public void ParseDiscriminatorWithFileSuffixedBareSchemaNameResolvesLocalTarget(string schemaName) + { + var openApi = $$""" + { + "openapi": "3.2.0", + "info": { "title": "Repro", "version": "1" }, + "paths": {}, + "components": { + "schemas": { + "{{schemaName}}": { + "type": "object", + "properties": { "bark": { "type": "boolean" } } + }, + "Pet": { + "type": "object", + "required": [ "petType" ], + "properties": { + "petType": { "type": "string" } + }, + "discriminator": { + "propertyName": "petType", + "mapping": { + "dog": "{{schemaName}}" + }, + "defaultMapping": "{{schemaName}}" + } + } + } + } + } + """; + + var readResult = OpenApiDocument.Parse(openApi, "json"); + var doc = readResult.Document; + Assert.NotNull(doc); + Assert.Empty(readResult.Diagnostic.Errors); + + var petSchema = Assert.IsType(doc.Components.Schemas["Pet"]); + Assert.NotNull(petSchema.Discriminator); + + var dogMapping = petSchema.Discriminator.Mapping["dog"]; + Assert.False(dogMapping.Reference.IsExternal); + Assert.Null(dogMapping.Reference.ExternalResource); + Assert.Equal(schemaName, dogMapping.Reference.Id); + + var dogSchema = Assert.IsType(doc.Components.Schemas[schemaName]); + Assert.Same(dogSchema, dogMapping.Target); + + Assert.NotNull(petSchema.Discriminator.DefaultMapping); + Assert.False(petSchema.Discriminator.DefaultMapping.Reference.IsExternal); + Assert.Same(dogSchema, petSchema.Discriminator.DefaultMapping.Target); + } + + [Fact] + public void ParseDiscriminatorWithForwardDeclaredSchemaMappingResolvesLocalTarget() + { + var openApi = """ + { + "openapi": "3.2.0", + "info": { "title": "Repro", "version": "1" }, + "paths": {}, + "components": { + "schemas": { + "Pet": { + "type": "object", + "required": [ "petType" ], + "properties": { + "petType": { "type": "string" } + }, + "discriminator": { + "propertyName": "petType", + "mapping": { + "dog": "Dog.json" + }, + "defaultMapping": "Dog.json" + } + }, + "Dog.json": { + "type": "object", + "properties": { "bark": { "type": "boolean" } } + } + } + } + } + """; + + var readResult = OpenApiDocument.Parse(openApi, "json"); + var doc = readResult.Document; + Assert.NotNull(doc); + Assert.Empty(readResult.Diagnostic.Errors); + + var petSchema = Assert.IsType(doc.Components.Schemas["Pet"]); + Assert.NotNull(petSchema.Discriminator); + + var dogMapping = petSchema.Discriminator.Mapping["dog"]; + Assert.False(dogMapping.Reference.IsExternal); + Assert.Null(dogMapping.Reference.ExternalResource); + Assert.Equal("Dog.json", dogMapping.Reference.Id); + + var dogSchema = Assert.IsType(doc.Components.Schemas["Dog.json"]); + Assert.Same(dogSchema, dogMapping.Target); + + Assert.NotNull(petSchema.Discriminator.DefaultMapping); + Assert.False(petSchema.Discriminator.DefaultMapping.Reference.IsExternal); + Assert.Same(dogSchema, petSchema.Discriminator.DefaultMapping.Target); + } } } diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiSchemaReferenceDeserializerTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiSchemaReferenceDeserializerTests.cs index 100e72b4e..63777e626 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiSchemaReferenceDeserializerTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiSchemaReferenceDeserializerTests.cs @@ -57,5 +57,32 @@ public void ShouldDeserializeSchemaReferenceAnnotations() Assert.Equal("example reference value", resultReference.Examples[0]?.ToString()); Assert.NotNull(resultReference.Target); } + + [Theory] + [InlineData("schemas/uuid.schema.json", "schemas/uuid.schema.json", "schemas/uuid.schema.json")] + [InlineData("./schemas/uuid.schema.json", "./schemas/uuid.schema.json", "./schemas/uuid.schema.json")] + [InlineData("uuid.schema.json", "uuid.schema.json", "uuid.schema.json")] + [InlineData("https://example.com/schemas/resource.json", "https://example.com/schemas/resource.json", "https://example.com/schemas/resource.json")] + public void ShouldDeserializeExternalSchemaReferenceWithoutFragment(string refPointer, string expectedId, string expectedExternalResource) + { + var json = $$""" + { + "$ref": "{{refPointer}}" + } + """; + + var hostDocument = new OpenApiDocument(); + var jsonNode = JsonNode.Parse(json); + + var result = OpenApiV32Deserializer.LoadSchema(jsonNode, hostDocument, new ParsingContext(new())); + + Assert.NotNull(result); + var resultReference = Assert.IsType(result); + + Assert.True(resultReference.Reference.IsExternal); + Assert.Equal(expectedId, resultReference.Reference.Id); + Assert.Equal(expectedExternalResource, resultReference.Reference.ExternalResource); + Assert.Equal(refPointer, resultReference.Reference.ReferenceV3); + } } diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs index 774d9ff2a..f3322a396 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs @@ -2754,6 +2754,100 @@ public void GetOperationById_ThrowsOnNullOrEmptyId() Assert.Throws(() => doc.GetOperationById(string.Empty)); } + [Theory] + [InlineData("schemas/uuid.schema.json")] + [InlineData("./schemas/uuid.schema.json")] + [InlineData("uuid.schema.json")] + public void ResolveReference_RelativeReferenceWithoutFragmentDoesNotThrowUriFormatException(string referenceId) + { + var doc = new OpenApiDocument + { + Info = new OpenApiInfo { Title = "Test", Version = "1.0" }, + BaseUri = new Uri("https://example.com/api/openapi.json"), + }; + + var schemaRef = new OpenApiSchemaReference(referenceId, doc, referenceId); + + var result = doc.ResolveReferenceTo(schemaRef.Reference, null); + Assert.Null(result); + Assert.Null(schemaRef.Target); + } + + [Fact] + public void ResolveReference_RelativeReferenceWithoutFragmentResolvesAgainstWorkspace() + { + var workspace = new OpenApiWorkspace(new Uri("https://example.com/api/")); + var targetSchema = new OpenApiSchema { Type = JsonSchemaType.String, Description = "UUID schema" }; + workspace.RegisterComponent("https://example.com/api/schemas/uuid.schema.json", targetSchema); + + var doc = new OpenApiDocument + { + Info = new OpenApiInfo { Title = "Test", Version = "1.0" }, + BaseUri = new Uri("https://example.com/api/openapi.json"), + Workspace = workspace, + }; + + var schemaRef = new OpenApiSchemaReference("schemas/uuid.schema.json", doc, "schemas/uuid.schema.json"); + + var result = doc.ResolveReferenceTo(schemaRef.Reference, null); + Assert.Same(targetSchema, result); + Assert.Same(targetSchema, schemaRef.Target); + } + + [Fact] + public void ResolveReference_ExternalComponentCollidingWithDocumentNameResolvesComponent() + { + var workspace = new OpenApiWorkspace(new Uri("https://example.com/api/")); + var targetSchema = new OpenApiSchema { Type = JsonSchemaType.String, Description = "Component named common.json" }; + workspace.AddDocumentId("common.json", new Uri("https://example.com/api/common.json")); + workspace.RegisterComponent("https://example.com/api/common.json#/components/schemas/common.json", targetSchema); + + var doc = new OpenApiDocument + { + Info = new OpenApiInfo { Title = "Test", Version = "1.0" }, + BaseUri = new Uri("https://example.com/api/openapi.json"), + Workspace = workspace, + }; + + var schemaRef = new OpenApiSchemaReference("common.json", doc, "common.json"); + schemaRef.Reference.SetJsonPointerPath("common.json#/components/schemas/common.json", new Microsoft.OpenApi.Reader.ParsingContext(new())); + + var result = doc.ResolveReferenceTo(schemaRef.Reference, null); + Assert.Same(targetSchema, result); + Assert.Same(targetSchema, schemaRef.Target); + } + + [Fact] + public void ResolveReference_ExternalNonSchemaComponentCollidingWithDocumentNameResolvesComponent() + { + var workspace = new OpenApiWorkspace(new Uri("https://example.com/api/")); + var targetResponse = new OpenApiResponse { Description = "Response named common.json" }; + var targetParam = new OpenApiParameter { Name = "common.json", In = ParameterLocation.Query }; + workspace.AddDocumentId("common.json", new Uri("https://example.com/api/common.json")); + workspace.RegisterComponent("https://example.com/api/common.json#/components/responses/common.json", targetResponse); + workspace.RegisterComponent("https://example.com/api/common.json#/components/parameters/common.json", targetParam); + + var doc = new OpenApiDocument + { + Info = new OpenApiInfo { Title = "Test", Version = "1.0" }, + BaseUri = new Uri("https://example.com/api/openapi.json"), + Workspace = workspace, + }; + + // Test publicly constructed non-schema reference + var responseRef = new OpenApiResponseReference("common.json", doc, "common.json"); + Assert.Equal("common.json#/components/responses/common.json", responseRef.Reference.ReferenceV3); + var resultResponse = doc.ResolveReferenceTo(responseRef.Reference, null); + Assert.Same(targetResponse, resultResponse); + Assert.Same(targetResponse, responseRef.Target); + + var paramRef = new OpenApiParameterReference("common.json", doc, "common.json"); + Assert.Equal("common.json#/components/parameters/common.json", paramRef.Reference.ReferenceV3); + var resultParam = doc.ResolveReferenceTo(paramRef.Reference, null); + Assert.Same(targetParam, resultParam); + Assert.Same(targetParam, paramRef.Target); + } + [Fact] public void OpenApiDocumentCopyConstructorPreservesPathsExtensions() { diff --git a/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiWorkspaceTests.cs b/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiWorkspaceTests.cs index 8bb018e9f..4b67dc13b 100644 --- a/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiWorkspaceTests.cs +++ b/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiWorkspaceTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. using System; @@ -172,6 +172,36 @@ private static OpenApiDocument CreateCommonDocument() return doc; } + + [Fact] + public void OpenApiWorkspacesCanResolveAbsoluteUriReferences() + { + var workspace = new OpenApiWorkspace(); + var targetSchema = new OpenApiSchema + { + Type = JsonSchemaType.String, + Description = "Schema at absolute URI" + }; + workspace.RegisterComponent("https://example.com/schemas/uuid.schema.json", targetSchema); + + var resolved = workspace.ResolveReference("https://example.com/schemas/uuid.schema.json"); + Assert.Same(targetSchema, resolved); + } + + [Fact] + public void OpenApiWorkspacesResolvesRootRelativeUriAgainstBaseUrl() + { + var workspace = new OpenApiWorkspace(new Uri("https://example.com/base/")); + var targetSchema = new OpenApiSchema + { + Type = JsonSchemaType.String, + Description = "Schema at root-relative URI" + }; + workspace.RegisterComponent("https://example.com/schemas/common.json", targetSchema); + + var resolved = workspace.ResolveReference("/schemas/common.json"); + Assert.Same(targetSchema, resolved); + } } public static class OpenApiFactoryExtensions