-
-
Notifications
You must be signed in to change notification settings - Fork 64
Update to return HttpResponseMessage for file downloads #877
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
Changes from 4 commits
a2269c3
1ed5677
9a3b0e4
1a03572
2633afc
a377a5b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -116,6 +116,12 @@ protected string GetTypeName(OpenApiOperation operation) | |
| return type is null or "void" ? GetAsyncOperationType(true) : $"{GetAsyncOperationType(false)}<{WellKnownNamespaces.TrimImportedNamespaces(type)}>"; | ||
| } | ||
|
|
||
| // Check if response is a file stream | ||
| if (IsFileStreamResponse(operation)) | ||
| { | ||
| return $"{GetAsyncOperationType(false)}<HttpResponseMessage>"; | ||
| } | ||
|
|
||
| // First check for explicit success status codes | ||
| var successCodes = new[] { "200", "201", "203", "206" }; | ||
| var returnTypeParameter = successCodes | ||
|
|
@@ -138,6 +144,54 @@ protected string GetTypeName(OpenApiOperation operation) | |
| return GetReturnType(returnTypeParameter); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Determines if the operation response is a file stream (binary content). | ||
| /// </summary> | ||
| /// <param name="operation">The OpenAPI operation to check.</param> | ||
| /// <returns>True if the response is a file stream, false otherwise.</returns> | ||
| private static bool IsFileStreamResponse(OpenApiOperation operation) | ||
| { | ||
| var successCodes = new[] { "200", "201", "203", "206", "2XX" }; | ||
|
|
||
| foreach (var code in successCodes) | ||
| { | ||
| if (!operation.Responses.TryGetValue(code, out var apiResponse)) | ||
| continue; | ||
|
|
||
| var response = apiResponse.ActualResponse; | ||
|
|
||
| if (response.Content?.Any() != true) | ||
| continue; | ||
|
|
||
| foreach (var contentEntry in response.Content) | ||
| { | ||
| if (IsFileContentType(contentEntry.Key)) | ||
| { | ||
| var schema = contentEntry.Value?.Schema; | ||
| if (schema?.Format == "binary" || schema?.Type == NJsonSchema.JsonObjectType.File) | ||
| return true; | ||
| } | ||
| } | ||
|
Comment on lines
+166
to
+174
|
||
| } | ||
|
Comment on lines
+156
to
+175
|
||
|
|
||
| return false; | ||
| } | ||
|
|
||
| private static bool IsFileContentType(string contentType) | ||
| { | ||
| return | ||
| contentType.StartsWith("application/octet-stream", StringComparison.OrdinalIgnoreCase) || | ||
| contentType.StartsWith("application/pdf", StringComparison.OrdinalIgnoreCase) || | ||
| contentType.StartsWith("application/vnd", StringComparison.OrdinalIgnoreCase) || | ||
| contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase) || | ||
| contentType.StartsWith("video/", StringComparison.OrdinalIgnoreCase) || | ||
| contentType.StartsWith("audio/", StringComparison.OrdinalIgnoreCase) || | ||
| contentType.StartsWith("application/zip", StringComparison.OrdinalIgnoreCase) || | ||
|
frogcrush marked this conversation as resolved.
|
||
| contentType.StartsWith("application/gzip", StringComparison.OrdinalIgnoreCase) || | ||
| (contentType.StartsWith("application/x-", StringComparison.OrdinalIgnoreCase) && | ||
| !contentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase)); | ||
| } | ||
|
|
||
| private string GetTypeName(string code, OpenApiOperation operation) | ||
| { | ||
| var schema = operation.Responses[code].ActualResponse.Schema; | ||
|
|
@@ -254,6 +308,16 @@ private string GetDefaultReturnType() | |
| /// <returns>True if the type is an ApiResponse Task or similar, false otherwise.</returns> | ||
| protected static bool IsApiResponseType(string typeName) | ||
| { | ||
| // Check for HttpResponseMessage | ||
| if (Regex.IsMatch( | ||
| typeName, | ||
| "(Task|IObservable)<HttpResponseMessage>", | ||
| RegexOptions.None, | ||
| TimeSpan.FromSeconds(1))) | ||
| { | ||
| return true; | ||
| } | ||
|
|
||
| return Regex.IsMatch( | ||
| typeName, | ||
| "(Task|IObservable)<(I)?ApiResponse(<[\\w<>]+>)?>", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,163 @@ | ||
| using FluentAssertions; | ||
| using Refitter.Core; | ||
| using Refitter.Tests.Build; | ||
| using Refitter.Tests.TestUtilities; | ||
| using TUnit.Core; | ||
|
|
||
| namespace Refitter.Tests.Examples; | ||
|
|
||
| public class FileStreamResponseTests | ||
| { | ||
| private const string OpenApiSpec = @" | ||
| openapi: '3.0.0' | ||
| info: | ||
| title: File Download API | ||
| version: 1.0.0 | ||
| paths: | ||
| '/files/{fileId}/download': | ||
| get: | ||
| operationId: downloadFile | ||
| summary: Download a file | ||
| parameters: | ||
| - name: fileId | ||
| in: path | ||
| required: true | ||
| schema: | ||
| type: string | ||
| responses: | ||
| '200': | ||
| description: File downloaded successfully | ||
| content: | ||
| application/octet-stream: | ||
| schema: | ||
| type: string | ||
| format: binary | ||
| '/images/{imageId}': | ||
| get: | ||
| operationId: getImage | ||
| summary: Get an image | ||
| parameters: | ||
| - name: imageId | ||
| in: path | ||
| required: true | ||
| schema: | ||
| type: string | ||
| responses: | ||
| '200': | ||
| description: Image retrieved successfully | ||
| content: | ||
| image/jpeg: | ||
| schema: | ||
| type: string | ||
| format: binary | ||
| image/png: | ||
| schema: | ||
| type: string | ||
| format: binary | ||
| '/documents/{docId}': | ||
| get: | ||
| operationId: getDocument | ||
| summary: Get a PDF document | ||
| parameters: | ||
| - name: docId | ||
| in: path | ||
| required: true | ||
| schema: | ||
| type: string | ||
| responses: | ||
| '200': | ||
| description: Document retrieved successfully | ||
| content: | ||
| application/pdf: | ||
| schema: | ||
| type: string | ||
| format: binary | ||
| '/forms/{formId}/submit': | ||
| get: | ||
| operationId: submitForm | ||
| summary: Submit form data | ||
| parameters: | ||
| - name: formId | ||
| in: path | ||
| required: true | ||
| schema: | ||
| type: string | ||
| responses: | ||
| '200': | ||
| description: Form submission result | ||
| content: | ||
| application/x-www-form-urlencoded: | ||
| schema: | ||
| type: object | ||
| properties: | ||
| result: | ||
| type: string | ||
| "; | ||
|
|
||
| [Test] | ||
| public async Task Can_Generate_Code() | ||
| { | ||
| string generatedCode = await GenerateCode(); | ||
| generatedCode.Should().NotBeNullOrWhiteSpace(); | ||
| } | ||
|
|
||
| [Test] | ||
| public async Task Generates_HttpResponseMessage_For_OctetStream() | ||
| { | ||
| string generatedCode = await GenerateCode(); | ||
| generatedCode.Should().Contain("Task<HttpResponseMessage> DownloadFile(string fileId"); | ||
| } | ||
|
|
||
| [Test] | ||
| public async Task Generates_HttpResponseMessage_For_ImageJpeg() | ||
| { | ||
| string generatedCode = await GenerateCode(); | ||
| generatedCode.Should().Contain("Task<HttpResponseMessage> GetImage(string imageId"); | ||
| } | ||
|
|
||
| [Test] | ||
| public async Task Generates_HttpResponseMessage_For_ApplicationPdf() | ||
| { | ||
| string generatedCode = await GenerateCode(); | ||
| generatedCode.Should().Contain("Task<HttpResponseMessage> GetDocument(string docId"); | ||
| } | ||
|
|
||
| [Test] | ||
| public async Task Does_Not_Generate_HttpResponseMessage_For_FormUrlEncoded() | ||
| { | ||
| string generatedCode = await GenerateCode(); | ||
| generatedCode.Should().Contain("submitForm"); | ||
| generatedCode.Should().NotContain("Task<HttpResponseMessage> SubmitForm("); | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| [Test] | ||
| public async Task Can_Build_Generated_Code() | ||
| { | ||
| string generatedCode = await GenerateCode(); | ||
| BuildHelper | ||
| .BuildCSharp(generatedCode) | ||
| .Should() | ||
| .BeTrue(); | ||
| } | ||
|
|
||
| [Test] | ||
| public async Task Generates_HttpResponseMessage_With_IApiResponse_Setting() | ||
| { | ||
| string generatedCode = await GenerateCode(returnIApiResponse: true); | ||
| generatedCode.Should().Contain("Task<HttpResponseMessage> DownloadFile(string fileId"); | ||
| } | ||
|
|
||
| private static async Task<string> GenerateCode(bool returnIApiResponse = false) | ||
| { | ||
| var swaggerFile = await SwaggerFileHelper.CreateSwaggerFile(OpenApiSpec); | ||
| var settings = new RefitGeneratorSettings | ||
| { | ||
| OpenApiPath = swaggerFile, | ||
| ReturnIApiResponse = returnIApiResponse | ||
| }; | ||
|
|
||
| var sut = await RefitGenerator.CreateAsync(settings); | ||
| var generatedCode = sut.Generate(); | ||
| return generatedCode; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Handle file content types with missing schemas.
OpenAPI allows a media type without a
schema; those would be treated as non-file here. If you want schema-less file responses to map toHttpResponseMessage, add a fallback (optionally excludingapplication/vnd*if you want to keep vendor JSON strict).💡 Possible fallback
if (IsFileContentType(contentEntry.Key)) { var schema = contentEntry.Value?.Schema; - if (schema?.Format == "binary" || schema?.Type == NJsonSchema.JsonObjectType.File) + if (schema == null) + return true; + if (schema.Format == "binary" || schema.Type == NJsonSchema.JsonObjectType.File) return true; }🤖 Prompt for AI Agents