Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
4015f13
Preserve examples in v2 files and write them out as extensions
MaggieKimani1 Dec 5, 2023
f503757
Clean up code; update tests and public API
MaggieKimani1 Dec 5, 2023
d275b57
Merge remote-tracking branch 'origin/release/2.0.0' into mk/fix-v2-ex…
MaggieKimani1 Dec 18, 2023
add6583
Add examples constant to temp storage keys
MaggieKimani1 Jan 10, 2024
4b9e776
Load "x-examples" as Examples; store in temp storage; retrieve and ap…
MaggieKimani1 Jan 10, 2024
97571dd
Add missing method to the writer interface
MaggieKimani1 Jan 10, 2024
9fb89b1
Remove whitespace
MaggieKimani1 Jan 10, 2024
a681e84
Default to an empty collection if examples is null
MaggieKimani1 Jan 10, 2024
f4a9fa8
Add a reference to the OpenApi.Tests project to access the string ext…
MaggieKimani1 Jan 10, 2024
ab69952
Add test to validate that a V2 doc with x-examples gets mapped to Med…
MaggieKimani1 Jan 10, 2024
4e4b515
Add unit tests
MaggieKimani1 Jan 11, 2024
3748ef1
Fix CodeQL warnings
MaggieKimani1 Jan 11, 2024
819e857
Map the sequence explicitly using 'Select'
MaggieKimani1 Jan 11, 2024
2f02d2f
Filter using 'Where'
MaggieKimani1 Jan 11, 2024
3b9cb1d
Make the Examples property auto-implemented and remove the backing field
MaggieKimani1 Jan 11, 2024
4537168
Update src/Microsoft.OpenApi.Readers/V2/OpenApiResponseDeserializer.cs
MaggieKimani1 Jan 11, 2024
ad85b87
- adds missing using
baywet Jan 11, 2024
6d6ee78
Use constant to define the Example extensions property for reuse; add…
MaggieKimani1 Jan 11, 2024
4217d40
Merge branch 'mk/fix-v2-examples-serialization' of https://github.com…
MaggieKimani1 Jan 11, 2024
63f5fca
Update API interface
MaggieKimani1 Jan 11, 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
57 changes: 50 additions & 7 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.
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.

using System;
using System.Collections.Generic;
using Json.Schema;
using Microsoft.OpenApi.Extensions;
Expand Down Expand Up @@ -29,6 +30,10 @@ internal static partial class OpenApiV2Deserializer
"examples",
LoadExamples
},
{
"x-examples",
LoadExamplesExtension
},
{
"schema",
(o, n) => n.Context.SetTempStorage(TempStorageKeys.ResponseSchema, LoadSchema(n), o)
Expand All @@ -38,7 +43,7 @@ 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), (o, p, n) => o.AddExtension(p, LoadExtension(p, n))}
};

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

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

foreach (var produce in produces)
{
Expand All @@ -85,20 +92,58 @@ 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 LoadExamplesExtension(OpenApiResponse response, 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 = valueNode.Value.CreateAny();
break;
case "externalValue":
example.ExternalValue = valueNode.Value.GetScalarValue();
break;
}
}

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

node.Context.SetTempStorage(TempStorageKeys.Examples, examples, response);
}

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 @@ -109,10 +154,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 @@ -142,6 +184,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 @@ -585,6 +585,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
8 changes: 7 additions & 1 deletion src/Microsoft.OpenApi/Models/OpenApiDocument.cs
Original file line number Diff line number Diff line change
Expand Up @@ -613,7 +613,13 @@ internal IOpenApiReferenceable ResolveReference(OpenApiReference reference, bool
}
}

///
/// <summary>
///
/// </summary>
/// <param name="pointer"></param>
/// <param name="options"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public JsonSchema FindSubschema(Json.Pointer.JsonPointer pointer, EvaluationOptions options)
{
throw new NotImplementedException();
Expand Down
25 changes: 16 additions & 9 deletions src/Microsoft.OpenApi/Models/OpenApiParameter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Linq;
using Json.Schema;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Extensions;
Expand Down Expand Up @@ -224,14 +224,7 @@ private void SerializeInternal(IOpenApiWriter writer, Action<IOpenApiWriter, IOp
/// <returns>OpenApiParameter</returns>
public OpenApiParameter GetEffective(OpenApiDocument doc)
{
if (Reference != null)
{
return doc.ResolveReferenceTo<OpenApiParameter>(Reference);
}
else
{
return this;
}
return Reference != null ? doc.ResolveReferenceTo<OpenApiParameter>(Reference) : this;
}

/// <summary>
Expand Down Expand Up @@ -433,6 +426,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);
writer.WriteV2Examples(writer, example.Value, OpenApiSpecVersion.OpenApi2_0);
}
writer.WriteEndObject();
}

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

Expand Down
26 changes: 6 additions & 20 deletions src/Microsoft.OpenApi/Models/OpenApiRequestBody.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 @@ -113,14 +113,7 @@ private void SerializeInternal(IOpenApiWriter writer, Action<IOpenApiWriter, IOp
/// <returns>OpenApiRequestBody</returns>
public OpenApiRequestBody GetEffective(OpenApiDocument doc)
{
if (Reference != null)
{
return doc.ResolveReferenceTo<OpenApiRequestBody>(Reference);
}
else
{
return this;
}
return Reference != null ? doc.ResolveReferenceTo<OpenApiRequestBody>(Reference) : this;
}

/// <summary>
Expand Down Expand Up @@ -186,12 +179,13 @@ internal OpenApiBodyParameter ConvertToBodyParameter()
// To allow round-tripping we use an extension to hold the name
Name = "body",
Schema = Content.Values.FirstOrDefault()?.Schema ?? new JsonSchemaBuilder().Build(),
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.
};
if (bodyParameter.Extensions.ContainsKey(OpenApiConstants.BodyName))
if (bodyParameter.Extensions.TryGetValue(OpenApiConstants.BodyName, out var bodyParameterName))
{
var bodyName = bodyParameter.Extensions[OpenApiConstants.BodyName] as OpenApiAny;
var bodyName = bodyParameterName as OpenApiAny;
bodyParameter.Name = string.IsNullOrEmpty(bodyName?.Node.ToString()) ? "body" : bodyName?.Node.ToString();
bodyParameter.Extensions.Remove(OpenApiConstants.BodyName);
}
Expand All @@ -205,20 +199,12 @@ internal IEnumerable<OpenApiFormDataParameter> ConvertToFormDataParameters()

foreach (var property in Content.First().Value.Schema.GetProperties())
{
var paramSchema = property.Value;
if (paramSchema.GetType().Equals(SchemaValueType.String)
&& ("binary".Equals(paramSchema.GetFormat().Key, StringComparison.OrdinalIgnoreCase)
|| "base64".Equals(paramSchema.GetFormat().Key, StringComparison.OrdinalIgnoreCase)))
{
// JsonSchema is immutable so these can't be set
//paramSchema.Type("file");
//paramSchema.Format(null);
}
yield return new()
{
Description = property.Value.GetDescription(),
Name = property.Key,
Schema = property.Value,
Examples = Content.Values.FirstOrDefault()?.Examples,
Required = Content.First().Value.Schema.GetRequired()?.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
Expand Up @@ -118,14 +118,7 @@ private void SerializeInternal(IOpenApiWriter writer, Action<IOpenApiWriter, IOp
/// <returns>OpenApiResponse</returns>
public OpenApiResponse GetEffective(OpenApiDocument doc)
{
if (Reference != null)
{
return doc.ResolveReferenceTo<OpenApiResponse>(Reference);
}
else
{
return this;
}
return Reference != null ? doc.ResolveReferenceTo<OpenApiResponse>(Reference) : this;
}

/// <summary>
Expand All @@ -140,7 +133,7 @@ public virtual void SerializeAsV31WithoutReference(IOpenApiWriter writer)
/// <summary>
/// Serialize to OpenAPI V3 document without using reference.
/// </summary>
public virtual void SerializeAsV3WithoutReference(IOpenApiWriter writer)
public virtual void SerializeAsV3WithoutReference(IOpenApiWriter writer)
{
SerializeInternalWithoutReference(writer, OpenApiSpecVersion.OpenApi3_0,
(writer, element) => element.SerializeAsV3(writer));
Expand Down Expand Up @@ -231,6 +224,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);
writer.WriteV2Examples(writer, example.Value, OpenApiSpecVersion.OpenApi2_0);
}

writer.WriteEndObject();
}

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

foreach (var key in mediatype.Value.Extensions.Keys)
Expand Down
9 changes: 9 additions & 0 deletions src/Microsoft.OpenApi/Writers/IOpenApiWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using Json.Schema;
using Microsoft.OpenApi.Models;

namespace Microsoft.OpenApi.Writers
{
Expand Down Expand Up @@ -99,5 +100,13 @@ public interface IOpenApiWriter
/// <param name="reference"></param>
/// <param name="version"></param>
void WriteJsonSchemaReference(IOpenApiWriter writer, Uri reference, OpenApiSpecVersion version);

/// <summary>
/// Writes out existing examples in a mediatype object
/// </summary>
/// <param name="writer"></param>
/// <param name="example"></param>
/// <param name="version"></param>
void WriteV2Examples(IOpenApiWriter writer, OpenApiExample example, OpenApiSpecVersion version);
}
}
23 changes: 23 additions & 0 deletions src/Microsoft.OpenApi/Writers/OpenApiWriterBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,29 @@ public void WriteJsonSchemaReference(IOpenApiWriter writer, Uri reference, OpenA
this.WriteProperty(OpenApiConstants.DollarRef, referenceItem);
WriteEndObject();
}

/// <inheritdoc/>
public void WriteV2Examples(IOpenApiWriter writer, OpenApiExample example, OpenApiSpecVersion version)
{
writer.WriteStartObject();

// summary
writer.WriteProperty(OpenApiConstants.Summary, example.Summary);

// description
writer.WriteProperty(OpenApiConstants.Description, example.Description);

// value
writer.WriteOptionalObject(OpenApiConstants.Value, example.Value, (w, v) => w.WriteAny(v));

// externalValue
writer.WriteProperty(OpenApiConstants.ExternalValue, example.ExternalValue);

// extensions
writer.WriteExtensions(example.Extensions, version);

writer.WriteEndObject();
}
}

internal class FindJsonSchemaRefs : OpenApiVisitorBase
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,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