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: Examples serialization in a response object during v2->v3 upcasting or vice versa #1538

Merged
merged 22 commits into from
Jan 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
b004ba6
Preserve examples in v2 files and write them out as extensions
MaggieKimani1 Jan 23, 2024
ef2b99d
Update tests and public API interface
MaggieKimani1 Jan 23, 2024
cfa49e9
Add examples constant to temp storage keys
MaggieKimani1 Jan 10, 2024
eda97ad
Load "x-examples" as Examples; store and retrieve from temp storage a…
MaggieKimani1 Jan 23, 2024
f600147
Default to an empty collection if examples is null
MaggieKimani1 Jan 23, 2024
61d50b5
Add a reference to the OpenApi.Tests project to access the string ext…
MaggieKimani1 Jan 10, 2024
e41b927
Add test to validate that a V2 doc with x-examples gets mapped to Med…
MaggieKimani1 Jan 10, 2024
eb9ba94
Add unit tests
MaggieKimani1 Jan 11, 2024
af5568c
Fix CodeQL warnings
MaggieKimani1 Jan 23, 2024
dc7cef8
Filter sequence using "Where"
MaggieKimani1 Jan 23, 2024
840c591
Update src/Microsoft.OpenApi.Readers/V2/OpenApiResponseDeserializer.cs
MaggieKimani1 Jan 11, 2024
9d226df
- adds missing using
baywet Jan 11, 2024
3f11c61
Add normalization; use constant for Examples extension
MaggieKimani1 Jan 23, 2024
5d69eda
Update API interface
MaggieKimani1 Jan 11, 2024
6cd8a82
Code cleanup and refactoring
MaggieKimani1 Jan 23, 2024
7c17a09
Refactor the LoadExtensions method for reuse
MaggieKimani1 Jan 25, 2024
37c097e
Fetch the examples from storage and append them to the resulting body…
MaggieKimani1 Jan 25, 2024
73cb87e
Revert change
MaggieKimani1 Jan 25, 2024
c8a4a00
Add tests to validate processing multiple examples in a body paramete…
MaggieKimani1 Jan 25, 2024
e3e8fc7
Refactor code for reuse
MaggieKimani1 Jan 25, 2024
fa9306f
Remove method from interface to prevent a breaking change
MaggieKimani1 Jan 25, 2024
7882fdd
Update public API interface
MaggieKimani1 Jan 25, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using System.Xml.Linq;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Extensions;
using Microsoft.OpenApi.Models;
Expand Down Expand Up @@ -194,7 +195,8 @@ internal static OpenApiRequestBody CreateRequestBody(
k => k,
_ => new OpenApiMediaType
{
Schema = bodyParameter.Schema
Schema = bodyParameter.Schema,
Examples = bodyParameter.Examples
}),
Extensions = bodyParameter.Extensions
};
Expand Down
21 changes: 20 additions & 1 deletion src/Microsoft.OpenApi.Readers/V2/OpenApiParameterDeserializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,17 @@ internal static partial class OpenApiV2Deserializer
"schema",
(o, n) => o.Schema = LoadSchema(n)
},
{
"x-examples",
LoadParameterExamplesExtension
},
};

private static readonly PatternFieldMap<OpenApiParameter> _parameterPatternFields =
new()
{
{s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p, n))}
{s => s.StartsWith("x-") && !s.Equals(OpenApiConstants.ExamplesExtension, StringComparison.OrdinalIgnoreCase),
(o, p, n) => o.AddExtension(p, LoadExtension(p, n))}
};

private static readonly AnyFieldMap<OpenApiParameter> _parameterAnyFields =
Expand Down Expand Up @@ -166,6 +171,12 @@ private static void LoadStyle(OpenApiParameter p, string v)
}
}

private static void LoadParameterExamplesExtension(OpenApiParameter parameter, ParseNode node)
{
var examples = LoadExamplesExtension(node);
node.Context.SetTempStorage(TempStorageKeys.Examples, examples, parameter);
}

private static OpenApiSchema GetOrCreateSchema(OpenApiParameter p)
{
if (p.Schema == null)
Expand Down Expand Up @@ -250,6 +261,14 @@ public static OpenApiParameter LoadParameter(ParseNode node, bool loadRequestBod
node.Context.SetTempStorage("schema", null);
}

// load examples from storage and add them to the parameter
var examples = node.Context.GetFromTempStorage<Dictionary<string, OpenApiExample>>(TempStorageKeys.Examples, parameter);
if (examples != null)
{
parameter.Examples = examples;
node.Context.SetTempStorage("examples", null);
}

var isBodyOrFormData = (bool)node.Context.GetFromTempStorage<object>(TempStorageKeys.ParameterIsBodyOrFormData);
if (isBodyOrFormData && !loadRequestBody)
{
Expand Down
62 changes: 56 additions & 6 deletions src/Microsoft.OpenApi.Readers/V2/OpenApiResponseDeserializer.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.

using System;
using System.Collections.Generic;
using Microsoft.OpenApi.Extensions;
using Microsoft.OpenApi.Models;
Expand Down Expand Up @@ -28,6 +29,10 @@ internal static partial class OpenApiV2Deserializer
"examples",
LoadExamples
},
{
"x-examples",
LoadResponseExamplesExtension
},
{
"schema",
(o, n) => n.Context.SetTempStorage(TempStorageKeys.ResponseSchema, LoadSchema(n), o)
Expand All @@ -37,7 +42,8 @@ internal static partial class OpenApiV2Deserializer
private static readonly PatternFieldMap<OpenApiResponse> _responsePatternFields =
new()
{
{s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p, n))}
{s => s.StartsWith("x-") && !s.Equals(OpenApiConstants.ExamplesExtension, StringComparison.OrdinalIgnoreCase),
(o, p, n) => o.AddExtension(p, LoadExtension(p, n))}
};

private static readonly AnyFieldMap<OpenApiMediaType> _mediaTypeAnyFields =
Expand Down Expand Up @@ -69,6 +75,8 @@ private static void ProcessProduces(MapNode mapNode, OpenApiResponse response, P
?? context.DefaultContentType ?? new List<string> { "application/octet-stream" };

var schema = context.GetFromTempStorage<OpenApiSchema>(TempStorageKeys.ResponseSchema, response);
var examples = context.GetFromTempStorage<Dictionary<string, OpenApiExample>>(TempStorageKeys.Examples, response)
?? new Dictionary<string, OpenApiExample>();

foreach (var produce in produces)
{
Expand All @@ -84,20 +92,64 @@ private static void ProcessProduces(MapNode mapNode, OpenApiResponse response, P
{
var mediaType = new OpenApiMediaType
{
Schema = schema
Schema = schema,
Examples = examples
};

response.Content.Add(produce, mediaType);
}
}

context.SetTempStorage(TempStorageKeys.ResponseSchema, null, response);
context.SetTempStorage(TempStorageKeys.Examples, null, response);
context.SetTempStorage(TempStorageKeys.ResponseProducesSet, true, response);
}

private static void LoadResponseExamplesExtension(OpenApiResponse response, ParseNode node)
{
var examples = LoadExamplesExtension(node);
node.Context.SetTempStorage(TempStorageKeys.Examples, examples, response);
}

private static Dictionary<string, OpenApiExample> LoadExamplesExtension(ParseNode node)
{
var mapNode = node.CheckMapNode(OpenApiConstants.ExamplesExtension);
var examples = new Dictionary<string, OpenApiExample>();

foreach (var examplesNode in mapNode)
{
// Load the media type node as an OpenApiExample object
var example = new OpenApiExample();
var exampleNode = examplesNode.Value.CheckMapNode(examplesNode.Name);
foreach (var valueNode in exampleNode)
{
switch (valueNode.Name.ToLowerInvariant())
{
case "summary":
example.Summary = valueNode.Value.GetScalarValue();
break;
case "description":
example.Description = valueNode.Value.GetScalarValue();
break;
case "value":
example.Value = OpenApiAnyConverter.GetSpecificOpenApiAny(valueNode.Value.CreateAny());
break;
case "externalValue":
example.ExternalValue = valueNode.Value.GetScalarValue();
break;
}
}

examples.Add(examplesNode.Name, example);
}

return examples;
}

private static void LoadExamples(OpenApiResponse response, ParseNode node)
{
var mapNode = node.CheckMapNode("examples");

foreach (var mediaTypeNode in mapNode)
{
LoadExample(response, mediaTypeNode.Name, mediaTypeNode.Value);
Expand All @@ -108,10 +160,7 @@ private static void LoadExample(OpenApiResponse response, string mediaType, Pars
{
var exampleNode = node.CreateAny();

if (response.Content == null)
{
response.Content = new Dictionary<string, OpenApiMediaType>();
}
response.Content ??= new Dictionary<string, OpenApiMediaType>();

OpenApiMediaType mediaTypeObject;
if (response.Content.TryGetValue(mediaType, out var value))
Expand Down Expand Up @@ -141,6 +190,7 @@ public static OpenApiResponse LoadResponse(ParseNode node)
}

var response = new OpenApiResponse();

foreach (var property in mapNode)
{
property.ParseField(response, _responseFixedFields, _responsePatternFields);
Expand Down
1 change: 1 addition & 0 deletions src/Microsoft.OpenApi.Readers/V2/TempStorageKeys.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@ internal static class TempStorageKeys
public const string GlobalConsumes = "globalConsumes";
public const string GlobalProduces = "globalProduces";
public const string ParameterIsBodyOrFormData = "parameterIsBodyOrFormData";
public const string Examples = "examples";
}
}
5 changes: 5 additions & 0 deletions src/Microsoft.OpenApi/Models/OpenApiConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,11 @@ public static class OpenApiConstants
/// </summary>
public const string BodyName = "x-bodyName";

/// <summary>
/// Field: Examples Extension
/// </summary>
public const string ExamplesExtension = "x-examples";

/// <summary>
/// Field: version3_0_0
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion src/Microsoft.OpenApi/Models/OpenApiDocument.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -437,7 +437,7 @@
return null;
}

// Todo: Verify if we need to check to see if this external reference is actually targeted at this document.

Check warning on line 440 in src/Microsoft.OpenApi/Models/OpenApiDocument.cs

View workflow job for this annotation

GitHub Actions / Build

Complete the task associated to this 'TODO' comment. (https://rules.sonarsource.com/csharp/RSPEC-1135)
if (useExternal)
{
if (this.Workspace == null)
Expand Down
12 changes: 11 additions & 1 deletion src/Microsoft.OpenApi/Models/OpenApiExample.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,16 @@ public OpenApiExample GetEffective(OpenApiDocument doc)
/// Serialize to OpenAPI V3 document without using reference.
/// </summary>
public void SerializeAsV3WithoutReference(IOpenApiWriter writer)
{
Serialize(writer, OpenApiSpecVersion.OpenApi3_0);
}

/// <summary>
/// Writes out existing examples in a mediatype object
/// </summary>
/// <param name="writer"></param>
/// <param name="version"></param>
public void Serialize(IOpenApiWriter writer, OpenApiSpecVersion version)
{
writer.WriteStartObject();

Expand All @@ -134,7 +144,7 @@ public void SerializeAsV3WithoutReference(IOpenApiWriter writer)
writer.WriteProperty(OpenApiConstants.ExternalValue, ExternalValue);

// extensions
writer.WriteExtensions(Extensions, OpenApiSpecVersion.OpenApi3_0);
writer.WriteExtensions(Extensions, version);

writer.WriteEndObject();
}
Expand Down
24 changes: 16 additions & 8 deletions src/Microsoft.OpenApi/Models/OpenApiParameter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Extensions;
using Microsoft.OpenApi.Interfaces;
Expand Down Expand Up @@ -204,14 +205,7 @@ public void SerializeAsV3(IOpenApiWriter writer)
/// <returns>OpenApiParameter</returns>
public OpenApiParameter GetEffective(OpenApiDocument doc)
{
if (this.Reference != null)
{
return doc.ResolveReferenceTo<OpenApiParameter>(this.Reference);
}
else
{
return this;
}
return Reference != null ? doc.ResolveReferenceTo<OpenApiParameter>(Reference) : this;
}

/// <summary>
Expand Down Expand Up @@ -394,6 +388,20 @@ public void SerializeAsV2WithoutReference(IOpenApiWriter writer)
}
}

//examples
if (Examples != null && Examples.Any())
{
writer.WritePropertyName(OpenApiConstants.ExamplesExtension);
writer.WriteStartObject();

foreach (var example in Examples)
{
writer.WritePropertyName(example.Key);
example.Value.Serialize(writer, OpenApiSpecVersion.OpenApi2_0);
}
writer.WriteEndObject();
}

// extensions
writer.WriteExtensions(extensionsClone, OpenApiSpecVersion.OpenApi2_0);

Expand Down
13 changes: 4 additions & 9 deletions src/Microsoft.OpenApi/Models/OpenApiRequestBody.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,14 +96,7 @@ public void SerializeAsV3(IOpenApiWriter writer)
/// <returns>OpenApiRequestBody</returns>
public OpenApiRequestBody GetEffective(OpenApiDocument doc)
{
if (this.Reference != null)
{
return doc.ResolveReferenceTo<OpenApiRequestBody>(this.Reference);
}
else
{
return this;
}
return Reference != null ? doc.ResolveReferenceTo<OpenApiRequestBody>(Reference) : this;
}

/// <summary>
Expand Down Expand Up @@ -153,6 +146,7 @@ internal OpenApiBodyParameter ConvertToBodyParameter()
// To allow round-tripping we use an extension to hold the name
Name = "body",
Schema = Content.Values.FirstOrDefault()?.Schema ?? new OpenApiSchema(),
Examples = Content.Values.FirstOrDefault()?.Examples,
Required = Required,
Extensions = Extensions.ToDictionary(static k => k.Key, static v => v.Value) // Clone extensions so we can remove the x-bodyName extensions from the output V2 model.
};
Expand Down Expand Up @@ -184,7 +178,8 @@ internal IEnumerable<OpenApiFormDataParameter> ConvertToFormDataParameters()
Description = property.Value.Description,
Name = property.Key,
Schema = property.Value,
Required = Content.First().Value.Schema.Required.Contains(property.Key)
Examples = Content.Values.FirstOrDefault()?.Examples,
Required = Content.First().Value.Schema.Required?.Contains(property.Key) ?? false
};
}
}
Expand Down
27 changes: 18 additions & 9 deletions src/Microsoft.OpenApi/Models/OpenApiResponse.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -101,14 +101,7 @@ public void SerializeAsV3(IOpenApiWriter writer)
/// <returns>OpenApiResponse</returns>
public OpenApiResponse GetEffective(OpenApiDocument doc)
{
if (this.Reference != null)
{
return doc.ResolveReferenceTo<OpenApiResponse>(this.Reference);
}
else
{
return this;
}
return Reference != null ? doc.ResolveReferenceTo<OpenApiResponse>(Reference) : this;
}

/// <summary>
Expand Down Expand Up @@ -201,6 +194,22 @@ public void SerializeAsV2WithoutReference(IOpenApiWriter writer)
writer.WriteEndObject();
}

if (Content.Values.Any(m => m.Examples != null && m.Examples.Any()))
{
writer.WritePropertyName(OpenApiConstants.ExamplesExtension);
writer.WriteStartObject();

foreach (var example in Content
.Where(mediaTypePair => mediaTypePair.Value.Examples != null && mediaTypePair.Value.Examples.Any())
.SelectMany(mediaTypePair => mediaTypePair.Value.Examples))
{
writer.WritePropertyName(example.Key);
example.Value.Serialize(writer, OpenApiSpecVersion.OpenApi2_0);
}

writer.WriteEndObject();
}

writer.WriteExtensions(mediatype.Value.Extensions, OpenApiSpecVersion.OpenApi2_0);

foreach (var key in mediatype.Value.Extensions.Keys)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.OpenApi\Microsoft.OpenApi.csproj" />
<ProjectReference Include="..\..\src\Microsoft.OpenApi.Readers\Microsoft.OpenApi.Readers.csproj" />
<ProjectReference Include="..\Microsoft.OpenApi.Tests\Microsoft.OpenApi.Tests.csproj" />
</ItemGroup>

</Project>
Loading
Loading