Skip to content

Commit

Permalink
Merge pull request #1538 from microsoft/mk/fix-v2-examples-serializat…
Browse files Browse the repository at this point in the history
…ion-bug

Fix: Examples serialization in a response object during v2->v3 upcasting or vice versa
  • Loading branch information
MaggieKimani1 authored Jan 26, 2024
2 parents 1316ee9 + 7882fdd commit c195b73
Show file tree
Hide file tree
Showing 22 changed files with 478 additions and 41 deletions.
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
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

0 comments on commit c195b73

Please sign in to comment.