Skip to content

Commit

Permalink
Add support for Wolverine-styled DescribeHandlerMatch
Browse files Browse the repository at this point in the history
- breaking change. Rename: ICanCreateTransformer -> ITransformerHandler, * Creator -> *Handler
  • Loading branch information
MichalBrylka committed Jan 8, 2024
1 parent 43ed59a commit 4068919
Show file tree
Hide file tree
Showing 25 changed files with 194 additions and 104 deletions.
31 changes: 24 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@

name: 'CI'
on:
workflow_dispatch:
workflow_dispatch:
inputs:
deploy_nuget_locally:
type: boolean
description: 🚀 deploy package to GH private
default: false
push:
branches:
- 'main'
Expand All @@ -16,7 +21,7 @@ on:
env:
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1
DOTNET_NOLOGO: true
NuGetDirectory: ${{ github.workspace}}/nuget
NuGetDirectory: ${{ github.workspace}}/nuget

defaults:
run:
Expand Down Expand Up @@ -95,22 +100,34 @@ jobs:
foreach($file in (Get-ChildItem -Recurse -Include *.trx)) {
$newFile = Join-Path (Get-Location).Path 'TR' "$($file.Directory.Parent.Name.Replace('Nemesis.TextParsers.', '').Replace('.Tests', ''))$($file.Name -replace 'Test_','_')"
Move-Item -Path "$($file.FullName)" -Destination "$newFile"
}
}
- name: ⬆️ upload test results
uses: actions/upload-artifact@v3
if: success() || failure() # run this step even if previous step failed
with:
name: TestResult
name: TestResults
if-no-files-found: error
retention-days: 7
path: "**/*.trx"


- name: 📊 tests results
uses: dorny/test-reporter@v1
if: success() || failure() # run this step even if previous step failed
with:
name: 📊 tests results
reporter: dotnet-trx
path: "**/*.trx"
fail-on-error: 'true'
fail-on-empty: 'true'


deploy:
name: 🚀 deploy
if: github.event_name == 'release' || github.event_name == 'workflow_dispatch'
if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.deploy_nuget_locally == 'true')
runs-on: ubuntu-latest
needs: [ create_nuget, run_test ]
steps:
steps:
- name: ⬇️ download packages
uses: actions/download-artifact@v3
with:
Expand Down
22 changes: 0 additions & 22 deletions .github/workflows/test-report.yml

This file was deleted.

6 changes: 3 additions & 3 deletions Nemesis.TextParsers.Tests/AnyTypeConverterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ public void CorrectUsage_SingleInstance()
var sut = Sut.GetTransformer<PointWithConverter>();


var actualTexts = data.Select(pwc => sut.Format(pwc)).ToList();
var actual = actualTexts.Select(text => sut.Parse(text)).ToList();
var actualTexts = data.Select(sut.Format).ToList();
var actual = actualTexts.Select(sut.Parse).ToList();


Assert.That(actual, Is.EqualTo(data));
Expand Down Expand Up @@ -56,7 +56,7 @@ public void BadConverter_NegativeTest()
[Test]
public void NoConverter_NegativeTest()
{
var sut = new TypeConverterTransformerCreator();
var sut = new TypeConverterTransformerHandler();
Assert.Multiple(() =>
{
Assert.That(
Expand Down
37 changes: 37 additions & 0 deletions Nemesis.TextParsers.Tests/Infrastructure/InfrastructureTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -293,4 +293,41 @@ private static void EmptyNullParsingTest_Helper<T>(string input, object expected
Assert.That(parsed2, Is.EqualTo(parsed1));
Assert.That(parsed2, Is.EqualTo(expectedOutput));
}

[TestCase(typeof(float), "Handled by SimpleTransformerHandler: Simple built-in type")]
[TestCase(typeof(Dictionary<decimal, string[]>), """
MISS -- Simple built-in type
MISS -- Key-value pair with properties supported for transformation
MISS -- Value tuple with properties supported for transformation
MISS -- Type decorated with TransformerAttribute pointing to valid transformer
MISS -- Type with method FromText(ReadOnlySpan<char> or string)
MISS -- Type decorated with TextFactoryAttribute pointing to factory with method FromText(ReadOnlySpan<char> or string)
MISS -- Enum based on system primitive number types
MISS -- Nullable with value supported for transformation
Handled by DictionaryTransformerHandler: Dictionary-like structure with key/value supported for transformation
""")]
[TestCase(typeof(PointWithoutConverter), """
MISS -- Simple built-in type
MISS -- Key-value pair with properties supported for transformation
MISS -- Value tuple with properties supported for transformation
MISS -- Type decorated with TransformerAttribute pointing to valid transformer
MISS -- Type with method FromText(ReadOnlySpan<char> or string)
MISS -- Type decorated with TextFactoryAttribute pointing to factory with method FromText(ReadOnlySpan<char> or string)
MISS -- Enum based on system primitive number types
MISS -- Nullable with value supported for transformation
MISS -- Dictionary-like structure with key/value supported for transformation
MISS -- Custom dictionary structure with key/value type supported for transformation
MISS -- Arrays (single dimensional and jagged) with element type supported for transformation
MISS -- Collections with element type supported for transformation
MISS -- LeanCollection with element type supported for transformation
MISS -- Custom collection with element type supported for transformation
MISS -- Deconstructable with properties types supported for transformation
MISS -- Type decorated with TypeConverter
MISS -- Sink for Transformer handler chain of responsibility
""")]
public void DescribeHandlerMatch_ShouldReturnValudDiagnostics(Type type, string expectedDiagnostics)
{
var actual = Sut.DefaultStore.DescribeHandlerMatch(type);
Assert.That(actual, Is.EqualTo(expectedDiagnostics).Using(IgnoreNewLinesComparer.EqualityComparer));
}
}
1 change: 0 additions & 1 deletion Nemesis.TextParsers.sln
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{
.github\workflows\ci.yml = .github\workflows\ci.yml
.github\workflows\codeql-analysis.yml = .github\workflows\codeql-analysis.yml
.github\workflows\license-scanning.yml = .github\workflows\license-scanning.yml
.github\workflows\test-report.yml = .github\workflows\test-report.yml
EndProjectSection
EndProject
Global
Expand Down
3 changes: 2 additions & 1 deletion Nemesis.TextParsers/01_Contracts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,11 +122,12 @@ public virtual bool TryParse(in ReadOnlySpan<char> input, out TElement result)
public override string ToString() => $"Transform {typeof(TElement).GetFriendlyName()}";
}

public interface ICanCreateTransformer
public interface ITransformerHandler
{
bool CanHandle(Type type);
sbyte Priority { get; }
ITransformer<TElement> CreateTransformer<TElement>();
string DescribeHandlerMatch();
}

public sealed class FormattableFormatter<TElement> : IFormatter<TElement> where TElement : IFormattable
Expand Down
63 changes: 43 additions & 20 deletions Nemesis.TextParsers/02_EntryPoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public interface ITransformerStore
ITransformer GetTransformer(Type type);

bool IsSupportedForTransformation(Type type);
string DescribeHandlerMatch(Type type);

SettingsStore SettingsStore { get; }
}
Expand All @@ -27,16 +28,16 @@ public static ITransformerStore GetDefaultStoreWith(SettingsStore settingsStore)

internal sealed class StandardTransformerStore : ITransformerStore
{
private readonly IEnumerable<ICanCreateTransformer> _transformerCreators;
private readonly IEnumerable<ITransformerHandler> _transformerHandlers;
public SettingsStore SettingsStore { get; }


private readonly ConcurrentDictionary<Type, ITransformer> _transformerCache = new();

public StandardTransformerStore([NotNull] IEnumerable<ICanCreateTransformer> transformerCreators,
public StandardTransformerStore([NotNull] IEnumerable<ITransformerHandler> transformerHandlers,
[NotNull] SettingsStore settingsStore)
{
_transformerCreators = transformerCreators ?? throw new ArgumentNullException(nameof(transformerCreators));
_transformerHandlers = transformerHandlers ?? throw new ArgumentNullException(nameof(transformerHandlers));
SettingsStore = settingsStore ?? throw new ArgumentNullException(nameof(settingsStore));
}

Expand All @@ -51,34 +52,34 @@ static bool IsUnique<TElement>(IEnumerable<TElement> list)
var types = Assembly.GetExecutingAssembly().GetTypes()
.Where(t => !t.IsAbstract && !t.IsInterface && !t.IsGenericType && !t.IsGenericTypeDefinition);

var transformerCreators = new List<ICanCreateTransformer>(16);
var store = new StandardTransformerStore(transformerCreators, settingsStore);
var handlers = new List<ITransformerHandler>(16);
var store = new StandardTransformerStore(handlers, settingsStore);

transformerCreators.AddRange(
handlers.AddRange(
from type in types
where typeof(ICanCreateTransformer).IsAssignableFrom(type)
select CreateTransformerCreator(type, store, settingsStore)
where typeof(ITransformerHandler).IsAssignableFrom(type)
select CreateTransformerHandler(type, store, settingsStore)
);

if (!IsUnique(transformerCreators.Select(d => d.Priority)))
throw new InvalidOperationException($"All priorities registered via {nameof(ICanCreateTransformer)} have to be unique");
if (!IsUnique(handlers.Select(d => d.Priority)))
throw new InvalidOperationException($"All priorities registered via {nameof(ITransformerHandler)} have to be unique");

transformerCreators.Sort((i1, i2) => i1.Priority.CompareTo(i2.Priority));
handlers.Sort((i1, i2) => i1.Priority.CompareTo(i2.Priority));

return store;
}

private static ICanCreateTransformer CreateTransformerCreator(Type type, ITransformerStore transformerStore, SettingsStore settingsStore)
private static ITransformerHandler CreateTransformerHandler(Type type, ITransformerStore transformerStore, SettingsStore settingsStore)
{
var ctors = type.GetConstructors(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
if (ctors.Length != 1)
throw new NotSupportedException($"Only single constructor is supported for transformer creator: {type.GetFriendlyName()}");
throw new NotSupportedException($"Only single constructor is supported for transformer handler: {type.GetFriendlyName()}");

var ctor = ctors[0];
var @params = ctor.GetParameters();

if (@params.Length == 0)
return (ICanCreateTransformer)Activator.CreateInstance(type, true);
return (ITransformerHandler)Activator.CreateInstance(type, true);
else
{
object GetArgument(Type argType)
Expand All @@ -96,7 +97,7 @@ object GetArgument(Type argType)
.Select(GetArgument)
.ToArray();

return (ICanCreateTransformer)Activator.CreateInstance(type, arguments);
return (ITransformerHandler)Activator.CreateInstance(type, arguments);
}
}

Expand Down Expand Up @@ -125,9 +126,9 @@ private ITransformer<TElement> GetTransformerCore<TElement>()
if (type.IsGenericTypeDefinition)
throw new NotSupportedException($"Text transformation for GenericTypeDefinition is not supported: {type.GetFriendlyName()}");

foreach (var creator in _transformerCreators)
if (creator.CanHandle(type))
return creator.CreateTransformer<TElement>();
foreach (var handler in _transformerHandlers)
if (handler.CanHandle(type))
return handler.CreateTransformer<TElement>();

throw new NotSupportedException($"Type '{type.GetFriendlyName()}' is not supported for string transformations. Provide appropriate chain of responsibility");
}
Expand All @@ -144,8 +145,30 @@ public bool IsSupportedForTransformation(Type type) =>

private bool IsSupportedForTransformationCore(Type type) =>
!type.IsGenericTypeDefinition &&
_transformerCreators.FirstOrDefault(c => c.CanHandle(type)) is { } creator &&
creator is not AnyTransformerCreator;
_transformerHandlers.FirstOrDefault(c => c.CanHandle(type)) is { } handler &&
handler is not SinkTransformerHandler;

#endregion

#region Diagnostics

public string DescribeHandlerMatch(Type type)
{
var sb = new StringBuilder();

foreach (var handler in _transformerHandlers)
if (handler.CanHandle(type) && handler is not SinkTransformerHandler)
{
sb.AppendLine($"Handled by {handler.GetType().Name}: {handler.DescribeHandlerMatch()}");
break;
}
else
{
sb.AppendLine($"MISS -- {handler.DescribeHandlerMatch()}");
}

return sb.ToString();
}

#endregion
}
8 changes: 5 additions & 3 deletions Nemesis.TextParsers/Parsers/03_SimpleTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@
namespace Nemesis.TextParsers.Parsers;

[UsedImplicitly]
public sealed class SimpleTransformerCreator : ICanCreateTransformer
public sealed class SimpleTransformerHandler : ITransformerHandler
{
private readonly IReadOnlyDictionary<Type, ITransformer> _simpleTransformers;

public SimpleTransformerCreator() => _simpleTransformers = GetDefaultTransformers();
public SimpleTransformerHandler() => _simpleTransformers = GetDefaultTransformers();

private static IReadOnlyDictionary<Type, ITransformer> GetDefaultTransformers(Assembly fromAssembly = null)
{
Expand Down Expand Up @@ -48,7 +48,7 @@ public ITransformer<TSimpleType> CreateTransformer<TSimpleType>()
return _simpleTransformers.TryGetValue(typeof(TSimpleType), out var transformer)
? (ITransformer<TSimpleType>)transformer
: throw new InvalidOperationException(
$"Internal state of {nameof(SimpleTransformerCreator)} was compromised");
$"Internal state of {nameof(SimpleTransformerHandler)} was compromised");
}

public bool CanHandle(Type type) => _simpleTransformers.ContainsKey(type);
Expand All @@ -57,6 +57,8 @@ public ITransformer<TSimpleType> CreateTransformer<TSimpleType>()

public override string ToString() =>
$"Create transformer for simple system types: {string.Join(", ", _simpleTransformers.Keys.Select(t => t.GetFriendlyName()))}";

string ITransformerHandler.DescribeHandlerMatch() => "Simple built-in type";
}

public static class NumberTransformerCache
Expand Down
10 changes: 6 additions & 4 deletions Nemesis.TextParsers/Parsers/03a_KeyValuePair.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@
namespace Nemesis.TextParsers.Parsers;

[UsedImplicitly]
public sealed class KeyValuePairTransformerCreator : ICanCreateTransformer
public sealed class KeyValuePairTransformerHandler : ITransformerHandler
{
private readonly TupleHelper _helper;
private readonly ITransformerStore _transformerStore;

public KeyValuePairTransformerCreator(ITransformerStore transformerStore, KeyValuePairSettings settings)
public KeyValuePairTransformerHandler(ITransformerStore transformerStore, KeyValuePairSettings settings)
{
_transformerStore = transformerStore;
_helper = settings.ToTupleHelper();
Expand All @@ -31,8 +31,8 @@ public ITransformer<TPair> CreateTransformer<TPair>()
}

private static readonly MethodInfo _createTransformerCoreMethod = Method.OfExpression<
Func<KeyValuePairTransformerCreator, ITransformer<KeyValuePair<int, int>>>
>(creator => creator.CreateTransformerCore<int, int>()).GetGenericMethodDefinition();
Func<KeyValuePairTransformerHandler, ITransformer<KeyValuePair<int, int>>>
>(handler => handler.CreateTransformerCore<int, int>()).GetGenericMethodDefinition();

private ITransformer<KeyValuePair<TKey, TValue>> CreateTransformerCore<TKey, TValue>() =>
new KeyValuePairTransformer<TKey, TValue>(_helper,
Expand Down Expand Up @@ -118,4 +118,6 @@ private static bool TryGetElements(Type type, out Type keyType, out Type valueTy

public override string ToString() =>
$"Create transformer for KeyValuePair struct with settings:{_helper}";

string ITransformerHandler.DescribeHandlerMatch() => "Key-value pair with properties supported for transformation";
}
6 changes: 4 additions & 2 deletions Nemesis.TextParsers/Parsers/03b_ValueTuple.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
namespace Nemesis.TextParsers.Parsers;

[UsedImplicitly]
public sealed class ValueTupleTransformerCreator : ICanCreateTransformer
public sealed class ValueTupleTransformerHandler : ITransformerHandler
{
private readonly TupleHelper _helper;

private readonly ITransformerStore _transformerStore;
public ValueTupleTransformerCreator(ITransformerStore transformerStore, ValueTupleSettings settings)
public ValueTupleTransformerHandler(ITransformerStore transformerStore, ValueTupleSettings settings)
{
_transformerStore = transformerStore;
_helper = settings.ToTupleHelper();
Expand Down Expand Up @@ -80,4 +80,6 @@ private bool IsSupported(Type type, out Type[] elementTypes) =>

public override string ToString() =>
$"Create transformer for ValueTuple struct with settings:{_helper}";

string ITransformerHandler.DescribeHandlerMatch() => "Value tuple with properties supported for transformation";
}
Loading

0 comments on commit 4068919

Please sign in to comment.