Skip to content

Commit adf3042

Browse files
authored
multipart/form-data in OpenAPI v2 Rendering Workaround (Interim) (#367)
1 parent 4256062 commit adf3042

File tree

8 files changed

+135
-17
lines changed

8 files changed

+135
-17
lines changed

samples/Microsoft.Azure.WebJobs.Extensions.OpenApi.FunctionApp.InProc/Startup.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ public class Startup : FunctionsStartup
1111
{
1212
public override void Configure(IFunctionsHostBuilder builder)
1313
{
14-
builder.Services.AddSingleton<Fixture>();
14+
var fixture = new Fixture();
15+
builder.Services.AddSingleton(fixture);
1516
}
1617
}
1718
}

src/Microsoft.Azure.WebJobs.Extensions.OpenApi.Core/Extensions/DocumentHelperExtensions.cs

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,14 +146,69 @@ public static OpenApiOperation GetOpenApiOperation(this IDocumentHelper helper,
146146
/// <param name="trigger"><see cref="HttpTriggerAttribute"/> instance.</param>
147147
/// <param name="namingStrategy"><see cref="NamingStrategy"/> instance to create the JSON schema from .NET Types.</param>
148148
/// <param name="collection"><see cref="VisitorCollection"/> instance to process parameters.</param>
149+
/// <param name="version">OpenAPI spec version.</param>
149150
/// <returns>List of <see cref="OpenApiParameter"/> instance.</returns>
150-
public static List<OpenApiParameter> GetOpenApiParameters(this IDocumentHelper helper, MethodInfo element, HttpTriggerAttribute trigger, NamingStrategy namingStrategy, VisitorCollection collection)
151+
public static List<OpenApiParameter> GetOpenApiParameters(this IDocumentHelper helper, MethodInfo element, HttpTriggerAttribute trigger, NamingStrategy namingStrategy, VisitorCollection collection, OpenApiVersionType version)
151152
{
152153
var parameters = element.GetCustomAttributes<OpenApiParameterAttribute>(inherit: false)
153154
.Where(p => p.Deprecated == false)
154155
.Select(p => p.ToOpenApiParameter(namingStrategy, collection))
155156
.ToList();
156157

158+
// This is the interim solution to resolve:
159+
// https://github.com/Azure/azure-functions-openapi-extension/issues/365
160+
//
161+
// It will be removed when the following issue is resolved:
162+
// https://github.com/microsoft/OpenAPI.NET/issues/747
163+
if (version == OpenApiVersionType.V3)
164+
{
165+
return parameters;
166+
}
167+
168+
var attributes = element.GetCustomAttributes<OpenApiRequestBodyAttribute>(inherit: false);
169+
if (!attributes.Any())
170+
{
171+
return parameters;
172+
}
173+
174+
var contents = attributes.Where(p => p.Deprecated == false)
175+
.Where(p => p.ContentType == "application/x-www-form-urlencoded" || p.ContentType == "multipart/form-data")
176+
.Select(p => p.ToOpenApiMediaType(namingStrategy, collection, version));
177+
if (!contents.Any())
178+
{
179+
return parameters;
180+
}
181+
182+
var @ref = contents.First().Schema.Reference;
183+
var schemas = helper.GetOpenApiSchemas(new[] { element }.ToList(), namingStrategy, collection);
184+
var schema = schemas.SingleOrDefault(p => p.Key == @ref.Id);
185+
if (schema.IsNullOrDefault())
186+
{
187+
return parameters;
188+
}
189+
190+
var properties = schema.Value.Properties;
191+
foreach (var property in properties)
192+
{
193+
var value = property.Value;
194+
if ((value.Type == "string" && value.Format == "binary") || (value.Type == "string" && value.Format == "base64"))
195+
{
196+
value.Type = "file";
197+
value.Format = null;
198+
}
199+
200+
var parameter = new OpenApiParameter()
201+
{
202+
Name = property.Key,
203+
Description = $"[formData]{value.Description}",
204+
Required = bool.TryParse($"{value.Required}", out var result) ? result : false,
205+
Deprecated = value.Deprecated,
206+
Schema = value,
207+
};
208+
209+
parameters.Add(parameter);
210+
}
211+
157212
// // TODO: Should this be forcibly provided?
158213
// // This needs to be provided separately.
159214
// if (trigger.AuthLevel != AuthorizationLevel.Anonymous)

src/Microsoft.Azure.WebJobs.Extensions.OpenApi.Core/Extensions/TypeExtensions.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,6 @@ public static List<IOpenApiAny> ToOpenApiIntegerCollection(this Type type)
290290
.ToList();
291291
}
292292

293-
294293
return null;
295294
}
296295

@@ -537,7 +536,6 @@ public static string GetOpenApiTypeName(this Type type, NamingStrategy namingStr
537536
return name;
538537
}
539538

540-
541539
/// <summary>
542540
/// Gets the sub type of the given <see cref="Type"/>.
543541
/// </summary>

src/Microsoft.Azure.WebJobs.Extensions.OpenApi.Core/Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
<PackageReference Include="System.IO.FileSystem.Primitives" Version="4.3.0" />
3333
<PackageReference Include="System.Runtime.Handles" Version="4.3.0" />
3434
<PackageReference Include="System.Runtime.InteropServices" Version="4.3.0" />
35+
<PackageReference Include="YamlDotNet" Version="11.2.1" />
3536
</ItemGroup>
3637

3738
<ItemGroup>

src/Microsoft.Azure.WebJobs.Extensions.OpenApi.Core/Visitors/ByteArrayTypeVisitor.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public override bool IsVisitable(Type type)
3030
/// <inheritdoc />
3131
public override void Visit(IAcceptor acceptor, KeyValuePair<string, Type> type, NamingStrategy namingStrategy, params Attribute[] attributes)
3232
{
33-
this.Visit(acceptor, name: type.Key, title: null, dataType: "string", dataFormat: "base64", attributes: attributes);
33+
this.Visit(acceptor, name: type.Key, title: null, dataType: "string", dataFormat: "binary", attributes: attributes);
3434
}
3535

3636
/// <inheritdoc />
@@ -44,7 +44,7 @@ public override bool IsParameterVisitable(Type type)
4444
/// <inheritdoc />
4545
public override OpenApiSchema ParameterVisit(Type type, NamingStrategy namingStrategy)
4646
{
47-
return this.ParameterVisit(dataType: "string", dataFormat: "base64");
47+
return this.ParameterVisit(dataType: "string", dataFormat: "binary");
4848
}
4949

5050
/// <inheritdoc />
@@ -58,7 +58,7 @@ public override bool IsPayloadVisitable(Type type)
5858
/// <inheritdoc />
5959
public override OpenApiSchema PayloadVisit(Type type, NamingStrategy namingStrategy)
6060
{
61-
return this.PayloadVisit(dataType: "string", dataFormat: "base64");
61+
return this.PayloadVisit(dataType: "string", dataFormat: "binary");
6262
}
6363
}
6464
}

src/Microsoft.Azure.WebJobs.Extensions.OpenApi/Document.cs

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Collections.Generic;
2+
using System.Dynamic;
23
using System.IO;
34
using System.Linq;
45
using System.Reflection;
@@ -11,8 +12,13 @@
1112
using Microsoft.OpenApi;
1213
using Microsoft.OpenApi.Models;
1314

15+
using Newtonsoft.Json;
16+
using Newtonsoft.Json.Converters;
17+
using Newtonsoft.Json.Linq;
1418
using Newtonsoft.Json.Serialization;
1519

20+
using YamlDotNet.Serialization;
21+
1622
namespace Microsoft.Azure.WebJobs.Extensions.OpenApi
1723
{
1824
/// <summary>
@@ -38,7 +44,6 @@ public Document(IDocumentHelper helper)
3844
public Document(OpenApiDocument openApiDocument)
3945
{
4046
this.OpenApiDocument = openApiDocument;
41-
4247
}
4348

4449
/// <inheritdoc />
@@ -168,7 +173,7 @@ public IDocument Build(Assembly assembly, OpenApiVersionType version = OpenApiVe
168173
}
169174

170175
operation.Security = this._helper.GetOpenApiSecurityRequirement(method, this._strategy);
171-
operation.Parameters = this._helper.GetOpenApiParameters(method, trigger, this._strategy, this._collection);
176+
operation.Parameters = this._helper.GetOpenApiParameters(method, trigger, this._strategy, this._collection, version);
172177
operation.RequestBody = this._helper.GetOpenApiRequestBody(method, this._strategy, this._collection, version);
173178
operation.Responses = this._helper.GetOpenApiResponses(method, this._strategy, this._collection, version);
174179

@@ -203,12 +208,70 @@ public async Task<string> RenderAsync(OpenApiSpecVersion version, OpenApiFormat
203208

204209
private string Render(OpenApiSpecVersion version, OpenApiFormat format)
205210
{
211+
//var serialised = default(string);
212+
//using (var sw = new StringWriter())
213+
//{
214+
// this.OpenApiDocument.Serialise(sw, version, format);
215+
// serialised = sw.ToString();
216+
//}
217+
218+
//return serialised;
219+
220+
// This is the interim solution to resolve:
221+
// https://github.com/Azure/azure-functions-openapi-extension/issues/365
222+
//
223+
// It will be removed when the following issue is resolved:
224+
// https://github.com/microsoft/OpenAPI.NET/issues/747
225+
var jserialised = default(string);
226+
using (var sw = new StringWriter())
227+
{
228+
this.OpenApiDocument.Serialise(sw, version, OpenApiFormat.Json);
229+
jserialised = sw.ToString();
230+
}
231+
232+
var yserialised = default(string);
206233
using (var sw = new StringWriter())
207234
{
208-
this.OpenApiDocument.Serialise(sw, version, format);
235+
this.OpenApiDocument.Serialise(sw, version, OpenApiFormat.Yaml);
236+
yserialised = sw.ToString();
237+
}
209238

210-
return sw.ToString();
239+
if (version != OpenApiSpecVersion.OpenApi2_0)
240+
{
241+
return format == OpenApiFormat.Json ? jserialised : yserialised;
211242
}
243+
244+
var jo = JsonConvert.DeserializeObject<JObject>(jserialised);
245+
var jts = jo.DescendantsAndSelf()
246+
.Where(p => p.Type == JTokenType.Property && (p as JProperty).Name == "parameters")
247+
.SelectMany(p => p.Values<JArray>().SelectMany(q => q.Children<JObject>()))
248+
.Where(p => p.Value<string>("in") == null)
249+
.Where(p => p.Value<string>("description") != null)
250+
.Where(p => p.Value<string>("description").Contains("[formData]"))
251+
.ToList();
252+
foreach (var jt in jts)
253+
{
254+
jt["in"] = "formData";
255+
jt["description"] = jt.Value<string>("description").Replace("[formData]", string.Empty);
256+
}
257+
258+
var serialised = JsonConvert.SerializeObject(jo, Formatting.Indented);
259+
if (format == OpenApiFormat.Json)
260+
{
261+
return serialised;
262+
}
263+
264+
var converter = new ExpandoObjectConverter();
265+
var deserialised = JsonConvert.DeserializeObject<ExpandoObject>(serialised, converter);
266+
serialised = new SerializerBuilder().Build().Serialize(deserialised);
267+
268+
return serialised;
212269
}
213270
}
271+
272+
public class ParameterFormDataIn
273+
{
274+
[JsonProperty("in")]
275+
public string In { get; set; }
276+
}
214277
}

test-integration/Microsoft.Azure.WebJobs.Extensions.OpenApi.Document.Tests/Post_ApplicationJson_ByteArrayObject_Tests.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public async Task Init()
3030
public void Given_OpenApiDocument_Then_It_Should_Return_OperationRequestBody(string path, string operationType)
3131
{
3232
var requestBody = this._doc["paths"][path][operationType]["requestBody"];
33-
33+
3434
requestBody.Should().NotBeNull();
3535
}
3636

@@ -44,7 +44,7 @@ public void Given_OpenApiDocument_Then_It_Should_Return_OperationRequestBodyCont
4444
}
4545

4646
[DataTestMethod]
47-
[DataRow("/post-applicationjson-bytearray", "post", "application/octet-stream", "string", "base64")]
47+
[DataRow("/post-applicationjson-bytearray", "post", "application/octet-stream", "string", "binary")]
4848
public void Given_OpenApiDocument_Then_It_Should_Return_OperationRequestBodyContentTypeSchema(string path, string operationType, string contentType, string propertyType, string propertyFormat)
4949
{
5050
var content = this._doc["paths"][path][operationType]["requestBody"]["content"];
@@ -98,7 +98,7 @@ public void Given_OpenApiDocument_Then_It_Should_Return_ComponentSchema(string @
9898
}
9999

100100
[DataTestMethod]
101-
[DataRow("byteArrayObjectModel", "byteArrayValue", "string", "base64")]
101+
[DataRow("byteArrayObjectModel", "byteArrayValue", "string", "binary")]
102102
public void Given_OpenApiDocument_Then_It_Should_Return_ComponentSchemaProperty(string @ref, string propertyName, string propertyType, string propertyFormat)
103103
{
104104
var properties = this._doc["components"]["schemas"][@ref]["properties"];

test/Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Tests/Visitors/ByteArrayTypeVisitorTests.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ public void Given_Type_When_IsPayloadVisitable_Invoked_Then_It_Should_Return_Res
7070
}
7171

7272
[DataTestMethod]
73-
[DataRow("string", "base64", "hello")]
73+
[DataRow("string", "binary", "hello")]
7474
public void Given_Type_When_Visit_Invoked_Then_It_Should_Return_Result(string dataType, string dataFormat, string name)
7575
{
7676
var acceptor = new OpenApiSchemaAcceptor();
@@ -150,7 +150,7 @@ public void Given_OpenApiSchemaVisibilityAttribute_When_Visit_Invoked_Then_It_Sh
150150
}
151151

152152
[DataTestMethod]
153-
[DataRow("string", "base64")]
153+
[DataRow("string", "binary")]
154154
public void Given_Type_When_ParameterVisit_Invoked_Then_It_Should_Return_Result(string dataType, string dataFormat)
155155
{
156156
var result = this._visitor.ParameterVisit(typeof(byte[]), this._strategy);
@@ -160,7 +160,7 @@ public void Given_Type_When_ParameterVisit_Invoked_Then_It_Should_Return_Result(
160160
}
161161

162162
[DataTestMethod]
163-
[DataRow("string", "base64")]
163+
[DataRow("string", "binary")]
164164
public void Given_Type_When_PayloadVisit_Invoked_Then_It_Should_Return_Result(string dataType, string dataFormat)
165165
{
166166
var result = this._visitor.PayloadVisit(typeof(byte[]), this._strategy);

0 commit comments

Comments
 (0)