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

multipart/form-data in OpenAPI v2 Rendering Workaround (Interim) #367

Merged
merged 4 commits into from
Feb 20, 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
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ public class Startup : FunctionsStartup
{
public override void Configure(IFunctionsHostBuilder builder)
{
builder.Services.AddSingleton<Fixture>();
var fixture = new Fixture();
builder.Services.AddSingleton(fixture);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -146,14 +146,69 @@ public static OpenApiOperation GetOpenApiOperation(this IDocumentHelper helper,
/// <param name="trigger"><see cref="HttpTriggerAttribute"/> instance.</param>
/// <param name="namingStrategy"><see cref="NamingStrategy"/> instance to create the JSON schema from .NET Types.</param>
/// <param name="collection"><see cref="VisitorCollection"/> instance to process parameters.</param>
/// <param name="version">OpenAPI spec version.</param>
/// <returns>List of <see cref="OpenApiParameter"/> instance.</returns>
public static List<OpenApiParameter> GetOpenApiParameters(this IDocumentHelper helper, MethodInfo element, HttpTriggerAttribute trigger, NamingStrategy namingStrategy, VisitorCollection collection)
public static List<OpenApiParameter> GetOpenApiParameters(this IDocumentHelper helper, MethodInfo element, HttpTriggerAttribute trigger, NamingStrategy namingStrategy, VisitorCollection collection, OpenApiVersionType version)
{
var parameters = element.GetCustomAttributes<OpenApiParameterAttribute>(inherit: false)
.Where(p => p.Deprecated == false)
.Select(p => p.ToOpenApiParameter(namingStrategy, collection))
.ToList();

// This is the interim solution to resolve:
// https://github.com/Azure/azure-functions-openapi-extension/issues/365
//
// It will be removed when the following issue is resolved:
// https://github.com/microsoft/OpenAPI.NET/issues/747
if (version == OpenApiVersionType.V3)
{
return parameters;
}

var attributes = element.GetCustomAttributes<OpenApiRequestBodyAttribute>(inherit: false);
if (!attributes.Any())
{
return parameters;
}

var contents = attributes.Where(p => p.Deprecated == false)
.Where(p => p.ContentType == "application/x-www-form-urlencoded" || p.ContentType == "multipart/form-data")
.Select(p => p.ToOpenApiMediaType(namingStrategy, collection, version));
if (!contents.Any())
{
return parameters;
}

var @ref = contents.First().Schema.Reference;
var schemas = helper.GetOpenApiSchemas(new[] { element }.ToList(), namingStrategy, collection);
var schema = schemas.SingleOrDefault(p => p.Key == @ref.Id);
if (schema.IsNullOrDefault())
{
return parameters;
}

var properties = schema.Value.Properties;
foreach (var property in properties)
{
var value = property.Value;
if ((value.Type == "string" && value.Format == "binary") || (value.Type == "string" && value.Format == "base64"))
{
value.Type = "file";
value.Format = null;
}

var parameter = new OpenApiParameter()
{
Name = property.Key,
Description = $"[formData]{value.Description}",
Required = bool.TryParse($"{value.Required}", out var result) ? result : false,
Deprecated = value.Deprecated,
Schema = value,
};

parameters.Add(parameter);
}

// // TODO: Should this be forcibly provided?
// // This needs to be provided separately.
// if (trigger.AuthLevel != AuthorizationLevel.Anonymous)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,6 @@ public static List<IOpenApiAny> ToOpenApiIntegerCollection(this Type type)
.ToList();
}


return null;
}

Expand Down Expand Up @@ -537,7 +536,6 @@ public static string GetOpenApiTypeName(this Type type, NamingStrategy namingStr
return name;
}


/// <summary>
/// Gets the sub type of the given <see cref="Type"/>.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
<PackageReference Include="System.IO.FileSystem.Primitives" Version="4.3.0" />
<PackageReference Include="System.Runtime.Handles" Version="4.3.0" />
<PackageReference Include="System.Runtime.InteropServices" Version="4.3.0" />
<PackageReference Include="YamlDotNet" Version="11.2.1" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public override bool IsVisitable(Type type)
/// <inheritdoc />
public override void Visit(IAcceptor acceptor, KeyValuePair<string, Type> type, NamingStrategy namingStrategy, params Attribute[] attributes)
{
this.Visit(acceptor, name: type.Key, title: null, dataType: "string", dataFormat: "base64", attributes: attributes);
this.Visit(acceptor, name: type.Key, title: null, dataType: "string", dataFormat: "binary", attributes: attributes);
}

/// <inheritdoc />
Expand All @@ -44,7 +44,7 @@ public override bool IsParameterVisitable(Type type)
/// <inheritdoc />
public override OpenApiSchema ParameterVisit(Type type, NamingStrategy namingStrategy)
{
return this.ParameterVisit(dataType: "string", dataFormat: "base64");
return this.ParameterVisit(dataType: "string", dataFormat: "binary");
}

/// <inheritdoc />
Expand All @@ -58,7 +58,7 @@ public override bool IsPayloadVisitable(Type type)
/// <inheritdoc />
public override OpenApiSchema PayloadVisit(Type type, NamingStrategy namingStrategy)
{
return this.PayloadVisit(dataType: "string", dataFormat: "base64");
return this.PayloadVisit(dataType: "string", dataFormat: "binary");
}
}
}
71 changes: 67 additions & 4 deletions src/Microsoft.Azure.WebJobs.Extensions.OpenApi/Document.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Dynamic;
using System.IO;
using System.Linq;
using System.Reflection;
Expand All @@ -11,8 +12,13 @@
using Microsoft.OpenApi;
using Microsoft.OpenApi.Models;

using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;

using YamlDotNet.Serialization;

namespace Microsoft.Azure.WebJobs.Extensions.OpenApi
{
/// <summary>
Expand All @@ -38,7 +44,6 @@ public Document(IDocumentHelper helper)
public Document(OpenApiDocument openApiDocument)
{
this.OpenApiDocument = openApiDocument;

}

/// <inheritdoc />
Expand Down Expand Up @@ -168,7 +173,7 @@ public IDocument Build(Assembly assembly, OpenApiVersionType version = OpenApiVe
}

operation.Security = this._helper.GetOpenApiSecurityRequirement(method, this._strategy);
operation.Parameters = this._helper.GetOpenApiParameters(method, trigger, this._strategy, this._collection);
operation.Parameters = this._helper.GetOpenApiParameters(method, trigger, this._strategy, this._collection, version);
operation.RequestBody = this._helper.GetOpenApiRequestBody(method, this._strategy, this._collection, version);
operation.Responses = this._helper.GetOpenApiResponses(method, this._strategy, this._collection, version);

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

private string Render(OpenApiSpecVersion version, OpenApiFormat format)
{
//var serialised = default(string);
//using (var sw = new StringWriter())
//{
// this.OpenApiDocument.Serialise(sw, version, format);
// serialised = sw.ToString();
//}

//return serialised;

// This is the interim solution to resolve:
// https://github.com/Azure/azure-functions-openapi-extension/issues/365
//
// It will be removed when the following issue is resolved:
// https://github.com/microsoft/OpenAPI.NET/issues/747
var jserialised = default(string);
using (var sw = new StringWriter())
{
this.OpenApiDocument.Serialise(sw, version, OpenApiFormat.Json);
jserialised = sw.ToString();
}

var yserialised = default(string);
using (var sw = new StringWriter())
{
this.OpenApiDocument.Serialise(sw, version, format);
this.OpenApiDocument.Serialise(sw, version, OpenApiFormat.Yaml);
yserialised = sw.ToString();
}

return sw.ToString();
if (version != OpenApiSpecVersion.OpenApi2_0)
{
return format == OpenApiFormat.Json ? jserialised : yserialised;
}

var jo = JsonConvert.DeserializeObject<JObject>(jserialised);
var jts = jo.DescendantsAndSelf()
.Where(p => p.Type == JTokenType.Property && (p as JProperty).Name == "parameters")
.SelectMany(p => p.Values<JArray>().SelectMany(q => q.Children<JObject>()))
.Where(p => p.Value<string>("in") == null)
.Where(p => p.Value<string>("description") != null)
.Where(p => p.Value<string>("description").Contains("[formData]"))
.ToList();
foreach (var jt in jts)
{
jt["in"] = "formData";
jt["description"] = jt.Value<string>("description").Replace("[formData]", string.Empty);
}

var serialised = JsonConvert.SerializeObject(jo, Formatting.Indented);
if (format == OpenApiFormat.Json)
{
return serialised;
}

var converter = new ExpandoObjectConverter();
var deserialised = JsonConvert.DeserializeObject<ExpandoObject>(serialised, converter);
serialised = new SerializerBuilder().Build().Serialize(deserialised);

return serialised;
}
}

public class ParameterFormDataIn
{
[JsonProperty("in")]
public string In { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public async Task Init()
public void Given_OpenApiDocument_Then_It_Should_Return_OperationRequestBody(string path, string operationType)
{
var requestBody = this._doc["paths"][path][operationType]["requestBody"];

requestBody.Should().NotBeNull();
}

Expand All @@ -44,7 +44,7 @@ public void Given_OpenApiDocument_Then_It_Should_Return_OperationRequestBodyCont
}

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

[DataTestMethod]
[DataRow("byteArrayObjectModel", "byteArrayValue", "string", "base64")]
[DataRow("byteArrayObjectModel", "byteArrayValue", "string", "binary")]
public void Given_OpenApiDocument_Then_It_Should_Return_ComponentSchemaProperty(string @ref, string propertyName, string propertyType, string propertyFormat)
{
var properties = this._doc["components"]["schemas"][@ref]["properties"];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public void Given_Type_When_IsPayloadVisitable_Invoked_Then_It_Should_Return_Res
}

[DataTestMethod]
[DataRow("string", "base64", "hello")]
[DataRow("string", "binary", "hello")]
public void Given_Type_When_Visit_Invoked_Then_It_Should_Return_Result(string dataType, string dataFormat, string name)
{
var acceptor = new OpenApiSchemaAcceptor();
Expand Down Expand Up @@ -150,7 +150,7 @@ public void Given_OpenApiSchemaVisibilityAttribute_When_Visit_Invoked_Then_It_Sh
}

[DataTestMethod]
[DataRow("string", "base64")]
[DataRow("string", "binary")]
public void Given_Type_When_ParameterVisit_Invoked_Then_It_Should_Return_Result(string dataType, string dataFormat)
{
var result = this._visitor.ParameterVisit(typeof(byte[]), this._strategy);
Expand All @@ -160,7 +160,7 @@ public void Given_Type_When_ParameterVisit_Invoked_Then_It_Should_Return_Result(
}

[DataTestMethod]
[DataRow("string", "base64")]
[DataRow("string", "binary")]
public void Given_Type_When_PayloadVisit_Invoked_Then_It_Should_Return_Result(string dataType, string dataFormat)
{
var result = this._visitor.PayloadVisit(typeof(byte[]), this._strategy);
Expand Down