Skip to content

Commit

Permalink
[WIP] NodaTime
Browse files Browse the repository at this point in the history
- Still needs support for LiteralSerializer
  • Loading branch information
brantburnett committed Feb 1, 2025
1 parent 8b7fc50 commit dee0339
Show file tree
Hide file tree
Showing 16 changed files with 316 additions and 4 deletions.
1 change: 1 addition & 0 deletions src/main/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<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" />
<PackageVersion Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" />
<PackageVersion Include="NuGet.Commands" Version="6.12.1" />
<PackageVersion Include="Serilog.Extensions.Logging" Version="8.0.0" />
<PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0" />
Expand Down
1 change: 1 addition & 0 deletions src/main/Yardarm.Client/Properties/ClientAssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
[assembly: InternalsVisibleTo("Yardarm.Client.UnitTests")]
[assembly: InternalsVisibleTo("Yardarm.MicrosoftExtensionsHttp.Client")]
[assembly: InternalsVisibleTo("Yardarm.NewtonsoftJson.Client")]
[assembly: InternalsVisibleTo("Yardarm.NodaTime.Client")]
[assembly: InternalsVisibleTo("Yardarm.SystemTextJson.Client")]
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public abstract class LiteralConverter<T> : LiteralConverter
/// <param name="format">Format from the OpenAPI specification.</param>
/// <returns>Value parsed from the string.</returns>
[return: NotNullIfNotNull(nameof(value))]
public abstract T Read(string? value, string? format);
public abstract T? Read(string? value, string? format);

/// <summary>
/// Write a value to a string.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ public static bool TrySerialize<T>(T value, ReadOnlySpan<char> format, Span<char
#endif

[return: NotNullIfNotNull(nameof(value))]
public static T Deserialize<T>(string? value, string? format = null)
public static T? Deserialize<T>(string? value, string? format = null)
{
if (LiteralConverterRegistry.Instance.TryGet(out LiteralConverter<T>? converter))
{
Expand All @@ -83,7 +83,7 @@ public static T Deserialize<T>(string? value, string? format = null)

if (value is null)
{
return default!;
return default;
}

// Fallback logic to cover enums, which generally aren't explicitly registered
Expand Down
4 changes: 3 additions & 1 deletion src/main/Yardarm.CommandLine/Yardarm.CommandLine.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RuntimeIdentifiers>win-x64;linux-x64</RuntimeIdentifiers>
<RuntimeIdentifiers>win-x64;linux-x64;linux-arm64</RuntimeIdentifiers>
<PublishReadyToRun>true</PublishReadyToRun>

<!--
If .NET 8 isn't installed, allow the command line tool to run on newer frameworks.
Expand Down Expand Up @@ -39,6 +40,7 @@
<ItemGroup>
<ProjectReference Include="..\Yardarm.MicrosoftExtensionsHttp\Yardarm.MicrosoftExtensionsHttp.csproj" />
<ProjectReference Include="..\Yardarm.NewtonsoftJson\Yardarm.NewtonsoftJson.csproj" />
<ProjectReference Include="..\Yardarm.NodaTime\Yardarm.NodaTime.csproj" />
<ProjectReference Include="..\Yardarm.SystemTextJson\Yardarm.SystemTextJson.csproj" />
<ProjectReference Include="..\Yardarm\Yardarm.csproj" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using NodaTime;
using NodaTime.Text;
using NodaTime.Utility;
using System;
using System.Diagnostics.CodeAnalysis;

namespace RootNamespace.Serialization.Literals;

internal static class NodaLiteralConverters
{
private static NodaPatternLiteralConverter<LocalDate>? s_localDateConverter;
private static NodaPatternLiteralConverter<LocalTime>? s_localTimeConverter;
private static NodaPatternLiteralConverter<OffsetDateTime>? s_offsetDateTimeConverter;
private static NodaPatternLiteralConverter<OffsetTime>? s_offsetTimeConverter;

/// <summary>
/// Converter for local dates, using the ISO-8601 date pattern.
/// </summary>
public static LiteralConverter<LocalDate> LocalDateConverter => s_localDateConverter ??=
new NodaPatternLiteralConverter<LocalDate>(
LocalDatePattern.Iso, CreateIsoValidator<LocalDate>(x => x.Calendar));

/// <summary>
/// Converter for local times, using the ISO-8601 time pattern, extended as required to accommodate nanoseconds.
/// </summary>
public static LiteralConverter<LocalTime> LocalTimeConverter => s_localTimeConverter ??=
new NodaPatternLiteralConverter<LocalTime>(LocalTimePattern.ExtendedIso);

/// <summary>
/// Converter for offset date/times.
/// </summary>
public static LiteralConverter<OffsetDateTime> OffsetDateTimeConverter => s_offsetDateTimeConverter ??=
new NodaPatternLiteralConverter<OffsetDateTime>(
OffsetDateTimePattern.Rfc3339, CreateIsoValidator<OffsetDateTime>(x => x.Calendar));

/// <summary>
/// Converter for offset times.
/// </summary>
public static LiteralConverter<OffsetTime> OffsetTimeConverter => s_offsetTimeConverter ??=
new NodaPatternLiteralConverter<OffsetTime>(OffsetTimePattern.ExtendedIso);

private static Action<T> CreateIsoValidator<T>(Func<T, CalendarSystem> calendarProjection) => value =>
{
CalendarSystem calendar = calendarProjection(value);

if (calendar != CalendarSystem.Iso)
{
ThrowInvalidNodaDataException($"Values of type {typeof(T).Name} must (currently) use the ISO calendar in order to be serialized.");
}
};

[DoesNotReturn]
private static void ThrowInvalidNodaDataException(string message)
{
throw new InvalidNodaDataException(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System;
using System.Diagnostics.CodeAnalysis;
using NodaTime.Text;

namespace RootNamespace.Serialization.Literals;

internal class NodaPatternLiteralConverter<T>(IPattern<T> pattern, Action<T>? validator) : LiteralConverter<T>
{
public NodaPatternLiteralConverter(IPattern<T> pattern)
: this(pattern, null)
{
}

[return: NotNullIfNotNull(nameof(value))]
public override T? Read(string? value, string? format)
{
if (value is null)
{
return default;
}

return pattern.Parse(value).Value!;
}

public override string Write(T value, string? format)
{
validator?.Invoke(value);

return pattern.Format(value);
}
}
24 changes: 24 additions & 0 deletions src/main/Yardarm.NodaTime.Client/Yardarm.NodaTime.Client.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0;net8.0</TargetFrameworks>
<OutputType>Library</OutputType>

<Nullable>enable</Nullable>
</PropertyGroup>

<PropertyGroup>
<EnableTrimAnalyzer Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net6.0'))">true</EnableTrimAnalyzer>
<EnableAotAnalyzer Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net7.0'))">true</EnableAotAnalyzer>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="NodaTime.Serialization.SystemTextJson" />
<PackageReference Include="System.Threading.Tasks.Extensions" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Yardarm.Client\Yardarm.Client.csproj" />
</ItemGroup>

</Project>
41 changes: 41 additions & 0 deletions src/main/Yardarm.NodaTime/Helpers/NodaTimeTypes.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;

namespace Yardarm.NodaTime.Helpers;

internal static class NodaTimeTypes
{
public static NameSyntax NodaTime { get; } =
AliasQualifiedName(IdentifierName(Token(SyntaxKind.GlobalKeyword)), IdentifierName("NodaTime"));

public static NameSyntax Duration { get; } =
QualifiedName(NodaTime, IdentifierName("Duration"));

public static NameSyntax LocalDate { get; } =
QualifiedName(NodaTime, IdentifierName("LocalDate"));

public static NameSyntax LocalTime { get; } =
QualifiedName(NodaTime, IdentifierName("LocalTime"));

public static NameSyntax OffsetDateTime { get; } =
QualifiedName(NodaTime, IdentifierName("OffsetDateTime"));

public static NameSyntax OffsetTime { get; } =
QualifiedName(NodaTime, IdentifierName("OffsetTime"));

public static class Serialization
{
public static NameSyntax Name { get; } =
QualifiedName(NodaTime, IdentifierName("Serialization"));

public static class SystemTextJson
{
public static NameSyntax Name { get; } =
QualifiedName(Serialization.Name, IdentifierName("SystemTextJson"));

public static NameSyntax NodaTimeDefaultJsonConverterFactory { get; } =
QualifiedName(Name, IdentifierName("NodaTimeDefaultJsonConverterFactory"));
}
}
}
10 changes: 10 additions & 0 deletions src/main/Yardarm.NodaTime/Internal/ClientGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Yardarm.Generation;
using Yardarm.Names;

namespace Yardarm.NodaTime.Internal;

internal class ClientGenerator(GenerationContext generationContext, IRootNamespace rootNamespace)
: ResourceSyntaxTreeGenerator(generationContext, rootNamespace)
{
protected override string ResourcePrefix => "Yardarm.NodaTime.Client.";
}
23 changes: 23 additions & 0 deletions src/main/Yardarm.NodaTime/NodaTimeDependencyGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System.Collections.Generic;
using NuGet.Frameworks;
using NuGet.LibraryModel;
using NuGet.Versioning;
using Yardarm.Packaging;

namespace Yardarm.NodaTime;

public class NodaTimeDependencyGenerator : IDependencyGenerator
{
public IEnumerable<LibraryDependency> GetDependencies(NuGetFramework targetFramework) =>
[
new LibraryDependency
{
LibraryRange = new LibraryRange
{
Name = "NodaTime.Serialization.SystemTextJson",
TypeConstraint = LibraryDependencyTarget.Package,
VersionRange = VersionRange.Parse("1.3.0")
}
}
];
}
22 changes: 22 additions & 0 deletions src/main/Yardarm.NodaTime/NodaTimeExtension.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using Microsoft.Extensions.DependencyInjection;
using Yardarm.Enrichment;
using Yardarm.Generation;
using Yardarm.NodaTime.Internal;
using Yardarm.Packaging;

namespace Yardarm.NodaTime;

public class NodaTimeExtension : YardarmExtension
{
public override bool IsOutputTrimmable(GenerationContext context) => true;

public override IServiceCollection ConfigureServices(IServiceCollection services)
{
services
.AddOpenApiSyntaxNodeEnricher<NodaTimePropertyEnricher>()
.AddSingleton<ISyntaxTreeGenerator, ClientGenerator>()
.AddSingleton<IDependencyGenerator, NodaTimeDependencyGenerator>();

return services;
}
}
58 changes: 58 additions & 0 deletions src/main/Yardarm.NodaTime/NodaTimePropertyEnricher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.OpenApi.Models;
using Yardarm.Enrichment;
using Yardarm.Enrichment.Schema;
using Yardarm.NodaTime.Helpers;
using Yardarm.SystemTextJson.Helpers;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;

namespace Yardarm.NodaTime;

/// <summary>
/// Replaces date and type JSON properties with NodaTime types and applies a JSON converter attribute.
/// </summary>
public class NodaTimePropertyEnricher : IOpenApiSyntaxNodeEnricher<PropertyDeclarationSyntax, OpenApiSchema>
{
public Type[] ExecuteBefore { get; } = [typeof(RequiredPropertyEnricher)];

public PropertyDeclarationSyntax Enrich(PropertyDeclarationSyntax target,
OpenApiEnrichmentContext<OpenApiSchema> context)
{
if (context.Element.Type != "string")
{
// Not a string, so no change
return target;
}

TypeSyntax? newType = context.Element.Format switch
{
"date-time" => NodaTimeTypes.OffsetDateTime,
"date" or "full-date" => NodaTimeTypes.LocalDate,
"partial-time" => NodaTimeTypes.LocalTime,
"time" or "full-time" => NodaTimeTypes.OffsetTime,
_ => null
};

if (newType is null)
{
// No change
return target;
}

if (target.Type is NullableTypeSyntax)
{
newType = NullableType(newType);
}

return target
.WithType(newType)
.AddAttributeLists(AttributeList(SingletonSeparatedList(
Attribute(
SystemTextJsonTypes.Serialization.JsonConverterAttributeName,
AttributeArgumentList(SingletonSeparatedList(
AttributeArgument(TypeOfExpression(NodaTimeTypes.Serialization.SystemTextJson.NodaTimeDefaultJsonConverterFactory)))))
.WithTrailingTrivia(ElasticCarriageReturnLineFeed))));
}
}
27 changes: 27 additions & 0 deletions src/main/Yardarm.NodaTime/Yardarm.NodaTime.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>

<Nullable>enable</Nullable>

<PublishRepositoryUrl>true</PublishRepositoryUrl>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<Description>Extension for Yardarm to generate SDKs that use NodaTime for date/time schemas.</Description>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Yardarm\Yardarm.csproj" />
<ProjectReference Include="..\Yardarm.SystemTextJson\Yardarm.SystemTextJson.csproj" />
</ItemGroup>

<ItemGroup>
<!-- Collect cs files from Yardarm.NodaTime.Client as resources so we can compile them into generated SDKs -->
<EmbeddedResource Include="../Yardarm.NodaTime.Client/**/*.cs" Exclude="../Yardarm.NodaTime.Client/bin/**;../Yardarm.NodaTime.Client/obj/**">
<Visible>False</Visible>
<LogicalName>$([System.String]::Copy('%(RelativeDir)').Substring(3).Replace('/', '.').Replace('\', '.'))%(Filename)%(Extension)</LogicalName>
</EmbeddedResource>
</ItemGroup>

</Project>
3 changes: 3 additions & 0 deletions src/main/Yardarm.SystemTextJson/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("Yardarm.NodaTime, PublicKey=00240000048000009400000006020000002400005253413100040000010001000d09345410b605ba41fa5c08c3e48e094da35fa75bbf0c5ded69ba29147de0a1401798641db4863302209826d0aa926267bb29abde27220de93e980d9b94e52a8b92768eacce7d26eacae4989770bdcd46c6153da0d0e228617d96a0973b717409f8b242d6a46eefc4bb1b7ba63bd6f8f929d8b100c744d5c8c4a4631556fec4")]
12 changes: 12 additions & 0 deletions src/main/Yardarm.sln
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,17 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Yardarm.MicrosoftExtensions
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Yardarm.Benchmarks", "Yardarm.Benchmarks\Yardarm.Benchmarks.csproj", "{A92143CD-F939-445D-B408-7493E36BB662}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Yardarm.NodaTime", "Yardarm.NodaTime\Yardarm.NodaTime.csproj", "{F0BD89F3-3363-4990-8F89-EDC4BC88FCCD}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{0F27CDEF-318E-4305-AE2B-AA44C670E7C2}"
ProjectSection(SolutionItems) = preProject
..\Directory.Build.props = ..\Directory.Build.props
..\Directory.Build.targets = ..\Directory.Build.targets
Directory.Packages.props = Directory.Packages.props
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Yardarm.NodaTime.Client", "Yardarm.NodaTime.Client\Yardarm.NodaTime.Client.csproj", "{86C00C4C-763D-47B1-AB0B-7737A5AC084D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -94,6 +98,14 @@ Global
{A92143CD-F939-445D-B408-7493E36BB662}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A92143CD-F939-445D-B408-7493E36BB662}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A92143CD-F939-445D-B408-7493E36BB662}.Release|Any CPU.Build.0 = Release|Any CPU
{F0BD89F3-3363-4990-8F89-EDC4BC88FCCD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F0BD89F3-3363-4990-8F89-EDC4BC88FCCD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F0BD89F3-3363-4990-8F89-EDC4BC88FCCD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F0BD89F3-3363-4990-8F89-EDC4BC88FCCD}.Release|Any CPU.Build.0 = Release|Any CPU
{86C00C4C-763D-47B1-AB0B-7737A5AC084D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{86C00C4C-763D-47B1-AB0B-7737A5AC084D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{86C00C4C-763D-47B1-AB0B-7737A5AC084D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{86C00C4C-763D-47B1-AB0B-7737A5AC084D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down

0 comments on commit dee0339

Please sign in to comment.