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 $ref serialization errors for requestBodies and responses #1033

Merged
merged 6 commits into from
Oct 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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)
};
}
}
}
}