From 759f2631d1856a44ebbc6d99a84ad20a71d578a3 Mon Sep 17 00:00:00 2001 From: Brant Burnett Date: Sat, 1 Feb 2025 13:51:35 -0500 Subject: [PATCH] Add support for DateOnly and TimeOnly 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. --- .editorconfig | 1 + src/main/Directory.Packages.props | 1 + .../Properties/launchSettings.json | 2 +- .../Schema/StringSchemaGenerator.cs | 13 ++++++- .../Schema/StringSchemaOptionsConfigurator.cs | 37 +++++++++++++++++++ src/main/Yardarm/GenerationContext.cs | 6 +++ .../Yardarm/Internal/GenerationOptions.cs | 6 +++ src/main/Yardarm/Yardarm.csproj | 1 + .../YardarmCoreServiceCollectionExtensions.cs | 3 ++ src/main/Yardarm/YardarmGenerationSettings.cs | 6 +-- 10 files changed, 70 insertions(+), 6 deletions(-) create mode 100644 src/main/Yardarm/Generation/Schema/StringSchemaOptionsConfigurator.cs create mode 100644 src/main/Yardarm/Internal/GenerationOptions.cs diff --git a/.editorconfig b/.editorconfig index 0147f3ee..489b4f2c 100644 --- a/.editorconfig +++ b/.editorconfig @@ -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 diff --git a/src/main/Directory.Packages.props b/src/main/Directory.Packages.props index 461858e5..b94ca635 100644 --- a/src/main/Directory.Packages.props +++ b/src/main/Directory.Packages.props @@ -10,6 +10,7 @@ + diff --git a/src/main/Yardarm.CommandLine/Properties/launchSettings.json b/src/main/Yardarm.CommandLine/Properties/launchSettings.json index 4f7bc709..3d676157 100644 --- a/src/main/Yardarm.CommandLine/Properties/launchSettings.json +++ b/src/main/Yardarm.CommandLine/Properties/launchSettings.json @@ -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", diff --git a/src/main/Yardarm/Generation/Schema/StringSchemaGenerator.cs b/src/main/Yardarm/Generation/Schema/StringSchemaGenerator.cs index 68e1d9be..ea174f02 100644 --- a/src/main/Yardarm/Generation/Schema/StringSchemaGenerator.cs +++ b/src/main/Yardarm/Generation/Schema/StringSchemaGenerator.cs @@ -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); @@ -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); @@ -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, diff --git a/src/main/Yardarm/Generation/Schema/StringSchemaOptionsConfigurator.cs b/src/main/Yardarm/Generation/Schema/StringSchemaOptionsConfigurator.cs new file mode 100644 index 00000000..4c43d234 --- /dev/null +++ b/src/main/Yardarm/Generation/Schema/StringSchemaOptionsConfigurator.cs @@ -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 +{ + 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; + } + } + } +} diff --git a/src/main/Yardarm/GenerationContext.cs b/src/main/Yardarm/GenerationContext.cs index 4573cec8..6a436af3 100644 --- a/src/main/Yardarm/GenerationContext.cs +++ b/src/main/Yardarm/GenerationContext.cs @@ -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; @@ -43,6 +45,9 @@ public NuGetFramework CurrentTargetFramework public IReadOnlySet PreprocessorSymbols => _preprocessorSymbols ??= GetPreprocessorSymbols(CurrentTargetFramework); + private readonly IOptions _options; + internal GenerationOptions Options => _options.Value; + public GenerationContext(IServiceProvider serviceProvider) : base(serviceProvider) { _openApiDocument = new Lazy(serviceProvider.GetRequiredService); @@ -52,6 +57,7 @@ public GenerationContext(IServiceProvider serviceProvider) : base(serviceProvide new Lazy(serviceProvider.GetRequiredService); _typeGeneratorRegistry = new Lazy(serviceProvider.GetRequiredService); + _options = serviceProvider.GetRequiredService>(); } private static HashSet GetPreprocessorSymbols(NuGetFramework targetFramework) => diff --git a/src/main/Yardarm/Internal/GenerationOptions.cs b/src/main/Yardarm/Internal/GenerationOptions.cs new file mode 100644 index 00000000..4a9b44fc --- /dev/null +++ b/src/main/Yardarm/Internal/GenerationOptions.cs @@ -0,0 +1,6 @@ +namespace Yardarm.Internal; + +internal class GenerationOptions +{ + public bool LegacyDateTimeHandling { get; set; } +} diff --git a/src/main/Yardarm/Yardarm.csproj b/src/main/Yardarm/Yardarm.csproj index 6276f997..3a1b6c22 100644 --- a/src/main/Yardarm/Yardarm.csproj +++ b/src/main/Yardarm/Yardarm.csproj @@ -18,6 +18,7 @@ + diff --git a/src/main/Yardarm/YardarmCoreServiceCollectionExtensions.cs b/src/main/Yardarm/YardarmCoreServiceCollectionExtensions.cs index ddaa63b8..3d30a3b2 100644 --- a/src/main/Yardarm/YardarmCoreServiceCollectionExtensions.cs +++ b/src/main/Yardarm/YardarmCoreServiceCollectionExtensions.cs @@ -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) @@ -139,6 +140,8 @@ public static IServiceCollection AddYardarm(this IServiceCollection services, Ya services.TryAddSingleton(); + services.ConfigureOptions(); + return services; } diff --git a/src/main/Yardarm/YardarmGenerationSettings.cs b/src/main/Yardarm/YardarmGenerationSettings.cs index 81b55ac9..6bb7a35a 100644 --- a/src/main/Yardarm/YardarmGenerationSettings.cs +++ b/src/main/Yardarm/YardarmGenerationSettings.cs @@ -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; @@ -97,8 +98,7 @@ public Stream XmlDocumentationOutput /// public bool NoRestore { get; set; } - public ImmutableArray TargetFrameworkMonikers { get; set; } = - new[] {"netstandard2.0"}.ToImmutableArray(); + public ImmutableArray TargetFrameworkMonikers { get; set; } = ["netstandard2.0"]; public Stream? NuGetOutput { get; set; } @@ -115,7 +115,7 @@ public Stream XmlDocumentationOutput /// /// Properties to alter the behavior of the generation process. /// - public Dictionary Properties { get; } = []; + public Dictionary Properties { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase); public CSharpCompilationOptions CompilationOptions { get; set; } = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)