From 73bc32534f2403ab0355614d22409688954f4208 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaros=C5=82aw=20Dutka?= Date: Tue, 21 Jan 2025 15:14:59 +0100 Subject: [PATCH 1/2] Proper support for multipart form --- src/Refitter.Core/ParameterExtractor.cs | 16 +- .../Examples/MultiPartFormDataAliasAsTests.cs | 156 ------------------ .../Examples/MultiPartFormDataTests.cs | 130 +++++++++------ 3 files changed, 89 insertions(+), 213 deletions(-) delete mode 100644 src/Refitter.Tests/Examples/MultiPartFormDataAliasAsTests.cs diff --git a/src/Refitter.Core/ParameterExtractor.cs b/src/Refitter.Core/ParameterExtractor.cs index efde39815..53d452984 100644 --- a/src/Refitter.Core/ParameterExtractor.cs +++ b/src/Refitter.Core/ParameterExtractor.cs @@ -39,16 +39,10 @@ public static IEnumerable GetParameters( .ToList(); } - var binaryBodyParameters = operationModel.Parameters - .Where(p => p.Kind == OpenApiParameterKind.Body && p.IsBinaryBodyParameter || p.IsFile) + var formParameters = operationModel.Parameters + .Where(p => p.Kind == OpenApiParameterKind.FormData && !p.IsBinaryBodyParameter) .Select(p => - { - var generatedAliasAsAttribute = string.IsNullOrWhiteSpace(GetAliasAsAttribute(p)) - ? string.Empty - : $"[{GetAliasAsAttribute(p)}]"; - - return $"{generatedAliasAsAttribute}StreamPart {p.VariableName}"; - }) + $"{GetParameterType(p, settings)} {p.VariableName}") .ToList(); var parameters = new List(); @@ -56,7 +50,7 @@ public static IEnumerable GetParameters( parameters.AddRange(queryParameters); parameters.AddRange(bodyParameters); parameters.AddRange(headerParameters); - parameters.AddRange(binaryBodyParameters); + parameters.AddRange(formParameters); parameters = ReOrderNullableParameters(parameters, settings); @@ -142,7 +136,7 @@ private static string GetQueryParameterType( } private static string FindSupportedType(string typeName) => - typeName == "FileResponse" ? "StreamPart" : typeName; + typeName is "FileResponse" or "FileParameter" ? "StreamPart" : typeName; private static List GetQueryParameters(CSharpOperationModel operationModel, RefitGeneratorSettings settings, string dynamicQuerystringParameterType, out string? dynamicQuerystringParameters) { diff --git a/src/Refitter.Tests/Examples/MultiPartFormDataAliasAsTests.cs b/src/Refitter.Tests/Examples/MultiPartFormDataAliasAsTests.cs deleted file mode 100644 index c4a47c43e..000000000 --- a/src/Refitter.Tests/Examples/MultiPartFormDataAliasAsTests.cs +++ /dev/null @@ -1,156 +0,0 @@ -using FluentAssertions; - -using Refitter.Core; -using Refitter.Tests.Build; - -using Xunit; - -namespace Refitter.Tests.Examples; - -public class MultiPartFormDataAliasAsTests -{ - private const string OpenApiSpec = @" -{ - ""openapi"": ""3.0.1"", - ""paths"": { - ""/animals"": { - ""post"": { - ""tags"": [ - ""Animals"" - ], - ""requestBody"": { - ""content"": { - ""multipart/form-data"": { - ""schema"": { - ""type"": ""object"", - ""properties"": { - ""Name"": { - ""type"": ""string"" - }, - ""AnimalClassFile"": { - ""type"": ""string"", - ""format"": ""binary"" - }, - ""AnimalCrowdFile"": { - ""type"": ""string"", - ""format"": ""binary"" - } - } - }, - ""encoding"": { - ""Name"": { - ""style"": ""form"" - }, - ""AnimalClassFile"": { - ""style"": ""form"" - }, - ""AnimalCrowdFile"": { - ""style"": ""form"" - } - } - } - } - }, - ""responses"": { - ""201"": { - ""description"": ""Created"", - ""content"": { - ""application/json"": { - ""schema"": { - ""$ref"": ""#/components/schemas/AnimalResponse"" - } - } - } - } - } - } - } - }, - ""components"": { - ""schemas"": { - ""AnimalResponse"": { - ""type"": ""object"", - ""properties"": { - ""id"": { - ""type"": ""string"", - ""format"": ""uuid"", - ""nullable"": true - }, - ""Name"": { - ""type"": ""string"", - ""nullable"": true - }, - ""AnimalClassFileUri"": { - ""type"": ""string"", - ""nullable"": true - }, - ""AnimalCrowdFileUri"": { - ""type"": ""string"", - ""nullable"": true - } - }, - ""additionalProperties"": false - } - } - } -} -"; - - [Fact] - public async Task Can_Generate_Code() - { - string generateCode = await GenerateCode(); - generateCode.Should().NotBeNullOrWhiteSpace(); - } - - [Fact] - public async Task Can_Build_Generated_Code() - { - string generateCode = await GenerateCode(); - BuildHelper - .BuildCSharp(generateCode) - .Should() - .BeTrue(); - } - - [Fact] - public async Task Generated_Code_Contains_MultiPart_Attribute() - { - string generateCode = await GenerateCode(); - generateCode.Should().Contain("[Multipart]"); - } - - [Fact] - public async Task Generated_Code_Contains_AliasAs_Attribute() - { - string generateCode = await GenerateCode(); - generateCode.Should().Contain("[AliasAs(\"AnimalClassFile\")]"); - } - - [Fact] - public async Task Generated_Code_Contains_StreamPart_Parameter() - { - string generateCode = await GenerateCode(); - generateCode.Should().Contain("StreamPart"); - } - - private static async Task GenerateCode() - { - var swaggerFile = await CreateSwaggerFile(OpenApiSpec); - var settings = new RefitGeneratorSettings { OpenApiPath = swaggerFile }; - - var sut = await RefitGenerator.CreateAsync(settings); - var generateCode = sut.Generate(); - return generateCode; - } - - private static async Task CreateSwaggerFile(string contents) - { - var filename = $"{Guid.NewGuid()}.json"; - var folder = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - Directory.CreateDirectory(folder); - var swaggerFile = Path.Combine(folder, filename); - await File.WriteAllTextAsync(swaggerFile, contents); - return swaggerFile; - } -} \ No newline at end of file diff --git a/src/Refitter.Tests/Examples/MultiPartFormDataTests.cs b/src/Refitter.Tests/Examples/MultiPartFormDataTests.cs index 5fa2888f4..39f3942d7 100644 --- a/src/Refitter.Tests/Examples/MultiPartFormDataTests.cs +++ b/src/Refitter.Tests/Examples/MultiPartFormDataTests.cs @@ -1,6 +1,8 @@ using FluentAssertions; + using Refitter.Core; using Refitter.Tests.Build; + using Xunit; namespace Refitter.Tests.Examples; @@ -9,52 +11,88 @@ public class MultiPartFormDataTests { private const string OpenApiSpec = @" { - ""openapi"":""3.0.2"", - ""paths"":{ - ""/foo/{id}/files"":{ - ""post"":{ - ""summary"":""uploads a file"", - ""operationId"":""uploadFile"", - ""parameters"":[ - { - ""name"":""id"", - ""in"":""path"", - ""description"":""Id of the foo resource"", - ""required"":true, - ""schema"":{ - ""type"":""integer"", - ""format"":""int64"" + ""openapi"": ""3.0.1"", + ""paths"": { + ""/animals"": { + ""post"": { + ""tags"": [ + ""Animals"" + ], + ""requestBody"": { + ""content"": { + ""multipart/form-data"": { + ""schema"": { + ""type"": ""object"", + ""properties"": { + ""Name"": { + ""type"": ""string"" + }, + ""AnimalClassFile"": { + ""type"": ""string"", + ""format"": ""binary"" + }, + ""AnimalCrowdFile"": { + ""type"": ""string"", + ""format"": ""binary"" } - } - ], - ""requestBody"":{ - ""content"":{ - ""multipart/form-data"":{ - ""schema"":{ - ""type"":""object"", - ""properties"":{ - ""formFile"":{ - ""type"":""string"", - ""format"":""binary"" - } - } - }, - ""encoding"":{ - ""formFile"":{ - ""style"":""form"" - } - } - } - } - }, - ""responses"":{ - ""200"":{ - ""description"":""successful operation"" - } + } + }, + ""encoding"": { + ""Name"": { + ""style"": ""form"" + }, + ""AnimalClassFile"": { + ""style"": ""form"" + }, + ""AnimalCrowdFile"": { + ""style"": ""form"" + } + } + } + } + }, + ""responses"": { + ""201"": { + ""description"": ""Created"", + ""content"": { + ""application/json"": { + ""schema"": { + ""$ref"": ""#/components/schemas/AnimalResponse"" + } + } } - } + } + } } - } + } + }, + ""components"": { + ""schemas"": { + ""AnimalResponse"": { + ""type"": ""object"", + ""properties"": { + ""id"": { + ""type"": ""string"", + ""format"": ""uuid"", + ""nullable"": true + }, + ""Name"": { + ""type"": ""string"", + ""nullable"": true + }, + ""AnimalClassFileUri"": { + ""type"": ""string"", + ""nullable"": true + }, + ""AnimalCrowdFileUri"": { + ""type"": ""string"", + ""nullable"": true + } + }, + ""additionalProperties"": false + } + } + } } "; @@ -83,10 +121,10 @@ public async Task Generated_Code_Contains_MultiPart_Attribute() } [Fact] - public async Task Generated_Code_Contains_StreamPart_Parameter() + public async Task Generated_Code_Contains_Correct_Parameters() { string generateCode = await GenerateCode(); - generateCode.Should().Contain("StreamPart"); + generateCode.Should().Contain("string name, StreamPart animalClassFile, StreamPart animalCrowdFile"); } private static async Task GenerateCode() @@ -108,4 +146,4 @@ private static async Task CreateSwaggerFile(string contents) await File.WriteAllTextAsync(swaggerFile, contents); return swaggerFile; } -} \ No newline at end of file +} From b35049ddbec7c7e79eb4da410858d5f2c5b3b5ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaros=C5=82aw=20Dutka?= Date: Wed, 22 Jan 2025 10:38:38 +0100 Subject: [PATCH 2/2] Fix for "application/octet-stream" after FormData changes. --- src/Refitter.Core/ParameterExtractor.cs | 13 +++ .../Examples/OctetStreamBodyTests.cs | 101 ++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 src/Refitter.Tests/Examples/OctetStreamBodyTests.cs diff --git a/src/Refitter.Core/ParameterExtractor.cs b/src/Refitter.Core/ParameterExtractor.cs index 53d452984..4453afc7d 100644 --- a/src/Refitter.Core/ParameterExtractor.cs +++ b/src/Refitter.Core/ParameterExtractor.cs @@ -45,12 +45,25 @@ public static IEnumerable GetParameters( $"{GetParameterType(p, settings)} {p.VariableName}") .ToList(); + var binaryBodyParameters = operationModel.Parameters + .Where(p => p.Kind == OpenApiParameterKind.Body && p.IsBinaryBodyParameter) + .Select(p => + { + var generatedAliasAsAttribute = string.IsNullOrWhiteSpace(GetAliasAsAttribute(p)) + ? string.Empty + : $"[{GetAliasAsAttribute(p)}]"; + + return $"{generatedAliasAsAttribute}StreamPart {p.VariableName}"; + }) + .ToList(); + var parameters = new List(); parameters.AddRange(routeParameters); parameters.AddRange(queryParameters); parameters.AddRange(bodyParameters); parameters.AddRange(headerParameters); parameters.AddRange(formParameters); + parameters.AddRange(binaryBodyParameters); parameters = ReOrderNullableParameters(parameters, settings); diff --git a/src/Refitter.Tests/Examples/OctetStreamBodyTests.cs b/src/Refitter.Tests/Examples/OctetStreamBodyTests.cs new file mode 100644 index 000000000..efe7a7899 --- /dev/null +++ b/src/Refitter.Tests/Examples/OctetStreamBodyTests.cs @@ -0,0 +1,101 @@ +using FluentAssertions; +using Refitter.Core; +using Refitter.Tests.Build; +using Xunit; + +namespace Refitter.Tests.Examples; + +public class OctetStreamBodyTests +{ + private const string OpenApiSpec = @" +openapi: '3.0.0' +paths: + '/pet/{petId}/uploadImage': + post: + tags: + - pet + summary: uploads an image + description: '' + operationId: uploadFile + parameters: + - name: petId + in: path + description: ID of pet to update + required: true + schema: + type: integer + format: int64 + - name: additionalMetadata + in: query + description: Additional Metadata + required: false + schema: + type: string + requestBody: + content: + application/octet-stream: + schema: + type: string + format: binary + responses: + '200': + description: successful operation +"; + + [Fact] + public async Task Can_Generate_Code() + { + string generateCode = await GenerateCode(); + generateCode.Should().NotBeNullOrWhiteSpace(); + } + + [Fact] + public async Task Generates_Pet_Interface() + { + string generateCode = await GenerateCode(); + generateCode.Should().Contain("partial interface IPetApi"); + } + + [Fact] + public async Task Generates_StreamPart_Parameter() + { + string generateCode = await GenerateCode(true); + generateCode.Should().Contain("long petId, [Query] string additionalMetadata, StreamPart body"); + } + + [Fact] + public async Task Can_Build_Generated_Code() + { + string generateCode = await GenerateCode(); + BuildHelper + .BuildCSharp(generateCode) + .Should() + .BeTrue(); + } + + private static async Task GenerateCode(bool useDynamicQuerystringParameters = false) + { + var swaggerFile = await CreateSwaggerFile(OpenApiSpec); + var settings = new RefitGeneratorSettings + { + OpenApiPath = swaggerFile, + MultipleInterfaces = MultipleInterfaces.ByTag, + UseDynamicQuerystringParameters = useDynamicQuerystringParameters, + ImmutableRecords = true + }; + + var sut = await RefitGenerator.CreateAsync(settings); + var generateCode = sut.Generate(); + return generateCode; + } + + private static async Task CreateSwaggerFile(string contents) + { + var filename = $"{Guid.NewGuid()}.yml"; + var folder = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(folder); + var swaggerFile = Path.Combine(folder, filename); + await File.WriteAllTextAsync(swaggerFile, contents); + return swaggerFile; + } +}