Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace Microsoft.OpenApi;
/// 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, IOpenApiReadOnlyExtensible, IShallowCopyable<IOpenApiResponse>, IOpenApiReferenceable
public interface IOpenApiResponse : IOpenApiDescribedElement, IOpenApiReadOnlyExtensible, IShallowCopyable<IOpenApiResponse>, IOpenApiReferenceable, IOpenApiSummarizedElement
{
/// <summary>
/// Maps a header name to its definition.
Expand Down
16 changes: 16 additions & 0 deletions src/Microsoft.OpenApi/Models/OpenApiResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ namespace Microsoft.OpenApi
/// </summary>
public class OpenApiResponse : IOpenApiExtensible, IOpenApiResponse
{
/// <inheritdoc/>
public string? Summary { get; set; }

/// <inheritdoc/>
public string? Description { get; set; }

Expand All @@ -38,6 +41,7 @@ public OpenApiResponse() { }
internal OpenApiResponse(IOpenApiResponse response)
{
Utils.CheckArgumentNull(response);
Summary = response.Summary ?? Summary;
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;
Expand Down Expand Up @@ -76,6 +80,12 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version

writer.WriteStartObject();

// summary - only for v3.2+
if (version >= OpenApiSpecVersion.OpenApi3_2)
{
writer.WriteProperty(OpenApiConstants.Summary, Summary);
}

// description
writer.WriteRequiredProperty(OpenApiConstants.Description, Description);

Expand All @@ -88,6 +98,12 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version
// links
writer.WriteOptionalMap(OpenApiConstants.Links, Links, callback);

// summary as extension for v3.1 and earlier
if (version < OpenApiSpecVersion.OpenApi3_2 && !string.IsNullOrEmpty(Summary))
{
writer.WriteProperty(OpenApiConstants.ExtensionFieldNamePrefix + "oai-" + OpenApiConstants.Summary, Summary);
}

// extension
writer.WriteExtensions(Extensions, version);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace Microsoft.OpenApi
/// <summary>
/// Response Object Reference.
/// </summary>
public class OpenApiResponseReference : BaseOpenApiReferenceHolder<OpenApiResponse, IOpenApiResponse, OpenApiReferenceWithDescription>, IOpenApiResponse
public class OpenApiResponseReference : BaseOpenApiReferenceHolder<OpenApiResponse, IOpenApiResponse, OpenApiReferenceWithDescriptionAndSummary>, IOpenApiResponse
{
/// <summary>
/// Constructor initializing the reference object.
Expand All @@ -32,6 +32,13 @@ private OpenApiResponseReference(OpenApiResponseReference openApiResponseReferen

}

/// <inheritdoc/>
public string? Summary
{
get => string.IsNullOrEmpty(Reference.Summary) ? Target?.Summary : Reference.Summary;
set => Reference.Summary = value;
}

/// <inheritdoc/>
public string? Description
{
Expand Down Expand Up @@ -63,9 +70,9 @@ public IOpenApiResponse CreateShallowCopy()
return new OpenApiResponseReference(this);
}
/// <inheritdoc/>
protected override OpenApiReferenceWithDescription CopyReference(OpenApiReferenceWithDescription sourceReference)
protected override OpenApiReferenceWithDescriptionAndSummary CopyReference(OpenApiReferenceWithDescriptionAndSummary sourceReference)
{
return new OpenApiReferenceWithDescription(sourceReference);
return new OpenApiReferenceWithDescriptionAndSummary(sourceReference);
}
}
}
12 changes: 11 additions & 1 deletion src/Microsoft.OpenApi/Reader/V3/OpenApiResponseDeserializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,17 @@ internal static partial class OpenApiV3Deserializer
private static readonly PatternFieldMap<OpenApiResponse> _responsePatternFields =
new()
{
{s => s.StartsWith(OpenApiConstants.ExtensionFieldNamePrefix, StringComparison.OrdinalIgnoreCase), (o, p, n, _) => o.AddExtension(p, LoadExtension(p,n))}
{s => s.StartsWith(OpenApiConstants.ExtensionFieldNamePrefix, StringComparison.OrdinalIgnoreCase), (o, p, n, _) =>
{
if (p.Equals("x-oai-summary", StringComparison.OrdinalIgnoreCase))
{
o.Summary = n.GetScalarValue();
}
else
{
o.AddExtension(p, LoadExtension(p,n));
}
}}
};

public static IOpenApiResponse LoadResponse(ParseNode node, OpenApiDocument hostDocument)
Expand Down
12 changes: 11 additions & 1 deletion src/Microsoft.OpenApi/Reader/V31/OpenApiResponseDeserializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,17 @@ internal static partial class OpenApiV31Deserializer
private static readonly PatternFieldMap<OpenApiResponse> _responsePatternFields =
new()
{
{s => s.StartsWith(OpenApiConstants.ExtensionFieldNamePrefix, StringComparison.OrdinalIgnoreCase), (o, p, n, _) => o.AddExtension(p, LoadExtension(p,n))}
{s => s.StartsWith(OpenApiConstants.ExtensionFieldNamePrefix, StringComparison.OrdinalIgnoreCase), (o, p, n, _) =>
{
if (p.Equals("x-oai-summary", StringComparison.OrdinalIgnoreCase))
{
o.Summary = n.GetScalarValue();
}
else
{
o.AddExtension(p, LoadExtension(p,n));
}
}}
};

public static IOpenApiResponse LoadResponse(ParseNode node, OpenApiDocument hostDocument)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ internal static partial class OpenApiV32Deserializer
{
private static readonly FixedFieldMap<OpenApiResponse> _responseFixedFields = new()
{
{
"summary", (o, n, _) =>
{
o.Summary = n.GetScalarValue();
}
},
{
"description", (o, n, _) =>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ public async Task LoadResponseReference()
Assert.Equivalent(
new OpenApiResponse
{
Summary = null,
Description = "Entity not found.",
Content = new Dictionary<string, OpenApiMediaType>()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,29 @@ public async Task ResponseWithReferencedHeaderShouldReferenceComponent()

Assert.Equal(expected.Description, actual.Description);
}

[Fact]
public async Task ResponseWithSummaryV32ShouldDeserializeCorrectly()
{
var result = await OpenApiDocument.LoadAsync(Path.Combine(SampleFolderPath, "responseWithSummary.yaml"), SettingsFixture.ReaderSettings);

var response = result.Document.Components.Responses["SuccessResponse"] as OpenApiResponse;

Assert.NotNull(response);
Assert.Equal("Successful response", response.Summary);
Assert.Equal("A successful response with summary", response.Description);
}

[Fact]
public async Task ResponseWithSummaryExtensionV31ShouldDeserializeCorrectly()
{
var result = await OpenApiDocument.LoadAsync(Path.Combine(SampleFolderPath, "responseWithSummaryExtension.yaml"), SettingsFixture.ReaderSettings);

var response = result.Document.Components.Responses["SuccessResponse"] as OpenApiResponse;

Assert.NotNull(response);
Assert.Equal("Successful response", response.Summary);
Assert.Equal("A successful response with summary extension", response.Description);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
openapi: 3.2.0
info:
title: Test API
version: 1.0.0
components:
responses:
SuccessResponse:
summary: Successful response
description: A successful response with summary
content:
application/json:
schema:
type: object
properties:
message:
type: string
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
openapi: 3.1.0
info:
title: Test API
version: 1.0.0
components:
responses:
SuccessResponse:
description: A successful response with summary extension
x-oai-summary: Successful response
content:
application/json:
schema:
type: object
properties:
message:
type: string
97 changes: 97 additions & 0 deletions test/Microsoft.OpenApi.Tests/Models/OpenApiResponseTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
using VerifyXunit;
using Xunit;
Expand Down Expand Up @@ -166,6 +167,22 @@ public class OpenApiResponseTests
}
};

private static OpenApiResponse ResponseWithSummary => new OpenApiResponse
{
Summary = "Successful response",
Description = "A detailed description of a successful response",
Content = new Dictionary<string, OpenApiMediaType>
{
["application/json"] = new OpenApiMediaType
{
Schema = new OpenApiSchema()
{
Type = JsonSchemaType.Object
}
}
}
};

[Theory]
[InlineData(OpenApiSpecVersion.OpenApi3_0, OpenApiConstants.Json)]
[InlineData(OpenApiSpecVersion.OpenApi2_0, OpenApiConstants.Json)]
Expand Down Expand Up @@ -399,5 +416,85 @@ public async Task SerializeReferencedResponseAsV2JsonWithoutReferenceWorksAsync(
// Assert
await Verifier.Verify(outputStringWriter).UseParameters(produceTerseOutput);
}

[Fact]
public async Task SerializeResponseWithSummaryAsV32Works()
{
// Arrange
var expected = @"{
""summary"": ""Successful response"",
""description"": ""A detailed description of a successful response"",
""content"": {
""application/json"": {
""schema"": {
""type"": ""object""
}
}
}
}";

// Act
var actual = await ResponseWithSummary.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_2);

// Assert
Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(actual)));
}

[Fact]
public async Task SerializeResponseWithSummaryAsV31Works()
{
// Arrange
var expected = @"{
""description"": ""A detailed description of a successful response"",
""content"": {
""application/json"": {
""schema"": {
""type"": ""object""
}
}
},
""x-oai-summary"": ""Successful response""
}";

// Act
var actual = await ResponseWithSummary.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_1);

// Assert
Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(actual)));
}

[Fact]
public async Task SerializeResponseWithSummaryAsV3Works()
{
// Arrange
var expected = @"{
""description"": ""A detailed description of a successful response"",
""content"": {
""application/json"": {
""schema"": {
""type"": ""object""
}
}
},
""x-oai-summary"": ""Successful response""
}";

// Act
var actual = await ResponseWithSummary.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_0);

// Assert
Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(actual)));
}

[Fact]
public void ResponseWithSummaryShouldImplementIOpenApiSummarizedElement()
{
// Arrange
var response = new OpenApiResponse { Summary = "Test summary" };

// Act & Assert
Assert.IsType<IOpenApiSummarizedElement>(response, exactMatch: false);
Assert.Equal("Test summary", response.Summary);
}
}
}
Loading