diff --git a/.github/workflows/qodana.yml b/.github/workflows/qodana.yml index aab76427..44b85705 100644 --- a/.github/workflows/qodana.yml +++ b/.github/workflows/qodana.yml @@ -20,6 +20,7 @@ jobs: fetch-depth: 0 # a full history is required for pull request analysis - name: 'Qodana Scan' uses: JetBrains/qodana-action@v2023.2 + continue-on-error: true with: push-fixes: pull-request pr-mode: false diff --git a/README.md b/README.md index bb9ad890..c39f4e49 100644 --- a/README.md +++ b/README.md @@ -137,9 +137,10 @@ The following is an example `.refitter` file "useIsoDateFormat": false, // Optional. Default=false "multipleInterfaces": "ByEndpoint", // Optional. May be one of "ByEndpoint" or "ByTag" "generateDeprecatedOperations": false, // Optional. Default=true - "operationNameTemplate": "{operationName}Async", // Optional. Must contain {operationName} + "operationNameTemplate": "{operationName}Async", // Optional. Must contain {operationName} when multipleInterfaces != ByEndpoint "optionalParameters": false, // Optional. Default=false "outputFolder": "../CustomOutput" // Optional. Default=./Generated + "outputFilename": "RefitInterface.cs", // Optional. Default=Output.cs for CLI tool "additionalNamespaces": [ // Optional "Namespace1", "Namespace2" @@ -215,6 +216,7 @@ The following is an example `.refitter` file - `useIsoDateFormat` - Set to `true` to explicitly format date query string parameters in ISO 8601 standard date format using delimiters (for example: 2023-06-15). Default is `false` - `multipleInterfaces` - Set to `ByEndpoint` to generate an interface for each endpoint, or `ByTag` to group Endpoints by their Tag (like SwaggerUI groups them). - `outputFolder` - a string describing a relative path to a desired output folder. Default is `./Generated` +- `outputFilename` - Output filename. Default is `Output.cs` when used from the CLI tool, otherwise its the .refitter filename. So `Petstore.refitter` becomes `Petstore.cs`. - `additionalNamespaces` - A collection of additional namespaces to include in the generated file. A use case for this is when you want to reuse contracts from a different namespace than the generated code. Default is empty - `includeTags` - A collection of tags to use a filter for including endpoints that contain this tag. - `includePathMatches` - A collection of regular expressions used to filter paths. diff --git a/docs/docfx_project/articles/refitter-file-format.md b/docs/docfx_project/articles/refitter-file-format.md index 56da178d..c6874e7a 100644 --- a/docs/docfx_project/articles/refitter-file-format.md +++ b/docs/docfx_project/articles/refitter-file-format.md @@ -23,9 +23,10 @@ The following is an example `.refitter` file "useIsoDateFormat": false, // Optional. Default=false "multipleInterfaces": "ByEndpoint", // Optional. May be one of "ByEndpoint" or "ByTag" "generateDeprecatedOperations": false, // Optional. Default=true - "operationNameTemplate": "{operationName}Async", // Optional. Must contain {operationName} + "operationNameTemplate": "{operationName}Async", // Optional. Must contain {operationName} when multipleInterfaces != ByEndpoint "optionalParameters": false, // Optional. Default=false "outputFolder": "../CustomOutput" // Optional. Default=./Generated + "outputFilename": "RefitInterface.cs", // Optional. Default=Output.cs for CLI tool "additionalNamespaces": [ // Optional "Namespace1", "Namespace2" @@ -39,7 +40,7 @@ The following is an example `.refitter` file "^/pet/.*", "^/store/.*" ], - "dependencyInjectionSettings": { + "dependencyInjectionSettings": { // Optional "baseUrl": "https://petstore3.swagger.io/api/v3", // Optional. Leave this blank to set the base address manually "httpMessageHandlers": [ // Optional "AuthorizationMessageHandler", @@ -86,8 +87,6 @@ The following is an example `.refitter` file } ``` -Here are some basic explanations of each property: - - `openApiPath` - points to the OpenAPI Specifications file. This can be the path to a file stored on disk, relative to the `.refitter` file. This can also be a URL to a remote file that will be downloaded over HTTP/HTTPS - `naming.useOpenApiTitle` - a boolean indicating whether the OpenApi title should be used. Default is `true` - `naming.interfaceName` - the name of the generated interface. The generated code will automatically prefix this with `I` so if this set to `MyApiClient` then the generated interface is called `IMyApiClient`. Default is `ApiClient` @@ -102,6 +101,7 @@ Here are some basic explanations of each property: - `useIsoDateFormat` - Set to `true` to explicitly format date query string parameters in ISO 8601 standard date format using delimiters (for example: 2023-06-15). Default is `false` - `multipleInterfaces` - Set to `ByEndpoint` to generate an interface for each endpoint, or `ByTag` to group Endpoints by their Tag (like SwaggerUI groups them). - `outputFolder` - a string describing a relative path to a desired output folder. Default is `./Generated` +- `outputFilename` - Output filename. Default is `Output.cs` when used from the CLI tool, otherwise its the .refitter filename. So `Petstore.refitter` becomes `Petstore.cs`. - `additionalNamespaces` - A collection of additional namespaces to include in the generated file. A use case for this is when you want to reuse contracts from a different namespace than the generated code. Default is empty - `includeTags` - A collection of tags to use a filter for including endpoints that contain this tag. - `includePathMatches` - A collection of regular expressions used to filter paths. diff --git a/src/Refitter.Core/Serializer.cs b/src/Refitter.Core/Serializer.cs index c08684f0..6ad778e1 100644 --- a/src/Refitter.Core/Serializer.cs +++ b/src/Refitter.Core/Serializer.cs @@ -12,7 +12,8 @@ public static class Serializer { private static readonly JsonSerializerOptions JsonSerializerOptions = new() { - PropertyNameCaseInsensitive = true + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; /// diff --git a/src/Refitter.Core/Settings/DependencyInjectionSettings.cs b/src/Refitter.Core/Settings/DependencyInjectionSettings.cs index e700493d..6c141583 100644 --- a/src/Refitter.Core/Settings/DependencyInjectionSettings.cs +++ b/src/Refitter.Core/Settings/DependencyInjectionSettings.cs @@ -1,6 +1,4 @@ -using System.Text.Json.Serialization; - -namespace Refitter.Core; +namespace Refitter.Core; /// /// Dependency Injection settings describing how the Refit client should be configured. @@ -11,31 +9,26 @@ public class DependencyInjectionSettings /// /// Base Address for the HttpClient /// - [JsonPropertyName("baseUrl")] public string? BaseUrl { get; set; } /// /// A collection of HttpMessageHandlers to be added to the HttpClient pipeline. /// This can be for telemetry logging, authorization, etc. /// - [JsonPropertyName("httpMessageHandlers")] public string[] HttpMessageHandlers { get; set; } = Array.Empty(); /// /// Set this to true to use Polly for transient fault handling. /// - [JsonPropertyName("usePolly")] public bool UsePolly { get; set; } /// /// Default max retry count for Polly. Default is 6. /// - [JsonPropertyName("pollyMaxRetryCount")] public int PollyMaxRetryCount { get; set; } = 6; /// /// The median delay to target before the first retry in seconds. Default is 1 second /// - [JsonPropertyName("firstBackoffRetryInSeconds")] public double FirstBackoffRetryInSeconds { get; set; } = 1.0; } \ No newline at end of file diff --git a/src/Refitter.Core/Settings/MultipleInterfaces.cs b/src/Refitter.Core/Settings/MultipleInterfaces.cs index e95ab9a7..f2d449c9 100644 --- a/src/Refitter.Core/Settings/MultipleInterfaces.cs +++ b/src/Refitter.Core/Settings/MultipleInterfaces.cs @@ -1,6 +1,4 @@ -using System.Text.Json.Serialization; - -namespace Refitter.Core; +namespace Refitter.Core; /// /// Enum representing the different options for generating multiple Refit interfaces. @@ -10,17 +8,16 @@ public enum MultipleInterfaces /// /// Do not generate multiple interfaces /// - [JsonPropertyName("unset")] Unset, + Unset, /// /// Generate a Refit interface for each endpoint with a single Execute() method. - /// The method name can be customized using the --operation-name-template command line option, - /// or the operationNameTemplate property in the settings file. + /// The method name can be customized using the setting. /// - [JsonPropertyName("byEndpoint")] ByEndpoint, + ByEndpoint, /// /// Generate a Refit interface for each tag /// - [JsonPropertyName("byTag")] ByTag + ByTag } \ No newline at end of file diff --git a/src/Refitter.Core/Settings/NamingSettings.cs b/src/Refitter.Core/Settings/NamingSettings.cs index 0554ff2d..c0d891a5 100644 --- a/src/Refitter.Core/Settings/NamingSettings.cs +++ b/src/Refitter.Core/Settings/NamingSettings.cs @@ -1,5 +1,4 @@ using System.Diagnostics.CodeAnalysis; -using System.Text.Json.Serialization; namespace Refitter.Core; @@ -12,12 +11,10 @@ public class NamingSettings /// /// Gets or sets a value indicating whether the OpenApi title should be used. Default is true. /// - [JsonPropertyName("useOpenApiTitle")] public bool UseOpenApiTitle { get; set; } = true; /// /// Gets or sets the name of the Interface. Default is "ApiClient". /// - [JsonPropertyName("interfaceName")] public string InterfaceName { get; set; } = "ApiClient"; } \ No newline at end of file diff --git a/src/Refitter.Core/Settings/RefitGeneratorSettings.cs b/src/Refitter.Core/Settings/RefitGeneratorSettings.cs index 0d50fcf2..8ef6dfba 100644 --- a/src/Refitter.Core/Settings/RefitGeneratorSettings.cs +++ b/src/Refitter.Core/Settings/RefitGeneratorSettings.cs @@ -9,138 +9,125 @@ namespace Refitter.Core; [ExcludeFromCodeCoverage] public class RefitGeneratorSettings { + public const string DefaultOutputFolder = "./Generated"; + /// /// Gets or sets the path to the Open API. /// - [JsonPropertyName("openApiPath")] public string OpenApiPath { get; set; } = null!; /// /// Gets or sets the namespace for the generated code. (default: GeneratedCode) /// - [JsonPropertyName("namespace")] public string Namespace { get; set; } = "GeneratedCode"; /// /// Gets or sets the naming settings. /// - [JsonPropertyName("naming")] public NamingSettings Naming { get; set; } = new(); /// /// Gets or sets a value indicating whether contracts should be generated. /// - [JsonPropertyName("generateContracts")] public bool GenerateContracts { get; set; } = true; /// /// Gets or sets a value indicating whether XML doc comments should be generated. /// - [JsonPropertyName("generateXmlDocCodeComments")] public bool GenerateXmlDocCodeComments { get; set; } = true; /// /// Gets or sets a value indicating whether to add auto-generated header. /// - [JsonPropertyName("addAutoGeneratedHeader")] public bool AddAutoGeneratedHeader { get; set; } = true; /// /// Gets or sets a value indicating whether to add accept headers [Headers("Accept: application/json")]. /// - [JsonPropertyName("addAcceptHeaders")] public bool AddAcceptHeaders { get; set; } = true; /// /// Gets or sets a value indicating whether to return IApiResponse objects. /// - [JsonPropertyName("returnIApiResponse")] public bool ReturnIApiResponse { get; set; } /// /// Gets or sets a value indicating whether to generate operation headers. /// - [JsonPropertyName("generateOperationHeaders")] public bool GenerateOperationHeaders { get; set; } = true; /// /// Gets or sets the generated type accessibility. (default: Public) /// - [JsonPropertyName("typeAccessibility")] public TypeAccessibility TypeAccessibility { get; set; } = TypeAccessibility.Public; /// /// Enable or disable the use of cancellation tokens. /// - [JsonPropertyName("useCancellationTokens")] public bool UseCancellationTokens { get; set; } /// /// Set to true to explicitly format date query string parameters /// in ISO 8601 standard date format using delimiters (for example: 2023-06-15) /// - [JsonPropertyName("useIsoDateFormat")] public bool UseIsoDateFormat { get; set; } /// /// Add additional namespace to generated types /// - [JsonPropertyName("additionalNamespaces")] public string[] AdditionalNamespaces { get; set; } = Array.Empty(); /// /// Set to true to Generate a Refit interface for each endpoint /// - [JsonPropertyName("multipleInterfaces")] [JsonConverter(typeof(JsonStringEnumConverter))] public MultipleInterfaces MultipleInterfaces { get; set; } /// /// Set to true to Generate a Refit interface for each endpoint /// - [JsonPropertyName("includePathMatches")] public string[] IncludePathMatches { get; set; } = Array.Empty(); /// /// Set to true to Generate a Refit interface for each endpoint /// - [JsonPropertyName("includeTags")] public string[] IncludeTags { get; set; } = Array.Empty(); /// /// Set to true to generate deprecated operations, otherwise false /// - [JsonPropertyName("generateDeprecatedOperations")] public bool GenerateDeprecatedOperations { get; set; } = true; /// /// Generate operation names using pattern. - /// When using , this is name of the Execute() method in the interface. + /// When using , this is name of the Execute() method in the interface. /// - [JsonPropertyName("operationNameTemplate")] public string? OperationNameTemplate { get; set; } /// /// Set to true to re-order optional parameters to the end of the parameter list /// - [JsonPropertyName("optionalParameters")] public bool OptionalParameters { get; set; } /// /// Gets or sets the relative path to a folder in which the output files are generated. (default: ./Generated) /// - [JsonPropertyName("outputFolder")] - public string OutputFolder { get; set; } = "./Generated"; + public string OutputFolder { get; set; } = DefaultOutputFolder; + + /// + /// Gets or sets the filename of the generated code. + /// For the CLI tool, the default is Output.cs + /// For the Source Generator, this is the name of the generated class and the default is [.refitter defined naming OR .refitter filename].g.cs) + /// + public string? OutputFilename { get; set; } /// /// Gets or sets the settings describing how to register generated interface to the .NET Core DI container /// - [JsonPropertyName("dependencyInjectionSettings")] public DependencyInjectionSettings? DependencyInjectionSettings { get; set; } /// /// Gets or sets the settings describing how to generate types using NSwag /// - [JsonPropertyName("codeGeneratorSettings")] public CodeGeneratorSettings? CodeGeneratorSettings { get; set; } } \ No newline at end of file diff --git a/src/Refitter.SourceGenerator.Tests/AdditionalFiles/SwaggerPetstoreCustomOutputFolder.refitter b/src/Refitter.SourceGenerator.Tests/AdditionalFiles/SwaggerPetstoreCustomOutputFolder.refitter index 70b81cff..adb78a0e 100644 --- a/src/Refitter.SourceGenerator.Tests/AdditionalFiles/SwaggerPetstoreCustomOutputFolder.refitter +++ b/src/Refitter.SourceGenerator.Tests/AdditionalFiles/SwaggerPetstoreCustomOutputFolder.refitter @@ -2,6 +2,7 @@ "openApiPath": "../Resources/V3/SwaggerPetstore.json", "namespace": "Refitter.Tests.CustomGenerated", "outputFolder": "../CustomGenerated", + "outputFileName": "CustomGenerated.cs", "generateContracts": false, "additionalNamespaces": ["Refitter.Tests.AdditionalFiles.SingeInterface"], "naming": { diff --git a/src/Refitter.SourceGenerator.Tests/CustomGenerated/SwaggerPetstoreCustomOutputFolder.g.cs b/src/Refitter.SourceGenerator.Tests/CustomGenerated/CustomGenerated.cs similarity index 100% rename from src/Refitter.SourceGenerator.Tests/CustomGenerated/SwaggerPetstoreCustomOutputFolder.g.cs rename to src/Refitter.SourceGenerator.Tests/CustomGenerated/CustomGenerated.cs diff --git a/src/Refitter.SourceGenerator.Tests/CustomOutputFolderGeneratorTests.cs b/src/Refitter.SourceGenerator.Tests/CustomOutputFolderGeneratorTests.cs index cccfce46..93ce9927 100644 --- a/src/Refitter.SourceGenerator.Tests/CustomOutputFolderGeneratorTests.cs +++ b/src/Refitter.SourceGenerator.Tests/CustomOutputFolderGeneratorTests.cs @@ -12,7 +12,7 @@ public class CustomOutputFolderGeneratorTests { [Fact] public void Can_Create_File_In_Custom_Path() => - File.Exists("../../../CustomGenerated/SwaggerPetstoreCustomOutputFolder.g.cs").Should().BeTrue(); + File.Exists("../../../CustomGenerated/CustomGenerated.cs").Should().BeTrue(); [Fact] public void Can_Resolve_Refit_Interface() => diff --git a/src/Refitter.SourceGenerator/README.md b/src/Refitter.SourceGenerator/README.md index 6e3f771d..0f57d376 100644 --- a/src/Refitter.SourceGenerator/README.md +++ b/src/Refitter.SourceGenerator/README.md @@ -46,6 +46,7 @@ The following is an example `.refitter` file "operationNameTemplate": "{operationName}Async", // Optional. Must contain {operationName} when multipleInterfaces != ByEndpoint "optionalParameters": false, // Optional. Default=false "outputFolder": "../CustomOutput" // Optional. Default=./Generated + "outputFilename": "RefitInterface.cs", // Optional. Default=Output.cs for CLI tool "additionalNamespaces": [ // Optional "Namespace1", "Namespace2" @@ -59,7 +60,7 @@ The following is an example `.refitter` file "^/pet/.*", "^/store/.*" ], - "dependencyInjectionSettings": { + "dependencyInjectionSettings": { // Optional "baseUrl": "https://petstore3.swagger.io/api/v3", // Optional. Leave this blank to set the base address manually "httpMessageHandlers": [ // Optional "AuthorizationMessageHandler", @@ -122,6 +123,7 @@ The following is an example `.refitter` file - `useIsoDateFormat` - Set to `true` to explicitly format date query string parameters in ISO 8601 standard date format using delimiters (for example: 2023-06-15). Default is `false` - `multipleInterfaces` - Set to `ByEndpoint` to generate an interface for each endpoint, or `ByTag` to group Endpoints by their Tag (like SwaggerUI groups them). - `outputFolder` - a string describing a relative path to a desired output folder. Default is `./Generated` +- `outputFilename` - Output filename. Default is `Output.cs` when used from the CLI tool, otherwise its the .refitter filename. So `Petstore.refitter` becomes `Petstore.cs`. - `additionalNamespaces` - A collection of additional namespaces to include in the generated file. A use case for this is when you want to reuse contracts from a different namespace than the generated code. Default is empty - `includeTags` - A collection of tags to use a filter for including endpoints that contain this tag. - `includePathMatches` - A collection of regular expressions used to filter paths. diff --git a/src/Refitter.SourceGenerator/RefitterSourceGenerator.cs b/src/Refitter.SourceGenerator/RefitterSourceGenerator.cs index e23c420f..465bd71a 100644 --- a/src/Refitter.SourceGenerator/RefitterSourceGenerator.cs +++ b/src/Refitter.SourceGenerator/RefitterSourceGenerator.cs @@ -65,12 +65,6 @@ private static List GenerateCode( try { - var filename = Path.GetFileName(file.Path).Replace(".refitter", ".g.cs"); - if (filename == ".g.cs") - { - filename = "Refitter.g.cs"; - } - var content = file.GetText(cancellationToken)!; var json = content.ToString(); var settings = Serializer.Deserialize(json); @@ -102,7 +96,13 @@ private static List GenerateCode( cancellationToken.ThrowIfCancellationRequested(); try { - var folder = Path.Combine(Path.GetDirectoryName(file.Path), settings.OutputFolder); + var filename = settings.OutputFilename ?? Path.GetFileName(file.Path).Replace(".refitter", ".g.cs"); + if (filename == ".g.cs") + { + filename = "Refitter.g.cs"; + } + + var folder = Path.Combine(Path.GetDirectoryName(file.Path)!, settings.OutputFolder); var output = Path.Combine(folder, filename); if (!Directory.Exists(folder)) { diff --git a/src/Refitter/GenerateCommand.cs b/src/Refitter/GenerateCommand.cs index 15f7dc1b..08b48844 100644 --- a/src/Refitter/GenerateCommand.cs +++ b/src/Refitter/GenerateCommand.cs @@ -1,5 +1,4 @@ using System.Diagnostics; -using System.Text.Json; using Microsoft.OpenApi.Models; @@ -20,41 +19,7 @@ public override ValidationResult Validate(CommandContext context, Settings setti if (!settings.NoLogging) Analytics.Configure(); - if (string.IsNullOrWhiteSpace(settings.OpenApiPath) && - string.IsNullOrWhiteSpace(settings.SettingsFilePath)) - return ValidationResult.Error("Input or settings file is required"); - - if (!string.IsNullOrWhiteSpace(settings.OpenApiPath) && - !string.IsNullOrWhiteSpace(settings.SettingsFilePath)) - return ValidationResult.Error( - "You should either specify an input URL/file directly " + - "or use specify it in 'openApiPath' from the settings file, " + - "not both"); - - if (!string.IsNullOrWhiteSpace(settings.SettingsFilePath)) - { - var json = File.ReadAllText(settings.SettingsFilePath); - var refitGeneratorSettings = Serializer.Deserialize(json); - settings.OpenApiPath = refitGeneratorSettings.OpenApiPath; - - if (string.IsNullOrWhiteSpace(refitGeneratorSettings.OpenApiPath)) - return ValidationResult.Error( - "The 'openApiPath' in settings file is required when " + - "URL or file path to OpenAPI Specification file " + - "is not specified in command line argument"); - } - - if (!string.IsNullOrWhiteSpace(settings.OperationNameTemplate) && - !settings.OperationNameTemplate.Contains("{operationName}") && - settings.MultipleInterfaces != MultipleInterfaces.ByEndpoint) - return ValidationResult.Error("'{operationName}' placeholder must be present in operation name template"); - - if (IsUrl(settings.OpenApiPath!)) - return base.Validate(context, settings); - - return File.Exists(settings.OpenApiPath) - ? base.Validate(context, settings) - : ValidationResult.Error($"File not found - {Path.GetFullPath(settings.OpenApiPath!)}"); + return SettingsValidator.Validate(settings); } public override async Task ExecuteAsync(CommandContext context, Settings settings) @@ -105,15 +70,13 @@ public override async Task ExecuteAsync(CommandContext context, Settings se var code = generator.Generate().ReplaceLineEndings(); AnsiConsole.MarkupLine($"[green]Length: {code.Length} bytes[/]"); - if (!string.IsNullOrWhiteSpace(settings.OutputPath)) - { - var directory = Path.GetDirectoryName(settings.OutputPath); - if (!string.IsNullOrWhiteSpace(directory) && !Directory.Exists(directory)) - Directory.CreateDirectory(directory); - } - - var outputPath = settings.OutputPath ?? "Output.cs"; + var outputPath = GetOutputPath(settings, refitGeneratorSettings); AnsiConsole.MarkupLine($"[green]Output: {Path.GetFullPath(outputPath)}[/]"); + + var directory = Path.GetDirectoryName(outputPath); + if (!string.IsNullOrWhiteSpace(directory) && !Directory.Exists(directory)) + Directory.CreateDirectory(directory); + await File.WriteAllTextAsync(outputPath, code); await Analytics.LogFeatureUsage(settings); @@ -134,6 +97,21 @@ public override async Task ExecuteAsync(CommandContext context, Settings se } } + private static string GetOutputPath(Settings settings, RefitGeneratorSettings refitGeneratorSettings) + { + var outputPath = settings.OutputPath != Settings.DefaultOutputPath && !string.IsNullOrWhiteSpace(settings.OutputPath) + ? settings.OutputPath + : refitGeneratorSettings.OutputFilename ?? "Output.cs"; + + if (!string.IsNullOrWhiteSpace(refitGeneratorSettings.OutputFolder) && + refitGeneratorSettings.OutputFolder != RefitGeneratorSettings.DefaultOutputFolder) + { + outputPath = Path.Combine(refitGeneratorSettings.OutputFolder, outputPath); + } + + return outputPath; + } + private static async Task ValidateOpenApiSpec(Settings settings) { var validationResult = await OpenApiValidator.Validate(settings.OpenApiPath!); @@ -171,10 +149,4 @@ private static void TryWriteLine( // ignored } } - - private static bool IsUrl(string openApiPath) - { - return Uri.TryCreate(openApiPath, UriKind.Absolute, out var uriResult) && - (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps); - } } \ No newline at end of file diff --git a/src/Refitter/README.md b/src/Refitter/README.md index 2b56c642..c5e9f269 100644 --- a/src/Refitter/README.md +++ b/src/Refitter/README.md @@ -93,6 +93,7 @@ The following is an example `.refitter` file "operationNameTemplate": "{operationName}Async", // Optional. Must contain {operationName} when multipleInterfaces != ByEndpoint "optionalParameters": false, // Optional. Default=false "outputFolder": "../CustomOutput" // Optional. Default=./Generated + "outputFilename": "RefitInterface.cs", // Optional. Default=Output.cs for CLI tool "additionalNamespaces": [ // Optional "Namespace1", "Namespace2" @@ -106,7 +107,7 @@ The following is an example `.refitter` file "^/pet/.*", "^/store/.*" ], - "dependencyInjectionSettings": { + "dependencyInjectionSettings": { // Optional "baseUrl": "https://petstore3.swagger.io/api/v3", // Optional. Leave this blank to set the base address manually "httpMessageHandlers": [ // Optional "AuthorizationMessageHandler", @@ -115,6 +116,41 @@ The following is an example `.refitter` file "usePolly": true, // Optional. Set this to true, to configure Polly with a retry policy that uses a jittered backoff. Default=false "pollyMaxRetryCount": 3, // Optional. Default=6 "firstBackoffRetryInSeconds": 0.5 // Optional. Default=1.0 + }, + "codeGeneratorSettings": { // Optional. Default settings are the values set in this example + "namespace": "GeneratedCode", + "requiredPropertiesMustBeDefined": true, + "generateDataAnnotations": true, + "anyType": "object", + "dateType": "System.DateTimeOffset", + "dateTimeType": "System.DateTimeOffset", + "timeType": "System.TimeSpan", + "timeSpanType": "System.TimeSpan", + "arrayType": "System.Collections.Generic.ICollection", + "dictionaryType": "System.Collections.Generic.IDictionary", + "arrayInstanceType": "System.Collections.ObjectModel.Collection", + "dictionaryInstanceType": "System.Collections.Generic.Dictionary", + "arrayBaseType": "System.Collections.ObjectModel.Collection", + "dictionaryBaseType": "System.Collections.Generic.Dictionary", + "propertySetterAccessModifier": "", + "generateImmutableArrayProperties": false, + "generateImmutableDictionaryProperties": false, + "handleReferences": false, + "jsonSerializerSettingsTransformationMethod": null, + "generateJsonMethods": false, + "enforceFlagEnums": false, + "inlineNamedDictionaries": false, + "inlineNamedTuples": true, + "inlineNamedArrays": false, + "generateOptionalPropertiesAsNullable": false, + "generateNullableReferenceTypes": false, + "generateNativeRecords": false, + "generateDefaultValues": true, + "inlineNamedAny": false, + "excludedTypeNames": [ + "ExcludedTypeFoo", + "ExcludedTypeBar" + ] } } ``` @@ -134,11 +170,12 @@ The following is an example `.refitter` file - `useIsoDateFormat` - Set to `true` to explicitly format date query string parameters in ISO 8601 standard date format using delimiters (for example: 2023-06-15). Default is `false` - `multipleInterfaces` - Set to `ByEndpoint` to generate an interface for each endpoint, or `ByTag` to group Endpoints by their Tag (like SwaggerUI groups them). - `outputFolder` - a string describing a relative path to a desired output folder. Default is `./Generated` +- `outputFilename` - Output filename. Default is `Output.cs` when used from the CLI tool, otherwise its the .refitter filename. So `Petstore.refitter` becomes `Petstore.cs`. - `additionalNamespaces` - A collection of additional namespaces to include in the generated file. A use case for this is when you want to reuse contracts from a different namespace than the generated code. Default is empty - `includeTags` - A collection of tags to use a filter for including endpoints that contain this tag. - `includePathMatches` - A collection of regular expressions used to filter paths. - `generateDeprecatedOperations` - a boolean indicating whether deprecated operations should be generated or skipped. Default is `true` -- `operationNameTemplate` - Generate operation names using pattern. This must contain the string {operationName}. An example usage of this could be `{operationName}Async` to suffix all method names with Async. When using `"multipleIinterfaces": "ByEndpoint"`, This is name of the Execute() method in the interface +- `operationNameTemplate` - Generate operation names using pattern. This must contain the string {operationName}. An example usage of this could be `{operationName}Async` to suffix all method names with Async - `optionalParameters` - Generate non-required parameters as nullable optional parameters - `dependencyInjectionSettings` - Setting this will generated extension methods to `IServiceCollection` for configuring Refit clients - `baseUrl` - Used as the HttpClient base address. Leave this blank to manually set the base URL diff --git a/src/Refitter/Settings.cs b/src/Refitter/Settings.cs index 713a26f3..917580b7 100644 --- a/src/Refitter/Settings.cs +++ b/src/Refitter/Settings.cs @@ -6,6 +6,8 @@ namespace Refitter; public sealed class Settings : CommandSettings { + public const string DefaultOutputPath = "Output.cs"; + [Description("URL or file path to OpenAPI Specification file")] [CommandArgument(0, "[URL or input file]")] [DefaultValue(null)] @@ -22,7 +24,7 @@ public sealed class Settings : CommandSettings [Description("Path to Output file")] [CommandOption("-o|--output")] - [DefaultValue("Output.cs")] + [DefaultValue(DefaultOutputPath)] public string? OutputPath { get; set; } [Description("Don't add header to output file")] diff --git a/src/Refitter/SettingsValidator.cs b/src/Refitter/SettingsValidator.cs new file mode 100644 index 00000000..4f0df347 --- /dev/null +++ b/src/Refitter/SettingsValidator.cs @@ -0,0 +1,115 @@ +using Refitter.Core; + +using Spectre.Console; + +namespace Refitter; + +public static class SettingsValidator +{ + public static ValidationResult Validate(Settings settings) + { + if (BothSettingsFilesAreEmpty(settings) || BothSettingsFilesArePresent(settings)) + { + return GetValidationErrorForSettingsFiles(); + } + + return !string.IsNullOrWhiteSpace(settings.SettingsFilePath) + ? ValidateFilePath(settings) + : ValidateOperationNameAndUrl(settings); + } + + private static bool BothSettingsFilesAreEmpty(Settings settings) + { + return string.IsNullOrWhiteSpace(settings.OpenApiPath) && + string.IsNullOrWhiteSpace(settings.SettingsFilePath); + } + + private static bool BothSettingsFilesArePresent(Settings settings) + { + return !string.IsNullOrWhiteSpace(settings.OpenApiPath) && + !string.IsNullOrWhiteSpace(settings.SettingsFilePath); + } + + private static ValidationResult GetValidationErrorForSettingsFiles() + { + return ValidationResult.Error( + "You should either specify an input URL/file directly " + + "or use specify it in 'openApiPath' from the settings file, " + + "not both"); + } + + private static ValidationResult ValidateFilePath(Settings settings) + { + var json = File.ReadAllText(settings.SettingsFilePath!); + var refitGeneratorSettings = Serializer.Deserialize(json); + settings.OpenApiPath = refitGeneratorSettings.OpenApiPath; + + return ValidateFileAndOutputSettings(settings, refitGeneratorSettings); + } + + private static ValidationResult ValidateFileAndOutputSettings( + Settings settings, + RefitGeneratorSettings refitGeneratorSettings) + { + if (string.IsNullOrWhiteSpace(refitGeneratorSettings.OpenApiPath)) + { + return GetValidationErrorForOpenApiPath(); + } + + if (!string.IsNullOrWhiteSpace(settings.OutputPath) && + settings.OutputPath != Settings.DefaultOutputPath && + (!string.IsNullOrWhiteSpace(refitGeneratorSettings.OutputFolder) || + !string.IsNullOrWhiteSpace(refitGeneratorSettings.OutputFilename))) + { + return GetValidationErrorForOutputPath(); + } + + return ValidateOperationNameAndUrl(settings); + } + + private static ValidationResult GetValidationErrorForOpenApiPath() + { + return ValidationResult.Error( + "The 'openApiPath' in settings file is required when " + + "URL or file path to OpenAPI Specification file " + + "is not specified in command line argument"); + } + + private static ValidationResult GetValidationErrorForOutputPath() + { + return ValidationResult.Error( + "You should either specify an output path directly from --output " + + "or use specify it in 'outputFolder' and 'outputFilename' from the settings file, " + + "not both"); + } + + private static ValidationResult ValidateOperationNameAndUrl(Settings settings) + { + if (!string.IsNullOrWhiteSpace(settings.OperationNameTemplate) && + !settings.OperationNameTemplate.Contains("{operationName}") && + settings.MultipleInterfaces != MultipleInterfaces.ByEndpoint) + { + return GetValidationErrorForOperationName(); + } + + return IsUrl(settings.OpenApiPath!) ? ValidationResult.Success() : ValidateFileExistence(settings); + } + + private static ValidationResult GetValidationErrorForOperationName() + { + return ValidationResult.Error("'{operationName}' placeholder must be present in operation name template"); + } + + private static ValidationResult ValidateFileExistence(Settings settings) + { + return File.Exists(settings.OpenApiPath) + ? ValidationResult.Success() + : ValidationResult.Error($"File not found - {Path.GetFullPath(settings.OpenApiPath!)}"); + } + + private static bool IsUrl(string openApiPath) + { + return Uri.TryCreate(openApiPath, UriKind.Absolute, out var uriResult) && + (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps); + } +} \ No newline at end of file diff --git a/test/petstore.refitter b/test/petstore.refitter index 60d54a4c..ccfcdaa0 100644 --- a/test/petstore.refitter +++ b/test/petstore.refitter @@ -1,6 +1,8 @@ { "openApiPath": "./OpenAPI/v3.0/petstore.json", "namespace": "Petstore", + "outputFolder": "GeneratedCode", + "outputFilename": "SwaggerPetstoreDirect.cs", "naming": { "useOpenApiTitle": false, "interfaceName": "SwaggerPetstoreDirect" diff --git a/test/smoke-tests.ps1 b/test/smoke-tests.ps1 index cee98fae..0d65394c 100644 --- a/test/smoke-tests.ps1 +++ b/test/smoke-tests.ps1 @@ -33,9 +33,9 @@ function GenerateAndBuild { Get-ChildItem '*.generated.cs' -Recurse | ForEach-Object { Remove-Item -Path $_.FullName } if ($args.Contains("settings-file")) { - Write-Host "refitter --output ./GeneratedCode/$outputPath --no-logging $args" + Write-Host "refitter --no-logging $args" $process = Start-Process "./bin/refitter" ` - -Args "--output ./GeneratedCode/$outputPath --no-logging $args" ` + -Args "--no-logging $args" ` -NoNewWindow ` -PassThru } else { @@ -93,7 +93,7 @@ function RunTests { Write-Host "dotnet publish ../src/Refitter/Refitter.csproj -p:TreatWarningsAsErrors=true -p:PublishReadyToRun=true -o bin" Start-Process "dotnet" -Args "publish ../src/Refitter/Refitter.csproj -p:TreatWarningsAsErrors=true -p:PublishReadyToRun=true -o bin" -NoNewWindow -PassThru | Wait-Process - GenerateAndBuild -format " " -namespace " " -outputPath "SwaggerPetstoreDirect.generated.cs" -args "--settings-file ./petstore.refitter" + GenerateAndBuild -format " " -namespace " " -outputPath "SwaggerPetstoreDirect.cs" -args "--settings-file ./petstore.refitter" "v3.0", "v2.0" | ForEach-Object { $version = $_