Skip to content

Commit

Permalink
Fix $ref serialization errors for requestBodies and responses (#1033)
Browse files Browse the repository at this point in the history
* Do not add empty consumes array if RequestBody content contains no elements

* Add requestbody references as parameter references in v2 serialization

* Add consumes and produces properties when we have reference request bodies and responses respectively during v2 serialization

* Update src/Microsoft.OpenApi/Models/OpenApiOperation.cs

Co-authored-by: Vincent Biret <vibiret@microsoft.com>

* Apply suggestions from code review

Co-authored-by: Vincent Biret <vibiret@microsoft.com>

* Address code review comments

Co-authored-by: Vincent Biret <vibiret@microsoft.com>
  • Loading branch information
millicentachieng and baywet authored Oct 6, 2022
1 parent 680ee90 commit 76321be
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 76 deletions.
15 changes: 13 additions & 2 deletions src/Microsoft.OpenApi/Models/OpenApiDocument.cs
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ public void SerializeAsV2(IOpenApiWriter writer)
// paths
writer.WriteRequiredObject(OpenApiConstants.Paths, Paths, (w, p) => p.SerializeAsV2(w));

// If references have been inlined we don't need the to render the components section
// If references have been inlined we don't need to render the components section
// however if they have cycles, then we will need a component rendered
if (writer.GetSettings().InlineLocalReferences)
{
Expand Down Expand Up @@ -208,9 +208,20 @@ public void SerializeAsV2(IOpenApiWriter writer)
});
}
// parameters
var parameters = Components?.Parameters != null
? new Dictionary<string, OpenApiParameter>(Components.Parameters)
: new Dictionary<string, OpenApiParameter>();

if (Components?.RequestBodies != null)
{
foreach (var requestBody in Components.RequestBodies.Where(b => !parameters.ContainsKey(b.Key)))
{
parameters.Add(requestBody.Key, requestBody.Value.ConvertToBodyParameter());
}
}
writer.WriteOptionalMap(
OpenApiConstants.Parameters,
Components?.Parameters,
parameters,
(w, key, component) =>
{
if (component.Reference != null &&
Expand Down
88 changes: 38 additions & 50 deletions src/Microsoft.OpenApi/Models/OpenApiOperation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ public void SerializeAsV2(IOpenApiWriter writer)
// operationId
writer.WriteProperty(OpenApiConstants.OperationId, OperationId);

IList<OpenApiParameter> parameters;
List<OpenApiParameter> parameters;
if (Parameters == null)
{
parameters = new List<OpenApiParameter>();
Expand All @@ -237,70 +237,58 @@ public void SerializeAsV2(IOpenApiWriter writer)
if (RequestBody != null)
{
// consumes
writer.WritePropertyName(OpenApiConstants.Consumes);
writer.WriteStartArray();
var consumes = RequestBody.Content.Keys.Distinct().ToList();
foreach (var mediaType in consumes)
if (consumes.Any())
{
writer.WriteValue(mediaType);
}

writer.WriteEndArray();

// This is form data. We need to split the request body into multiple parameters.
if (consumes.Contains("application/x-www-form-urlencoded") ||
consumes.Contains("multipart/form-data"))
{
foreach (var property in RequestBody.Content.First().Value.Schema.Properties)
// This is form data. We need to split the request body into multiple parameters.
if (consumes.Contains("application/x-www-form-urlencoded") ||
consumes.Contains("multipart/form-data"))
{
var paramSchema = property.Value;
if ("string".Equals(paramSchema.Type, StringComparison.OrdinalIgnoreCase)
&& ("binary".Equals(paramSchema.Format, StringComparison.OrdinalIgnoreCase)
|| "base64".Equals(paramSchema.Format, StringComparison.OrdinalIgnoreCase)))
{
paramSchema.Type = "file";
paramSchema.Format = null;
}
parameters.Add(
new OpenApiFormDataParameter
{
Description = property.Value.Description,
Name = property.Key,
Schema = property.Value,
Required = RequestBody.Content.First().Value.Schema.Required.Contains(property.Key)

});
parameters.AddRange(RequestBody.ConvertToFormDataParameters());
}
else
{
parameters.Add(RequestBody.ConvertToBodyParameter());
}
}
else
else if (RequestBody.Reference != null)
{
var content = RequestBody.Content.Values.FirstOrDefault();
parameters.Add(
new OpenApiParameter
{
UnresolvedReference = true,
Reference = RequestBody.Reference
});

var bodyParameter = new OpenApiBodyParameter
if (RequestBody.Reference.HostDocument != null)
{
Description = RequestBody.Description,
// V2 spec actually allows the body to have custom name.
// To allow round-tripping we use an extension to hold the name
Name = "body",
Schema = content?.Schema ?? new OpenApiSchema(),
Required = RequestBody.Required,
Extensions = RequestBody.Extensions.ToDictionary(k => k.Key, v => v.Value) // Clone extensions so we can remove the x-bodyName extensions from the output V2 model.
};

if (bodyParameter.Extensions.ContainsKey(OpenApiConstants.BodyName))
var effectiveRequestBody = RequestBody.GetEffective(RequestBody.Reference.HostDocument);
if (effectiveRequestBody != null)
consumes = effectiveRequestBody.Content.Keys.Distinct().ToList();
}
}

if (consumes.Any())
{
writer.WritePropertyName(OpenApiConstants.Consumes);
writer.WriteStartArray();
foreach (var mediaType in consumes)
{
bodyParameter.Name = (RequestBody.Extensions[OpenApiConstants.BodyName] as OpenApiString)?.Value ?? "body";
bodyParameter.Extensions.Remove(OpenApiConstants.BodyName);
writer.WriteValue(mediaType);
}

parameters.Add(bodyParameter);
writer.WriteEndArray();
}
}

if (Responses != null)
{
var produces = Responses.Where(r => r.Value.Content != null)
.SelectMany(r => r.Value.Content?.Keys)
var produces = Responses
.Where(static r => r.Value.Content != null)
.SelectMany(static r => r.Value.Content?.Keys)
.Concat(
Responses
.Where(static r => r.Value.Reference != null && r.Value.Reference.HostDocument != null)
.SelectMany(static r => r.Value.GetEffective(r.Value.Reference.HostDocument)?.Content?.Keys))
.Distinct()
.ToList();

Expand Down
34 changes: 10 additions & 24 deletions src/Microsoft.OpenApi/Models/OpenApiReference.cs
Original file line number Diff line number Diff line change
Expand Up @@ -214,31 +214,17 @@ private string GetExternalReferenceV2()

private string GetReferenceTypeNameAsV2(ReferenceType type)
{
switch (type)
return type switch
{
case ReferenceType.Schema:
return OpenApiConstants.Definitions;

case ReferenceType.Parameter:
return OpenApiConstants.Parameters;

case ReferenceType.Response:
return OpenApiConstants.Responses;

case ReferenceType.Header:
return OpenApiConstants.Headers;

case ReferenceType.Tag:
return OpenApiConstants.Tags;

case ReferenceType.SecurityScheme:
return OpenApiConstants.SecurityDefinitions;

default:
// If the reference type is not supported in V2, simply return null
// to indicate that the reference is not pointing to any object.
return null;
}
ReferenceType.Schema => OpenApiConstants.Definitions,
ReferenceType.Parameter or ReferenceType.RequestBody => OpenApiConstants.Parameters,
ReferenceType.Response => OpenApiConstants.Responses,
ReferenceType.Header => OpenApiConstants.Headers,
ReferenceType.Tag => OpenApiConstants.Tags,
ReferenceType.SecurityScheme => OpenApiConstants.SecurityDefinitions,
_ => null,// If the reference type is not supported in V2, simply return null
// to indicate that the reference is not pointing to any object.
};
}
}
}
47 changes: 47 additions & 0 deletions src/Microsoft.OpenApi/Models/OpenApiRequestBody.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Interfaces;
using Microsoft.OpenApi.Writers;
Expand Down Expand Up @@ -144,5 +146,50 @@ public void SerializeAsV2WithoutReference(IOpenApiWriter writer)
{
// RequestBody object does not exist in V2.
}

internal OpenApiBodyParameter ConvertToBodyParameter()
{
var bodyParameter = new OpenApiBodyParameter
{
Description = Description,
// V2 spec actually allows the body to have custom name.
// To allow round-tripping we use an extension to hold the name
Name = "body",
Schema = Content.Values.FirstOrDefault()?.Schema ?? new OpenApiSchema(),
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))
{
bodyParameter.Name = (Extensions[OpenApiConstants.BodyName] as OpenApiString)?.Value ?? "body";
bodyParameter.Extensions.Remove(OpenApiConstants.BodyName);
}
return bodyParameter;
}

internal IEnumerable<OpenApiFormDataParameter> ConvertToFormDataParameters()
{
if (Content == null || !Content.Any())
yield break;

foreach (var property in Content.First().Value.Schema.Properties)
{
var paramSchema = property.Value;
if ("string".Equals(paramSchema.Type, StringComparison.OrdinalIgnoreCase)
&& ("binary".Equals(paramSchema.Format, StringComparison.OrdinalIgnoreCase)
|| "base64".Equals(paramSchema.Format, StringComparison.OrdinalIgnoreCase)))
{
paramSchema.Type = "file";
paramSchema.Format = null;
}
yield return new OpenApiFormDataParameter
{
Description = property.Value.Description,
Name = property.Key,
Schema = property.Value,
Required = Content.First().Value.Schema.Required.Contains(property.Key)
};
}
}
}
}

0 comments on commit 76321be

Please sign in to comment.