diff --git a/src/Refitter.Core/CSharpClientGeneratorFactory.cs b/src/Refitter.Core/CSharpClientGeneratorFactory.cs index cd86d2d5d..279b69585 100644 --- a/src/Refitter.Core/CSharpClientGeneratorFactory.cs +++ b/src/Refitter.Core/CSharpClientGeneratorFactory.cs @@ -19,6 +19,7 @@ public CustomCSharpClientGenerator Create() } } + FixMissingTypesWithIntegerFormat(); ApplyCustomIntegerType(); var csharpClientGeneratorSettings = new CSharpClientGeneratorSettings @@ -64,6 +65,104 @@ public CustomCSharpClientGenerator Create() return generator; } + private void FixMissingTypesWithIntegerFormat() + { + ProcessSchemasForMissingTypes(document.Components.Schemas); + + foreach (var path in document.Paths) + { + if (path.Value == null) continue; + + foreach (var operation in path.Value.Values) + { + if (operation == null) continue; + + foreach (var parameter in operation.Parameters) + { + FixSchemaTypeFromFormat(parameter.ActualSchema); + } + + if (operation.RequestBody?.Content != null) + { + foreach (var content in operation.RequestBody.Content.Values) + { + ProcessSchemaForMissingTypes(content.Schema); + } + } + + foreach (var response in operation.Responses.Values) + { + if (response.Content != null) + { + foreach (var content in response.Content.Values) + { + ProcessSchemaForMissingTypes(content.Schema); + } + } + } + } + } + } + + private void ProcessSchemasForMissingTypes(IDictionary schemas) + { + foreach (var schema in schemas.Values) + { + ProcessSchemaForMissingTypes(schema); + } + } + + private void ProcessSchemaForMissingTypes(JsonSchema? schema) + { + if (schema == null) return; + + var actualSchema = schema.ActualSchema; + + FixSchemaTypeFromFormat(actualSchema); + + foreach (var property in actualSchema.Properties.Values) + { + ProcessSchemaForMissingTypes(property); + } + + if (actualSchema.Item != null) + { + ProcessSchemaForMissingTypes(actualSchema.Item); + } + + if (actualSchema.AdditionalPropertiesSchema != null) + { + ProcessSchemaForMissingTypes(actualSchema.AdditionalPropertiesSchema); + } + + var subSchemas = actualSchema.AllOf + .Concat(actualSchema.OneOf) + .Concat(actualSchema.AnyOf) + .ToArray(); + + foreach (var subSchema in subSchemas) + { + ProcessSchemaForMissingTypes(subSchema); + } + } + + private void FixSchemaTypeFromFormat(JsonSchema schema) + { + // If type is not set but format indicates a numeric type (int32, int64, float, double), set the type based on format + if ((schema.Type == JsonObjectType.None || schema.Type == JsonObjectType.Null) && + !string.IsNullOrEmpty(schema.Format)) + { + if (schema.Format == "int32" || schema.Format == "int64") + { + schema.Type = JsonObjectType.Integer; + } + else if (schema.Format == "float" || schema.Format == "double") + { + schema.Type = JsonObjectType.Number; + } + } + } + private void ApplyCustomIntegerType() { var customIntegerType = settings.CodeGeneratorSettings?.IntegerType ?? IntegerType.Int32; diff --git a/src/Refitter.Tests/Examples/Int32FormatWithPatternTests.cs b/src/Refitter.Tests/Examples/Int32FormatWithPatternTests.cs new file mode 100644 index 000000000..53c8918f4 --- /dev/null +++ b/src/Refitter.Tests/Examples/Int32FormatWithPatternTests.cs @@ -0,0 +1,93 @@ +using FluentAssertions; +using Refitter.Core; +using Refitter.Tests.Build; +using Refitter.Tests.TestUtilities; +using TUnit.Core; + +namespace Refitter.Tests.Examples; + +public class Int32FormatWithPatternTests +{ + private const string OpenApiSpec = @" +openapi: 3.0.4 +info: + version: '1.0.0' + title: 'WeatherForecast API' +servers: + - url: 'https://api.example.com/v1' +paths: + /weatherforecast: + get: + operationId: 'getWeatherForecast' + responses: + '200': + description: 'Success' + content: + application/json: + schema: + type: 'array' + items: + $ref: '#/components/schemas/WeatherForecast' +components: + schemas: + WeatherForecast: + type: object + properties: + date: + type: string + format: date + temperatureC: + pattern: '^-?(?:0|[1-9]\\d*)$' + format: int32 + temperatureF: + pattern: '^-?(?:0|[1-9]\\d*)$' + format: int32 + summary: + type: string + nullable: true +"; + + [Test] + public async Task Can_Generate_Code() + { + string generatedCode = await GenerateCode(); + generatedCode.Should().NotBeNullOrWhiteSpace(); + } + + [Test] + public async Task Should_Generate_Int_For_Properties_With_Format_Int32_And_Pattern() + { + string generatedCode = await GenerateCode(); + + // Should generate int, not object + generatedCode.Should().Contain("public int TemperatureC { get; set; }"); + generatedCode.Should().Contain("public int TemperatureF { get; set; }"); + + // Should not generate as object + generatedCode.Should().NotContain("public object TemperatureC { get; set; }"); + generatedCode.Should().NotContain("public object TemperatureF { get; set; }"); + } + + [Test] + public async Task Can_Build_Generated_Code() + { + string generatedCode = await GenerateCode(); + BuildHelper + .BuildCSharp(generatedCode) + .Should() + .BeTrue(); + } + + private static async Task GenerateCode() + { + var swaggerFile = await SwaggerFileHelper.CreateSwaggerFile(OpenApiSpec); + var settings = new RefitGeneratorSettings + { + OpenApiPath = swaggerFile, + }; + + var sut = await RefitGenerator.CreateAsync(settings); + var generatedCode = sut.Generate(); + return generatedCode; + } +} diff --git a/src/Refitter.Tests/Examples/NumericFormatWithPatternTests.cs b/src/Refitter.Tests/Examples/NumericFormatWithPatternTests.cs new file mode 100644 index 000000000..fa9edddfd --- /dev/null +++ b/src/Refitter.Tests/Examples/NumericFormatWithPatternTests.cs @@ -0,0 +1,110 @@ +using FluentAssertions; +using Refitter.Core; +using Refitter.Tests.Build; +using Refitter.Tests.TestUtilities; + +namespace Refitter.Tests.Examples +{ + public class NumericFormatWithPatternTests + { + private const string OpenApiSpec = @" +openapi: 3.0.4 +info: + version: '1.0.0' + title: 'Sensor Data API' +servers: + - url: 'https://api.example.com/v1' +paths: + /sensor: + get: + operationId: 'getSensorData' + responses: + '200': + description: 'Success' + content: + application/json: + schema: + $ref: '#/components/schemas/SensorData' +components: + schemas: + SensorData: + type: object + properties: + temperature: + pattern: '^-?\\d+\\.\\d+$' + format: float + humidity: + pattern: '^-?\\d+\\.\\d+$' + format: double + pressure: + pattern: '^-?(?:0|[1-9]\\d*)$' + format: int64 +"; + + [Test] + public async Task Can_Generate_Code() + { + string generatedCode = await GenerateCode(); + generatedCode.Should().NotBeNullOrWhiteSpace(); + } + + [Test] + public async Task Should_Generate_Float_For_Properties_With_Format_Float_And_Pattern() + { + string generatedCode = await GenerateCode(); + + // Should generate float, not object + generatedCode.Should().Contain("public float Temperature { get; set; }"); + + // Should not generate as object + generatedCode.Should().NotContain("public object Temperature { get; set; }"); + } + + [Test] + public async Task Should_Generate_Double_For_Properties_With_Format_Double_And_Pattern() + { + string generatedCode = await GenerateCode(); + + // Should generate double, not object + generatedCode.Should().Contain("public double Humidity { get; set; }"); + + // Should not generate as object + generatedCode.Should().NotContain("public object Humidity { get; set; }"); + } + + [Test] + public async Task Should_Generate_Long_For_Properties_With_Format_Int64_And_Pattern() + { + string generatedCode = await GenerateCode(); + + // Should generate long, not object + generatedCode.Should().Contain("public long Pressure { get; set; }"); + + // Should not generate as object + generatedCode.Should().NotContain("public object Pressure { get; set; }"); + } + + [Test] + public async Task Can_Build_Generated_Code() + { + string generatedCode = await GenerateCode(); + BuildHelper + .BuildCSharp(generatedCode) + .Should() + .BeTrue(); + } + + private static async Task GenerateCode() + { + var swaggerFile = await SwaggerFileHelper.CreateSwaggerFile(OpenApiSpec); + var settings = new RefitGeneratorSettings + { + OpenApiPath = swaggerFile, + }; + + var sut = await RefitGenerator.CreateAsync(settings); + var generatedCode = sut.Generate(); + return generatedCode; + } + } +}