From 45d694f377bf371ae68830b4424ac5e1f0b00e07 Mon Sep 17 00:00:00 2001 From: Amund Myrbostad Date: Tue, 10 Dec 2024 09:55:12 +0100 Subject: [PATCH 1/5] Added support for custom date format --- src/Refitter.Core/ParameterExtractor.cs | 10 +- .../Settings/CodeGeneratorSettings.cs | 2 + src/Refitter.SourceGenerator/README.md | 12 +- .../Examples/CustomDateFormatTests.cs | 133 ++++++++++++++ src/Refitter/README.md | 168 +++++++++--------- 5 files changed, 232 insertions(+), 93 deletions(-) create mode 100644 src/Refitter.Tests/Examples/CustomDateFormatTests.cs diff --git a/src/Refitter.Core/ParameterExtractor.cs b/src/Refitter.Core/ParameterExtractor.cs index f6f82751b..a09fb62e2 100644 --- a/src/Refitter.Core/ParameterExtractor.cs +++ b/src/Refitter.Core/ParameterExtractor.cs @@ -11,7 +11,7 @@ public static IEnumerable GetParameters( CSharpOperationModel operationModel, OpenApiOperation operation, RefitGeneratorSettings settings, - string dynamicQuerystringParameterType, + string dynamicQuerystringParameterType, out string? dynamicQuerystringParameters) { var routeParameters = operationModel.Parameters @@ -46,7 +46,7 @@ public static IEnumerable GetParameters( var generatedAliasAsAttribute = string.IsNullOrWhiteSpace(GetAliasAsAttribute(p)) ? string.Empty : $"[{GetAliasAsAttribute(p)}]"; - + return $"{generatedAliasAsAttribute}StreamPart {p.VariableName}"; }) .ToList(); @@ -90,7 +90,7 @@ private static string GetQueryAttribute(CSharpParameterModel parameter, RefitGen return (parameter, settings) switch { { parameter.IsArray: true } => "Query(CollectionFormat.Multi)", - { parameter.IsDate: true, settings.UseIsoDateFormat: true } => "Query(Format = \"yyyy-MM-dd\")", + { parameter.IsDate: true, settings.UseIsoDateFormat: true } => $"Query(Format = \"{ (settings.CodeGeneratorSettings is not null ? settings.CodeGeneratorSettings?.DateFormat : "yyyy-MM-dd")}\")", _ => "Query", }; } @@ -211,7 +211,7 @@ private static List GetQueryParameters(CSharpOperationModel operationMod } dynamicQuerystringParametersCodeBuilder.AppendLine( - $$""" + $$""" {{modifier}} {{classStyle}} {{dynamicQuerystringParameterType}} { """); @@ -224,7 +224,7 @@ private static List GetQueryParameters(CSharpOperationModel operationMod { {{initializedParametersCodeBuilder}} } - """); + """); } dynamicQuerystringParametersCodeBuilder.AppendLine( diff --git a/src/Refitter.Core/Settings/CodeGeneratorSettings.cs b/src/Refitter.Core/Settings/CodeGeneratorSettings.cs index ed97dc2ba..b56556bde 100644 --- a/src/Refitter.Core/Settings/CodeGeneratorSettings.cs +++ b/src/Refitter.Core/Settings/CodeGeneratorSettings.cs @@ -159,6 +159,8 @@ public class CodeGeneratorSettings /// public string[] ExcludedTypeNames { get; set; } = Array.Empty(); + public string DateFormat { get; set; } = "yyyy-MM-dd"; + /// /// Gets or sets a custom . /// diff --git a/src/Refitter.SourceGenerator/README.md b/src/Refitter.SourceGenerator/README.md index 1c21858f4..c85aaddbd 100644 --- a/src/Refitter.SourceGenerator/README.md +++ b/src/Refitter.SourceGenerator/README.md @@ -2,7 +2,7 @@ Refitter is available as a C# Source Generator that uses the [Refitter.Core](https://github.com/christianhelle/refitter/tree/main/src/Refitter.Core) library for generating a REST API Client using the [Refit](https://github.com/reactiveui/refit) library. Refitter can generate the Refit interface from OpenAPI specifications. Refitter could format the generated Refit interface to be managed by [Apizr](https://www.apizr.net) (v6+) and generate some registration helpers too. -The Refitter source generator is a bit untraditional in a sense that it creates a folder called `Generated` in the same location as the `.refitter` file and generates files to disk under the `Generated` folder (can be changed with `--outputFolder`). The source generator output should be included in the project and committed to source control. This is done because there is no other way to trigger the Refit source generator to pickup the Refitter generated code +The Refitter source generator is a bit untraditional in a sense that it creates a folder called `Generated` in the same location as the `.refitter` file and generates files to disk under the `Generated` folder (can be changed with `--outputFolder`). The source generator output should be included in the project and committed to source control. This is done because there is no other way to trigger the Refit source generator to pickup the Refitter generated code ***(Translation: I couldn't for the life of me figure how to get that to work, sorry)*** @@ -71,9 +71,9 @@ The following is an example `.refitter` file "dependencyInjectionSettings": { // Optional "baseUrl": "https://petstore3.swagger.io/api/v3", // Optional. Leave this blank to set the base address manually "httpMessageHandlers": [ // Optional - "AuthorizationMessageHandler", - "TelemetryMessageHandler" - ], + "AuthorizationMessageHandler", + "TelemetryMessageHandler" + ], "transientErrorHandler": "Polly", // Optional. Value=None|Polly|HttpResilience "maxRetryCount": 3, // Optional. Default=6 "firstBackoffRetryInSeconds": 0.5 // Optional. Default=1.0 @@ -116,6 +116,7 @@ The following is an example `.refitter` file "generateNativeRecords": false, "generateDefaultValues": true, "inlineNamedAny": false, + "dateFormat": "yyyy-MM-dd" "excludedTypeNames": [ "ExcludedTypeFoo", "ExcludedTypeBar" @@ -151,7 +152,7 @@ The following is an example `.refitter` file - `dependencyInjectionSettings` - Setting this will generated extension methods to `IServiceCollection` for configuring Refit clients. See https://github.com/reactiveui/refit?tab=readme-ov-file#dynamic-querystring-parameters for more information. - `baseUrl` - Used as the HttpClient base address. Leave this blank to manually set the base URL - `httpMessageHandlers` - A collection of `HttpMessageHandler` that is added to the HttpClient pipeline - - `transientErrorHandler` - This is the transient error handler to use. Possible values are `None`, `Polly`, and `HttpResilience`. Default is `None` + - `transientErrorHandler` - This is the transient error handler to use. Possible values are `None`, `Polly`, and `HttpResilience`. Default is `None` - `maxRetryCount` - This is the max retry count used in the Polly retry policy. Default is 6 - `firstBackoffRetryInSeconds` - This is the duration of the initial retry backoff. Default is 1 second - `apizrSettings` - Setting this will format Refit interface to be managed by Apizr. See https://www.apizr.net for more information @@ -191,4 +192,5 @@ The following is an example `.refitter` file - `generateNativeRecords` - Default is false - `generateDefaultValues` - Default is true - `inlineNamedAny` - Default is false + - `dateFormat` - Default is `yyyy-MM-dd` - `excludedTypeNames` - Default is empty diff --git a/src/Refitter.Tests/Examples/CustomDateFormatTests.cs b/src/Refitter.Tests/Examples/CustomDateFormatTests.cs new file mode 100644 index 000000000..7fcb8b42b --- /dev/null +++ b/src/Refitter.Tests/Examples/CustomDateFormatTests.cs @@ -0,0 +1,133 @@ +using FluentAssertions; +using Refitter.Core; +using Refitter.Tests.Build; +using Xunit; + +namespace Refitter.Tests.Examples; + +public class CustomDateFormatTests +{ + + private const string OpenApiSpec = @" +{ + ""swagger"": ""2.0"", + ""info"": { + ""title"": ""XX"", + ""version"": ""0.0.0"" + }, + ""host"": ""x.io"", + ""basePath"": ""/"", + ""schemes"": [ + ""https"" + ], + ""paths"": { + ""/t/dummy/{employee_id}"": { + ""get"": { + ""summary"": ""X"", + ""description"": ""X"", + ""operationId"": ""dummy"", + ""parameters"": [ + { + ""name"": ""employee_id"", + ""in"": ""path"", + ""description"": ""the specific employee"", + ""required"": true, + ""format"": ""int64"", + ""type"": ""integer"" + }, + { + ""name"": ""valid_from"", + ""in"": ""query"", + ""description"": ""the start of the period"", + ""required"": true, + ""format"": ""date"", + ""type"": ""string"" + }, + { + ""name"": ""valid_to"", + ""in"": ""query"", + ""description"": ""the end of the period"", + ""required"": true, + ""format"": ""date"", + ""type"": ""string"" + }, + { + ""name"": ""test_time"", + ""in"": ""query"", + ""description"": ""test parameter"", + ""required"": true, + ""format"": ""time"", + ""type"": ""string"" + } + ], + ""responses"": { + ""200"": { + ""description"": ""No response was specified"" + } + } + } + }, + } +} +"; + + + [Fact] + public async Task Can_Generate_Code() + { + string generateCode = await GenerateCode(); + generateCode.Should().NotBeNullOrWhiteSpace(); + } + + [Fact] + public async Task GeneratedCode_Contains_Date_Format_String() + { + string generateCode = await GenerateCode(); + generateCode.Should().Contain(@"[Query(Format = ""yyyy-MM-ddThh:mm:ssZ"")] "); + } + + [Fact] + public async Task GeneratedCode_Contains_TimeSpan_Parameter() + { + string generateCode = await GenerateCode(); + generateCode.Should().Contain("[Query] System.TimeSpan"); + } + + [Fact] + public async Task Can_Build_Generated_Code() + { + string generateCode = await GenerateCode(); + BuildHelper + .BuildCSharp(generateCode) + .Should() + .BeTrue(); + } + + private static async Task GenerateCode() + { + var swaggerFile = await CreateSwaggerFile(OpenApiSpec); + var settings = new RefitGeneratorSettings + { + OpenApiPath = swaggerFile, + UseIsoDateFormat = true, + CodeGeneratorSettings = new CodeGeneratorSettings + { + DateFormat = "yyyy-MM-ddThh:mm:ssZ" + } + }; + + var sut = await RefitGenerator.CreateAsync(settings); + var generateCode = sut.Generate(); + return generateCode; + } + + private static async Task CreateSwaggerFile(string contents) + { + var filename = $"{Guid.NewGuid()}.json"; + var folder = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(folder); + var swaggerFile = Path.Combine(folder, filename); + await File.WriteAllTextAsync(swaggerFile, contents); + return swaggerFile; + } +} diff --git a/src/Refitter/README.md b/src/Refitter/README.md index cde830125..9f6b34964 100644 --- a/src/Refitter/README.md +++ b/src/Refitter/README.md @@ -47,68 +47,68 @@ ARGUMENTS: [URL or input file] URL or file path to OpenAPI Specification file OPTIONS: - DEFAULT - -h, --help Prints help information - -v, --version Prints version information - -s, --settings-file Path to .refitter settings file. Specifying this will ignore all other settings (except for --output) - -n, --namespace GeneratedCode Default namespace to use for generated types - --contracts-namespace Default namespace to use for generated contracts - -o, --output Output.cs Path to Output file or folder (if multiple files are generated) - --contracts-output Output path for generated contracts. Enabling this automatically enables generating multiple files - --no-auto-generated-header Don't add header to output file - --no-accept-headers Don't add header to output file - --interface-only Don't generate contract types - --contract-only Don't generate clients - --use-api-response Return Task> instead of Task - --use-observable-response Return IObservable instead of Task - --internal Set the accessibility of the generated types to 'internal' - --cancellation-tokens Use cancellation tokens - --no-operation-headers Don't generate operation headers - --no-logging Don't log errors or collect telemetry - --additional-namespace Add additional namespace to generated types - --exclude-namespace Exclude namespace on generated types - --use-iso-date-format Explicitly format date query string parameters in ISO 8601 standard date format using delimiters (2023-06-15) - --multiple-interfaces Generate a Refit interface for each endpoint. May be one of ByEndpoint, ByTag - --multiple-files Generate multiple files instead of a single large file. - The output files can be the following: - - RefitInterfaces.cs - - DependencyInjection.cs - - Contracts.cs - --match-path Only include Paths that match the provided regular expression. May be set multiple times - --tag Only include Endpoints that contain this tag. May be set multiple times and result in OR'ed evaluation - --skip-validation Skip validation of the OpenAPI specification - --no-deprecated-operations Don't generate deprecated operations + DEFAULT + -h, --help Prints help information + -v, --version Prints version information + -s, --settings-file Path to .refitter settings file. Specifying this will ignore all other settings (except for --output) + -n, --namespace GeneratedCode Default namespace to use for generated types + --contracts-namespace Default namespace to use for generated contracts + -o, --output Output.cs Path to Output file or folder (if multiple files are generated) + --contracts-output Output path for generated contracts. Enabling this automatically enables generating multiple files + --no-auto-generated-header Don't add header to output file + --no-accept-headers Don't add header to output file + --interface-only Don't generate contract types + --contract-only Don't generate clients + --use-api-response Return Task> instead of Task + --use-observable-response Return IObservable instead of Task + --internal Set the accessibility of the generated types to 'internal' + --cancellation-tokens Use cancellation tokens + --no-operation-headers Don't generate operation headers + --no-logging Don't log errors or collect telemetry + --additional-namespace Add additional namespace to generated types + --exclude-namespace Exclude namespace on generated types + --use-iso-date-format Explicitly format date query string parameters in ISO 8601 standard date format using delimiters (2023-06-15) + --multiple-interfaces Generate a Refit interface for each endpoint. May be one of ByEndpoint, ByTag + --multiple-files Generate multiple files instead of a single large file. + The output files can be the following: + - RefitInterfaces.cs + - DependencyInjection.cs + - Contracts.cs + --match-path Only include Paths that match the provided regular expression. May be set multiple times + --tag Only include Endpoints that contain this tag. May be set multiple times and result in OR'ed evaluation + --skip-validation Skip validation of the OpenAPI specification + --no-deprecated-operations Don't generate deprecated operations --operation-name-template Generate operation names using pattern. When using --multiple-interfaces ByEndpoint, this is name of the Execute() method in the interface - --optional-nullable-parameters Generate nullable parameters as optional parameters - --trim-unused-schema Removes unreferenced components schema to keep the generated output to a minimum - --keep-schema Force to keep matching schema, uses regular expressions. Use together with "--trim-unused-schema". Can be set multiple times - --no-banner Don't show donation banner - --skip-default-additional-properties Set to true to skip default additional properties - --operation-name-generator Default The NSwag IOperationNameGenerator implementation to use. - May be one of: - - Default - - MultipleClientsFromOperationId - - MultipleClientsFromPathSegments - - MultipleClientsFromFirstTagAndOperationId - - MultipleClientsFromFirstTagAndOperationName - - MultipleClientsFromFirstTagAndPathSegments - - SingleClientFromOperationId - - SingleClientFromPathSegments - See https://refitter.github.io/api/Refitter.Core.OperationNameGeneratorTypes.html for more information - --immutable-records Generate contracts as immutable records instead of classes - --use-apizr Use Apizr by: - - Adding a final IApizrRequestOptions options parameter to all generated methods - - Providing cancellation tokens by Apizr request options instead of a dedicated parameter - - Using method overloads instead of optional parameters - See https://refitter.github.io for more information and https://www.apizr.net to get started with Apizr - --use-dynamic-querystring-parameters Enable wrapping multiple query parameters into a single complex one. Default is no wrapping. - See https://github.com/reactiveui/refit?tab=readme-ov-file#dynamic-querystring-parameters for more information - --use-polymorphic-serialization Use System.Text.Json polymorphic serialization. - Replaces NSwag JsonInheritanceConverter attributes with System.Text.Json JsonPolymorphicAttributes. - To have the native support of inheritance (de)serialization and fallback to base types when - payloads with (yet) unknown types are offered by newer versions of an API - See https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/polymorphism for more information - --disposable Generate refit clients that implement IDisposable + --optional-nullable-parameters Generate nullable parameters as optional parameters + --trim-unused-schema Removes unreferenced components schema to keep the generated output to a minimum + --keep-schema Force to keep matching schema, uses regular expressions. Use together with "--trim-unused-schema". Can be set multiple times + --no-banner Don't show donation banner + --skip-default-additional-properties Set to true to skip default additional properties + --operation-name-generator Default The NSwag IOperationNameGenerator implementation to use. + May be one of: + - Default + - MultipleClientsFromOperationId + - MultipleClientsFromPathSegments + - MultipleClientsFromFirstTagAndOperationId + - MultipleClientsFromFirstTagAndOperationName + - MultipleClientsFromFirstTagAndPathSegments + - SingleClientFromOperationId + - SingleClientFromPathSegments + See https://refitter.github.io/api/Refitter.Core.OperationNameGeneratorTypes.html for more information + --immutable-records Generate contracts as immutable records instead of classes + --use-apizr Use Apizr by: + - Adding a final IApizrRequestOptions options parameter to all generated methods + - Providing cancellation tokens by Apizr request options instead of a dedicated parameter + - Using method overloads instead of optional parameters + See https://refitter.github.io for more information and https://www.apizr.net to get started with Apizr + --use-dynamic-querystring-parameters Enable wrapping multiple query parameters into a single complex one. Default is no wrapping. + See https://github.com/reactiveui/refit?tab=readme-ov-file#dynamic-querystring-parameters for more information + --use-polymorphic-serialization Use System.Text.Json polymorphic serialization. + Replaces NSwag JsonInheritanceConverter attributes with System.Text.Json JsonPolymorphicAttributes. + To have the native support of inheritance (de)serialization and fallback to base types when + payloads with (yet) unknown types are offered by newer versions of an API + See https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/polymorphism for more information + --disposable Generate refit clients that implement IDisposable ``` ### .Refitter File format @@ -174,8 +174,8 @@ The following is an example `.refitter` file "dependencyInjectionSettings": { // Optional "baseUrl": "https://petstore3.swagger.io/api/v3", // Optional. Leave this blank to set the base address manually "httpMessageHandlers": [ // Optional - "AuthorizationMessageHandler", - "TelemetryMessageHandler" + "AuthorizationMessageHandler", + "TelemetryMessageHandler" ], "transientErrorHandler": "Polly", // Optional. Value=None|Polly|HttpResilience "maxRetryCount": 3, // Optional. Default=6 @@ -219,6 +219,7 @@ The following is an example `.refitter` file "generateNativeRecords": false, "generateDefaultValues": true, "inlineNamedAny": false, + "dateFormat": "yyyy-MM-dd" "excludedTypeNames": [ "ExcludedTypeFoo", "ExcludedTypeBar" @@ -301,6 +302,7 @@ The following is an example `.refitter` file - `generateNativeRecords` - Default is false - `generateDefaultValues` - Default is true - `inlineNamedAny` - Default is false + - `dateFormat` - Default is `yyyy-MM-dd` - `excludedTypeNames` - Default is empty To generate code from an OpenAPI specifications file, run the following: @@ -1519,38 +1521,38 @@ public partial interface ISwaggerPetstoreOpenAPI30 Task DeleteUser(string username); -} +} public class UpdatePetWithFormQueryParams { - + /// /// Name of pet that needs to be updated /// - [Query] + [Query] public string Name { get; set; } /// /// Status of pet that needs to be updated /// - [Query] + [Query] public string Status { get; set; } } public class LoginUserQueryParams { - + /// /// The user name for login /// - [Query] + [Query] public string Username { get; set; } /// /// The password for login in clear text /// - [Query] + [Query] public string Password { get; set; } } @@ -1592,7 +1594,7 @@ internal class Program } ``` -The `RestService` class generates an implementation of `ISwaggerPetstore` that uses `HttpClient` to make its calls. +The `RestService` class generates an implementation of `ISwaggerPetstore` that uses `HttpClient` to make its calls. The code above when run will output something like this: @@ -1691,8 +1693,8 @@ which will generate an extension method to `IServiceCollection` called `Configur ```cs public static IServiceCollection ConfigureRefitClients( - this IServiceCollection services, - Action? builder = default, + this IServiceCollection services, + Action? builder = default, RefitSettings? settings = default) { var clientBuilderISwaggerPetstore = services @@ -1736,8 +1738,8 @@ Will generate a single `ConfigureRefitClients()` extension methods that may cont ```csharp public static IServiceCollection ConfigureRefitClients( - this IServiceCollection services, - Action? builder = default, + this IServiceCollection services, + Action? builder = default, RefitSettings? settings = default) { var clientBuilderIPetApi = services @@ -1874,7 +1876,7 @@ public static IServiceCollection ConfigurePetstoreApiApizrManager( .WithAutoMapperMappingHandler() .WithPriority() .WithFileTransferMediation(); - + return services.AddApizrManagerFor(optionsBuilder); } ``` @@ -1887,7 +1889,7 @@ This comes in handy especially when generating multiple interfaces, by tag or en "namespace": "Petstore", "useDynamicQuerystringParameters": true, "multipleInterfaces": "ByTag", - "naming": { + "naming": { "useOpenApiTitle": false, "interfaceName": "Petstore" }, @@ -1935,7 +1937,7 @@ public static IServiceCollection ConfigurePetstoreApizrManagers( .WithAutoMapperMappingHandler() .WithPriority() .WithFileTransferMediation(); - + return services.AddApizr( registry => registry .AddManagerFor() @@ -1980,8 +1982,8 @@ public static IApizrManager BuildPetstore30ApizrManag .WithAutoMapperMappingHandler(new MapperConfiguration(config => { /* YOUR_MAPPINGS_HERE */ })) .WithPriority() .WithFileTransfer(); - - return ApizrBuilder.Current.CreateManagerFor(optionsBuilder); + + return ApizrBuilder.Current.CreateManagerFor(optionsBuilder); } ``` @@ -1993,7 +1995,7 @@ This comes in handy especially when generating multiple interfaces, by tag or en "namespace": "Petstore", "useDynamicQuerystringParameters": true, "multipleInterfaces": "ByTag", - "naming": { + "naming": { "useOpenApiTitle": false, "interfaceName": "Petstore" }, @@ -2027,7 +2029,7 @@ public static IApizrRegistry BuildPetstoreApizrManagers(Action { /* YOUR_MAPPINGS_HERE */ })) .WithPriority() .WithFileTransferMediation(); - + return ApizrBuilder.Current.CreateRegistry( registry => registry .AddManagerFor() @@ -2051,7 +2053,7 @@ To know how to make Apizr fit your needs, please refer to the [Apizr documentati Once you called the generated method, you will get an `IApizrManager` instance that you can use to make requests to the API. Here's an example of how to use it: ```csharp -var result = await petstoreManager.ExecuteAsync((api, opt) => api.GetPetById(1, opt), +var result = await petstoreManager.ExecuteAsync((api, opt) => api.GetPetById(1, opt), options => options // Whatever final request options you want to apply .WithPriority(Priority.Background) .WithHeaders(["HeaderKey1: HeaderValue1"]) From 4305994f683adb1d0f9d2715ee0b4050525ed135 Mon Sep 17 00:00:00 2001 From: Amund Myrbostad Date: Tue, 10 Dec 2024 10:04:01 +0100 Subject: [PATCH 2/5] Updated ParameterExtractor.cs to hopefully be more backwards compatible --- src/Refitter.Core/ParameterExtractor.cs | 3 ++- src/Refitter.Tests/Examples/CustomDateFormatTests.cs | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Refitter.Core/ParameterExtractor.cs b/src/Refitter.Core/ParameterExtractor.cs index a09fb62e2..bc94e7745 100644 --- a/src/Refitter.Core/ParameterExtractor.cs +++ b/src/Refitter.Core/ParameterExtractor.cs @@ -90,7 +90,8 @@ private static string GetQueryAttribute(CSharpParameterModel parameter, RefitGen return (parameter, settings) switch { { parameter.IsArray: true } => "Query(CollectionFormat.Multi)", - { parameter.IsDate: true, settings.UseIsoDateFormat: true } => $"Query(Format = \"{ (settings.CodeGeneratorSettings is not null ? settings.CodeGeneratorSettings?.DateFormat : "yyyy-MM-dd")}\")", + { parameter.IsDate: true, settings.UseIsoDateFormat: false } => $"Query(Format = \"{ (settings.CodeGeneratorSettings is not null ? settings.CodeGeneratorSettings?.DateFormat : "yyyy-MM-dd")}\")", + { parameter.IsDate: true, settings.UseIsoDateFormat: true } => "Query(Format = \"yyyy-MM-dd\")", _ => "Query", }; } diff --git a/src/Refitter.Tests/Examples/CustomDateFormatTests.cs b/src/Refitter.Tests/Examples/CustomDateFormatTests.cs index 7fcb8b42b..0f23a145b 100644 --- a/src/Refitter.Tests/Examples/CustomDateFormatTests.cs +++ b/src/Refitter.Tests/Examples/CustomDateFormatTests.cs @@ -109,7 +109,6 @@ private static async Task GenerateCode() var settings = new RefitGeneratorSettings { OpenApiPath = swaggerFile, - UseIsoDateFormat = true, CodeGeneratorSettings = new CodeGeneratorSettings { DateFormat = "yyyy-MM-ddThh:mm:ssZ" From 4ccb2815bd508ef12b7ac6777118b39ab536c39b Mon Sep 17 00:00:00 2001 From: Amund Myrbostad Date: Tue, 10 Dec 2024 10:28:27 +0100 Subject: [PATCH 3/5] Updated ParameterExtractor.cs --- src/Refitter.Core/ParameterExtractor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Refitter.Core/ParameterExtractor.cs b/src/Refitter.Core/ParameterExtractor.cs index bc94e7745..11c6bbfde 100644 --- a/src/Refitter.Core/ParameterExtractor.cs +++ b/src/Refitter.Core/ParameterExtractor.cs @@ -90,8 +90,8 @@ private static string GetQueryAttribute(CSharpParameterModel parameter, RefitGen return (parameter, settings) switch { { parameter.IsArray: true } => "Query(CollectionFormat.Multi)", - { parameter.IsDate: true, settings.UseIsoDateFormat: false } => $"Query(Format = \"{ (settings.CodeGeneratorSettings is not null ? settings.CodeGeneratorSettings?.DateFormat : "yyyy-MM-dd")}\")", { parameter.IsDate: true, settings.UseIsoDateFormat: true } => "Query(Format = \"yyyy-MM-dd\")", + { parameter.IsDate: true, settings.CodeGeneratorSettings: not null } => $"Query(Format = \"{settings.CodeGeneratorSettings?.DateFormat}\")", _ => "Query", }; } From ab530cb5be77898c0221a6fa1b8089493ae7173b Mon Sep 17 00:00:00 2001 From: Amund Myrbostad Date: Tue, 10 Dec 2024 10:35:36 +0100 Subject: [PATCH 4/5] Updated readmes --- README.md | 172 +++++++++++++------------ src/Refitter.SourceGenerator/README.md | 2 +- src/Refitter/README.md | 2 +- 3 files changed, 89 insertions(+), 87 deletions(-) diff --git a/README.md b/README.md index c2dbe8a94..590b93e7a 100644 --- a/README.md +++ b/README.md @@ -63,68 +63,68 @@ ARGUMENTS: [URL or input file] URL or file path to OpenAPI Specification file OPTIONS: - DEFAULT - -h, --help Prints help information - -v, --version Prints version information - -s, --settings-file Path to .refitter settings file. Specifying this will ignore all other settings (except for --output) - -n, --namespace GeneratedCode Default namespace to use for generated types - --contracts-namespace Default namespace to use for generated contracts - -o, --output Output.cs Path to Output file or folder (if multiple files are generated) - --contracts-output Output path for generated contracts. Enabling this automatically enables generating multiple files - --no-auto-generated-header Don't add header to output file - --no-accept-headers Don't add header to output file - --interface-only Don't generate contract types - --contract-only Don't generate clients - --use-api-response Return Task> instead of Task - --use-observable-response Return IObservable instead of Task - --internal Set the accessibility of the generated types to 'internal' - --cancellation-tokens Use cancellation tokens - --no-operation-headers Don't generate operation headers - --no-logging Don't log errors or collect telemetry - --additional-namespace Add additional namespace to generated types - --exclude-namespace Exclude namespace on generated types - --use-iso-date-format Explicitly format date query string parameters in ISO 8601 standard date format using delimiters (2023-06-15) - --multiple-interfaces Generate a Refit interface for each endpoint. May be one of ByEndpoint, ByTag - --multiple-files Generate multiple files instead of a single large file. - The output files can be the following: - - RefitInterfaces.cs - - DependencyInjection.cs - - Contracts.cs - --match-path Only include Paths that match the provided regular expression. May be set multiple times - --tag Only include Endpoints that contain this tag. May be set multiple times and result in OR'ed evaluation - --skip-validation Skip validation of the OpenAPI specification - --no-deprecated-operations Don't generate deprecated operations + DEFAULT + -h, --help Prints help information + -v, --version Prints version information + -s, --settings-file Path to .refitter settings file. Specifying this will ignore all other settings (except for --output) + -n, --namespace GeneratedCode Default namespace to use for generated types + --contracts-namespace Default namespace to use for generated contracts + -o, --output Output.cs Path to Output file or folder (if multiple files are generated) + --contracts-output Output path for generated contracts. Enabling this automatically enables generating multiple files + --no-auto-generated-header Don't add header to output file + --no-accept-headers Don't add header to output file + --interface-only Don't generate contract types + --contract-only Don't generate clients + --use-api-response Return Task> instead of Task + --use-observable-response Return IObservable instead of Task + --internal Set the accessibility of the generated types to 'internal' + --cancellation-tokens Use cancellation tokens + --no-operation-headers Don't generate operation headers + --no-logging Don't log errors or collect telemetry + --additional-namespace Add additional namespace to generated types + --exclude-namespace Exclude namespace on generated types + --use-iso-date-format Explicitly format date query string parameters in ISO 8601 standard date format using delimiters (2023-06-15) + --multiple-interfaces Generate a Refit interface for each endpoint. May be one of ByEndpoint, ByTag + --multiple-files Generate multiple files instead of a single large file. + The output files can be the following: + - RefitInterfaces.cs + - DependencyInjection.cs + - Contracts.cs + --match-path Only include Paths that match the provided regular expression. May be set multiple times + --tag Only include Endpoints that contain this tag. May be set multiple times and result in OR'ed evaluation + --skip-validation Skip validation of the OpenAPI specification + --no-deprecated-operations Don't generate deprecated operations --operation-name-template Generate operation names using pattern. When using --multiple-interfaces ByEndpoint, this is name of the Execute() method in the interface - --optional-nullable-parameters Generate nullable parameters as optional parameters - --trim-unused-schema Removes unreferenced components schema to keep the generated output to a minimum - --keep-schema Force to keep matching schema, uses regular expressions. Use together with "--trim-unused-schema". Can be set multiple times - --no-banner Don't show donation banner - --skip-default-additional-properties Set to true to skip default additional properties - --operation-name-generator Default The NSwag IOperationNameGenerator implementation to use. - May be one of: - - Default - - MultipleClientsFromOperationId - - MultipleClientsFromPathSegments - - MultipleClientsFromFirstTagAndOperationId - - MultipleClientsFromFirstTagAndOperationName - - MultipleClientsFromFirstTagAndPathSegments - - SingleClientFromOperationId - - SingleClientFromPathSegments - See https://refitter.github.io/api/Refitter.Core.OperationNameGeneratorTypes.html for more information - --immutable-records Generate contracts as immutable records instead of classes - --use-apizr Use Apizr by: - - Adding a final IApizrRequestOptions options parameter to all generated methods - - Providing cancellation tokens by Apizr request options instead of a dedicated parameter - - Using method overloads instead of optional parameters - See https://refitter.github.io for more information and https://www.apizr.net to get started with Apizr - --use-dynamic-querystring-parameters Enable wrapping multiple query parameters into a single complex one. Default is no wrapping. - See https://github.com/reactiveui/refit?tab=readme-ov-file#dynamic-querystring-parameters for more information - --use-polymorphic-serialization Use System.Text.Json polymorphic serialization. - Replaces NSwag JsonInheritanceConverter attributes with System.Text.Json JsonPolymorphicAttributes. - To have the native support of inheritance (de)serialization and fallback to base types when - payloads with (yet) unknown types are offered by newer versions of an API - See https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/polymorphism for more information - --disposable Generate refit clients that implement IDisposable + --optional-nullable-parameters Generate nullable parameters as optional parameters + --trim-unused-schema Removes unreferenced components schema to keep the generated output to a minimum + --keep-schema Force to keep matching schema, uses regular expressions. Use together with "--trim-unused-schema". Can be set multiple times + --no-banner Don't show donation banner + --skip-default-additional-properties Set to true to skip default additional properties + --operation-name-generator Default The NSwag IOperationNameGenerator implementation to use. + May be one of: + - Default + - MultipleClientsFromOperationId + - MultipleClientsFromPathSegments + - MultipleClientsFromFirstTagAndOperationId + - MultipleClientsFromFirstTagAndOperationName + - MultipleClientsFromFirstTagAndPathSegments + - SingleClientFromOperationId + - SingleClientFromPathSegments + See https://refitter.github.io/api/Refitter.Core.OperationNameGeneratorTypes.html for more information + --immutable-records Generate contracts as immutable records instead of classes + --use-apizr Use Apizr by: + - Adding a final IApizrRequestOptions options parameter to all generated methods + - Providing cancellation tokens by Apizr request options instead of a dedicated parameter + - Using method overloads instead of optional parameters + See https://refitter.github.io for more information and https://www.apizr.net to get started with Apizr + --use-dynamic-querystring-parameters Enable wrapping multiple query parameters into a single complex one. Default is no wrapping. + See https://github.com/reactiveui/refit?tab=readme-ov-file#dynamic-querystring-parameters for more information + --use-polymorphic-serialization Use System.Text.Json polymorphic serialization. + Replaces NSwag JsonInheritanceConverter attributes with System.Text.Json JsonPolymorphicAttributes. + To have the native support of inheritance (de)serialization and fallback to base types when + payloads with (yet) unknown types are offered by newer versions of an API + See https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/polymorphism for more information + --disposable Generate refit clients that implement IDisposable ``` To generate code from an OpenAPI specifications file, run the following: @@ -139,7 +139,7 @@ This will generate a file called `Output.cs` which contains the Refit interface Refitter is available as a C# Source Generator that uses the [Refitter.Core](https://github.com/christianhelle/refitter/tree/main/src/Refitter.Core) library for generating a REST API Client using the [Refit](https://github.com/reactiveui/refit) library. Refitter can generate the Refit interface from OpenAPI specifications. Refitter could format the generated Refit interface to be managed by [Apizr](https://www.apizr.net) and generate some registration helpers too. -The Refitter source generator is a bit untraditional in a sense that it creates a folder called `Generated` in the same location as the `.refitter` file and generates files to disk under the `Generated` folder (can be changed with `--outputFolder`). The source generator output should be included in the project and committed to source control. This is done because there is no other way to trigger the Refit source generator to pickup the Refitter generated code +The Refitter source generator is a bit untraditional in a sense that it creates a folder called `Generated` in the same location as the `.refitter` file and generates files to disk under the `Generated` folder (can be changed with `--outputFolder`). The source generator output should be included in the project and committed to source control. This is done because there is no other way to trigger the Refit source generator to pickup the Refitter generated code ***(Translation: I couldn't for the life of me figure how to get that to work, sorry)*** @@ -220,8 +220,8 @@ The following is an example `.refitter` file "dependencyInjectionSettings": { // Optional "baseUrl": "https://petstore3.swagger.io/api/v3", // Optional. Leave this blank to set the base address manually "httpMessageHandlers": [ // Optional - "AuthorizationMessageHandler", - "TelemetryMessageHandler" + "AuthorizationMessageHandler", + "TelemetryMessageHandler" ], "usePolly": true, // DEPRECATED - Use "transientErrorHandler": "None|Polly|HttpResilience" instead "transientErrorHandler": "HttpResilience", // Optional. Set this to configure transient error handling with a retry policy that uses a jittered backoff. May be one of None, Polly, HttpResilience @@ -267,6 +267,7 @@ The following is an example `.refitter` file "generateNativeRecords": false, "generateDefaultValues": true, "inlineNamedAny": false, + "dateFormat": "yyyy-MM-dd", "excludedTypeNames": [ "ExcludedTypeFoo", "ExcludedTypeBar" @@ -350,6 +351,7 @@ The following is an example `.refitter` file - `generateNativeRecords` - Default is false - `generateDefaultValues` - Default is true - `inlineNamedAny` - Default is false + - `dateFormat` - Default is `yyyy-MM-dd` - `excludedTypeNames` - Default is empty ## MSBuild @@ -398,7 +400,7 @@ In the example above, the `nuget.config` file is placed under the `refitter` fol ### Refitter.MSBuild package -Refitter ships with an MSBuild custom task that is distributed as a NuGet package and includes the Refitter CLI binary. This will simplify generating code from OpenAPI specifications at build time. +Refitter ships with an MSBuild custom task that is distributed as a NuGet package and includes the Refitter CLI binary. This will simplify generating code from OpenAPI specifications at build time. To use the package, install `Refitter.MSBuild` @@ -2073,38 +2075,38 @@ public partial interface ISwaggerPetstoreOpenAPI30 Task DeleteUser(string username); -} +} public class UpdatePetWithFormQueryParams { - + /// /// Name of pet that needs to be updated /// - [Query] + [Query] public string Name { get; set; } /// /// Status of pet that needs to be updated /// - [Query] + [Query] public string Status { get; set; } } public class LoginUserQueryParams { - + /// /// The user name for login /// - [Query] + [Query] public string Username { get; set; } /// /// The password for login in clear text /// - [Query] + [Query] public string Password { get; set; } } @@ -2146,7 +2148,7 @@ internal class Program } ``` -The `RestService` class generates an implementation of `ISwaggerPetstore` that uses `HttpClient` to make its calls. +The `RestService` class generates an implementation of `ISwaggerPetstore` that uses `HttpClient` to make its calls. The code above when run will output something like this: @@ -2246,8 +2248,8 @@ which will generate an extension method to `IServiceCollection` called `Configur ```cs public static IServiceCollection ConfigureRefitClients( - this IServiceCollection services, - Action? builder = default, + this IServiceCollection services, + Action? builder = default, RefitSettings? settings = default) { var clientBuilderISwaggerPetstore = services @@ -2291,8 +2293,8 @@ Will generate a single `ConfigureRefitClients()` extension methods that may cont ```csharp public static IServiceCollection ConfigureRefitClients( - this IServiceCollection services, - Action? builder = default, + this IServiceCollection services, + Action? builder = default, RefitSettings? settings = default) { var clientBuilderIPetApi = services @@ -2431,7 +2433,7 @@ public static IServiceCollection ConfigurePetstoreApiApizrManager( .WithPriority() .WithOptionalMediation() .WithFileTransferOptionalMediation(); - + return services.AddApizrManagerFor(optionsBuilder); } ``` @@ -2444,7 +2446,7 @@ This comes in handy especially when generating multiple interfaces, by tag or en "namespace": "Petstore", "useDynamicQuerystringParameters": true, "multipleInterfaces": "ByTag", - "naming": { + "naming": { "useOpenApiTitle": false, "interfaceName": "Petstore" }, @@ -2494,7 +2496,7 @@ public static IServiceCollection ConfigurePetstoreApizrManagers( .WithPriority() .WithOptionalMediation() .WithFileTransferOptionalMediation(); - + return services.AddApizr( registry => registry .AddManagerFor() @@ -2538,8 +2540,8 @@ public static IApizrManager BuildPetstore30ApizrManag .WithAkavacheCacheHandler() .WithAutoMapperMappingHandler(new MapperConfiguration(config => { /* YOUR_MAPPINGS_HERE */ })) .WithPriority(); - - return ApizrBuilder.Current.CreateManagerFor(optionsBuilder); + + return ApizrBuilder.Current.CreateManagerFor(optionsBuilder); } ``` @@ -2551,7 +2553,7 @@ This comes in handy especially when generating multiple interfaces, by tag or en "namespace": "Petstore", "useDynamicQuerystringParameters": true, "multipleInterfaces": "ByTag", - "naming": { + "naming": { "useOpenApiTitle": false, "interfaceName": "Petstore" }, @@ -2585,7 +2587,7 @@ public static IApizrRegistry BuildPetstoreApizrManagers(Action { /* YOUR_MAPPINGS_HERE */ })) .WithPriority(); - + return ApizrBuilder.Current.CreateRegistry( registry => registry .AddManagerFor() @@ -2609,7 +2611,7 @@ To know how to make Apizr fit your needs, please refer to the [Apizr documentati Once you called the generated method, you will get an `IApizrManager` instance that you can use to make requests to the API. Here's an example of how to use it: ```csharp -var result = await petstoreManager.ExecuteAsync((api, opt) => api.GetPetById(1, opt), +var result = await petstoreManager.ExecuteAsync((api, opt) => api.GetPetById(1, opt), options => options // Whatever final request options you want to apply .WithPriority(Priority.Background) .WithHeaders(["HeaderKey1: HeaderValue1"]) diff --git a/src/Refitter.SourceGenerator/README.md b/src/Refitter.SourceGenerator/README.md index c85aaddbd..0dc859a29 100644 --- a/src/Refitter.SourceGenerator/README.md +++ b/src/Refitter.SourceGenerator/README.md @@ -116,7 +116,7 @@ The following is an example `.refitter` file "generateNativeRecords": false, "generateDefaultValues": true, "inlineNamedAny": false, - "dateFormat": "yyyy-MM-dd" + "dateFormat": "yyyy-MM-dd", "excludedTypeNames": [ "ExcludedTypeFoo", "ExcludedTypeBar" diff --git a/src/Refitter/README.md b/src/Refitter/README.md index 9f6b34964..7e9fcbb51 100644 --- a/src/Refitter/README.md +++ b/src/Refitter/README.md @@ -219,7 +219,7 @@ The following is an example `.refitter` file "generateNativeRecords": false, "generateDefaultValues": true, "inlineNamedAny": false, - "dateFormat": "yyyy-MM-dd" + "dateFormat": "yyyy-MM-dd", "excludedTypeNames": [ "ExcludedTypeFoo", "ExcludedTypeBar" From 80a37bcb1dcb7602e5e79aab91878bb057106348 Mon Sep 17 00:00:00 2001 From: Amund Myrbostad Date: Tue, 10 Dec 2024 11:01:24 +0100 Subject: [PATCH 5/5] Now supports date-time and not only date --- src/Refitter.Core/ParameterExtractor.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Refitter.Core/ParameterExtractor.cs b/src/Refitter.Core/ParameterExtractor.cs index 11c6bbfde..efde39815 100644 --- a/src/Refitter.Core/ParameterExtractor.cs +++ b/src/Refitter.Core/ParameterExtractor.cs @@ -90,8 +90,8 @@ private static string GetQueryAttribute(CSharpParameterModel parameter, RefitGen return (parameter, settings) switch { { parameter.IsArray: true } => "Query(CollectionFormat.Multi)", - { parameter.IsDate: true, settings.UseIsoDateFormat: true } => "Query(Format = \"yyyy-MM-dd\")", - { parameter.IsDate: true, settings.CodeGeneratorSettings: not null } => $"Query(Format = \"{settings.CodeGeneratorSettings?.DateFormat}\")", + { parameter.IsDateOrDateTime: true, settings.UseIsoDateFormat: true } => "Query(Format = \"yyyy-MM-dd\")", + { parameter.IsDateOrDateTime: true, settings.CodeGeneratorSettings: not null } => $"Query(Format = \"{settings.CodeGeneratorSettings?.DateFormat}\")", _ => "Query", }; }