Skip to content

Commit

Permalink
Add support for DateOnly and TimeOnly
Browse files Browse the repository at this point in the history
Motivation
----------
For "date", "partial-date", "time", and "partial-time" string formats,
DateOnly and TimeOnly are a better fit when available in .NET 6 or
later.

Modifications
-------------
- Add a dependency on Microsoft.Extensions.Options
- Create a new internal option to control date/time handling, and
  initialize based on command-line properties and target frameworks
- Use the new option to choose schema types
- Treat "time" format the same as "partial-time"

Breaking Change
---------------
When building targeting only .NET 6 or later, the default type used
for "date", "partial-date", and "partial-time" is now DateOnly or
TimeOnly instead of DateTime or TimeSpan. This may cause backward
compatibility issues in generated SDKs. Use the command line switch
`-p LegacyDateTimeHandling=true` to restore the previous behavior. This
is not necessary if targeting .NET Standard 2.0.
  • Loading branch information
brantburnett committed Feb 1, 2025
1 parent 8b7fc50 commit 759f263
Show file tree
Hide file tree
Showing 10 changed files with 70 additions and 6 deletions.
1 change: 1 addition & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ csharp_indent_case_contents = true
csharp_indent_case_contents_when_block = true
csharp_indent_switch_labels = true
csharp_indent_labels = one_less_than_current
csharp_style_namespace_declarations = file_scoped

# Modifier preferences
csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion
Expand Down
1 change: 1 addition & 0 deletions src/main/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="8.0.2" />
<PackageVersion Include="Microsoft.OpenApi" Version="1.6.22" />
<PackageVersion Include="Microsoft.OpenApi.Readers" Version="1.6.22" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"profiles": {
"Generate": {
"commandName": "Project",
"commandLineArgs": "generate --no-restore -n Test.Yardarm -x Yardarm.SystemTextJson.dll Yardarm.MicrosoftExtensionsHttp.dll -f net8.0 --embed -o output/ --intermediate-dir output/obj -v 1.0.0 -i mashtub.json"
"commandLineArgs": "generate --no-restore -n Test.Yardarm -x Yardarm.SystemTextJson.dll Yardarm.MicrosoftExtensionsHttp.dll -p LegacyDateTimeHandling=false -f net8.0 --embed -o output/ --intermediate-dir output/obj -v 1.0.0 -i mashtub.json"
},
"Restore": {
"commandName": "Project",
Expand Down
13 changes: 11 additions & 2 deletions src/main/Yardarm/Generation/Schema/StringSchemaGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ public class StringSchemaGenerator(
private static YardarmTypeInfo String => s_string ??= new YardarmTypeInfo(
PredefinedType(Token(SyntaxKind.StringKeyword)), isGenerated: false);

private static YardarmTypeInfo? s_dateOnly;
private static YardarmTypeInfo DateOnly => s_dateOnly ??= new YardarmTypeInfo(
QualifiedName(IdentifierName("System"), IdentifierName("DateOnly")), isGenerated: false);

private static YardarmTypeInfo? s_dateTime;
private static YardarmTypeInfo DateTime => s_dateTime ??= new YardarmTypeInfo(
QualifiedName(IdentifierName("System"), IdentifierName("DateTime")), isGenerated: false);
Expand All @@ -28,6 +32,10 @@ public class StringSchemaGenerator(
private static YardarmTypeInfo DateTimeOffset => s_dateTimeOffset ??= new YardarmTypeInfo(
QualifiedName(IdentifierName("System"), IdentifierName("DateTimeOffset")), isGenerated: false);

private static YardarmTypeInfo? s_timeOnly;
private static YardarmTypeInfo TimeOnly => s_timeOnly ??= new YardarmTypeInfo(
QualifiedName(IdentifierName("System"), IdentifierName("TimeOnly")), isGenerated: false);

private static YardarmTypeInfo? s_timeSpan;
private static YardarmTypeInfo TimeSpan => s_timeSpan ??= new YardarmTypeInfo(
QualifiedName(IdentifierName("System"), IdentifierName("TimeSpan")), isGenerated: false);
Expand All @@ -54,8 +62,9 @@ public class StringSchemaGenerator(
protected override YardarmTypeInfo GetTypeInfo() =>
Element.Element.Format switch
{
"date" or "full-date" => DateTime,
"partial-time" or "date-span" => TimeSpan,
"date" or "full-date" => Context.Options.LegacyDateTimeHandling ? DateTime : DateOnly,
"partial-time" or "time" => Context.Options.LegacyDateTimeHandling ? TimeSpan : TimeOnly,
"date-span" => TimeSpan,
"date-time" => DateTimeOffset,
"uuid" => Guid,
"uri" => Uri,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System;
using Microsoft.Extensions.Options;
using NuGet.Frameworks;
using Yardarm.Internal;
using Yardarm.Packaging;

namespace Yardarm.Generation.Schema;

internal class StringSchemaOptionsConfigurator(
YardarmGenerationSettings settings)
: IConfigureOptions<GenerationOptions>
{
public void Configure(GenerationOptions options)
{
// Use legacy handling for DateOnly and TimeOnly if explicitly requested
if (settings.Properties.TryGetValue("LegacyDateTimeHandling", out string? legacyDateTimeHandling) &&
string.Equals(legacyDateTimeHandling, "true", StringComparison.OrdinalIgnoreCase))
{
options.LegacyDateTimeHandling = true;
return;
}

// Also, use legacy handling if any target framework is less than .NET 6. This is because .NET Standard or
// earlier .NET Core versions do not support DateOnly and TimeOnly. Because public APIs should be forward
// compatible from older targets, we need to use legacy handling for all targets if any target is unsupported
// to ensure consistent API surface.
foreach (string moniker in settings.TargetFrameworkMonikers)
{
var framework = NuGetFramework.Parse(moniker);
if (framework.Framework != NuGetFrameworkConstants.NetCoreApp || framework.Version.Major < 6)
{
options.LegacyDateTimeHandling = true;
return;
}
}
}
}
6 changes: 6 additions & 0 deletions src/main/Yardarm/GenerationContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Models;
using NuGet.Frameworks;
using Yardarm.Generation;
using Yardarm.Internal;
using Yardarm.Names;
using Yardarm.Packaging;
using Yardarm.Spec;
Expand Down Expand Up @@ -43,6 +45,9 @@ public NuGetFramework CurrentTargetFramework
public IReadOnlySet<string> PreprocessorSymbols =>
_preprocessorSymbols ??= GetPreprocessorSymbols(CurrentTargetFramework);

private readonly IOptions<GenerationOptions> _options;
internal GenerationOptions Options => _options.Value;

public GenerationContext(IServiceProvider serviceProvider) : base(serviceProvider)
{
_openApiDocument = new Lazy<OpenApiDocument>(serviceProvider.GetRequiredService<OpenApiDocument>);
Expand All @@ -52,6 +57,7 @@ public GenerationContext(IServiceProvider serviceProvider) : base(serviceProvide
new Lazy<INameFormatterSelector>(serviceProvider.GetRequiredService<INameFormatterSelector>);
_typeGeneratorRegistry =
new Lazy<ITypeGeneratorRegistry>(serviceProvider.GetRequiredService<ITypeGeneratorRegistry>);
_options = serviceProvider.GetRequiredService<IOptions<GenerationOptions>>();
}

private static HashSet<string> GetPreprocessorSymbols(NuGetFramework targetFramework) =>
Expand Down
6 changes: 6 additions & 0 deletions src/main/Yardarm/Internal/GenerationOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Yardarm.Internal;

internal class GenerationOptions
{
public bool LegacyDateTimeHandling { get; set; }
}
1 change: 1 addition & 0 deletions src/main/Yardarm/Yardarm.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<PackageReference Include="Microsoft.Extensions.Logging" />
<PackageReference Include="Microsoft.OpenApi" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Microsoft.Extensions.Options" />
<PackageReference Include="NuGet.Commands" />
<PackageReference Include="System.Linq.Async" />
</ItemGroup>
Expand Down
3 changes: 3 additions & 0 deletions src/main/Yardarm/YardarmCoreServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public static class YardarmCoreServiceCollectionExtensions
{
public static IServiceCollection AddYardarm(this IServiceCollection services, YardarmGenerationSettings settings, OpenApiDocument? document)
{
services.AddOptions();
services.AddDefaultEnrichers();

if (settings.ReferencedAssemblies is null || settings.ReferencedAssemblies.Count == 0)
Expand Down Expand Up @@ -139,6 +140,8 @@ public static IServiceCollection AddYardarm(this IServiceCollection services, Ya

services.TryAddSingleton<IOpenApiElementRegistry, OpenApiElementRegistry>();

services.ConfigureOptions<StringSchemaOptionsConfigurator>();

return services;
}

Expand Down
6 changes: 3 additions & 3 deletions src/main/Yardarm/YardarmGenerationSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.OpenApi.Models;
using NuGet.Configuration;
using NuGet.Packaging.Core;
using Yardarm.Packaging;

Expand Down Expand Up @@ -97,8 +98,7 @@ public Stream XmlDocumentationOutput
/// </summary>
public bool NoRestore { get; set; }

public ImmutableArray<string> TargetFrameworkMonikers { get; set; } =
new[] {"netstandard2.0"}.ToImmutableArray();
public ImmutableArray<string> TargetFrameworkMonikers { get; set; } = ["netstandard2.0"];

public Stream? NuGetOutput { get; set; }

Expand All @@ -115,7 +115,7 @@ public Stream XmlDocumentationOutput
/// <summary>
/// Properties to alter the behavior of the generation process.
/// </summary>
public Dictionary<string, string> Properties { get; } = [];
public Dictionary<string, string> Properties { get; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

public CSharpCompilationOptions CompilationOptions { get; set; } =
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
Expand Down

0 comments on commit 759f263

Please sign in to comment.