From 0280c29c31cb51ea2e3528270835e9b2e72cc5fb Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Thu, 24 Apr 2025 19:43:13 +0000 Subject: [PATCH 1/4] Enhance return type resolution for OpenAPI operations --- src/Refitter.Core/RefitInterfaceGenerator.cs | 31 ++++++++++++++++---- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/src/Refitter.Core/RefitInterfaceGenerator.cs b/src/Refitter.Core/RefitInterfaceGenerator.cs index 65803f19d..1bd1cf94b 100644 --- a/src/Refitter.Core/RefitInterfaceGenerator.cs +++ b/src/Refitter.Core/RefitInterfaceGenerator.cs @@ -108,11 +108,32 @@ protected string GetTypeName(OpenApiOperation operation) return type is null or "void" ? GetAsyncOperationType(true) : $"{GetAsyncOperationType(false)}<{WellKnownNamesspaces.TrimImportedNamespaces(type)}>"; } - var returnTypeParameter = - (new[] { "200", "201", "203", "206" }) - .Where(operation.Responses.ContainsKey) - .Select(code => GetTypeName(code, operation)) - .FirstOrDefault(); + // First check for explicit success status codes + var successCodes = new[] { "200", "201", "203", "206" }; + var returnTypeParameter = successCodes + .Where(operation.Responses.ContainsKey) + .Select(code => GetTypeName(code, operation)) + .FirstOrDefault(); + + // If no explicit success codes found, check for range responses in precedence order + string[] ranges = { "2XX", "200", "1XX", "3XX", "4XX", "5XX" }; + if (returnTypeParameter == null) + { + foreach (var rangeCode in ranges) + { + if (operation.Responses.ContainsKey(rangeCode)) + { + returnTypeParameter = GetTypeName(rangeCode, operation); + break; + } + } + } + + // If no success codes or ranges found, check for default response + if (returnTypeParameter == null && operation.Responses.ContainsKey("default")) + { + returnTypeParameter = GetTypeName("default", operation); + } return GetReturnType(returnTypeParameter); } From 7d15ddae476bfb3e38ef36f0813f1133a3889de7 Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Thu, 24 Apr 2025 20:08:22 +0000 Subject: [PATCH 2/4] Add unit tests for default and range response generation in OpenAPI --- .../Examples/DefaultResponseObjectTests.cs | 189 ++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 src/Refitter.Tests/Examples/DefaultResponseObjectTests.cs diff --git a/src/Refitter.Tests/Examples/DefaultResponseObjectTests.cs b/src/Refitter.Tests/Examples/DefaultResponseObjectTests.cs new file mode 100644 index 000000000..a7ec1e42c --- /dev/null +++ b/src/Refitter.Tests/Examples/DefaultResponseObjectTests.cs @@ -0,0 +1,189 @@ +using FluentAssertions; +using Refitter.Core; +using Refitter.Tests.Build; +using Xunit; + +namespace Refitter.Tests.Examples; + +public class DefaultResponseObjectTests +{ + private const string OpenApiSpec = @" +openapi: '3.0.0' +info: + version: '1.0.0' + title: 'Default Response API' + description: 'An API that uses default responses and range responses' +servers: + - url: 'https://api.example.com/v1' +paths: + /users: + get: + tags: + - 'Users' + summary: 'Get users list using default response' + description: 'Returns a list of users using default response type' + operationId: 'getUsersWithDefault' + responses: + default: + description: 'List of users with unknown status code' + content: + application/json: + schema: + type: 'array' + items: + $ref: '#/components/schemas/User' + /users/{id}: + get: + tags: + - 'Users' + summary: 'Get user by ID using default response' + description: 'Returns a user by ID using default response type' + operationId: 'getUserByIdWithDefault' + parameters: + - name: 'id' + in: 'path' + description: 'User ID' + required: true + schema: + type: 'string' + responses: + default: + description: 'User data with unknown status code' + content: + application/json: + schema: + $ref: '#/components/schemas/User' + /categories: + get: + tags: + - 'Categories' + summary: 'Get categories using 2XX range response' + description: 'Returns a list of categories using 2XX range response' + operationId: 'getCategoriesWithRange' + responses: + 2XX: + description: 'List of categories with 2XX status code' + content: + application/json: + schema: + type: 'array' + items: + $ref: '#/components/schemas/Category' + /categories/{id}: + get: + tags: + - 'Categories' + summary: 'Get category by ID using 2XX range response' + description: 'Returns a category by ID using 2XX range response' + operationId: 'getCategoryByIdWithRange' + parameters: + - name: 'id' + in: 'path' + description: 'Category ID' + required: true + schema: + type: 'string' + responses: + 2XX: + description: 'Category data with 2XX status code' + content: + application/json: + schema: + $ref: '#/components/schemas/Category' +components: + schemas: + User: + type: 'object' + properties: + id: + type: 'string' + name: + type: 'string' + email: + type: 'string' + format: 'email' + Category: + type: 'object' + properties: + id: + type: 'string' + name: + type: 'string' +"; + + [Fact] + public async Task Can_Generate_Code() + { + string generatedCode = await GenerateCode(); + generatedCode.Should().NotBeNullOrWhiteSpace(); + } + + [Fact] + public async Task Can_Build_Generated_Code() + { + string generatedCode = await GenerateCode(); + BuildHelper + .BuildCSharp(generatedCode) + .Should() + .BeTrue(); + } + + [Fact] + public async Task Should_Generate_Default_Response_Return_Types() + { + string generatedCode = await GenerateCode(); + generatedCode.Should().Contain("Task GetUserByIdWithDefault(string id);"); + generatedCode.Should().Contain("Task> GetUsersWithDefault();"); + } + + [Fact] + public async Task Should_Generate_Range_Response_Return_Types() + { + string generatedCode = await GenerateCode(); + generatedCode.Should().Contain("Task GetCategoryByIdWithRange(string id);"); + generatedCode.Should().Contain("Task> GetCategoriesWithRange();"); + } + + [Fact] + public async Task Should_Generate_User_Contract() + { + string generatedCode = await GenerateCode(); + generatedCode.Should().Contain("public partial class User"); + generatedCode.Should().Contain("public string Id { get; set; }"); + generatedCode.Should().Contain("public string Name { get; set; }"); + generatedCode.Should().Contain("public string Email { get; set; }"); + } + + [Fact] + public async Task Should_Generate_Category_Contract() + { + string generatedCode = await GenerateCode(); + generatedCode.Should().Contain("public partial class Category"); + generatedCode.Should().Contain("public string Id { get; set; }"); + generatedCode.Should().Contain("public string Name { get; set; }"); + } + + private static async Task GenerateCode() + { + var swaggerFile = await CreateSwaggerFile(OpenApiSpec); + var settings = new RefitGeneratorSettings + { + OpenApiPath = swaggerFile, + UseCancellationTokens = false + }; + + var sut = await RefitGenerator.CreateAsync(settings); + var generatedCode = sut.Generate(); + return generatedCode; + } + + 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; + } +} From bae0de4d901459efc1e8ada3e3584506f99a26b2 Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Fri, 25 Apr 2025 00:50:55 +0200 Subject: [PATCH 3/4] Fix assert to check for ICollection instead of IEnumerable --- src/Refitter.Tests/Examples/DefaultResponseObjectTests.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Refitter.Tests/Examples/DefaultResponseObjectTests.cs b/src/Refitter.Tests/Examples/DefaultResponseObjectTests.cs index a7ec1e42c..a4f88e024 100644 --- a/src/Refitter.Tests/Examples/DefaultResponseObjectTests.cs +++ b/src/Refitter.Tests/Examples/DefaultResponseObjectTests.cs @@ -133,7 +133,7 @@ public async Task Should_Generate_Default_Response_Return_Types() { string generatedCode = await GenerateCode(); generatedCode.Should().Contain("Task GetUserByIdWithDefault(string id);"); - generatedCode.Should().Contain("Task> GetUsersWithDefault();"); + generatedCode.Should().Contain("Task> GetUsersWithDefault();"); } [Fact] @@ -141,7 +141,7 @@ public async Task Should_Generate_Range_Response_Return_Types() { string generatedCode = await GenerateCode(); generatedCode.Should().Contain("Task GetCategoryByIdWithRange(string id);"); - generatedCode.Should().Contain("Task> GetCategoriesWithRange();"); + generatedCode.Should().Contain("Task> GetCategoriesWithRange();"); } [Fact] @@ -166,8 +166,8 @@ public async Task Should_Generate_Category_Contract() private static async Task GenerateCode() { var swaggerFile = await CreateSwaggerFile(OpenApiSpec); - var settings = new RefitGeneratorSettings - { + var settings = new RefitGeneratorSettings + { OpenApiPath = swaggerFile, UseCancellationTokens = false }; From 4ec0ea9f1be1487eaa6b2094b527b90ff5c15abc Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Fri, 25 Apr 2025 09:35:27 +0200 Subject: [PATCH 4/4] Reorder HTTP response range codes for precedence --- src/Refitter.Core/RefitInterfaceGenerator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Refitter.Core/RefitInterfaceGenerator.cs b/src/Refitter.Core/RefitInterfaceGenerator.cs index 1bd1cf94b..7bfcc9984 100644 --- a/src/Refitter.Core/RefitInterfaceGenerator.cs +++ b/src/Refitter.Core/RefitInterfaceGenerator.cs @@ -116,7 +116,7 @@ protected string GetTypeName(OpenApiOperation operation) .FirstOrDefault(); // If no explicit success codes found, check for range responses in precedence order - string[] ranges = { "2XX", "200", "1XX", "3XX", "4XX", "5XX" }; + string[] ranges = { "1XX", "2XX", "3XX", "4XX", "5XX" }; if (returnTypeParameter == null) { foreach (var rangeCode in ranges)