diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 000000000..cc3250ced --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,13 @@ +# Refitter + +Refitter generates C# REST API clients — Refit interfaces and their contracts — +from OpenAPI specifications. It ships as a CLI tool, an MSBuild task, and a C# +source generator, all driven by the same `.refitter` settings. + +## Language + +**Parameter list**: +The ordered set of a generated Refit method's parameters, derived from an OpenAPI +operation's path, query, header, body, and form inputs, plus any trailing +request-options or cancellation-token argument. +_Avoid_: arguments, args diff --git a/src/Refitter.Core/Generation/InterfaceGenerator.cs b/src/Refitter.Core/Generation/InterfaceGenerator.cs index 30c95a5de..59d0607e7 100644 --- a/src/Refitter.Core/Generation/InterfaceGenerator.cs +++ b/src/Refitter.Core/Generation/InterfaceGenerator.cs @@ -19,8 +19,7 @@ internal InterfaceGenerator( RefitGeneratorSettings settings, OpenApiDocument document, CustomCSharpClientGenerator generator, - XmlDocumentationGenerator docGenerator, - IParameterExtractor? parameterExtractor = null) + XmlDocumentationGenerator docGenerator) : this( settings, document, @@ -28,7 +27,7 @@ internal InterfaceGenerator( docGenerator, new ReturnTypeGenerator(settings, generator), new MethodAttributeGenerator(settings, document), - new MethodSignatureGenerator(settings, parameterExtractor ?? new ParameterAggregator())) + new MethodSignatureGenerator(settings)) { } diff --git a/src/Refitter.Core/Generation/MethodSignatureGenerator.cs b/src/Refitter.Core/Generation/MethodSignatureGenerator.cs index 7f87e5e7e..81e2d168e 100644 --- a/src/Refitter.Core/Generation/MethodSignatureGenerator.cs +++ b/src/Refitter.Core/Generation/MethodSignatureGenerator.cs @@ -3,27 +3,22 @@ namespace Refitter.Core; -internal class MethodSignatureGenerator( - RefitGeneratorSettings settings, - IParameterExtractor? parameterExtractor = null) +internal class MethodSignatureGenerator(RefitGeneratorSettings settings) : IMethodSignatureGenerator { - private readonly IParameterExtractor parameterExtractor = parameterExtractor ?? new ParameterAggregator(); + private readonly ParameterListBuilder parameterListBuilder = new(settings); public (string ParametersString, IReadOnlyList Parameters, string? DynamicQuerystringParameters) Generate( CSharpOperationModel operationModel, OpenApiOperation operation, string dynamicQuerystringParameterType) { - var parameters = parameterExtractor.ExtractParameters( - operationModel, - operation, - settings, - dynamicQuerystringParameterType, - out var operationDynamicQuerystringParameters) - .ToList(); + var parameterList = parameterListBuilder.Build( + operationModel, + operation, + dynamicQuerystringParameterType); - var parametersString = string.Join(", ", parameters); - return (parametersString, parameters, operationDynamicQuerystringParameters); + var parametersString = string.Join(", ", parameterList.Parameters); + return (parametersString, parameterList.Parameters, parameterList.DynamicQuerystringCode); } } diff --git a/src/Refitter.Core/ParameterExtraction/BodyParameterExtractor.cs b/src/Refitter.Core/ParameterExtraction/BodyParameterExtractor.cs index d59af30f5..0ea09efe1 100644 --- a/src/Refitter.Core/ParameterExtraction/BodyParameterExtractor.cs +++ b/src/Refitter.Core/ParameterExtraction/BodyParameterExtractor.cs @@ -3,7 +3,7 @@ namespace Refitter.Core; -internal class BodyParameterExtractor : IParameterTypeExtractor +internal sealed class BodyParameterExtractor { public IEnumerable Extract( CSharpOperationModel operationModel, @@ -14,8 +14,8 @@ public IEnumerable Extract( .Where(p => p.Kind == OpenApiParameterKind.Body && !p.IsBinaryBodyParameter) .Select(p => { - var variableName = ParameterShared.GetVariableName(p); - return $"{ParameterShared.JoinAttributes(ParameterShared.GetBodyAttribute(p, settings), ParameterShared.GetAliasAsAttribute(p.Name, variableName))}{ParameterShared.GetParameterType(p, settings)} {variableName}"; + var variableName = ParameterNaming.GetVariableName(p); + return $"{ParameterAttributeFormatter.JoinAttributes(ParameterAttributeFormatter.GetBodyAttribute(p, settings), ParameterAttributeFormatter.GetAliasAsAttribute(p.Name, variableName))}{ParameterTypeResolver.GetParameterType(p, settings)} {variableName}"; }) .ToList(); @@ -23,8 +23,8 @@ public IEnumerable Extract( .Where(p => p.Kind == OpenApiParameterKind.Body && p.IsBinaryBodyParameter) .Select(p => { - var variableName = ParameterShared.GetVariableName(p); - var aliasAsAttribute = ParameterShared.GetAliasAsAttribute(p.Name, variableName); + var variableName = ParameterNaming.GetVariableName(p); + var aliasAsAttribute = ParameterAttributeFormatter.GetAliasAsAttribute(p.Name, variableName); var generatedAliasAsAttribute = string.IsNullOrWhiteSpace(aliasAsAttribute) ? string.Empty : $"[{aliasAsAttribute}]"; diff --git a/src/Refitter.Core/ParameterExtraction/DynamicQuerystringParameterBuilder.cs b/src/Refitter.Core/ParameterExtraction/DynamicQuerystringParameterBuilder.cs index 636a20a96..09267826e 100644 --- a/src/Refitter.Core/ParameterExtraction/DynamicQuerystringParameterBuilder.cs +++ b/src/Refitter.Core/ParameterExtraction/DynamicQuerystringParameterBuilder.cs @@ -27,9 +27,9 @@ public static string Build( foreach (var operationParameter in queryParameters) { - var propertyType = ParameterShared.GetQueryParameterType(operationParameter, settings); - var variableName = ParameterShared.GetVariableName(operationParameter); - var attributes = $"{ParameterShared.JoinAttributes(ParameterShared.GetQueryAttribute(operationParameter, settings), ParameterShared.GetAliasAsAttribute(operationParameter.Name, variableName))}"; + var propertyType = ParameterTypeResolver.GetQueryParameterType(operationParameter, settings); + var variableName = ParameterNaming.GetVariableName(operationParameter); + var attributes = $"{ParameterAttributeFormatter.JoinAttributes(ParameterAttributeFormatter.GetQueryAttribute(operationParameter, settings), ParameterAttributeFormatter.GetAliasAsAttribute(operationParameter.Name, variableName))}"; var propertyName = variableName.CapitalizeFirstCharacter(); if (operationParameter.IsRequired) { @@ -48,7 +48,7 @@ public static string Build( if (settings.GenerateXmlDocCodeComments && !string.IsNullOrWhiteSpace(operationParameter.Description)) { var escapedDescription = XmlDocumentationGenerator.SanitizeResponseDescription(operationParameter.Description); - ParameterShared.AppendXmlDocComment(escapedDescription, propertiesCodeBuilder); + AppendXmlDocComment(escapedDescription, propertiesCodeBuilder); } propertiesCodeBuilder.Append( @@ -59,7 +59,7 @@ public static string Build( var defaultValue = operationParameter.Schema.Default; if (defaultValue != null) { - var formattedDefaultValue = ParameterShared.FormatDefaultValue(defaultValue, propertyType); + var formattedDefaultValue = ParameterDefaultValueFormatter.FormatDefaultValue(defaultValue, propertyType); propertiesCodeBuilder.Append($" = {formattedDefaultValue};"); } @@ -91,4 +91,32 @@ public static string Build( return codeBuilder.ToString(); } + + internal static void AppendXmlDocComment(string description, StringBuilder codeBuilder) + { + codeBuilder.Append( +""" + /// +"""); + + var lines = description.Split( + ["\r\n", "\r", "\n"], + StringSplitOptions.None); + + foreach (var line in lines) + { + codeBuilder.AppendLine(); + codeBuilder.Append( +$$""" + /// {{line.Trim()}} +"""); + } + + codeBuilder.AppendLine(); + codeBuilder.Append( +""" + /// +"""); + codeBuilder.AppendLine(); + } } diff --git a/src/Refitter.Core/ParameterExtraction/FormParameterExtractor.cs b/src/Refitter.Core/ParameterExtraction/FormParameterExtractor.cs index b72d6c45b..fb7d9f0b3 100644 --- a/src/Refitter.Core/ParameterExtraction/FormParameterExtractor.cs +++ b/src/Refitter.Core/ParameterExtraction/FormParameterExtractor.cs @@ -4,7 +4,7 @@ namespace Refitter.Core; -internal class FormParameterExtractor : IParameterTypeExtractor +internal sealed class FormParameterExtractor { public IEnumerable Extract( CSharpOperationModel operationModel, @@ -16,10 +16,10 @@ public IEnumerable Extract( foreach (var p in operationModel.Parameters.Where(p => p.Kind == OpenApiParameterKind.FormData && !p.IsBinaryBodyParameter)) { - var variableName = ParameterShared.ConvertToVariableName(p.VariableName); + var variableName = ParameterNaming.ConvertToVariableName(p.VariableName); if (seenFormParameterNames.Add(variableName)) { - formParameters.Add($"{ParameterShared.JoinAttributes(ParameterShared.GetAliasAsAttribute(p.Name, variableName))}{ParameterShared.GetParameterType(p, settings)} {variableName}"); + formParameters.Add($"{ParameterAttributeFormatter.JoinAttributes(ParameterAttributeFormatter.GetAliasAsAttribute(p.Name, variableName))}{ParameterTypeResolver.GetParameterType(p, settings)} {variableName}"); } } @@ -40,13 +40,13 @@ public IEnumerable Extract( if (!isBinary) { - var propertyType = ParameterShared.GetCSharpType(propertySchema, settings); - var variableName = ParameterShared.ConvertToVariableName(property.Key); + var propertyType = ParameterTypeResolver.GetCSharpType(propertySchema, settings); + var variableName = ParameterNaming.ConvertToVariableName(property.Key); if (seenFormParameterNames.Add(variableName)) { - var aliasAttribute = ParameterShared.GetAliasAsAttribute(property.Key, variableName); - var parameter = $"{ParameterShared.JoinAttributes(aliasAttribute)}{propertyType} {variableName}"; + var aliasAttribute = ParameterAttributeFormatter.GetAliasAsAttribute(property.Key, variableName); + var parameter = $"{ParameterAttributeFormatter.JoinAttributes(aliasAttribute)}{propertyType} {variableName}"; formParameters.Add(parameter); } } diff --git a/src/Refitter.Core/ParameterExtraction/HeaderParameterExtractor.cs b/src/Refitter.Core/ParameterExtraction/HeaderParameterExtractor.cs index 228f87deb..95537582d 100644 --- a/src/Refitter.Core/ParameterExtraction/HeaderParameterExtractor.cs +++ b/src/Refitter.Core/ParameterExtraction/HeaderParameterExtractor.cs @@ -3,7 +3,7 @@ namespace Refitter.Core; -internal class HeaderParameterExtractor : IParameterTypeExtractor +internal sealed class HeaderParameterExtractor { public IEnumerable Extract( CSharpOperationModel operationModel, @@ -26,8 +26,8 @@ public IEnumerable Extract( .Where(p => !anyIgnoredHeaders || !ignoredHeaders.Contains(p.Name, StringComparer.OrdinalIgnoreCase)) .Select(p => { - var variableName = ParameterShared.GetVariableName(p); - return $"{ParameterShared.JoinAttributes($"Header(\"{p.Name}\")")}{ParameterShared.GetParameterType(p, settings)} {variableName}"; + var variableName = ParameterNaming.GetVariableName(p); + return $"{ParameterAttributeFormatter.JoinAttributes($"Header(\"{p.Name}\")")}{ParameterTypeResolver.GetParameterType(p, settings)} {variableName}"; }) .ToList(); } @@ -47,7 +47,7 @@ public IEnumerable Extract( && securityScheme.In == OpenApiSecurityApiKeyLocation.Header && !operationModel.Parameters.Any(p => p.Kind == OpenApiParameterKind.Header && p.IsHeader && p.Name == securityScheme.Name)) { - headerParameters.Add($"[Header(\"{securityScheme.Name}\")] string {ParameterShared.ReplaceUnsafeCharacters(securityScheme.Name)}"); + headerParameters.Add($"[Header(\"{securityScheme.Name}\")] string {ParameterNaming.ReplaceUnsafeCharacters(securityScheme.Name)}"); } else if (securityScheme is { Type: OpenApiSecuritySchemeType.Http } && string.Equals(securityScheme.Scheme, "bearer", StringComparison.OrdinalIgnoreCase)) diff --git a/src/Refitter.Core/ParameterExtraction/IParameterExtractor.cs b/src/Refitter.Core/ParameterExtraction/IParameterExtractor.cs deleted file mode 100644 index f7dea4e9b..000000000 --- a/src/Refitter.Core/ParameterExtraction/IParameterExtractor.cs +++ /dev/null @@ -1,14 +0,0 @@ -using NSwag; -using NSwag.CodeGeneration.CSharp.Models; - -namespace Refitter.Core; - -internal interface IParameterExtractor -{ - IEnumerable ExtractParameters( - CSharpOperationModel operationModel, - OpenApiOperation operation, - RefitGeneratorSettings settings, - string dynamicQuerystringParameterType, - out string? dynamicQuerystringParameters); -} diff --git a/src/Refitter.Core/ParameterExtraction/IParameterTypeExtractor.cs b/src/Refitter.Core/ParameterExtraction/IParameterTypeExtractor.cs deleted file mode 100644 index b3c892df6..000000000 --- a/src/Refitter.Core/ParameterExtraction/IParameterTypeExtractor.cs +++ /dev/null @@ -1,12 +0,0 @@ -using NSwag; -using NSwag.CodeGeneration.CSharp.Models; - -namespace Refitter.Core; - -internal interface IParameterTypeExtractor -{ - IEnumerable Extract( - CSharpOperationModel operationModel, - OpenApiOperation operation, - RefitGeneratorSettings settings); -} diff --git a/src/Refitter.Core/ParameterExtraction/OptionalParameterReorderer.cs b/src/Refitter.Core/ParameterExtraction/OptionalParameterReorderer.cs index 8632740ae..0d33c841f 100644 --- a/src/Refitter.Core/ParameterExtraction/OptionalParameterReorderer.cs +++ b/src/Refitter.Core/ParameterExtraction/OptionalParameterReorderer.cs @@ -23,7 +23,7 @@ public static List Reorder( if (NullablePattern.IsMatch(parameters[index])) { var parameterString = parameters[index]; - var defaultValue = ParameterShared.GetDefaultValueForParameter(parameterString, parameterModels); + var defaultValue = ParameterDefaultValueFormatter.GetDefaultValueForParameter(parameterString, parameterModels); parameters[index] = parameterString + " = " + defaultValue; } } diff --git a/src/Refitter.Core/ParameterExtraction/ParameterAggregator.cs b/src/Refitter.Core/ParameterExtraction/ParameterAggregator.cs deleted file mode 100644 index ee39ec8e4..000000000 --- a/src/Refitter.Core/ParameterExtraction/ParameterAggregator.cs +++ /dev/null @@ -1,70 +0,0 @@ -using NSwag; -using NSwag.CodeGeneration.CSharp.Models; - -namespace Refitter.Core; - -internal class ParameterAggregator( - IReadOnlyList extractors) : IParameterExtractor -{ - public ParameterAggregator() - : this(GetDefaultExtractors()) - { - } - - public IEnumerable ExtractParameters( - CSharpOperationModel operationModel, - OpenApiOperation operation, - RefitGeneratorSettings settings, - string dynamicQuerystringParameterType, - out string? dynamicQuerystringParameters) - { - var allParameters = new List(); - - // Route parameters - allParameters.AddRange(GetExtractor().Extract(operationModel, operation, settings)); - - // Query parameters (with dynamic querystring support) - var queryExtractor = GetExtractor(); - queryExtractor.DynamicQuerystringParameterType = dynamicQuerystringParameterType; - allParameters.AddRange(queryExtractor.Extract(operationModel, operation, settings)); - dynamicQuerystringParameters = queryExtractor.DynamicQuerystringCode; - - // Body parameters (including binary body) - allParameters.AddRange(GetExtractor().Extract(operationModel, operation, settings)); - - // Header parameters - allParameters.AddRange(GetExtractor().Extract(operationModel, operation, settings)); - - // Form parameters - allParameters.AddRange(GetExtractor().Extract(operationModel, operation, settings)); - - allParameters = OptionalParameterReorderer.Reorder( - allParameters, - settings, - operationModel.Parameters); - - if (settings.ApizrSettings?.WithRequestOptions == true) - allParameters.Add("[RequestOptions] IApizrRequestOptions options"); - else if (settings.UseCancellationTokens) - allParameters.Add("CancellationToken cancellationToken = default"); - - return allParameters; - } - - private T GetExtractor() where T : IParameterTypeExtractor - { - return extractors.OfType().First(); - } - - private static IReadOnlyList GetDefaultExtractors() - { - return new IParameterTypeExtractor[] - { - new RouteParameterExtractor(), - new QueryParameterExtractor(), - new BodyParameterExtractor(), - new HeaderParameterExtractor(), - new FormParameterExtractor(), - }; - } -} diff --git a/src/Refitter.Core/ParameterExtraction/ParameterAttributeFormatter.cs b/src/Refitter.Core/ParameterExtraction/ParameterAttributeFormatter.cs new file mode 100644 index 000000000..771c2892a --- /dev/null +++ b/src/Refitter.Core/ParameterExtraction/ParameterAttributeFormatter.cs @@ -0,0 +1,63 @@ +using NSwag.CodeGeneration.CSharp.Models; + +namespace Refitter.Core; + +/// +/// Builds the Refit attributes (AliasAs, Query, Body) emitted alongside generated parameters. +/// +internal static class ParameterAttributeFormatter +{ + public static string GetAliasAsAttribute(CSharpParameterModel parameterModel) => + string.Equals(parameterModel.Name, parameterModel.VariableName) + ? string.Empty + : $"AliasAs(\"{ParameterNaming.EscapeString(parameterModel.Name)}\")"; + + public static string GetAliasAsAttribute(string originalName, string variableName) => + string.Equals(originalName, variableName, StringComparison.Ordinal) + ? string.Empty + : $"AliasAs(\"{ParameterNaming.EscapeString(originalName)}\")"; + + public static string JoinAttributes(params string[] attributes) + { + var filteredAttributes = attributes + .Where(a => !string.IsNullOrWhiteSpace(a)) + .ToList(); + + if (filteredAttributes.Count == 0) + return string.Empty; + + return "[" + string.Join(", ", filteredAttributes) + "] "; + } + + public static string GetBodyAttribute(CSharpParameterModel parameter, RefitGeneratorSettings settings) + { + var anyType = settings.CodeGeneratorSettings?.AnyType ?? "object"; + var parameterType = ParameterTypeResolver.ResolveType(parameter.Type); + + if (parameterType.Equals(anyType, StringComparison.OrdinalIgnoreCase) || + parameterType.Contains("JsonElement", StringComparison.OrdinalIgnoreCase)) + { + return "Body(BodySerializationMethod.Serialized)"; + } + + return "Body"; + } + + public static string GetQueryAttribute(CSharpParameterModel parameter, RefitGeneratorSettings settings) + { + return (parameter, settings) switch + { + { parameter.IsArray: true } + => $"Query(CollectionFormat.{settings.CollectionFormat})", + { parameter.IsDate: true, settings.UseIsoDateFormat: true } + => "Query(Format = \"yyyy-MM-dd\")", + { parameter.IsDate: true, settings.CodeGeneratorSettings.DateFormat: not null } + => $"Query(Format = \"{settings.CodeGeneratorSettings?.DateFormat}\")", + { + parameter.IsDateOrDateTime: true, parameter.Schema.Format: "date-time", + settings.CodeGeneratorSettings.DateTimeFormat: not null + } => $"Query(Format = \"{settings.CodeGeneratorSettings?.DateTimeFormat}\")", + _ => "Query", + }; + } +} diff --git a/src/Refitter.Core/ParameterExtraction/ParameterDefaultValueFormatter.cs b/src/Refitter.Core/ParameterExtraction/ParameterDefaultValueFormatter.cs new file mode 100644 index 000000000..a062acc70 --- /dev/null +++ b/src/Refitter.Core/ParameterExtraction/ParameterDefaultValueFormatter.cs @@ -0,0 +1,80 @@ +using System.Globalization; +using NSwag.CodeGeneration.CSharp.Models; + +namespace Refitter.Core; + +/// +/// Formats default values and numeric literals for generated parameters. +/// +internal static class ParameterDefaultValueFormatter +{ + public static bool IsNumericType(string type) + { + return type is "int" or "Int32" or "long" or "Int64" or "short" or "Int16" + or "byte" or "Byte" or "decimal" or "Decimal" or "float" or "Single" + or "double" or "Double" or "sbyte" or "SByte" or "uint" or "UInt32" + or "ulong" or "UInt64" or "ushort" or "UInt16"; + } + + private static string FormatDoubleLiteral(string numericString) + { + if (numericString.Contains('.') || numericString.Contains('e') || numericString.Contains('E')) + return numericString; + + return numericString + ".0"; + } + + public 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", + "double" or "Double" => FormatDoubleLiteral(numericString), + "long" or "Int64" => $"{numericString}L", + "ulong" or "UInt64" => $"{numericString}UL", + "uint" or "UInt32" => $"{numericString}U", + _ => numericString + }; + } + + public static string FormatDefaultValue(object? defaultValue, string parameterType) + { + if (defaultValue == null) + return "default"; + + var type = parameterType.TrimEnd('?').Trim(); + + return type switch + { + "bool" => defaultValue.ToString()?.ToLowerInvariant() ?? "default", + "string" => $"\"{ParameterNaming.EscapeString(defaultValue.ToString() ?? string.Empty)}\"", + _ when IsNumericType(type) => FormatNumericValue(defaultValue, type), + _ => "default" + }; + } + + public static string GetDefaultValueForParameter( + string parameterString, + ICollection parameterModels) + { + var parts = parameterString.Split([' '], StringSplitOptions.RemoveEmptyEntries); + if (parts.Length == 0) + return "default"; + + var variableName = parts[parts.Length - 1].TrimEnd(';', ','); + + var parameterModel = parameterModels.FirstOrDefault(p => p.VariableName == variableName); + parameterModel ??= parameterModels.FirstOrDefault(p => ParameterNaming.GetVariableName(p) == variableName); + if (parameterModel?.Schema?.Default != null && !string.IsNullOrEmpty(parameterModel.Type)) + { + return FormatDefaultValue(parameterModel.Schema.Default, parameterModel.Type); + } + + return "default"; + } +} diff --git a/src/Refitter.Core/ParameterExtraction/ParameterList.cs b/src/Refitter.Core/ParameterExtraction/ParameterList.cs new file mode 100644 index 000000000..7ce2dd360 --- /dev/null +++ b/src/Refitter.Core/ParameterExtraction/ParameterList.cs @@ -0,0 +1,9 @@ +namespace Refitter.Core; + +/// +/// The ordered set of a generated Refit method's parameters, together with any +/// dynamic querystring wrapper type source that must be emitted for the operation. +/// +internal sealed record ParameterList( + IReadOnlyList Parameters, + string? DynamicQuerystringCode); diff --git a/src/Refitter.Core/ParameterExtraction/ParameterListBuilder.cs b/src/Refitter.Core/ParameterExtraction/ParameterListBuilder.cs new file mode 100644 index 000000000..13bd39f1a --- /dev/null +++ b/src/Refitter.Core/ParameterExtraction/ParameterListBuilder.cs @@ -0,0 +1,51 @@ +using NSwag; +using NSwag.CodeGeneration.CSharp.Models; + +namespace Refitter.Core; + +/// +/// Builds the ordered parameter list for a generated Refit method from an OpenAPI operation. +/// Owns all parameter rules: route, query, body, header, and form extraction, optional-parameter +/// reordering, and any trailing request-options or cancellation-token argument. +/// +internal sealed class ParameterListBuilder(RefitGeneratorSettings settings) +{ + private readonly RouteParameterExtractor routeExtractor = new(); + private readonly QueryParameterExtractor queryExtractor = new(); + private readonly BodyParameterExtractor bodyExtractor = new(); + private readonly HeaderParameterExtractor headerExtractor = new(); + private readonly FormParameterExtractor formExtractor = new(); + + public ParameterList Build( + CSharpOperationModel operationModel, + OpenApiOperation operation, + string dynamicQuerystringParameterType) + { + var parameters = new List(); + + parameters.AddRange(routeExtractor.Extract(operationModel, operation, settings)); + + var (queryParameters, dynamicQuerystringCode) = queryExtractor.Extract( + operationModel, + operation, + settings, + dynamicQuerystringParameterType); + parameters.AddRange(queryParameters); + + parameters.AddRange(bodyExtractor.Extract(operationModel, operation, settings)); + parameters.AddRange(headerExtractor.Extract(operationModel, operation, settings)); + parameters.AddRange(formExtractor.Extract(operationModel, operation, settings)); + + parameters = OptionalParameterReorderer.Reorder( + parameters, + settings, + operationModel.Parameters); + + if (settings.ApizrSettings?.WithRequestOptions == true) + parameters.Add("[RequestOptions] IApizrRequestOptions options"); + else if (settings.UseCancellationTokens) + parameters.Add("CancellationToken cancellationToken = default"); + + return new ParameterList(parameters, dynamicQuerystringCode); + } +} diff --git a/src/Refitter.Core/ParameterExtraction/ParameterNaming.cs b/src/Refitter.Core/ParameterExtraction/ParameterNaming.cs new file mode 100644 index 000000000..7af0b76e6 --- /dev/null +++ b/src/Refitter.Core/ParameterExtraction/ParameterNaming.cs @@ -0,0 +1,78 @@ +using System.Text; +using NSwag.CodeGeneration.Models; + +namespace Refitter.Core; + +/// +/// Identifier and string-escaping helpers for generated parameter names. +/// +internal static class ParameterNaming +{ + public static string ReplaceUnsafeCharacters(string unsafeText) + { + return IdentifierUtils.ToCompilableIdentifier(unsafeText); + } + + public static string EscapeString(string value) + { + var sb = new StringBuilder(value.Length + 10); + 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; + 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; + } + } + + return sb.ToString(); + } + + public static string ConvertToVariableName(string propertyName) + { + if (string.IsNullOrEmpty(propertyName)) + return "value"; + + var identifier = IdentifierUtils.ToCompilableIdentifier(propertyName); + + if (identifier.Length > 0 && char.IsUpper(identifier[0])) + { + return char.ToLowerInvariant(identifier[0]) + identifier.Substring(1); + } + + return identifier; + } + + public static string GetVariableName(ParameterModelBase parameterModel) + { + return IdentifierUtils.ToCompilableIdentifier(parameterModel.VariableName); + } +} diff --git a/src/Refitter.Core/ParameterExtraction/ParameterShared.cs b/src/Refitter.Core/ParameterExtraction/ParameterShared.cs deleted file mode 100644 index f894dd98e..000000000 --- a/src/Refitter.Core/ParameterExtraction/ParameterShared.cs +++ /dev/null @@ -1,322 +0,0 @@ -using System.Globalization; -using System.Text; -using NJsonSchema; -using NSwag.CodeGeneration.CSharp.Models; -using NSwag.CodeGeneration.Models; - -namespace Refitter.Core; - -internal static class ParameterShared -{ - public static string ReplaceUnsafeCharacters(string unsafeText) - { - return IdentifierUtils.ToCompilableIdentifier(unsafeText); - } - - public static string EscapeString(string value) - { - var sb = new StringBuilder(value.Length + 10); - 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; - 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; - } - } - - return sb.ToString(); - } - - public static bool IsNumericType(string type) - { - return type is "int" or "Int32" or "long" or "Int64" or "short" or "Int16" - or "byte" or "Byte" or "decimal" or "Decimal" or "float" or "Single" - or "double" or "Double" or "sbyte" or "SByte" or "uint" or "UInt32" - or "ulong" or "UInt64" or "ushort" or "UInt16"; - } - - private static string FormatDoubleLiteral(string numericString) - { - if (numericString.Contains('.') || numericString.Contains('e') || numericString.Contains('E')) - return numericString; - - return numericString + ".0"; - } - - public 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", - "double" or "Double" => FormatDoubleLiteral(numericString), - "long" or "Int64" => $"{numericString}L", - "ulong" or "UInt64" => $"{numericString}UL", - "uint" or "UInt32" => $"{numericString}U", - _ => numericString - }; - } - - public static string FormatDefaultValue(object? defaultValue, string parameterType) - { - if (defaultValue == null) - return "default"; - - var type = parameterType.TrimEnd('?').Trim(); - - return type switch - { - "bool" => defaultValue.ToString()?.ToLowerInvariant() ?? "default", - "string" => $"\"{EscapeString(defaultValue.ToString() ?? string.Empty)}\"", - _ when IsNumericType(type) => FormatNumericValue(defaultValue, type), - _ => "default" - }; - } - - public static string GetDefaultValueForParameter( - string parameterString, - ICollection parameterModels) - { - var parts = parameterString.Split([' '], StringSplitOptions.RemoveEmptyEntries); - if (parts.Length == 0) - return "default"; - - var variableName = parts[parts.Length - 1].TrimEnd(';', ','); - - var parameterModel = parameterModels.FirstOrDefault(p => p.VariableName == variableName); - parameterModel ??= parameterModels.FirstOrDefault(p => GetVariableName(p) == variableName); - if (parameterModel?.Schema?.Default != null && !string.IsNullOrEmpty(parameterModel.Type)) - { - return FormatDefaultValue(parameterModel.Schema.Default, parameterModel.Type); - } - - return "default"; - } - - public static string GetAliasAsAttribute(CSharpParameterModel parameterModel) => - string.Equals(parameterModel.Name, parameterModel.VariableName) - ? string.Empty - : $"AliasAs(\"{EscapeString(parameterModel.Name)}\")"; - - public static string GetAliasAsAttribute(string originalName, string variableName) => - string.Equals(originalName, variableName, StringComparison.Ordinal) - ? string.Empty - : $"AliasAs(\"{EscapeString(originalName)}\")"; - - public static string JoinAttributes(params string[] attributes) - { - var filteredAttributes = attributes - .Where(a => !string.IsNullOrWhiteSpace(a)) - .ToList(); - - if (filteredAttributes.Count == 0) - return string.Empty; - - return "[" + string.Join(", ", filteredAttributes) + "] "; - } - - public static string FindSupportedType(string typeName) - { - if (typeName is "FileResponse" or "FileParameter") - return "StreamPart"; - - if (typeName.Contains("FileParameter") || typeName.Contains("FileResponse")) - { - return typeName - .Replace("FileParameter", "StreamPart") - .Replace("FileResponse", "StreamPart"); - } - - return typeName; - } - - private static string TrimImportedNamespaces(string returnTypeParameter) => - returnTypeParameter.StartsWith("System.Collections.Generic.", StringComparison.OrdinalIgnoreCase) - ? returnTypeParameter.Replace("System.Collections.Generic.", string.Empty) - : returnTypeParameter; - - public static string GetParameterType( - ParameterModelBase parameterModel, - RefitGeneratorSettings settings) - { - var type = TrimImportedNamespaces( - FindSupportedType( - parameterModel.Type)); - - if (settings.OptionalParameters && - !type.EndsWith("?") && - (parameterModel.IsNullable || parameterModel.IsOptional || !parameterModel.IsRequired)) - type += "?"; - - return type; - } - - public static string GetQueryParameterType( - ParameterModelBase parameterModel, - RefitGeneratorSettings settings) - { - var type = GetParameterType(parameterModel, settings); - - if (parameterModel.IsQuery && - parameterModel.Type.Equals("object", StringComparison.OrdinalIgnoreCase)) - type = "string"; - - return type; - } - - public static string GetBodyAttribute(CSharpParameterModel parameter, RefitGeneratorSettings settings) - { - var anyType = settings.CodeGeneratorSettings?.AnyType ?? "object"; - var parameterType = TrimImportedNamespaces(FindSupportedType(parameter.Type)); - - if (parameterType.Equals(anyType, StringComparison.OrdinalIgnoreCase) || - parameterType.Contains("JsonElement", StringComparison.OrdinalIgnoreCase)) - { - return "Body(BodySerializationMethod.Serialized)"; - } - - return "Body"; - } - - public static string GetQueryAttribute(CSharpParameterModel parameter, RefitGeneratorSettings settings) - { - return (parameter, settings) switch - { - { parameter.IsArray: true } - => $"Query(CollectionFormat.{settings.CollectionFormat})", - { parameter.IsDate: true, settings.UseIsoDateFormat: true } - => "Query(Format = \"yyyy-MM-dd\")", - { parameter.IsDate: true, settings.CodeGeneratorSettings.DateFormat: not null } - => $"Query(Format = \"{settings.CodeGeneratorSettings?.DateFormat}\")", - { - parameter.IsDateOrDateTime: true, parameter.Schema.Format: "date-time", - settings.CodeGeneratorSettings.DateTimeFormat: not null - } => $"Query(Format = \"{settings.CodeGeneratorSettings?.DateTimeFormat}\")", - _ => "Query", - }; - } - - public static string GetCSharpType(JsonSchema propertySchema, RefitGeneratorSettings settings) - { - var type = propertySchema.Type switch - { - JsonObjectType.String => "string", - JsonObjectType.Integer => GetIntegerTypeName(propertySchema, settings), - JsonObjectType.Number => "double", - JsonObjectType.Boolean => "bool", - JsonObjectType.Array => GetArrayType(propertySchema, settings), - JsonObjectType.Object => "object", - _ => "object" - }; - - if (settings.OptionalParameters && propertySchema.IsNullable(SchemaType.OpenApi3)) - { - type += "?"; - } - - return type; - } - - public static string GetIntegerTypeName(JsonSchema schema, RefitGeneratorSettings settings) - { - if (schema.Format == "int64") - return "long"; - if (schema.Format == "int32") - return "int"; - - var integerType = settings.CodeGeneratorSettings?.IntegerType ?? IntegerType.Int32; - return integerType == IntegerType.Int64 ? "long" : "int"; - } - - public static string GetArrayType(JsonSchema arraySchema, RefitGeneratorSettings settings) - { - if (arraySchema.Item != null) - { - var itemType = GetCSharpType(arraySchema.Item, settings); - return $"{itemType}[]"; - } - - return "object[]"; - } - - public static string ConvertToVariableName(string propertyName) - { - if (string.IsNullOrEmpty(propertyName)) - return "value"; - - var identifier = IdentifierUtils.ToCompilableIdentifier(propertyName); - - if (identifier.Length > 0 && char.IsUpper(identifier[0])) - { - return char.ToLowerInvariant(identifier[0]) + identifier.Substring(1); - } - - return identifier; - } - - public static string GetVariableName(ParameterModelBase parameterModel) - { - return IdentifierUtils.ToCompilableIdentifier(parameterModel.VariableName); - } - - public static void AppendXmlDocComment(string description, StringBuilder codeBuilder) - { - codeBuilder.Append( -""" - /// -"""); - - var lines = description.Split( - ["\r\n", "\r", "\n"], - StringSplitOptions.None); - - foreach (var line in lines) - { - codeBuilder.AppendLine(); - codeBuilder.Append( -$$""" - /// {{line.Trim()}} -"""); - } - - codeBuilder.AppendLine(); - codeBuilder.Append( -""" - /// -"""); - codeBuilder.AppendLine(); - } -} diff --git a/src/Refitter.Core/ParameterExtraction/ParameterTypeResolver.cs b/src/Refitter.Core/ParameterExtraction/ParameterTypeResolver.cs new file mode 100644 index 000000000..9ce922ead --- /dev/null +++ b/src/Refitter.Core/ParameterExtraction/ParameterTypeResolver.cs @@ -0,0 +1,105 @@ +using NJsonSchema; +using NSwag.CodeGeneration.Models; + +namespace Refitter.Core; + +/// +/// Resolves the C# type rendered for a generated parameter from its OpenAPI schema. +/// +internal static class ParameterTypeResolver +{ + public static string FindSupportedType(string typeName) + { + if (typeName is "FileResponse" or "FileParameter") + return "StreamPart"; + + if (typeName.Contains("FileParameter") || typeName.Contains("FileResponse")) + { + return typeName + .Replace("FileParameter", "StreamPart") + .Replace("FileResponse", "StreamPart"); + } + + return typeName; + } + + private static string TrimImportedNamespaces(string returnTypeParameter) => + returnTypeParameter.StartsWith("System.Collections.Generic.", StringComparison.OrdinalIgnoreCase) + ? returnTypeParameter.Replace("System.Collections.Generic.", string.Empty) + : returnTypeParameter; + + public static string ResolveType(string typeName) => + TrimImportedNamespaces(FindSupportedType(typeName)); + + public static string GetParameterType( + ParameterModelBase parameterModel, + RefitGeneratorSettings settings) + { + var type = TrimImportedNamespaces( + FindSupportedType( + parameterModel.Type)); + + if (settings.OptionalParameters && + !type.EndsWith("?") && + (parameterModel.IsNullable || parameterModel.IsOptional || !parameterModel.IsRequired)) + type += "?"; + + return type; + } + + public static string GetQueryParameterType( + ParameterModelBase parameterModel, + RefitGeneratorSettings settings) + { + var type = GetParameterType(parameterModel, settings); + + if (parameterModel.IsQuery && + parameterModel.Type.Equals("object", StringComparison.OrdinalIgnoreCase)) + type = "string"; + + return type; + } + + public static string GetCSharpType(JsonSchema propertySchema, RefitGeneratorSettings settings) + { + var type = propertySchema.Type switch + { + JsonObjectType.String => "string", + JsonObjectType.Integer => GetIntegerTypeName(propertySchema, settings), + JsonObjectType.Number => "double", + JsonObjectType.Boolean => "bool", + JsonObjectType.Array => GetArrayType(propertySchema, settings), + JsonObjectType.Object => "object", + _ => "object" + }; + + if (settings.OptionalParameters && propertySchema.IsNullable(SchemaType.OpenApi3)) + { + type += "?"; + } + + return type; + } + + public static string GetIntegerTypeName(JsonSchema schema, RefitGeneratorSettings settings) + { + if (schema.Format == "int64") + return "long"; + if (schema.Format == "int32") + return "int"; + + var integerType = settings.CodeGeneratorSettings?.IntegerType ?? IntegerType.Int32; + return integerType == IntegerType.Int64 ? "long" : "int"; + } + + public static string GetArrayType(JsonSchema arraySchema, RefitGeneratorSettings settings) + { + if (arraySchema.Item != null) + { + var itemType = GetCSharpType(arraySchema.Item, settings); + return $"{itemType}[]"; + } + + return "object[]"; + } +} diff --git a/src/Refitter.Core/ParameterExtraction/QueryParameterExtractor.cs b/src/Refitter.Core/ParameterExtraction/QueryParameterExtractor.cs index eb89e9c3b..f05236708 100644 --- a/src/Refitter.Core/ParameterExtraction/QueryParameterExtractor.cs +++ b/src/Refitter.Core/ParameterExtraction/QueryParameterExtractor.cs @@ -3,60 +3,57 @@ namespace Refitter.Core; -internal class QueryParameterExtractor : IParameterTypeExtractor +internal sealed class QueryParameterExtractor { - public string? DynamicQuerystringParameterType { get; set; } - public string? DynamicQuerystringCode { get; private set; } - - public IEnumerable Extract( + public (IReadOnlyList Parameters, string? DynamicQuerystringCode) Extract( CSharpOperationModel operationModel, OpenApiOperation operation, - RefitGeneratorSettings settings) + RefitGeneratorSettings settings, + string dynamicQuerystringParameterType) { var queryParameters = operationModel.Parameters .Where(p => p.Kind == OpenApiParameterKind.Query) .ToList(); - if (settings.UseDynamicQuerystringParameters && queryParameters.Count >= 2) - return ExtractDynamic(queryParameters, settings); - - return ExtractSimple(queryParameters, settings); + return settings.UseDynamicQuerystringParameters && queryParameters.Count >= 2 + ? ExtractDynamic(queryParameters, settings, dynamicQuerystringParameterType) + : (ExtractSimple(queryParameters, settings), null); } - private List ExtractDynamic( + private static (IReadOnlyList Parameters, string? DynamicQuerystringCode) ExtractDynamic( List queryParameters, - RefitGeneratorSettings settings) + RefitGeneratorSettings settings, + string dynamicQuerystringParameterType) { var dynamicQuerystringCode = DynamicQuerystringParameterBuilder.Build( queryParameters, - DynamicQuerystringParameterType!, + dynamicQuerystringParameterType, settings); - DynamicQuerystringCode = !string.IsNullOrWhiteSpace(dynamicQuerystringCode) + var code = !string.IsNullOrWhiteSpace(dynamicQuerystringCode) ? dynamicQuerystringCode : null; var allNullable = queryParameters.All(p => - ParameterShared.GetQueryParameterType(p, settings).EndsWith("?")); + ParameterTypeResolver.GetQueryParameterType(p, settings).EndsWith("?")); - var dynamicQuerystringParameter = $"[Query] {DynamicQuerystringParameterType}"; + var dynamicQuerystringParameter = $"[Query] {dynamicQuerystringParameterType}"; if (allNullable) dynamicQuerystringParameter += "?"; dynamicQuerystringParameter += " queryParams"; - return [dynamicQuerystringParameter]; + + return (new[] { dynamicQuerystringParameter }, code); } - private List ExtractSimple( + private static List ExtractSimple( List queryParameters, RefitGeneratorSettings settings) { - DynamicQuerystringCode = string.Empty; - return queryParameters .Select(p => { - var variableName = ParameterShared.GetVariableName(p); - return $"{ParameterShared.JoinAttributes(ParameterShared.GetQueryAttribute(p, settings), ParameterShared.GetAliasAsAttribute(p.Name, variableName))}{ParameterShared.GetQueryParameterType(p, settings)} {variableName}"; + var variableName = ParameterNaming.GetVariableName(p); + return $"{ParameterAttributeFormatter.JoinAttributes(ParameterAttributeFormatter.GetQueryAttribute(p, settings), ParameterAttributeFormatter.GetAliasAsAttribute(p.Name, variableName))}{ParameterTypeResolver.GetQueryParameterType(p, settings)} {variableName}"; }) .ToList(); } diff --git a/src/Refitter.Core/ParameterExtraction/RouteParameterExtractor.cs b/src/Refitter.Core/ParameterExtraction/RouteParameterExtractor.cs index 88d09d37a..b6a320616 100644 --- a/src/Refitter.Core/ParameterExtraction/RouteParameterExtractor.cs +++ b/src/Refitter.Core/ParameterExtraction/RouteParameterExtractor.cs @@ -3,7 +3,7 @@ namespace Refitter.Core; -internal class RouteParameterExtractor : IParameterTypeExtractor +internal sealed class RouteParameterExtractor { public IEnumerable Extract( CSharpOperationModel operationModel, @@ -14,8 +14,8 @@ public IEnumerable Extract( .Where(p => p.Kind == OpenApiParameterKind.Path) .Select(p => { - var variableName = ParameterShared.GetVariableName(p); - return $"{ParameterShared.JoinAttributes(ParameterShared.GetAliasAsAttribute(p.Name, variableName))}{p.Type} {variableName}"; + var variableName = ParameterNaming.GetVariableName(p); + return $"{ParameterAttributeFormatter.JoinAttributes(ParameterAttributeFormatter.GetAliasAsAttribute(p.Name, variableName))}{p.Type} {variableName}"; }) .ToList(); } diff --git a/src/Refitter.Tests/MethodSignatureGeneratorTests.cs b/src/Refitter.Tests/MethodSignatureGeneratorTests.cs index 56d5abfad..dc2afe290 100644 --- a/src/Refitter.Tests/MethodSignatureGeneratorTests.cs +++ b/src/Refitter.Tests/MethodSignatureGeneratorTests.cs @@ -171,7 +171,7 @@ public async Task Generate_With_DynamicQuerystring_Returns_Generated_Code() """; var document = await OpenApiYamlDocument.FromYamlAsync(spec); - var settings = new RefitGeneratorSettings(); + var settings = new RefitGeneratorSettings { UseDynamicQuerystringParameters = true }; var generator = new CSharpClientGeneratorFactory(settings, document).Create(); var sut = new MethodSignatureGenerator(settings); diff --git a/src/Refitter.Tests/Parameters/ParameterExtractorPrivateCoverageTests.cs b/src/Refitter.Tests/Parameters/ParameterExtractorPrivateCoverageTests.cs index 0ba3353e6..06bdca004 100644 --- a/src/Refitter.Tests/Parameters/ParameterExtractorPrivateCoverageTests.cs +++ b/src/Refitter.Tests/Parameters/ParameterExtractorPrivateCoverageTests.cs @@ -12,7 +12,7 @@ public class ParameterExtractorPrivateCoverageTests [Test] public void GetDefaultValueForParameter_Returns_Default_For_Empty_Parameter_String() { - var result = ParameterShared.GetDefaultValueForParameter( + var result = ParameterDefaultValueFormatter.GetDefaultValueForParameter( string.Empty, new List()); @@ -22,8 +22,8 @@ public void GetDefaultValueForParameter_Returns_Default_For_Empty_Parameter_Stri [Test] public void FormatDefaultValue_Returns_Default_For_Null_And_Unsupported_Types() { - var nullResult = ParameterShared.FormatDefaultValue(null!, "string"); - var unsupportedTypeResult = ParameterShared.FormatDefaultValue(42, "CustomType"); + var nullResult = ParameterDefaultValueFormatter.FormatDefaultValue(null!, "string"); + var unsupportedTypeResult = ParameterDefaultValueFormatter.FormatDefaultValue(42, "CustomType"); nullResult.Should().Be("default"); unsupportedTypeResult.Should().Be("default"); @@ -32,7 +32,7 @@ public void FormatDefaultValue_Returns_Default_For_Null_And_Unsupported_Types() [Test] public void EscapeString_Handles_Vertical_Tab_And_Null_Characters() { - var result = ParameterShared.EscapeString("before\vbetween\0after"); + var result = ParameterNaming.EscapeString("before\vbetween\0after"); result.Should().Be("before\\vbetween\\0after"); } @@ -42,7 +42,7 @@ public void EscapeString_Handles_Vertical_Tab_And_Null_Characters() [Arguments("UInt32")] public void FormatNumericValue_Appends_U_Suffix_For_UInt_Types(string numericType) { - var result = ParameterShared.FormatNumericValue(42, numericType); + var result = ParameterDefaultValueFormatter.FormatNumericValue(42, numericType); result.Should().Be("42U"); } @@ -72,7 +72,7 @@ public void FormatNumericValue_Appends_U_Suffix_For_UInt_Types(string numericTyp [Arguments("UInt16")] public void IsNumericType_Returns_True_For_Supported_Numeric_Types(string numericType) { - var result = ParameterShared.IsNumericType(numericType); + var result = ParameterDefaultValueFormatter.IsNumericType(numericType); result.Should().BeTrue(); } @@ -84,7 +84,7 @@ public void IsNumericType_Returns_True_For_Supported_Numeric_Types(string numeri [Arguments("numbers")] public void IsNumericType_Returns_False_For_Non_Numeric_Types(string numericType) { - var result = ParameterShared.IsNumericType(numericType); + var result = ParameterDefaultValueFormatter.IsNumericType(numericType); result.Should().BeFalse(); } @@ -92,9 +92,9 @@ public void IsNumericType_Returns_False_For_Non_Numeric_Types(string numericType [Test] public void GetAliasAsAttribute_StringOverload_Escapes_Special_Characters() { - var unchanged = ParameterShared.GetAliasAsAttribute("same", "same"); - var withQuote = ParameterShared.GetAliasAsAttribute("user\"id", "userId"); - var withBackslash = ParameterShared.GetAliasAsAttribute("user\\id", "userId"); + var unchanged = ParameterAttributeFormatter.GetAliasAsAttribute("same", "same"); + var withQuote = ParameterAttributeFormatter.GetAliasAsAttribute("user\"id", "userId"); + var withBackslash = ParameterAttributeFormatter.GetAliasAsAttribute("user\\id", "userId"); unchanged.Should().BeEmpty(); withQuote.Should().Be("AliasAs(\"user\\\"id\")"); @@ -106,19 +106,19 @@ public void GetCSharpType_Handles_Number_Object_Unknown_And_Nullable_String() { var settings = new RefitGeneratorSettings { OptionalParameters = true }; - var numberType = ParameterShared.GetCSharpType( + var numberType = ParameterTypeResolver.GetCSharpType( new JsonSchema { Type = JsonObjectType.Number }, settings); - var objectType = ParameterShared.GetCSharpType( + var objectType = ParameterTypeResolver.GetCSharpType( new JsonSchema { Type = JsonObjectType.Object }, settings); - var unknownType = ParameterShared.GetCSharpType( + var unknownType = ParameterTypeResolver.GetCSharpType( new JsonSchema { Type = JsonObjectType.None }, settings); - var nullableStringType = ParameterShared.GetCSharpType( + var nullableStringType = ParameterTypeResolver.GetCSharpType( new JsonSchema { Type = JsonObjectType.String, IsNullableRaw = true }, settings); @@ -133,11 +133,11 @@ public void GetIntegerTypeName_Uses_Explicit_Int_Formats() { var settings = new RefitGeneratorSettings(); - var int64Type = ParameterShared.GetIntegerTypeName( + var int64Type = ParameterTypeResolver.GetIntegerTypeName( new JsonSchema { Format = "int64" }, settings); - var int32Type = ParameterShared.GetIntegerTypeName( + var int32Type = ParameterTypeResolver.GetIntegerTypeName( new JsonSchema { Format = "int32" }, settings); @@ -148,7 +148,7 @@ public void GetIntegerTypeName_Uses_Explicit_Int_Formats() [Test] public void GetArrayType_Returns_Object_Array_When_Item_Is_Missing() { - var result = ParameterShared.GetArrayType( + var result = ParameterTypeResolver.GetArrayType( new JsonSchema { Type = JsonObjectType.Array }, new RefitGeneratorSettings()); @@ -156,9 +156,9 @@ public void GetArrayType_Returns_Object_Array_When_Item_Is_Missing() } [Test] - public void ReplaceUnsafeCharacters_Delegates_To_ParameterShared() + public void ReplaceUnsafeCharacters_Delegates_To_ParameterNaming() { - var result = ParameterShared.ReplaceUnsafeCharacters("unsafe-name!"); + var result = ParameterNaming.ReplaceUnsafeCharacters("unsafe-name!"); result.Should().NotBeNullOrWhiteSpace(); } @@ -177,49 +177,49 @@ public void ReOrderNullableParameters_Delegates_To_OptionalParameterReorderer() [Test] public void FormatDefaultValue_Adds_PointZero_For_Double_Integer_Values() { - var result = ParameterShared.FormatDefaultValue(42, "double"); + var result = ParameterDefaultValueFormatter.FormatDefaultValue(42, "double"); result.Should().Be("42.0"); } [Test] public void JoinAttributes_Returns_Empty_For_Empty_Input() { - var result = ParameterShared.JoinAttributes(); + var result = ParameterAttributeFormatter.JoinAttributes(); result.Should().Be(string.Empty); } [Test] public void JoinAttributes_Returns_Combined_Attributes() { - var result = ParameterShared.JoinAttributes("AliasAs(\"name\")", "Query()"); + var result = ParameterAttributeFormatter.JoinAttributes("AliasAs(\"name\")", "Query()"); result.Should().Be("[AliasAs(\"name\"), Query()] "); } [Test] public void JoinAttributes_Returns_Single_Attribute() { - var result = ParameterShared.JoinAttributes("AliasAs(\"name\")"); + var result = ParameterAttributeFormatter.JoinAttributes("AliasAs(\"name\")"); result.Should().Be("[AliasAs(\"name\")] "); } [Test] public void FindSupportedType_Passes_Through_Type_Name() { - var result = ParameterShared.FindSupportedType("string"); + var result = ParameterTypeResolver.FindSupportedType("string"); result.Should().Be("string"); } [Test] public void ConvertToVariableName_Replaces_Unsafe_Characters() { - var result = ParameterShared.ConvertToVariableName("field-name"); + var result = ParameterNaming.ConvertToVariableName("field-name"); result.Should().Be("field_name"); } [Test] public void ConvertToVariableName_Returns_Value_For_Empty_String() { - var result = ParameterShared.ConvertToVariableName(string.Empty); + var result = ParameterNaming.ConvertToVariableName(string.Empty); result.Should().Be("value"); } @@ -227,7 +227,7 @@ public void ConvertToVariableName_Returns_Value_For_Empty_String() public void AppendXmlDocComment_Extends_CodeBuilder() { var codeBuilder = new StringBuilder(); - ParameterShared.AppendXmlDocComment("Some description", codeBuilder); + DynamicQuerystringParameterBuilder.AppendXmlDocComment("Some description", codeBuilder); codeBuilder.ToString().Should().Contain("Some description"); } } diff --git a/src/Refitter.Tests/Parameters/ParameterListBuilderTests.cs b/src/Refitter.Tests/Parameters/ParameterListBuilderTests.cs new file mode 100644 index 000000000..0b3246ca7 --- /dev/null +++ b/src/Refitter.Tests/Parameters/ParameterListBuilderTests.cs @@ -0,0 +1,152 @@ +using FluentAssertions; +using NSwag; +using NSwag.CodeGeneration.CSharp.Models; +using Refitter.Core; +using TUnit.Core; + +namespace Refitter.Tests.Parameters; + +public class ParameterListBuilderTests +{ + private const string TwoQueryParameterSpec = """ + openapi: 3.0.0 + info: + title: Test + version: "1.0" + paths: + /test: + get: + operationId: getTest + parameters: + - name: page + in: query + schema: + type: integer + - name: limit + in: query + schema: + type: integer + responses: + '200': + description: Success + """; + + private const string NoParameterSpec = """ + openapi: 3.0.0 + info: + title: Test + version: "1.0" + paths: + /test: + get: + operationId: getTest + responses: + '200': + description: Success + """; + + private const string RequiredAndOptionalQuerySpec = """ + openapi: 3.0.0 + info: + title: Test + version: "1.0" + paths: + /test: + get: + operationId: getTest + parameters: + - name: page + in: query + required: true + schema: + type: integer + - name: limit + in: query + schema: + type: integer + responses: + '200': + description: Success + """; + + [Test] + public async Task Build_With_DynamicQuerystring_Returns_Wrapper_Parameter_And_Code() + { + var (operationModel, operation, settings) = await SetupAsync( + TwoQueryParameterSpec, + new RefitGeneratorSettings { UseDynamicQuerystringParameters = true }); + + var result = new ParameterListBuilder(settings) + .Build(operationModel, operation, "TestQueryParams"); + + result.DynamicQuerystringCode.Should().NotBeNullOrWhiteSpace(); + result.Parameters.Should().ContainSingle() + .Which.Should().Contain("[Query] TestQueryParams").And.Contain("queryParams"); + } + + [Test] + public async Task Build_Without_DynamicQuerystring_Returns_Null_Code_And_Individual_Parameters() + { + var (operationModel, operation, settings) = await SetupAsync( + TwoQueryParameterSpec, + new RefitGeneratorSettings()); + + var result = new ParameterListBuilder(settings) + .Build(operationModel, operation, "TestQueryParams"); + + result.DynamicQuerystringCode.Should().BeNull(); + result.Parameters.Should().HaveCount(2); + } + + [Test] + public async Task Build_Appends_CancellationToken_When_Enabled() + { + var (operationModel, operation, settings) = await SetupAsync( + NoParameterSpec, + new RefitGeneratorSettings { UseCancellationTokens = true }); + + var result = new ParameterListBuilder(settings) + .Build(operationModel, operation, string.Empty); + + result.Parameters.Should().ContainSingle() + .Which.Should().Be("CancellationToken cancellationToken = default"); + } + + [Test] + public async Task Build_Appends_ApizrRequestOptions_When_Enabled() + { + var (operationModel, operation, settings) = await SetupAsync( + NoParameterSpec, + new RefitGeneratorSettings { ApizrSettings = new ApizrSettings { WithRequestOptions = true } }); + + var result = new ParameterListBuilder(settings) + .Build(operationModel, operation, string.Empty); + + result.Parameters.Should().ContainSingle() + .Which.Should().Contain("IApizrRequestOptions options"); + } + + [Test] + public async Task Build_Orders_Optional_Parameters_After_Required() + { + var (operationModel, operation, settings) = await SetupAsync( + RequiredAndOptionalQuerySpec, + new RefitGeneratorSettings { OptionalParameters = true }); + + var result = new ParameterListBuilder(settings) + .Build(operationModel, operation, "TestQueryParams"); + + result.Parameters.Last().Should().Contain("limit").And.Contain("= default"); + } + + private static async Task<(CSharpOperationModel OperationModel, OpenApiOperation Operation, RefitGeneratorSettings Settings)> SetupAsync( + string spec, + RefitGeneratorSettings settings) + { + var document = await OpenApiYamlDocument.FromYamlAsync(spec); + var generator = new CSharpClientGeneratorFactory(settings, document).Create(); + var operation = document.Paths["/test"]["get"]; + var operationModel = generator.CreateOperationModel(operation); + return (operationModel, operation, settings); + } +}