From 01f7e142c413c2a1fd9398a0d2d10f09b942f3ad Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Nov 2025 13:55:32 +0000 Subject: [PATCH 1/5] Initial plan From 797b1384ec6a8f149d49ff6743410f756796cdc4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Nov 2025 14:04:23 +0000 Subject: [PATCH 2/5] Address code review feedback for optional parameter default values Co-authored-by: christianhelle <710400+christianhelle@users.noreply.github.com> --- src/Refitter.Core/ParameterExtractor.cs | 30 +- ...rametersWithDefaultValuesEdgeCasesTests.cs | 268 ++++++++++++++++++ 2 files changed, 295 insertions(+), 3 deletions(-) create mode 100644 src/Refitter.Tests/Examples/OptionalParametersWithDefaultValuesEdgeCasesTests.cs diff --git a/src/Refitter.Core/ParameterExtractor.cs b/src/Refitter.Core/ParameterExtractor.cs index 003d0ef9..67eebf97 100644 --- a/src/Refitter.Core/ParameterExtractor.cs +++ b/src/Refitter.Core/ParameterExtractor.cs @@ -145,7 +145,7 @@ private static string GetDefaultValueForParameter(string parameterString, IColle var variableName = parts[parts.Length - 1].TrimEnd(';', ','); var parameterModel = parameterModels.FirstOrDefault(p => p.VariableName == variableName); - return parameterModel?.Schema?.Default != null + return parameterModel?.Schema?.Default != null && !string.IsNullOrEmpty(parameterModel?.Type) ? FormatDefaultValue(parameterModel.Schema.Default, parameterModel.Type) : "default"; } @@ -160,12 +160,36 @@ private static string FormatDefaultValue(object? defaultValue, string parameterT return type switch { "bool" => defaultValue.ToString()?.ToLowerInvariant() ?? "default", - "string" => $"\"{defaultValue}\"", - _ when IsNumericType(type) => defaultValue.ToString() ?? "default", + "string" => $"\"{EscapeString(defaultValue.ToString() ?? string.Empty)}\"", + _ when IsNumericType(type) => FormatNumericValue(defaultValue, type), _ => "default" }; } + private static string EscapeString(string value) + { + return value + .Replace("\\", "\\\\") + .Replace("\"", "\\\"") + .Replace("\n", "\\n") + .Replace("\r", "\\r") + .Replace("\t", "\\t"); + } + + private static string FormatNumericValue(object defaultValue, string type) + { + var numericString = defaultValue is IFormattable formattable + ? formattable.ToString(null, CultureInfo.InvariantCulture) + : (defaultValue.ToString() ?? "default"); + + return type switch + { + "float" or "Single" => $"{numericString}f", + "decimal" or "Decimal" => $"{numericString}m", + _ => numericString + }; + } + private static bool IsNumericType(string type) { return type is "int" or "Int32" or "long" or "Int64" or "short" or "Int16" diff --git a/src/Refitter.Tests/Examples/OptionalParametersWithDefaultValuesEdgeCasesTests.cs b/src/Refitter.Tests/Examples/OptionalParametersWithDefaultValuesEdgeCasesTests.cs new file mode 100644 index 00000000..2a69606c --- /dev/null +++ b/src/Refitter.Tests/Examples/OptionalParametersWithDefaultValuesEdgeCasesTests.cs @@ -0,0 +1,268 @@ +using FluentAssertions; +using Refitter.Core; +using Refitter.Tests.Build; +using Refitter.Tests.TestUtilities; +using Xunit; + +namespace Refitter.Tests.Examples; + +public class OptionalParametersWithDefaultValuesEdgeCasesTests +{ + [Fact] + public async Task String_Default_Values_Should_Be_Escaped() + { + const string openApiSpec = + """ + { + "openapi": "3.0.1", + "info": { + "title": "Test API", + "version": "v1" + }, + "paths": { + "/api/test": { + "get": { + "operationId": "TestEscaping", + "parameters": [ + { + "name": "quotedString", + "in": "query", + "required": false, + "schema": { + "type": "string", + "default": "value with \"quotes\"" + } + }, + { + "name": "backslashString", + "in": "query", + "required": false, + "schema": { + "type": "string", + "default": "path\\to\\file" + } + }, + { + "name": "newlineString", + "in": "query", + "required": false, + "schema": { + "type": "string", + "default": "line1\nline2" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + } + } + } + } + """; + + string generatedCode = await GenerateCode(openApiSpec); + + // Verify strings are properly escaped + generatedCode.Should().Contain("string? quotedString = \"value with \\\"quotes\\\"\""); + generatedCode.Should().Contain("string? backslashString = \"path\\\\to\\\\file\""); + generatedCode.Should().Contain("string? newlineString = \"line1\\nline2\""); + } + + [Fact] + public async Task Float_Default_Values_Should_Have_Type_Suffix() + { + const string openApiSpec = + """ + { + "openapi": "3.0.1", + "info": { + "title": "Test API", + "version": "v1" + }, + "paths": { + "/api/test": { + "get": { + "operationId": "TestFloat", + "parameters": [ + { + "name": "floatValue", + "in": "query", + "required": false, + "schema": { + "type": "number", + "format": "float", + "default": 1.5 + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + } + } + } + } + """; + + string generatedCode = await GenerateCode(openApiSpec); + + // Verify float has 'f' suffix + generatedCode.Should().Contain("float? floatValue = 1.5f"); + } + + [Fact] + public async Task Decimal_Default_Values_Should_Have_Type_Suffix() + { + const string openApiSpec = + """ + { + "openapi": "3.0.1", + "info": { + "title": "Test API", + "version": "v1" + }, + "paths": { + "/api/test": { + "get": { + "operationId": "TestDecimal", + "parameters": [ + { + "name": "decimalValue", + "in": "query", + "required": false, + "schema": { + "type": "number", + "format": "decimal", + "default": 99.99 + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + } + } + } + } + """; + + string generatedCode = await GenerateCode(openApiSpec); + + // Verify decimal has 'm' suffix + generatedCode.Should().Contain("decimal? decimalValue = 99.99m"); + } + + [Fact] + public async Task Generated_Code_With_Escaped_Strings_Should_Build() + { + const string openApiSpec = + """ + { + "openapi": "3.0.1", + "info": { + "title": "Test API", + "version": "v1" + }, + "paths": { + "/api/test": { + "get": { + "operationId": "TestBuild", + "parameters": [ + { + "name": "escapedString", + "in": "query", + "required": false, + "schema": { + "type": "string", + "default": "value with \"quotes\" and \\backslashes\\" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + } + } + } + } + """; + + string generatedCode = await GenerateCode(openApiSpec); + BuildHelper.BuildCSharp(generatedCode).Should().BeTrue(); + } + + [Fact] + public async Task Generated_Code_With_Float_Decimal_Should_Build() + { + const string openApiSpec = + """ + { + "openapi": "3.0.1", + "info": { + "title": "Test API", + "version": "v1" + }, + "paths": { + "/api/test": { + "get": { + "operationId": "TestBuild", + "parameters": [ + { + "name": "floatValue", + "in": "query", + "required": false, + "schema": { + "type": "number", + "format": "float", + "default": 1.5 + } + }, + { + "name": "decimalValue", + "in": "query", + "required": false, + "schema": { + "type": "number", + "format": "decimal", + "default": 99.99 + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + } + } + } + } + """; + + string generatedCode = await GenerateCode(openApiSpec); + BuildHelper.BuildCSharp(generatedCode).Should().BeTrue(); + } + + private static async Task GenerateCode(string openApiSpec) + { + var swaggerFile = await SwaggerFileHelper.CreateSwaggerFile(openApiSpec); + var settings = new RefitGeneratorSettings + { + OpenApiPath = swaggerFile, + OptionalParameters = true + }; + + var sut = await RefitGenerator.CreateAsync(settings); + var code = sut.Generate(); + return code; + } +} From e17d7321d46e23cf444a7af83aba0ec1beaeae1a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 9 Nov 2025 09:24:37 +0000 Subject: [PATCH 3/5] Fix SonarCloud issues: optimize string escaping and fix null reference warning Co-authored-by: christianhelle <710400+christianhelle@users.noreply.github.com> --- src/Refitter.Core/ParameterExtractor.cs | 40 +++++++++++++++++++------ 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/src/Refitter.Core/ParameterExtractor.cs b/src/Refitter.Core/ParameterExtractor.cs index 67eebf97..6b1dd033 100644 --- a/src/Refitter.Core/ParameterExtractor.cs +++ b/src/Refitter.Core/ParameterExtractor.cs @@ -145,9 +145,11 @@ private static string GetDefaultValueForParameter(string parameterString, IColle var variableName = parts[parts.Length - 1].TrimEnd(';', ','); var parameterModel = parameterModels.FirstOrDefault(p => p.VariableName == variableName); - return parameterModel?.Schema?.Default != null && !string.IsNullOrEmpty(parameterModel?.Type) - ? FormatDefaultValue(parameterModel.Schema.Default, parameterModel.Type) - : "default"; + if (parameterModel?.Schema?.Default != null && !string.IsNullOrEmpty(parameterModel.Type)) + { + return FormatDefaultValue(parameterModel.Schema.Default, parameterModel.Type); + } + return "default"; } private static string FormatDefaultValue(object? defaultValue, string parameterType) @@ -168,12 +170,32 @@ _ when IsNumericType(type) => FormatNumericValue(defaultValue, type), private static string EscapeString(string value) { - return value - .Replace("\\", "\\\\") - .Replace("\"", "\\\"") - .Replace("\n", "\\n") - .Replace("\r", "\\r") - .Replace("\t", "\\t"); + var sb = new StringBuilder(value.Length * 2); + foreach (var c in value) + { + switch (c) + { + case '\\': + sb.Append("\\\\"); + break; + case '"': + sb.Append("\\\""); + break; + case '\n': + sb.Append("\\n"); + break; + case '\r': + sb.Append("\\r"); + break; + case '\t': + sb.Append("\\t"); + break; + default: + sb.Append(c); + break; + } + } + return sb.ToString(); } private static string FormatNumericValue(object defaultValue, string type) From 7402111136d4104330d7036f76d9450cb0cd58b9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 9 Nov 2025 09:52:18 +0000 Subject: [PATCH 4/5] Add comprehensive test coverage for string escaping and numeric literal formatting Co-authored-by: christianhelle <710400+christianhelle@users.noreply.github.com> --- ...amicQueryStringParametersEdgeCasesTests.cs | 413 ++++++++++++++++++ ...rametersWithDefaultValuesEdgeCasesTests.cs | 258 +++++++++++ 2 files changed, 671 insertions(+) create mode 100644 src/Refitter.Tests/Examples/DynamicQueryStringParametersEdgeCasesTests.cs diff --git a/src/Refitter.Tests/Examples/DynamicQueryStringParametersEdgeCasesTests.cs b/src/Refitter.Tests/Examples/DynamicQueryStringParametersEdgeCasesTests.cs new file mode 100644 index 00000000..208b4079 --- /dev/null +++ b/src/Refitter.Tests/Examples/DynamicQueryStringParametersEdgeCasesTests.cs @@ -0,0 +1,413 @@ +using FluentAssertions; +using Refitter.Core; +using Refitter.Tests.Build; +using Refitter.Tests.TestUtilities; +using Xunit; + +namespace Refitter.Tests.Examples; + +public class DynamicQueryStringParametersEdgeCasesTests +{ + [Fact] + public async Task Dynamic_QueryString_With_Escaped_String_Defaults() + { + const string openApiSpec = + """ + { + "openapi": "3.0.1", + "info": { + "title": "Test API", + "version": "v1" + }, + "paths": { + "/api/test": { + "get": { + "operationId": "TestEscaping", + "parameters": [ + { + "name": "requiredParam", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "quotedString", + "in": "query", + "required": false, + "schema": { + "type": "string", + "default": "value with \"quotes\"" + } + }, + { + "name": "backslashString", + "in": "query", + "required": false, + "schema": { + "type": "string", + "default": "path\\to\\file" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + } + } + } + } + """; + + string generatedCode = await GenerateCode(openApiSpec); + + // Verify strings are properly escaped in dynamic query string class + generatedCode.Should().Contain("string? QuotedString { get; set; } = \"value with \\\"quotes\\\"\""); + generatedCode.Should().Contain("string? BackslashString { get; set; } = \"path\\\\to\\\\file\""); + } + + [Fact] + public async Task Dynamic_QueryString_With_All_Escape_Characters() + { + const string openApiSpec = + """ + { + "openapi": "3.0.1", + "info": { + "title": "Test API", + "version": "v1" + }, + "paths": { + "/api/test": { + "get": { + "operationId": "TestAllEscapes", + "parameters": [ + { + "name": "requiredParam", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "newlineString", + "in": "query", + "required": false, + "schema": { + "type": "string", + "default": "line1\nline2" + } + }, + { + "name": "carriageReturnString", + "in": "query", + "required": false, + "schema": { + "type": "string", + "default": "line1\rline2" + } + }, + { + "name": "tabString", + "in": "query", + "required": false, + "schema": { + "type": "string", + "default": "col1\tcol2" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + } + } + } + } + """; + + string generatedCode = await GenerateCode(openApiSpec); + + // Verify all special characters are properly escaped + generatedCode.Should().Contain("string? NewlineString { get; set; } = \"line1\\nline2\""); + generatedCode.Should().Contain("string? CarriageReturnString { get; set; } = \"line1\\rline2\""); + generatedCode.Should().Contain("string? TabString { get; set; } = \"col1\\tcol2\""); + } + + [Fact] + public async Task Dynamic_QueryString_With_Float_And_Decimal_Defaults() + { + const string openApiSpec = + """ + { + "openapi": "3.0.1", + "info": { + "title": "Test API", + "version": "v1" + }, + "paths": { + "/api/test": { + "get": { + "operationId": "TestNumeric", + "parameters": [ + { + "name": "requiredParam", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "floatValue", + "in": "query", + "required": false, + "schema": { + "type": "number", + "format": "float", + "default": 1.5 + } + }, + { + "name": "decimalValue", + "in": "query", + "required": false, + "schema": { + "type": "number", + "format": "decimal", + "default": 99.99 + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + } + } + } + } + """; + + string generatedCode = await GenerateCode(openApiSpec); + + // Verify numeric types have proper suffixes + generatedCode.Should().Contain("float? FloatValue { get; set; } = 1.5f"); + generatedCode.Should().Contain("decimal? DecimalValue { get; set; } = 99.99m"); + } + + [Fact] + public async Task Dynamic_QueryString_With_Boolean_Defaults() + { + const string openApiSpec = + """ + { + "openapi": "3.0.1", + "info": { + "title": "Test API", + "version": "v1" + }, + "paths": { + "/api/test": { + "get": { + "operationId": "TestBool", + "parameters": [ + { + "name": "requiredParam", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "isEnabled", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "default": true + } + }, + { + "name": "isDisabled", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "default": false + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + } + } + } + } + """; + + string generatedCode = await GenerateCode(openApiSpec); + + // Verify booleans are lowercase + generatedCode.Should().Contain("bool? IsEnabled { get; set; } = true"); + generatedCode.Should().Contain("bool? IsDisabled { get; set; } = false"); + } + + [Fact] + public async Task Dynamic_QueryString_With_Integer_Defaults() + { + const string openApiSpec = + """ + { + "openapi": "3.0.1", + "info": { + "title": "Test API", + "version": "v1" + }, + "paths": { + "/api/test": { + "get": { + "operationId": "TestInt", + "parameters": [ + { + "name": "requiredParam", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "intValue", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 42 + } + }, + { + "name": "longValue", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int64", + "default": 9999 + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + } + } + } + } + """; + + string generatedCode = await GenerateCode(openApiSpec); + + // Verify integers don't have suffixes + generatedCode.Should().Contain("int? IntValue { get; set; } = 42"); + generatedCode.Should().Contain("long? LongValue { get; set; } = 9999"); + } + + [Fact] + public async Task Dynamic_QueryString_Generated_Code_Should_Build() + { + const string openApiSpec = + """ + { + "openapi": "3.0.1", + "info": { + "title": "Test API", + "version": "v1" + }, + "paths": { + "/api/test": { + "get": { + "operationId": "TestBuild", + "parameters": [ + { + "name": "requiredParam", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "escapedString", + "in": "query", + "required": false, + "schema": { + "type": "string", + "default": "value with \"quotes\" and \\backslashes\\" + } + }, + { + "name": "floatValue", + "in": "query", + "required": false, + "schema": { + "type": "number", + "format": "float", + "default": 1.5 + } + }, + { + "name": "decimalValue", + "in": "query", + "required": false, + "schema": { + "type": "number", + "format": "decimal", + "default": 99.99 + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + } + } + } + } + """; + + string generatedCode = await GenerateCode(openApiSpec); + BuildHelper.BuildCSharp(generatedCode).Should().BeTrue(); + } + + private static async Task GenerateCode(string openApiSpec) + { + var swaggerFile = await SwaggerFileHelper.CreateSwaggerFile(openApiSpec); + var settings = new RefitGeneratorSettings + { + OpenApiPath = swaggerFile, + OptionalParameters = true, + UseDynamicQuerystringParameters = true + }; + + var sut = await RefitGenerator.CreateAsync(settings); + var code = sut.Generate(); + return code; + } +} diff --git a/src/Refitter.Tests/Examples/OptionalParametersWithDefaultValuesEdgeCasesTests.cs b/src/Refitter.Tests/Examples/OptionalParametersWithDefaultValuesEdgeCasesTests.cs index 2a69606c..adb122cb 100644 --- a/src/Refitter.Tests/Examples/OptionalParametersWithDefaultValuesEdgeCasesTests.cs +++ b/src/Refitter.Tests/Examples/OptionalParametersWithDefaultValuesEdgeCasesTests.cs @@ -71,6 +71,69 @@ public async Task String_Default_Values_Should_Be_Escaped() generatedCode.Should().Contain("string? newlineString = \"line1\\nline2\""); } + [Fact] + public async Task String_Default_Values_With_All_Escape_Characters_Should_Be_Escaped() + { + const string openApiSpec = + """ + { + "openapi": "3.0.1", + "info": { + "title": "Test API", + "version": "v1" + }, + "paths": { + "/api/test": { + "get": { + "operationId": "TestAllEscapes", + "parameters": [ + { + "name": "carriageReturnString", + "in": "query", + "required": false, + "schema": { + "type": "string", + "default": "line1\rline2" + } + }, + { + "name": "tabString", + "in": "query", + "required": false, + "schema": { + "type": "string", + "default": "col1\tcol2" + } + }, + { + "name": "allSpecialChars", + "in": "query", + "required": false, + "schema": { + "type": "string", + "default": "test\n\r\t\\\"" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + } + } + } + } + """; + + string generatedCode = await GenerateCode(openApiSpec); + + // Verify all special characters are properly escaped + generatedCode.Should().Contain("string? carriageReturnString = \"line1\\rline2\""); + generatedCode.Should().Contain("string? tabString = \"col1\\tcol2\""); + generatedCode.Should().Contain("string? allSpecialChars = \"test\\n\\r\\t\\\\\\\"\""); + } + [Fact] public async Task Float_Default_Values_Should_Have_Type_Suffix() { @@ -252,6 +315,201 @@ public async Task Generated_Code_With_Float_Decimal_Should_Build() BuildHelper.BuildCSharp(generatedCode).Should().BeTrue(); } + [Fact] + public async Task Integer_Default_Values_Should_Not_Have_Suffix() + { + const string openApiSpec = + """ + { + "openapi": "3.0.1", + "info": { + "title": "Test API", + "version": "v1" + }, + "paths": { + "/api/test": { + "get": { + "operationId": "TestInt", + "parameters": [ + { + "name": "intValue", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 42 + } + }, + { + "name": "longValue", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int64", + "default": 9999 + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + } + } + } + } + """; + + string generatedCode = await GenerateCode(openApiSpec); + + // Verify integers don't have suffixes + generatedCode.Should().Contain("int? intValue = 42"); + generatedCode.Should().Contain("long? longValue = 9999"); + } + + [Fact] + public async Task Double_Default_Values_Should_Not_Have_Suffix() + { + const string openApiSpec = + """ + { + "openapi": "3.0.1", + "info": { + "title": "Test API", + "version": "v1" + }, + "paths": { + "/api/test": { + "get": { + "operationId": "TestDouble", + "parameters": [ + { + "name": "doubleValue", + "in": "query", + "required": false, + "schema": { + "type": "number", + "format": "double", + "default": 3.14159 + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + } + } + } + } + """; + + string generatedCode = await GenerateCode(openApiSpec); + + // Verify double doesn't have suffix (default for floating point literals in C#) + generatedCode.Should().Contain("double? doubleValue = 3.14159"); + } + + [Fact] + public async Task Boolean_Default_Values_Should_Be_Lowercase() + { + const string openApiSpec = + """ + { + "openapi": "3.0.1", + "info": { + "title": "Test API", + "version": "v1" + }, + "paths": { + "/api/test": { + "get": { + "operationId": "TestBool", + "parameters": [ + { + "name": "isEnabled", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "default": true + } + }, + { + "name": "isDisabled", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "default": false + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + } + } + } + } + """; + + string generatedCode = await GenerateCode(openApiSpec); + + // Verify booleans are lowercase + generatedCode.Should().Contain("bool? isEnabled = true"); + generatedCode.Should().Contain("bool? isDisabled = false"); + } + + [Fact] + public async Task Empty_String_Default_Value_Should_Be_Handled() + { + const string openApiSpec = + """ + { + "openapi": "3.0.1", + "info": { + "title": "Test API", + "version": "v1" + }, + "paths": { + "/api/test": { + "get": { + "operationId": "TestEmpty", + "parameters": [ + { + "name": "emptyString", + "in": "query", + "required": false, + "schema": { + "type": "string", + "default": "" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + } + } + } + } + """; + + string generatedCode = await GenerateCode(openApiSpec); + + // Verify empty string is properly quoted + generatedCode.Should().Contain("string? emptyString = \"\""); + } + private static async Task GenerateCode(string openApiSpec) { var swaggerFile = await SwaggerFileHelper.CreateSwaggerFile(openApiSpec); From c7ca8f8ede8dc7d17a41d9f8711ebc92a2dfdfed Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 11 Nov 2025 21:31:14 +0000 Subject: [PATCH 5/5] Apply code review feedback: improve string escaping and numeric literal formatting Co-authored-by: christianhelle <710400+christianhelle@users.noreply.github.com> --- src/Refitter.Core/ParameterExtractor.cs | 28 ++- ...amicQueryStringParametersEdgeCasesTests.cs | 177 ++++++++++++++++++ ...rametersWithDefaultValuesEdgeCasesTests.cs | 116 +++++++++++- 3 files changed, 318 insertions(+), 3 deletions(-) diff --git a/src/Refitter.Core/ParameterExtractor.cs b/src/Refitter.Core/ParameterExtractor.cs index 6b1dd033..2e2310db 100644 --- a/src/Refitter.Core/ParameterExtractor.cs +++ b/src/Refitter.Core/ParameterExtractor.cs @@ -170,7 +170,7 @@ _ when IsNumericType(type) => FormatNumericValue(defaultValue, type), private static string EscapeString(string value) { - var sb = new StringBuilder(value.Length * 2); + var sb = new StringBuilder(value.Length + 10); foreach (var c in value) { switch (c) @@ -190,6 +190,18 @@ private static string EscapeString(string value) case '\t': sb.Append("\\t"); break; + case '\f': + sb.Append("\\f"); + break; + case '\v': + sb.Append("\\v"); + break; + case '\b': + sb.Append("\\b"); + break; + case '\0': + sb.Append("\\0"); + break; default: sb.Append(c); break; @@ -208,10 +220,24 @@ private static string FormatNumericValue(object defaultValue, string type) { "float" or "Single" => $"{numericString}f", "decimal" or "Decimal" => $"{numericString}m", + "double" or "Double" => FormatDoubleLiteral(numericString), + "long" or "Int64" => $"{numericString}L", + "ulong" or "UInt64" => $"{numericString}UL", + "uint" or "UInt32" => $"{numericString}U", _ => numericString }; } + private static string FormatDoubleLiteral(string numericString) + { + // If the string already contains a decimal point or exponent, return as-is + if (numericString.Contains('.') || numericString.Contains('e') || numericString.Contains('E')) + return numericString; + + // Otherwise, append .0 to make it a double literal + return numericString + ".0"; + } + private static bool IsNumericType(string type) { return type is "int" or "Int32" or "long" or "Int64" or "short" or "Int16" diff --git a/src/Refitter.Tests/Examples/DynamicQueryStringParametersEdgeCasesTests.cs b/src/Refitter.Tests/Examples/DynamicQueryStringParametersEdgeCasesTests.cs index 208b4079..7cb2133b 100644 --- a/src/Refitter.Tests/Examples/DynamicQueryStringParametersEdgeCasesTests.cs +++ b/src/Refitter.Tests/Examples/DynamicQueryStringParametersEdgeCasesTests.cs @@ -396,6 +396,183 @@ public async Task Dynamic_QueryString_Generated_Code_Should_Build() BuildHelper.BuildCSharp(generatedCode).Should().BeTrue(); } + [Fact] + public async Task Dynamic_QueryString_With_Mixed_Escape_Characters() + { + const string openApiSpec = + """ + { + "openapi": "3.0.1", + "info": { + "title": "Test API", + "version": "v1" + }, + "paths": { + "/api/test": { + "get": { + "operationId": "TestMixedEscapes", + "parameters": [ + { + "name": "requiredParam", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "complexString", + "in": "query", + "required": false, + "schema": { + "type": "string", + "default": "path\\to\\file with \"quotes\"\nand newlines" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + } + } + } + } + """; + + string generatedCode = await GenerateCode(openApiSpec); + + // Verify mixed escape sequences + generatedCode.Should().Contain("string? ComplexString { get; set; } = \"path\\\\to\\\\file with \\\"quotes\\\"\\nand newlines\""); + } + + [Fact] + public async Task Dynamic_QueryString_With_Long_And_ULong_Defaults() + { + const string openApiSpec = + """ + { + "openapi": "3.0.1", + "info": { + "title": "Test API", + "version": "v1" + }, + "paths": { + "/api/test": { + "get": { + "operationId": "TestLongTypes", + "parameters": [ + { + "name": "requiredParam", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "longValue", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int64", + "default": 3000000000 + } + }, + { + "name": "ulongValue", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "uint64", + "default": 5000000000 + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + } + } + } + } + """; + + string generatedCode = await GenerateCode(openApiSpec); + + // Verify numeric type suffixes + generatedCode.Should().Contain("long? LongValue { get; set; } = 3000000000L"); + generatedCode.Should().Contain("ulong? UlongValue { get; set; } = 5000000000UL"); + } + + [Fact] + public async Task Dynamic_QueryString_With_Double_Integer_Value() + { + const string openApiSpec = + """ + { + "openapi": "3.0.1", + "info": { + "title": "Test API", + "version": "v1" + }, + "paths": { + "/api/test": { + "get": { + "operationId": "TestDouble", + "parameters": [ + { + "name": "requiredParam", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "doubleFloatValue", + "in": "query", + "required": false, + "schema": { + "type": "number", + "format": "double", + "default": 3.14 + } + }, + { + "name": "doubleIntValue", + "in": "query", + "required": false, + "schema": { + "type": "number", + "format": "double", + "default": 10 + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + } + } + } + } + """; + + string generatedCode = await GenerateCode(openApiSpec); + + // Verify double formatting + generatedCode.Should().Contain("double? DoubleFloatValue { get; set; } = 3.14"); + generatedCode.Should().Contain("double? DoubleIntValue { get; set; } = 10.0"); + } + private static async Task GenerateCode(string openApiSpec) { var swaggerFile = await SwaggerFileHelper.CreateSwaggerFile(openApiSpec); diff --git a/src/Refitter.Tests/Examples/OptionalParametersWithDefaultValuesEdgeCasesTests.cs b/src/Refitter.Tests/Examples/OptionalParametersWithDefaultValuesEdgeCasesTests.cs index adb122cb..2dfd2577 100644 --- a/src/Refitter.Tests/Examples/OptionalParametersWithDefaultValuesEdgeCasesTests.cs +++ b/src/Refitter.Tests/Examples/OptionalParametersWithDefaultValuesEdgeCasesTests.cs @@ -371,7 +371,7 @@ public async Task Integer_Default_Values_Should_Not_Have_Suffix() } [Fact] - public async Task Double_Default_Values_Should_Not_Have_Suffix() + public async Task Double_Default_Values_Should_Have_Decimal_Point() { const string openApiSpec = """ @@ -395,6 +395,16 @@ public async Task Double_Default_Values_Should_Not_Have_Suffix() "format": "double", "default": 3.14159 } + }, + { + "name": "doubleIntValue", + "in": "query", + "required": false, + "schema": { + "type": "number", + "format": "double", + "default": 10 + } } ], "responses": { @@ -410,8 +420,10 @@ public async Task Double_Default_Values_Should_Not_Have_Suffix() string generatedCode = await GenerateCode(openApiSpec); - // Verify double doesn't have suffix (default for floating point literals in C#) + // Verify double with decimal point stays as-is generatedCode.Should().Contain("double? doubleValue = 3.14159"); + // Verify double with integer value gets .0 appended + generatedCode.Should().Contain("double? doubleIntValue = 10.0"); } [Fact] @@ -510,6 +522,106 @@ public async Task Empty_String_Default_Value_Should_Be_Handled() generatedCode.Should().Contain("string? emptyString = \"\""); } + [Fact] + public async Task Mixed_Escape_Sequences_Should_Be_Handled() + { + const string openApiSpec = + """ + { + "openapi": "3.0.1", + "info": { + "title": "Test API", + "version": "v1" + }, + "paths": { + "/api/test": { + "get": { + "operationId": "TestMixedEscapes", + "parameters": [ + { + "name": "complexString", + "in": "query", + "required": false, + "schema": { + "type": "string", + "default": "path\\to\\file with \"quotes\" and\nnewlines\tand tabs" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + } + } + } + } + """; + + string generatedCode = await GenerateCode(openApiSpec); + + // Verify all escape sequences are properly handled in combination + generatedCode.Should().Contain("string? complexString = \"path\\\\to\\\\file with \\\"quotes\\\" and\\nnewlines\\tand tabs\""); + } + + [Fact] + public async Task Long_And_ULong_Default_Values_Should_Have_Suffixes() + { + const string openApiSpec = + """ + { + "openapi": "3.0.1", + "info": { + "title": "Test API", + "version": "v1" + }, + "paths": { + "/api/test": { + "get": { + "operationId": "TestLong", + "parameters": [ + { + "name": "longValue", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int64", + "default": 3000000000 + } + }, + { + "name": "ulongValue", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "uint64", + "default": 5000000000 + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + } + } + } + } + """; + + string generatedCode = await GenerateCode(openApiSpec); + + // Verify long and ulong have proper suffixes + generatedCode.Should().Contain("long? longValue = 3000000000L"); + generatedCode.Should().Contain("ulong? ulongValue = 5000000000UL"); + } + + + private static async Task GenerateCode(string openApiSpec) { var swaggerFile = await SwaggerFileHelper.CreateSwaggerFile(openApiSpec);