From 6e555116b4d3b017332fd90511ac0199716324c7 Mon Sep 17 00:00:00 2001 From: Millicent Achieng Date: Thu, 6 Oct 2022 04:41:34 +0300 Subject: [PATCH 1/6] Do not add empty consumes array if RequestBody content contains no elements --- .../Models/OpenApiOperation.cs | 96 ++++++++++--------- 1 file changed, 49 insertions(+), 47 deletions(-) diff --git a/src/Microsoft.OpenApi/Models/OpenApiOperation.cs b/src/Microsoft.OpenApi/Models/OpenApiOperation.cs index 7983a243e..d369d3193 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiOperation.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiOperation.cs @@ -237,63 +237,65 @@ 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.WritePropertyName(OpenApiConstants.Consumes); + writer.WriteStartArray(); + foreach (var mediaType in consumes) + { + writer.WriteValue(mediaType); + } - writer.WriteEndArray(); + 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))) + foreach (var property in RequestBody.Content.First().Value.Schema.Properties) { - paramSchema.Type = "file"; - paramSchema.Format = null; - } - parameters.Add( - new OpenApiFormDataParameter + var paramSchema = property.Value; + if ("string".Equals(paramSchema.Type, StringComparison.OrdinalIgnoreCase) + && ("binary".Equals(paramSchema.Format, StringComparison.OrdinalIgnoreCase) + || "base64".Equals(paramSchema.Format, StringComparison.OrdinalIgnoreCase))) { - Description = property.Value.Description, - Name = property.Key, - Schema = property.Value, - Required = RequestBody.Content.First().Value.Schema.Required.Contains(property.Key) - - }); + 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) + }); + } } - } - else - { - var content = RequestBody.Content.Values.FirstOrDefault(); - - var bodyParameter = new OpenApiBodyParameter + else { - 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)) - { - bodyParameter.Name = (RequestBody.Extensions[OpenApiConstants.BodyName] as OpenApiString)?.Value ?? "body"; - bodyParameter.Extensions.Remove(OpenApiConstants.BodyName); + var content = RequestBody.Content.Values.FirstOrDefault(); + + var bodyParameter = new OpenApiBodyParameter + { + 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)) + { + bodyParameter.Name = (RequestBody.Extensions[OpenApiConstants.BodyName] as OpenApiString)?.Value ?? "body"; + bodyParameter.Extensions.Remove(OpenApiConstants.BodyName); + } + + parameters.Add(bodyParameter); } - - parameters.Add(bodyParameter); } } From f2da65f35231a3869ab7caf8fdfd9d019072af68 Mon Sep 17 00:00:00 2001 From: Millicent Achieng Date: Thu, 6 Oct 2022 07:40:19 +0300 Subject: [PATCH 2/6] Add requestbody references as parameter references in v2 serialization --- .../Models/OpenApiDocument.cs | 12 +++++-- .../Models/OpenApiOperation.cs | 30 ++++++---------- .../Models/OpenApiReference.cs | 34 ++++++------------- .../Models/OpenApiRequestBody.cs | 21 ++++++++++++ 4 files changed, 51 insertions(+), 46 deletions(-) diff --git a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs index 93d88b310..fb57a9774 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs @@ -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) { @@ -208,9 +208,17 @@ public void SerializeAsV2(IOpenApiWriter writer) }); } // parameters + IDictionary parameters = Components?.Parameters; + if (Components?.RequestBodies != null) + { + foreach (var requestBody in Components.RequestBodies) + { + parameters.Add(requestBody.Key, requestBody.Value.ConvertToBodyParameter()); + } + } writer.WriteOptionalMap( OpenApiConstants.Parameters, - Components?.Parameters, + parameters, (w, key, component) => { if (component.Reference != null && diff --git a/src/Microsoft.OpenApi/Models/OpenApiOperation.cs b/src/Microsoft.OpenApi/Models/OpenApiOperation.cs index d369d3193..c81ce764e 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiOperation.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiOperation.cs @@ -275,28 +275,18 @@ public void SerializeAsV2(IOpenApiWriter writer) } else { - var content = RequestBody.Content.Values.FirstOrDefault(); - - var bodyParameter = new OpenApiBodyParameter - { - 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)) - { - bodyParameter.Name = (RequestBody.Extensions[OpenApiConstants.BodyName] as OpenApiString)?.Value ?? "body"; - bodyParameter.Extensions.Remove(OpenApiConstants.BodyName); - } - - parameters.Add(bodyParameter); + parameters.Add(RequestBody.ConvertToBodyParameter()); } } + else if (RequestBody.Reference != null) + { + parameters.Add( + new OpenApiParameter + { + UnresolvedReference = true, + Reference = RequestBody.Reference + }); + } } if (Responses != null) diff --git a/src/Microsoft.OpenApi/Models/OpenApiReference.cs b/src/Microsoft.OpenApi/Models/OpenApiReference.cs index 31cc5a6e8..ecc643dc3 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiReference.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiReference.cs @@ -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. + }; } } } diff --git a/src/Microsoft.OpenApi/Models/OpenApiRequestBody.cs b/src/Microsoft.OpenApi/Models/OpenApiRequestBody.cs index 9016fd7a3..67092fef6 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiRequestBody.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiRequestBody.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. using System.Collections.Generic; +using System.Linq; using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Interfaces; using Microsoft.OpenApi.Writers; @@ -144,5 +145,25 @@ 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(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)) + { + bodyParameter.Name = (Extensions[OpenApiConstants.BodyName] as OpenApiString)?.Value ?? "body"; + bodyParameter.Extensions.Remove(OpenApiConstants.BodyName); + } + return bodyParameter; + } } } From 62e4999646f331d21aef484c2dcf80705db2f7eb Mon Sep 17 00:00:00 2001 From: Millicent Achieng Date: Thu, 6 Oct 2022 09:14:37 +0300 Subject: [PATCH 3/6] Add consumes and produces properties when we have reference request bodies and responses respectively during v2 serialization --- .../Models/OpenApiOperation.cs | 56 ++++++++----------- .../Models/OpenApiRequestBody.cs | 23 ++++++++ 2 files changed, 47 insertions(+), 32 deletions(-) diff --git a/src/Microsoft.OpenApi/Models/OpenApiOperation.cs b/src/Microsoft.OpenApi/Models/OpenApiOperation.cs index c81ce764e..84d5bb2ea 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiOperation.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiOperation.cs @@ -224,7 +224,7 @@ public void SerializeAsV2(IOpenApiWriter writer) // operationId writer.WriteProperty(OpenApiConstants.OperationId, OperationId); - IList parameters; + List parameters; if (Parameters == null) { parameters = new List(); @@ -240,38 +240,11 @@ public void SerializeAsV2(IOpenApiWriter writer) var consumes = RequestBody.Content.Keys.Distinct().ToList(); if (consumes.Any()) { - writer.WritePropertyName(OpenApiConstants.Consumes); - writer.WriteStartArray(); - foreach (var mediaType in consumes) - { - 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) - { - 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 { @@ -281,18 +254,37 @@ public void SerializeAsV2(IOpenApiWriter writer) else if (RequestBody.Reference != null) { parameters.Add( - new OpenApiParameter - { + new OpenApiParameter + { UnresolvedReference = true, Reference = RequestBody.Reference }); + + if (RequestBody.Reference.HostDocument != null) + consumes = RequestBody.GetEffective(RequestBody.Reference.HostDocument)?.Content.Keys.Distinct().ToList(); + } + + if (consumes.Any()) + { + writer.WritePropertyName(OpenApiConstants.Consumes); + writer.WriteStartArray(); + foreach (var mediaType in consumes) + { + writer.WriteValue(mediaType); + } + writer.WriteEndArray(); } } if (Responses != null) { - var produces = Responses.Where(r => r.Value.Content != null) + var produces = Responses + .Where(r => r.Value.Content != null) .SelectMany(r => r.Value.Content?.Keys) + .Concat( + Responses + .Where(r => r.Value.Reference != null && r.Value.Reference.HostDocument != null) + .SelectMany(r => r.Value.GetEffective(r.Value.Reference.HostDocument)?.Content?.Keys)) .Distinct() .ToList(); diff --git a/src/Microsoft.OpenApi/Models/OpenApiRequestBody.cs b/src/Microsoft.OpenApi/Models/OpenApiRequestBody.cs index 67092fef6..8ed763637 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiRequestBody.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiRequestBody.cs @@ -1,6 +1,7 @@ // 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; @@ -165,5 +166,27 @@ internal OpenApiBodyParameter ConvertToBodyParameter() } return bodyParameter; } + + internal IEnumerable ConvertToFormDataParameters() + { + 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) + }; + } + } } } From e5ed1a6cbd07001a57a3ef612cf29af4e1440ab9 Mon Sep 17 00:00:00 2001 From: Millicent Achieng Date: Thu, 6 Oct 2022 17:05:44 +0300 Subject: [PATCH 4/6] Update src/Microsoft.OpenApi/Models/OpenApiOperation.cs Co-authored-by: Vincent Biret --- src/Microsoft.OpenApi/Models/OpenApiOperation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.OpenApi/Models/OpenApiOperation.cs b/src/Microsoft.OpenApi/Models/OpenApiOperation.cs index 84d5bb2ea..5beecc065 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiOperation.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiOperation.cs @@ -283,7 +283,7 @@ public void SerializeAsV2(IOpenApiWriter writer) .SelectMany(r => r.Value.Content?.Keys) .Concat( Responses - .Where(r => r.Value.Reference != null && r.Value.Reference.HostDocument != null) + .Where(static r => r.Value.Reference != null && r.Value.Reference.HostDocument != null) .SelectMany(r => r.Value.GetEffective(r.Value.Reference.HostDocument)?.Content?.Keys)) .Distinct() .ToList(); From bd577c6f38e9b878293a748ee6551bb6aa7b856e Mon Sep 17 00:00:00 2001 From: Millicent Achieng Date: Thu, 6 Oct 2022 17:07:44 +0300 Subject: [PATCH 5/6] Apply suggestions from code review Co-authored-by: Vincent Biret --- src/Microsoft.OpenApi/Models/OpenApiOperation.cs | 6 +++--- src/Microsoft.OpenApi/Models/OpenApiRequestBody.cs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.OpenApi/Models/OpenApiOperation.cs b/src/Microsoft.OpenApi/Models/OpenApiOperation.cs index 5beecc065..abfda31bc 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiOperation.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiOperation.cs @@ -279,12 +279,12 @@ public void SerializeAsV2(IOpenApiWriter writer) if (Responses != null) { var produces = Responses - .Where(r => r.Value.Content != null) - .SelectMany(r => r.Value.Content?.Keys) + .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(r => r.Value.GetEffective(r.Value.Reference.HostDocument)?.Content?.Keys)) + .SelectMany(static r => r.Value.GetEffective(r.Value.Reference.HostDocument)?.Content?.Keys)) .Distinct() .ToList(); diff --git a/src/Microsoft.OpenApi/Models/OpenApiRequestBody.cs b/src/Microsoft.OpenApi/Models/OpenApiRequestBody.cs index 8ed763637..8c82c54d3 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiRequestBody.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiRequestBody.cs @@ -157,7 +157,7 @@ internal OpenApiBodyParameter ConvertToBodyParameter() Name = "body", Schema = Content.Values.FirstOrDefault()?.Schema ?? new OpenApiSchema(), Required = Required, - Extensions = Extensions.ToDictionary(k => k.Key, v => v.Value) // Clone extensions so we can remove the x-bodyName extensions from the output V2 model. + 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)) { From 1de587cc1f5cfc664cdefe4e6b9969e3a68f1705 Mon Sep 17 00:00:00 2001 From: Millicent Achieng Date: Thu, 6 Oct 2022 18:36:01 +0300 Subject: [PATCH 6/6] Address code review comments --- src/Microsoft.OpenApi/Models/OpenApiDocument.cs | 7 +++++-- src/Microsoft.OpenApi/Models/OpenApiOperation.cs | 6 +++++- src/Microsoft.OpenApi/Models/OpenApiRequestBody.cs | 7 +++++-- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs index fb57a9774..5177e4f45 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs @@ -208,10 +208,13 @@ public void SerializeAsV2(IOpenApiWriter writer) }); } // parameters - IDictionary parameters = Components?.Parameters; + var parameters = Components?.Parameters != null + ? new Dictionary(Components.Parameters) + : new Dictionary(); + if (Components?.RequestBodies != null) { - foreach (var requestBody in Components.RequestBodies) + foreach (var requestBody in Components.RequestBodies.Where(b => !parameters.ContainsKey(b.Key))) { parameters.Add(requestBody.Key, requestBody.Value.ConvertToBodyParameter()); } diff --git a/src/Microsoft.OpenApi/Models/OpenApiOperation.cs b/src/Microsoft.OpenApi/Models/OpenApiOperation.cs index abfda31bc..d047b9cb6 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiOperation.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiOperation.cs @@ -261,7 +261,11 @@ public void SerializeAsV2(IOpenApiWriter writer) }); if (RequestBody.Reference.HostDocument != null) - consumes = RequestBody.GetEffective(RequestBody.Reference.HostDocument)?.Content.Keys.Distinct().ToList(); + { + var effectiveRequestBody = RequestBody.GetEffective(RequestBody.Reference.HostDocument); + if (effectiveRequestBody != null) + consumes = effectiveRequestBody.Content.Keys.Distinct().ToList(); + } } if (consumes.Any()) diff --git a/src/Microsoft.OpenApi/Models/OpenApiRequestBody.cs b/src/Microsoft.OpenApi/Models/OpenApiRequestBody.cs index 8c82c54d3..70f1f742a 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiRequestBody.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiRequestBody.cs @@ -169,6 +169,9 @@ internal OpenApiBodyParameter ConvertToBodyParameter() internal IEnumerable ConvertToFormDataParameters() { + if (Content == null || !Content.Any()) + yield break; + foreach (var property in Content.First().Value.Schema.Properties) { var paramSchema = property.Value; @@ -180,11 +183,11 @@ internal IEnumerable ConvertToFormDataParameters() 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) + Required = Content.First().Value.Schema.Required.Contains(property.Key) }; } }