Skip to content
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

fix: response reference proxy design pattern implementation #2099

Merged
merged 1 commit into from
Jan 28, 2025
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
29 changes: 29 additions & 0 deletions src/Microsoft.OpenApi/Models/Interfaces/IOpenApiResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System.Collections.Generic;
using Microsoft.OpenApi.Interfaces;

namespace Microsoft.OpenApi.Models.Interfaces;

/// <summary>
/// Defines the base properties for the response object.
/// This interface is provided for type assertions but should not be implemented by package consumers beyond automatic mocking.
/// </summary>
public interface IOpenApiResponse : IOpenApiDescribedElement, IOpenApiSerializable, IOpenApiReadOnlyExtensible
{
/// <summary>
/// Maps a header name to its definition.
/// </summary>
public IDictionary<string, IOpenApiHeader> Headers { get; }

/// <summary>
/// A map containing descriptions of potential response payloads.
/// The key is a media type or media type range and the value describes it.
/// </summary>
public IDictionary<string, OpenApiMediaType> Content { get; }

/// <summary>
/// A map of operations links that can be followed from the response.
/// The key of the map is a short name for the link,
/// following the naming constraints of the names for Component Objects.
/// </summary>
public IDictionary<string, IOpenApiLink> Links { get; }
}
6 changes: 3 additions & 3 deletions src/Microsoft.OpenApi/Models/OpenApiComponents.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ public class OpenApiComponents : IOpenApiSerializable, IOpenApiExtensible
public IDictionary<string, OpenApiSchema>? Schemas { get; set; } = new Dictionary<string, OpenApiSchema>();

/// <summary>
/// An object to hold reusable <see cref="OpenApiResponse"/> Objects.
/// An object to hold reusable <see cref="IOpenApiResponse"/> Objects.
/// </summary>
public IDictionary<string, OpenApiResponse>? Responses { get; set; } = new Dictionary<string, OpenApiResponse>();
public IDictionary<string, IOpenApiResponse>? Responses { get; set; } = new Dictionary<string, IOpenApiResponse>();

/// <summary>
/// An object to hold reusable <see cref="IOpenApiParameter"/> Objects.
Expand Down Expand Up @@ -86,7 +86,7 @@ public OpenApiComponents() { }
public OpenApiComponents(OpenApiComponents? components)
{
Schemas = components?.Schemas != null ? new Dictionary<string, OpenApiSchema>(components.Schemas) : null;
Responses = components?.Responses != null ? new Dictionary<string, OpenApiResponse>(components.Responses) : null;
Responses = components?.Responses != null ? new Dictionary<string, IOpenApiResponse>(components.Responses) : null;
Parameters = components?.Parameters != null ? new Dictionary<string, IOpenApiParameter>(components.Parameters) : null;
Examples = components?.Examples != null ? new Dictionary<string, IOpenApiExample>(components.Examples) : null;
RequestBodies = components?.RequestBodies != null ? new Dictionary<string, IOpenApiRequestBody>(components.RequestBodies) : null;
Expand Down
2 changes: 1 addition & 1 deletion src/Microsoft.OpenApi/Models/OpenApiDocument.cs
Original file line number Diff line number Diff line change
Expand Up @@ -596,7 +596,7 @@ public bool AddComponent<T>(string id, T componentToRegister)
Components.Parameters.Add(id, openApiParameter);
break;
case OpenApiResponse openApiResponse:
Components.Responses ??= new Dictionary<string, OpenApiResponse>();
Components.Responses ??= new Dictionary<string, IOpenApiResponse>();
Components.Responses.Add(id, openApiResponse);
break;
case OpenApiRequestBody openApiRequestBody:
Expand Down
6 changes: 4 additions & 2 deletions src/Microsoft.OpenApi/Models/OpenApiOperation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -279,8 +279,10 @@ public void SerializeAsV2(IOpenApiWriter writer)
.SelectMany(static r => r.Value.Content?.Keys ?? [])
.Concat(
Responses
.Where(static r => r.Value.Reference is {HostDocument: not null})
.SelectMany(static r => r.Value.Content?.Keys ?? []))
.Select(static r => r.Value)
.OfType<OpenApiResponseReference>()
.Where(static r => r.Reference is {HostDocument: not null})
.SelectMany(static r => r.Content?.Keys ?? []))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();

Expand Down
60 changes: 18 additions & 42 deletions src/Microsoft.OpenApi/Models/OpenApiResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,82 +13,58 @@ namespace Microsoft.OpenApi.Models
/// <summary>
/// Response object.
/// </summary>
public class OpenApiResponse : IOpenApiReferenceable, IOpenApiExtensible
public class OpenApiResponse : IOpenApiReferenceable, IOpenApiExtensible, IOpenApiResponse
{
/// <summary>
/// REQUIRED. A short description of the response.
/// </summary>
public virtual string Description { get; set; }

/// <summary>
/// Maps a header name to its definition.
/// </summary>
public virtual IDictionary<string, IOpenApiHeader> Headers { get; set; } = new Dictionary<string, IOpenApiHeader>();

/// <summary>
/// A map containing descriptions of potential response payloads.
/// The key is a media type or media type range and the value describes it.
/// </summary>
public virtual IDictionary<string, OpenApiMediaType> Content { get; set; } = new Dictionary<string, OpenApiMediaType>();
/// <inheritdoc/>
public string Description { get; set; }

/// <summary>
/// A map of operations links that can be followed from the response.
/// The key of the map is a short name for the link,
/// following the naming constraints of the names for Component Objects.
/// </summary>
public virtual IDictionary<string, IOpenApiLink> Links { get; set; } = new Dictionary<string, IOpenApiLink>();
/// <inheritdoc/>
public IDictionary<string, IOpenApiHeader> Headers { get; set; } = new Dictionary<string, IOpenApiHeader>();

/// <summary>
/// This object MAY be extended with Specification Extensions.
/// </summary>
public virtual IDictionary<string, IOpenApiExtension> Extensions { get; set; } = new Dictionary<string, IOpenApiExtension>();
/// <inheritdoc/>
public IDictionary<string, OpenApiMediaType> Content { get; set; } = new Dictionary<string, OpenApiMediaType>();

/// <summary>
/// Indicates if object is populated with data or is just a reference to the data
/// </summary>
public bool UnresolvedReference { get; set; }
/// <inheritdoc/>
public IDictionary<string, IOpenApiLink> Links { get; set; } = new Dictionary<string, IOpenApiLink>();

/// <summary>
/// Reference pointer.
/// </summary>
public OpenApiReference Reference { get; set; }
/// <inheritdoc/>
public IDictionary<string, IOpenApiExtension> Extensions { get; set; } = new Dictionary<string, IOpenApiExtension>();

/// <summary>
/// Parameterless constructor
/// </summary>
public OpenApiResponse() { }

/// <summary>
/// Initializes a copy of <see cref="OpenApiResponse"/> object
/// Initializes a copy of <see cref="IOpenApiResponse"/> object
/// </summary>
public OpenApiResponse(OpenApiResponse response)
public OpenApiResponse(IOpenApiResponse response)
{
Utils.CheckArgumentNull(response);
Description = response?.Description ?? Description;
Headers = response?.Headers != null ? new Dictionary<string, IOpenApiHeader>(response.Headers) : null;
Content = response?.Content != null ? new Dictionary<string, OpenApiMediaType>(response.Content) : null;
Links = response?.Links != null ? new Dictionary<string, IOpenApiLink>(response.Links) : null;
Extensions = response?.Extensions != null ? new Dictionary<string, IOpenApiExtension>(response.Extensions) : null;
UnresolvedReference = response?.UnresolvedReference ?? UnresolvedReference;
Reference = response?.Reference != null ? new(response?.Reference) : null;
}

/// <summary>
/// Serialize <see cref="OpenApiResponse"/> to Open Api v3.1
/// </summary>
public virtual void SerializeAsV31(IOpenApiWriter writer)
public void SerializeAsV31(IOpenApiWriter writer)
{
SerializeInternal(writer, OpenApiSpecVersion.OpenApi3_1, (writer, element) => element.SerializeAsV31(writer));
}

/// <summary>
/// Serialize <see cref="OpenApiResponse"/> to Open Api v3.0.
/// </summary>
public virtual void SerializeAsV3(IOpenApiWriter writer)
public void SerializeAsV3(IOpenApiWriter writer)
{
SerializeInternal(writer, OpenApiSpecVersion.OpenApi3_0, (writer, element) => element.SerializeAsV3(writer));
}

internal virtual void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version,
private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version,
Action<IOpenApiWriter, IOpenApiSerializable> callback)
{
Utils.CheckArgumentNull(writer);
Expand Down Expand Up @@ -116,7 +92,7 @@ internal virtual void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersio
/// <summary>
/// Serialize to OpenAPI V2 document without using reference.
/// </summary>
public virtual void SerializeAsV2(IOpenApiWriter writer)
public void SerializeAsV2(IOpenApiWriter writer)
{
Utils.CheckArgumentNull(writer);

Expand Down
4 changes: 3 additions & 1 deletion src/Microsoft.OpenApi/Models/OpenApiResponses.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.

using Microsoft.OpenApi.Models.Interfaces;

namespace Microsoft.OpenApi.Models
{
/// <summary>
/// Responses object.
/// </summary>
public class OpenApiResponses : OpenApiExtensibleDictionary<OpenApiResponse>
public class OpenApiResponses : OpenApiExtensibleDictionary<IOpenApiResponse>
{
/// <summary>
/// Parameterless constructor
Expand Down
115 changes: 18 additions & 97 deletions src/Microsoft.OpenApi/Models/References/OpenApiResponseReference.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,14 @@
using System.Collections.Generic;
using Microsoft.OpenApi.Interfaces;
using Microsoft.OpenApi.Models.Interfaces;
using Microsoft.OpenApi.Writers;

namespace Microsoft.OpenApi.Models.References
{
/// <summary>
/// Response Object Reference.
/// </summary>
public class OpenApiResponseReference : OpenApiResponse, IOpenApiReferenceHolder<OpenApiResponse>
public class OpenApiResponseReference : BaseOpenApiReferenceHolder<OpenApiResponse, IOpenApiResponse>, IOpenApiResponse
{
internal OpenApiResponse _target;
private readonly OpenApiReference _reference;
private string _description;

/// <summary>
/// Gets the target response.
/// </summary>
/// <remarks>
/// If the reference is not resolved, this will return null.
/// </remarks>
public OpenApiResponse Target
{
get
{
_target ??= Reference.HostDocument?.ResolveReferenceTo<OpenApiResponse>(_reference);
return _target;
}
}

/// <summary>
/// Constructor initializing the reference object.
/// </summary>
Expand All @@ -43,102 +23,43 @@ public OpenApiResponse Target
/// 1. a absolute/relative file path, for example: ../commons/pet.json
/// 2. a Url, for example: http://localhost/pet.json
/// </param>
public OpenApiResponseReference(string referenceId, OpenApiDocument hostDocument, string externalResource = null)
public OpenApiResponseReference(string referenceId, OpenApiDocument hostDocument, string externalResource = null):base(referenceId, hostDocument, ReferenceType.Response, externalResource)
{
Utils.CheckArgumentNullOrEmpty(referenceId);

_reference = new OpenApiReference()
{
Id = referenceId,
HostDocument = hostDocument,
Type = ReferenceType.Response,
ExternalResource = externalResource
};

Reference = _reference;
}

internal OpenApiResponseReference(string referenceId, OpenApiResponse target)
internal OpenApiResponseReference(OpenApiResponse target, string referenceId):base(target, referenceId, ReferenceType.Response)
{
_target ??= target;

_reference = new OpenApiReference()
{
Id = referenceId,
Type = ReferenceType.Response,
};

Reference = _reference;
}

/// <inheritdoc/>
public override string Description
public string Description
{
get => string.IsNullOrEmpty(_description) ? Target?.Description : _description;
set => _description = value;
get => string.IsNullOrEmpty(Reference?.Description) ? Target?.Description : Reference.Description;
set
{
if (Reference is not null)
{
Reference.Description = value;
}
}
}

private IDictionary<string, OpenApiMediaType> _content;
/// <inheritdoc/>
public override IDictionary<string, OpenApiMediaType> Content { get => _content is not null ? _content : Target?.Content; set => _content = value; }
public IDictionary<string, OpenApiMediaType> Content { get => Target?.Content; }

private IDictionary<string, IOpenApiHeader> _headers;
/// <inheritdoc/>
public override IDictionary<string, IOpenApiHeader> Headers { get => _headers is not null ? _headers : Target?.Headers; set => _headers = value; }
public IDictionary<string, IOpenApiHeader> Headers { get => Target?.Headers; }

private IDictionary<string, IOpenApiLink> _links;
/// <inheritdoc/>
public override IDictionary<string, IOpenApiLink> Links { get => _links is not null ? _links : Target?.Links; set => _links = value; }
public IDictionary<string, IOpenApiLink> Links { get => Target?.Links; }

private IDictionary<string, IOpenApiExtension> _extensions;
/// <inheritdoc/>
public override IDictionary<string, IOpenApiExtension> Extensions { get => _extensions is not null ? _extensions : Target?.Extensions; set => _extensions = value; }

/// <inheritdoc/>
public override void SerializeAsV3(IOpenApiWriter writer)
{
if (!writer.GetSettings().ShouldInlineReference(_reference))
{
_reference.SerializeAsV3(writer);
}
else
{
SerializeInternal(writer, (writer, element) => element.SerializeAsV3(writer));
}
}

/// <inheritdoc/>
public override void SerializeAsV31(IOpenApiWriter writer)
{
if (!writer.GetSettings().ShouldInlineReference(_reference))
{
_reference.SerializeAsV31(writer);
}
else
{
SerializeInternal(writer, (writer, element) => element.SerializeAsV31(writer));
}
}

/// <inheritdoc/>
public override void SerializeAsV2(IOpenApiWriter writer)
{
if (!writer.GetSettings().ShouldInlineReference(_reference))
{
_reference.SerializeAsV2(writer);
}
else
{
SerializeInternal(writer, (writer, element) => element.SerializeAsV2(writer));
}
}
public IDictionary<string, IOpenApiExtension> Extensions { get => Target?.Extensions; }

/// <inheritdoc/>
private void SerializeInternal(IOpenApiWriter writer,
Action<IOpenApiWriter, IOpenApiReferenceable> action)
public override IOpenApiResponse CopyReferenceAsTargetElementWithOverrides(IOpenApiResponse source)
{
Utils.CheckArgumentNull(writer);
action(writer, this);
return source is OpenApiResponse ? new OpenApiResponse(this) : source;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ public static OpenApiDocument LoadOpenApi(RootNode rootNode)
rootNode.GetMap(),
openApiDoc.Paths.Values
.SelectMany(path => path.Operations?.Values ?? Enumerable.Empty<OpenApiOperation>())
.SelectMany(operation => operation.Responses?.Values ?? Enumerable.Empty<OpenApiResponse>()),
.SelectMany(operation => operation.Responses?.Values ?? Enumerable.Empty<IOpenApiResponse>()),
openApiNode.Context);
}

Expand All @@ -257,11 +257,11 @@ public static OpenApiDocument LoadOpenApi(RootNode rootNode)
return openApiDoc;
}

private static void ProcessResponsesMediaTypes(MapNode mapNode, IEnumerable<OpenApiResponse> responses, ParsingContext context)
private static void ProcessResponsesMediaTypes(MapNode mapNode, IEnumerable<IOpenApiResponse> responses, ParsingContext context)
{
if (responses != null)
{
foreach (var response in responses)
foreach (var response in responses.OfType<OpenApiResponse>())
{
ProcessProduces(mapNode, response, context);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ internal static OpenApiOperation LoadOperation(ParseNode node, OpenApiDocument h
}
}

foreach (var response in operation.Responses.Values)
foreach (var response in operation.Responses.Values.OfType<OpenApiResponse>())
{
ProcessProduces(node.CheckMapNode("responses"), response, node.Context);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ private static void LoadExample(OpenApiResponse response, string mediaType, Pars
mediaTypeObject.Example = exampleNode;
}

public static OpenApiResponse LoadResponse(ParseNode node, OpenApiDocument hostDocument)
public static IOpenApiResponse LoadResponse(ParseNode node, OpenApiDocument hostDocument)
{
var mapNode = node.CheckMapNode("response");

Expand Down
Loading
Loading