Skip to content

Commit

Permalink
Merge pull request #73 from christianhelle/iso-date-format
Browse files Browse the repository at this point in the history
  • Loading branch information
christianhelle authored Jun 15, 2023
2 parents dbfceb9 + 7d74535 commit b7b5bf9
Show file tree
Hide file tree
Showing 9 changed files with 200 additions and 24 deletions.
9 changes: 9 additions & 0 deletions .github/workflows/template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ jobs:
${{ inputs.command }} ./openapi.${{ inputs.format }} --namespace "$namespace.Internal" --output "Internal$outputPath" --internal --no-logging
${{ inputs.command }} ./openapi.${{ inputs.format }} --namespace "$namespace.Interface" --output "I$outputPath" --interface-only --no-logging
${{ inputs.command }} ./openapi.${{ inputs.format }} --namespace "$namespace.UsingApiResponse" --output "IApi$outputPath" --interface-only --return-api-response --no-logging
${{ inputs.command }} ./openapi.${{ inputs.format }} --namespace "$namespace.UsingIsoDateFormat" --output "UsingIsoDateFormat$outputPath" --use-iso-date-format --no-logging
Copy-Item $outputPath ./OpenAPI/${{ inputs.version }}/${{ inputs.openapi }}.${{ inputs.format }}.cs
Copy-Item $outputPath ./ConsoleApp/Net6/
Copy-Item $outputPath ./ConsoleApp/Net7/
Expand Down Expand Up @@ -96,6 +97,14 @@ jobs:
Copy-Item "WithCancellation$outputPath" ./ConsoleApp/Net481/
Copy-Item "WithCancellation$outputPath" ./ConsoleApp/NetStandard20/
Copy-Item "WithCancellation$outputPath" ./ConsoleApp/NetStandard21/
Copy-Item "UsingIsoDateFormat$outputPath" ./ConsoleApp/Net6/
Copy-Item "UsingIsoDateFormat$outputPath" ./ConsoleApp/Net7/
Copy-Item "UsingIsoDateFormat$outputPath" ./ConsoleApp/Net48/
Copy-Item "UsingIsoDateFormat$outputPath" ./ConsoleApp/Net472/
Copy-Item "UsingIsoDateFormat$outputPath" ./ConsoleApp/Net462/
Copy-Item "UsingIsoDateFormat$outputPath" ./ConsoleApp/Net481/
Copy-Item "UsingIsoDateFormat$outputPath" ./ConsoleApp/NetStandard20/
Copy-Item "UsingIsoDateFormat$outputPath" ./ConsoleApp/NetStandard21/
working-directory: test
shell: pwsh
if: steps.prepare_openapi_spec.outputs.exists == 'True'
Expand Down
42 changes: 23 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,25 @@ EXAMPLES:
refitter ./openapi.json --use-api-response
refitter ./openapi.json --cancellation-tokens
refitter ./openapi.json --no-operation-headers
refitter ./openapi.json --use-iso-date-format
ARGUMENTS:
[URL or input file] URL or file path to OpenAPI Specification file
OPTIONS:
DEFAULT
-h, --help Prints help information
-n, --namespace GeneratedCode Default namespace to use for generated types
-o, --output Output.cs Path to Output file
--no-auto-generated-header Don't add <auto-generated> header to output file
--interface-only Don't generate contract types
--use-api-response Return Task<IApiResponse<T>> instead of Task<T>
--internal Set the accessibility of the generated types to 'internal'
--cancellation-tokens Use cancellation tokens
--no-operation-headers Don't generate operation headers
DEFAULT
-h, --help Prints help information
-n, --namespace GeneratedCode Default namespace to use for generated types
-o, --output Output.cs Path to Output file
--no-auto-generated-header Don't add <auto-generated> header to output file
--interface-only Don't generate contract types
--use-api-response Return Task<IApiResponse<T>> instead of Task<T>
--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
--use-iso-date-format Explicitly format date query string parameters in ISO 8601
standard date format using delimiters (2023-06-15)
```

To generate code from an OpenAPI specifications file, run the following:
Expand Down Expand Up @@ -94,7 +98,7 @@ namespace Your.Namespace.Of.Choice.GeneratedCode
/// Multiple status values can be provided with comma separated strings
/// </summary>
[Get("/pet/findByStatus")]
Task<ICollection<Pet>> FindPetsByStatus([Query(CollectionFormat.Multi)] Status? status);
Task<ICollection<Pet>> FindPetsByStatus([Query] Status? status);

/// <summary>
/// Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.
Expand All @@ -109,13 +113,13 @@ namespace Your.Namespace.Of.Choice.GeneratedCode
Task<Pet> GetPetById(long petId);

[Post("/pet/{petId}")]
Task UpdatePetWithForm(long petId, [Query(CollectionFormat.Multi)] string name, [Query(CollectionFormat.Multi)] string status);
Task UpdatePetWithForm(long petId, [Query] string name, [Query] string status);

[Delete("/pet/{petId}")]
Task DeletePet(long petId, [Header("api_key")] string api_key);

[Post("/pet/{petId}/uploadImage")]
Task<ApiResponse> UploadFile(long petId, [Query(CollectionFormat.Multi)] string additionalMetadata, [Body(BodySerializationMethod.UrlEncoded)] Dictionary<string, object> body);
Task<ApiResponse> UploadFile(long petId, [Query] string additionalMetadata, StreamPart body);

/// <summary>
/// Returns a map of status codes to quantities
Expand Down Expand Up @@ -154,7 +158,7 @@ namespace Your.Namespace.Of.Choice.GeneratedCode
Task<User> CreateUsersWithListInput([Body] IEnumerable<User> body);

[Get("/user/login")]
Task<string> LoginUser([Query(CollectionFormat.Multi)] string username, [Query(CollectionFormat.Multi)] string password);
Task<string> LoginUser([Query] string username, [Query] string password);

[Get("/user/logout")]
Task LogoutUser();
Expand Down Expand Up @@ -208,7 +212,7 @@ namespace Your.Namespace.Of.Choice.GeneratedCode.WithApiResponse
/// Multiple status values can be provided with comma separated strings
/// </summary>
[Get("/pet/findByStatus")]
Task<IApiResponse<ICollection<Pet>>> FindPetsByStatus([Query(CollectionFormat.Multi)] Status? status);
Task<IApiResponse<ICollection<Pet>>> FindPetsByStatus([Query] Status? status);

/// <summary>
/// Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.
Expand All @@ -223,13 +227,13 @@ namespace Your.Namespace.Of.Choice.GeneratedCode.WithApiResponse
Task<IApiResponse<Pet>> GetPetById(long petId);

[Post("/pet/{petId}")]
Task UpdatePetWithForm(long petId, [Query(CollectionFormat.Multi)] string name, [Query(CollectionFormat.Multi)] string status);
Task UpdatePetWithForm(long petId, [Query] string name, [Query] string status);

[Delete("/pet/{petId}")]
Task DeletePet(long petId);
Task DeletePet(long petId, [Header("api_key")] string api_key);

[Post("/pet/{petId}/uploadImage")]
Task<IApiResponse<ApiResponse>> UploadFile(long petId, [Query(CollectionFormat.Multi)] string additionalMetadata, [Body(BodySerializationMethod.UrlEncoded)] Dictionary<string, object> body);
Task<IApiResponse<ApiResponse>> UploadFile(long petId, [Query] string additionalMetadata, StreamPart body);

/// <summary>
/// Returns a map of status codes to quantities
Expand Down Expand Up @@ -268,7 +272,7 @@ namespace Your.Namespace.Of.Choice.GeneratedCode.WithApiResponse
Task<IApiResponse<User>> CreateUsersWithListInput([Body] IEnumerable<User> body);

[Get("/user/login")]
Task<IApiResponse<string>> LoginUser([Query(CollectionFormat.Multi)] string username, [Query(CollectionFormat.Multi)] string password);
Task<IApiResponse<string>> LoginUser([Query] string username, [Query] string password);

[Get("/user/logout")]
Task LogoutUser();
Expand Down
9 changes: 5 additions & 4 deletions src/Refitter.Core/ParameterExtractor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public static IEnumerable<string> GetParameters(

var queryParameters = operationModel.Parameters
.Where(p => p.Kind == OpenApiParameterKind.Query)
.Select(p => $"{JoinAttributes(GetQueryAttribute(p), GetAliasAsAttribute(p))}{GetBodyParameterType(p)} {p.VariableName}")
.Select(p => $"{JoinAttributes(GetQueryAttribute(p, settings), GetAliasAsAttribute(p))}{GetBodyParameterType(p)} {p.VariableName}")
.ToList();

var bodyParameters = operationModel.Parameters
Expand Down Expand Up @@ -56,11 +56,12 @@ public static IEnumerable<string> GetParameters(
return parameters;
}

private static string GetQueryAttribute(CSharpParameterModel p)
private static string GetQueryAttribute(CSharpParameterModel parameter, RefitGeneratorSettings settings)
{
return p switch
return (parameter, settings) switch
{
{ IsArray: true } => "Query(CollectionFormat.Multi)",
{ parameter.IsArray: true } => "Query(CollectionFormat.Multi)",
{ parameter.IsDate: true, settings.UseIsoDateFormat: true } => "Query(Format = \"yyyy-MM-dd\")",
_ => "Query",
};
}
Expand Down
2 changes: 2 additions & 0 deletions src/Refitter.Core/RefitGeneratorSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ public class RefitGeneratorSettings
public TypeAccessibility TypeAccessibility { get; set; } = TypeAccessibility.Public;

public bool UseCancellationTokens { get; set; }

public bool UseIsoDateFormat { get; set; }
}

[ExcludeFromCodeCoverage]
Expand Down
127 changes: 127 additions & 0 deletions src/Refitter.Tests/Examples/UseIsoDateFormatTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
using FluentAssertions;
using Refitter.Core;
using Refitter.Tests.Build;
using Xunit;

namespace Refitter.Tests.Examples;

public class UseIsoDateFormatTests
{
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-dd"")] ");
}

[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<string> GenerateCode()
{
var swaggerFile = await CreateSwaggerFile(OpenApiSpec);
var settings = new RefitGeneratorSettings
{
OpenApiPath = swaggerFile,
UseIsoDateFormat = true
};

var sut = await RefitGenerator.CreateAsync(settings);
var generateCode = sut.Generate();
return generateCode;
}

private static async Task<string> 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;
}
}
1 change: 1 addition & 0 deletions src/Refitter/GenerateCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public override async Task<int> ExecuteAsync(CommandContext context, Settings se
ReturnIApiResponse = settings.ReturnIApiResponse,
UseCancellationTokens = settings.UseCancellationTokens,
GenerateOperationHeaders = !settings.NoOperationHeaders,
UseIsoDateFormat = settings.UseIsoDateFormat,
TypeAccessibility = settings.InternalTypeAccessibility
? TypeAccessibility.Internal
: TypeAccessibility.Public
Expand Down
8 changes: 8 additions & 0 deletions src/Refitter/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,14 @@
"./openapi.json",
"--no-operation-headers"
});

configuration
.AddExample(
new[]
{
"./openapi.json",
"--use-iso-date-format"
});
});

return app.Run(args);
5 changes: 5 additions & 0 deletions src/Refitter/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,9 @@ public sealed class Settings : CommandSettings
[CommandOption("--no-logging")]
[DefaultValue(false)]
public bool NoLogging { get; set; }

[Description("Explicitly format date query string parameters in ISO 8601 standard date format using delimiters (2023-06-15)")]
[CommandOption("--use-iso-date-format")]
[DefaultValue(false)]
public bool UseIsoDateFormat { get; set; }
}
21 changes: 20 additions & 1 deletion test/smoke-tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ function RunTests {
throw "Refitter failed"
}

Write-Host "dotnet run --project ../src/Refitter/Refitter.csproj ./openapi.$format --namespace $namespace.UsingApiResponse --output I$outputPath --use-api-response --interface-only --no-logging"
Write-Host "dotnet run --project ../src/Refitter/Refitter.csproj ./openapi.$format --namespace $namespace.UsingApiResponse --output IApi$outputPath --use-api-response --interface-only --no-logging"
$process = Start-Process "dotnet" `
-Args "run --project ../src/Refitter/Refitter.csproj ./openapi.$format --namespace $namespace.UsingApiResponse --output IApi$outputPath --use-api-response --interface-only --no-logging" `
-NoNewWindow `
Expand All @@ -102,6 +102,16 @@ function RunTests {
throw "Refitter failed"
}

Write-Host "dotnet run --project ../src/Refitter/Refitter.csproj ./openapi.$format --namespace $namespace.UsingIsoDateFormat --output UsingIsoDateFormat$outputPath --use-iso-date-format --no-logging"
$process = Start-Process "dotnet" `
-Args "run --project ../src/Refitter/Refitter.csproj ./openapi.$format --namespace $namespace.UsingIsoDateFormat --output UsingIsoDateFormat$outputPath --use-iso-date-format --no-logging" `
-NoNewWindow `
-PassThru
$process | Wait-Process
if ($process.ExitCode -ne 0) {
throw "Refitter failed"
}

Copy-Item $outputPath "./$version-$_-$format.cs"
Copy-Item $outputPath "./ConsoleApp/Net7/" -Force
Copy-Item $outputPath "./ConsoleApp/Net6/" -Force
Expand Down Expand Up @@ -148,6 +158,15 @@ function RunTests {
Copy-Item "WithCancellation$outputPath" "./ConsoleApp/NetStandard20/" -Force
Copy-Item "WithCancellation$outputPath" "./ConsoleApp/NetStandard21/" -Force
Copy-Item "WithCancellation$outputPath" "./MinimalApi/" -Force
Copy-Item "UsingIsoDateFormat$outputPath" "./ConsoleApp/Net7/" -Force
Copy-Item "UsingIsoDateFormat$outputPath" "./ConsoleApp/Net6/" -Force
Copy-Item "UsingIsoDateFormat$outputPath" "./ConsoleApp/Net48/" -Force
Copy-Item "UsingIsoDateFormat$outputPath" "./ConsoleApp/Net481/" -Force
Copy-Item "UsingIsoDateFormat$outputPath" "./ConsoleApp/Net472/" -Force
Copy-Item "UsingIsoDateFormat$outputPath" "./ConsoleApp/Net462/" -Force
Copy-Item "UsingIsoDateFormat$outputPath" "./ConsoleApp/NetStandard20/" -Force
Copy-Item "UsingIsoDateFormat$outputPath" "./ConsoleApp/NetStandard21/" -Force
Copy-Item "UsingIsoDateFormat$outputPath" "./MinimalApi/" -Force
Remove-Item $outputPath -Force
}
}
Expand Down

0 comments on commit b7b5bf9

Please sign in to comment.