Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions SupportedCountries.tt
Original file line number Diff line number Diff line change
Expand Up @@ -133,4 +133,39 @@ private void RenderIncludedCountries(IIbanRegistryProvider provider)
}
}
}

private sealed class IbanCountryCodeComparer : IEqualityComparer<IbanCountry>
{
/// <inheritdoc />
public bool Equals(IbanCountry? x, IbanCountry? y)
{
if (ReferenceEquals(x, y))
{
return true;
}

if (ReferenceEquals(x, null))
{
return false;
}

if (ReferenceEquals(y, null))
{
return false;
}

if (x.GetType() != y.GetType())
{
return false;
}

return x.TwoLetterISORegionName == y.TwoLetterISORegionName;
}

/// <inheritdoc />
public int GetHashCode(IbanCountry obj)
{
return obj?.TwoLetterISORegionName.GetHashCode() ?? 0;
}
}
#>
2 changes: 1 addition & 1 deletion src/Directory.Build.targets
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<!-- https://github.com/dotnet/sourcelink -->
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<IncludeSymbols>true</IncludeSymbols>
<IncludeSymbols Condition="'$(IncludeSymbols)' == ''">true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using Microsoft.CodeAnalysis;

namespace IbanNet.CodeGen.Extensions;

internal static class SourceProductionContextExtensions
{
internal static void ReportWarning
(
this SourceProductionContext ctx,
string errorCode,
string title,
string message,
string category,
Location? location = null
)
{
ctx.ReportDiagnostic(
Diagnostic.Create(
new DiagnosticDescriptor(
errorCode,
title,
message,
category,
DiagnosticSeverity.Warning,
true
),
location ?? Location.None
)
);
}
}
9 changes: 9 additions & 0 deletions src/IbanNet.CodeGen/IRegistryDataSource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using IbanNet.CodeGen.Swift;

namespace IbanNet.CodeGen;

internal interface IRegistryDataSource
{
bool IsDataSource(string path);
SwiftCsvRecord[] GetCountryDefinitions(string text);
}
41 changes: 0 additions & 41 deletions src/IbanNet.CodeGen/IbanCountryCodeComparer.cs

This file was deleted.

61 changes: 47 additions & 14 deletions src/IbanNet.CodeGen/IbanNet.CodeGen.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,68 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<IsPackable>false</IsPackable>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<NoWarn>$(NoWarn);CS1591</NoWarn>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<IncludeBuildOutput>false</IncludeBuildOutput>
<IsRoslynComponent>true</IsRoslynComponent>
<RootNamespace>IbanNet.CodeGen</RootNamespace>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<WarningsNotAsErrors>$(WarningsNotAsErrors);CS1591</WarningsNotAsErrors>
</PropertyGroup>

<ItemGroup>
<None Include="..\..\SupportedCountries.md">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>SupportedCountries.tt</DependentUpon>
</None>
<None Include="..\..\SupportedCountries.tt" Link="SupportedCountries.tt">
<Generator>TextTemplatingFileGenerator</Generator>
<LastGenOutput>SupportedCountries.md</LastGenOutput>
</None>
<EmbeddedResource Include="Liquid\PatternClass.liquid" />
<EmbeddedResource Include="Liquid\RegistryProvider.liquid" />
</ItemGroup>

<ItemGroup>
<Compile Include="..\IbanNet\Resources.Designer.cs" Link="Resources.Designer.cs" />
<Compile Include="..\IbanNet\Extensions\CharExtensions.cs" Link="Internal\Extensions\CharExtensions.cs" />
<Compile Include="..\IbanNet\Extensions\ChunkExtensions.cs" Link="Internal\Extensions\ChunkExtensions.cs" />
<Compile Include="..\IbanNet\Internal\**\*.cs" Link="Internal" />
<Compile Include="..\IbanNet\Registry\Patterns\*.cs" Link="Internal\Registry\Patterns" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="CsvHelper" Version="33.0.1" />
<PackageReference Include="Fluid.Core" Version="2.25.0" />
<PackageReference Include="HtmlAgilityPack" Version="1.12.1" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="6.0.1" />
<PackageReference Include="System.Text.Json" Version="8.0.5" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="8.0.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\IbanNet\IbanNet.csproj" />
</ItemGroup>
<!--
We don't have a need to distribute the code generator via NuGet since it's only used in our own solution.
But to be able to debug analyzers/generators using a project reference, all transient dependencies must be copied.
https://turnerj.com/blog/csharp-source-generator-pain-points-february-2022-update
https://github.com/dotnet/sdk/issues/17775#issuecomment-848451355
-->
<PropertyGroup>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<GetTargetPathDependsOn>$(GetTargetPathDependsOn);GetDependencyTargetPaths</GetTargetPathDependsOn>
</PropertyGroup>

<Target Name="GetDependencyTargetPaths" AfterTargets="ResolvePackageDependenciesForBuild">
<ItemGroup>
<TargetPathWithTargetPlatformMoniker Include="@(ReferenceCopyLocalPaths)" IncludeRuntimeDependency="false" />
</ItemGroup>
</Target>

<ItemGroup>
<Service Include="{508349b6-6b84-4df5-91f0-309beebad82d}" />
<None Include="..\..\SupportedCountries.md">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>SupportedCountries.tt</DependentUpon>
</None>
<None Include="..\..\SupportedCountries.tt" Link="SupportedCountries.tt">
<Generator>TextTemplatingFileGenerator</Generator>
<LastGenOutput>SupportedCountries.md</LastGenOutput>
</None>
</ItemGroup>

</Project>
132 changes: 132 additions & 0 deletions src/IbanNet.CodeGen/Liquid/FluidProviderGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
using System.Text;
using Fluid;
using Fluid.Ast;
using Fluid.Parser;
using Fluid.Values;
using IbanNet.CodeGen.Extensions;
using IbanNet.CodeGen.Swift;
using IbanNet.CodeGen.Syntax;
using IbanNet.Registry.Patterns;
using Microsoft.CodeAnalysis;
using Microsoft.Extensions.FileProviders;

namespace IbanNet.CodeGen.Liquid;

internal sealed class FluidProviderGenerator
{
public string Compile(SourceProductionContext ctx, RegistryProviderTarget target, IEnumerable<SwiftCsvRecord> model)
{
IFluidTemplate template = LoadTemplate(ctx, target, "RegistryProvider");

var opts = new TemplateOptions
{
// We're not distributing the code generator, it is only used by ourselves, so it is safe to allow all properties.
MemberAccessStrategy = new UnsafeMemberAccessStrategy { IgnoreCasing = true }, FileProvider = new EmbeddedFileProvider(GetType().Assembly, GetType().Namespace!)
};
opts.ValueConverters.Add(value => value is AsciiCategory v ? Enum.GetName(typeof(AsciiCategory), v) : null);
opts.ValueConverters.Add(value => value is IbanCsvData v ? new StructureValue(v, s => new PatternWrapper(s, v.Tokenizer)) : null);
opts.ValueConverters.Add(value => value is PatternCsvData v ? new StructureValue(v, s => new PatternWrapper(s, v.Tokenizer)) : null);
var m = new
{
Generator = new { Name = nameof(RegistryProviderTransformGenerator), Version = "2.0" },
Syntax = target,
Datasource = target.InputSourcePath,
Countries = model
.Where(record => !Boycott(record.CountryCode))
.OrderBy(record => record.CountryCode)
.ToList()
};

var context = new TemplateContext(m, opts);
try
{
return template.Render(context);
}
catch (ParseException ex)
{
ctx.ReportWarning("IBAN9103", "Liquid template rendering failed.", ex.Message, GetType().FullName!, target.Location);
return string.Empty;
}
}

private IFluidTemplate LoadTemplate(SourceProductionContext ctx, RegistryProviderTarget target, string name)
{
string resourceName = $"{name}.liquid";
string errorCode, errorTitle, errorMsg;

using Stream? stream = typeof(FluidProviderGenerator).Assembly.GetManifestResourceStream(typeof(FluidProviderGenerator), resourceName);
if (stream is null)
{
errorCode = "IBAN9101";
errorTitle = "Cannot load liquid template from embedded resource.";
errorMsg = $"The liquid template in resource stream '{resourceName}' could not be loaded.";
}
else
{
using var sr = new StreamReader(stream, Encoding.UTF8);
string source = sr.ReadToEnd();

var opts = new FluidParserOptions { AllowFunctions = true };
var parser = new FluidParser(opts);

RegisterRepeatBlock(parser);

if (parser.TryParse(source, out IFluidTemplate? template, out errorMsg))
{
return template;
}

errorCode = "IBAN9102";
errorTitle = "Failed to parse liquid template.";
}

ctx.ReportWarning(errorCode, errorTitle, errorMsg, GetType().FullName!, target.Location);

return new FluidTemplate();
}

private static void RegisterRepeatBlock(FluidParser parser)
{
parser.RegisterExpressionBlock("repeat",
async (value, statements, writer, encoder, context) =>
{
FluidValue? repeatCount = await value.EvaluateAsync(context).ConfigureAwait(false);
for (int i = 0; i < repeatCount.ToNumberValue(); i++)
{
await statements.RenderStatementsAsync(writer, encoder, context).ConfigureAwait(false);
}

return Completion.Normal;
});
}

private sealed class StructureValue
: ObjectValueBase
{
private readonly Func<string, Pattern> _patternConverter;

public StructureValue(object value, Func<string, Pattern> patternConverter)
: base(value)
{
_patternConverter = patternConverter;
}

public override ValueTask<FluidValue> GetValueAsync(string name, TemplateContext context)
{
if (name != "pattern")
{
return base.GetValueAsync(name, context);
}

string? pattern = (Value as PatternCsvData)?.Pattern ?? (Value as IbanCsvData)?.Pattern;
return pattern is null
? base.GetValueAsync(name, context)
: Create(_patternConverter(pattern), context.Options);
}
}

private static bool Boycott(string countryCode)
{
return countryCode == "RU"; // Go Ukraine!
}
}
Loading
Loading