Skip to content

Commit

Permalink
feat: new pipes caster and its safe version (#102)
Browse files Browse the repository at this point in the history
  • Loading branch information
Seddryck authored Sep 22, 2024
1 parent 879e8dd commit 65d0bc4
Show file tree
Hide file tree
Showing 7 changed files with 382 additions and 6 deletions.
2 changes: 2 additions & 0 deletions Streamistry.Core/Fluent/BasePipeBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ public FilterBuilder<TOutput> Filter(Func<TOutput?, bool>? function)
=> new(this, function);
public PluckerBuilder<TOutput, TNext> Pluck<TNext>(Expression<Func<TOutput, TNext?>> expr)
=> new(this, expr);
public CasterBuilder<TOutput, TNext> Cast<TNext>()
=> new(this);
public SplitterBuilder<TOutput, TNext> Split<TNext>(Func<TOutput?, TNext[]?>? function)
=> new(this, function);

Expand Down
34 changes: 34 additions & 0 deletions Streamistry.Core/Fluent/CasterBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Threading.Tasks;
using Streamistry.Pipes.Mappers;

namespace Streamistry.Fluent;
public class CasterBuilder<TInput, TOutput> : PipeElementBuilder<TInput, TOutput>, ISafeBuilder<CasterBuilder<TInput, TOutput>>
{
private bool IsSafe { get; set; } = false;

public CasterBuilder(IPipeBuilder<TInput> upstream)
: base(upstream)
{ }

public CasterBuilder<TInput, TOutput> Safe()
{
IsSafe = true;
return this;
}

public override IChainablePort<TOutput> OnBuildPipeElement()
=> IsSafe
? new SafeCaster<TInput, TOutput>(
Upstream.BuildPipeElement()
)
: new Caster<TInput, TOutput>(
Upstream.BuildPipeElement()
);
}


11 changes: 11 additions & 0 deletions Streamistry.Core/Fluent/ISafeBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Streamistry.Fluent;
internal interface ISafeBuilder<T>
{
T Safe();
}
17 changes: 15 additions & 2 deletions Streamistry.Core/Fluent/MapperBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,20 +1,33 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;
using System.Threading.Tasks;

namespace Streamistry.Fluent;
public class MapperBuilder<TInput, TOutput> : PipeElementBuilder<TInput, TOutput>
public class MapperBuilder<TInput, TOutput> : PipeElementBuilder<TInput, TOutput>, ISafeBuilder<MapperBuilder<TInput, TOutput>>
{
protected Func<TInput?, TOutput?>? Function { get; set; }
private bool IsSafe { get; set; } = false;

public MapperBuilder(IPipeBuilder<TInput> upstream, Func<TInput?, TOutput?>? function)
: base(upstream)
=> (Function) = (function);

public MapperBuilder<TInput, TOutput> Safe()
{
IsSafe = true;
return this;
}

public override IChainablePort<TOutput> OnBuildPipeElement()
=> new Mapper<TInput, TOutput>(
=> IsSafe
? new ExceptionMapper<TInput, TOutput>(
Upstream.BuildPipeElement()
, Function ?? throw new InvalidOperationException()
)
: new Mapper<TInput, TOutput>(
Upstream.BuildPipeElement()
, Function ?? throw new InvalidOperationException()
);
Expand Down
199 changes: 199 additions & 0 deletions Streamistry.Core/Pipes/Mappers/Caster.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using static System.Runtime.InteropServices.JavaScript.JSType;

namespace Streamistry.Pipes.Mappers;
internal class CasterHelper<TInput, TOutput>
{
public static bool HasInheritanceConversion()
{
return typeof(TOutput).IsAssignableFrom(typeof(TInput));
}

public static Func<TInput, TOutput>? GetImplicitOperator()
{
// Check for implicit conversion operator methods in the source type
var implicitOperator = typeof(TInput).GetMethods(BindingFlags.Static | BindingFlags.Public)
.FirstOrDefault(m => m.Name == "op_Implicit"
&& m.ReturnType == typeof(TOutput)
&& m.GetParameters().Length == 1
&& m.GetParameters()[0].ParameterType == typeof(TInput));

implicitOperator ??= typeof(TOutput).GetMethods(BindingFlags.Static | BindingFlags.Public)
.FirstOrDefault(m => m.Name == "op_Implicit"
&& m.ReturnType == typeof(TOutput)
&& m.GetParameters().Length == 1
&& m.GetParameters()[0].ParameterType == typeof(TInput));

if (implicitOperator != null)
{
// Convert the MethodInfo to a delegate
return (Func<TInput, TOutput>)Delegate.CreateDelegate(typeof(Func<TInput, TOutput>), implicitOperator);
}
return null;
}

public static Func<TInput, TOutput>? GetExplicitOperator()
{
// Now check for explicit conversion operator methods in the source type
var explicitOperator = typeof(TInput).GetMethods(BindingFlags.Static | BindingFlags.Public)
.FirstOrDefault(m => m.Name == "op_Explicit"
&& m.ReturnType == typeof(TOutput)
&& m.GetParameters().Length == 1
&& m.GetParameters()[0].ParameterType == typeof(TInput));

// Check for explicit conversion operator methods in the destination type
explicitOperator ??= typeof(TOutput).GetMethods(BindingFlags.Static | BindingFlags.Public)
.FirstOrDefault(m => m.Name == "op_Explicit"
&& m.ReturnType == typeof(TOutput)
&& m.GetParameters().Length == 1
&& m.GetParameters()[0].ParameterType == typeof(TInput));

if (explicitOperator != null)
{
// Convert the MethodInfo to a delegate
return (Func<TInput, TOutput>)Delegate.CreateDelegate(typeof(Func<TInput, TOutput>), explicitOperator);
}
return null;
}


}

public class Caster<TInput, TOutput> : Mapper<TInput, TOutput>
{
public Caster()
: base(input => new InternalCaster(
CasterHelper<TInput, TOutput>.GetImplicitOperator()).Cast(input))
{ }

public Caster(IChainablePort<TInput> upstream)
: base(input => new InternalCaster(
CasterHelper<TInput, TOutput>.GetImplicitOperator()).Cast(input)
, upstream)
{ }

private class InternalCaster
{
private Func<TInput, TOutput>? ImplicitOperator { get; }

public InternalCaster(Func<TInput, TOutput>? implicitOperator)
{
ImplicitOperator = implicitOperator;
}

public TOutput? Cast(TInput? input)
{
if (input is null)
return default;

if (ImplicitOperator is not null)
return ImplicitOperator.Invoke(input);
else if (input is TOutput outputCasted)
return outputCasted;
if (input is IConvertible)
try
{
return (TOutput)Convert.ChangeType(input, typeof(TOutput));
}
catch (Exception ex)
{
throw new InvalidCastException($"Specified cast from '{typeof(TInput).Name}' to '{typeof(TOutput).Name}' is not valid.", ex);
}
else
try
{
return (TOutput)(object)input;
}
catch (Exception ex)
{
throw new InvalidCastException($"Specified cast from '{typeof(TInput).Name}' to '{typeof(TOutput).Name}' is not valid.", ex);
}
}
}
}

public class SafeCaster<TInput, TOutput> : TryRouterPipe<TInput, TOutput>
{
private InternalCaster Caster { get; }

public SafeCaster()
: this(null)
{ }

public SafeCaster(IChainablePort<TInput>? upstream)
: base(upstream)
{
Caster = new InternalCaster(
CasterHelper<TInput, TOutput>.GetImplicitOperator(),
CasterHelper<TInput, TOutput>.GetExplicitOperator());
}

protected override bool TryInvoke(TInput? obj, [NotNullWhen(true)] out TOutput? value)
=> Caster.TryCast(obj, out value);

private class InternalCaster
{
private Func<TInput, TOutput>? ImplicitOperator { get; }
private Func<TInput, TOutput>? ExplicitOperator { get; }

public InternalCaster(Func<TInput, TOutput>? implicitOperator, Func<TInput, TOutput>? explicitOperator)
{
ImplicitOperator = implicitOperator;
ExplicitOperator = explicitOperator;
}

public bool TryCast(TInput? input, out TOutput? output)
{
if (input is null)
{
output = default;
return true;
}

if (ImplicitOperator is not null)
{
output = ImplicitOperator.Invoke(input);
return true;
}

if (ExplicitOperator is not null)
{
try
{
output = ExplicitOperator.Invoke(input);
return true;
}
catch
{
output = default;
return false;
}
}

if (input is TOutput outputCasted)
{
output = outputCasted;
return true;
}

if (input is IConvertible)
try
{
output = (TOutput)Convert.ChangeType(input, typeof(TOutput));
return true;
}
catch
{ }

output = default;
return false;
}
}
}
8 changes: 4 additions & 4 deletions Streamistry.Testing/Fluent/PipelineBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -642,8 +642,8 @@ public void Build_WithUnionButDifferentType_Failure()
}

public record Animal(string Name);
public record Carnivore(string Name) : Animal(Name);
public record Frugivore(string Name) : Animal(Name);
public record Carnivore(string Name) : Animal(Name) { public string? Eat { get; set; } }
public record Frugivore(string Name) : Animal(Name) { public string? Eat { get; set; } }

[Test]
public void Build_WithUnionButDifferentTypeLinkedByInheritance_Failure()
Expand All @@ -663,8 +663,8 @@ public void Build_WithUnionButDifferentTypeLinkedByInheritance_Failure()
public void Build_WithMoreThanTwoUpstreamsUnion_Success()
{
var common = new Segment<Animal, Animal>(x => x.Filter(y => y is not Carnivore && y is not Frugivore).Map(y => y));
var carnivore = new Segment<Animal, Animal>(x => x.Filter(y => y is Carnivore).Map(y => y));
var frugivore = new Segment<Animal, Animal>(x => x.Filter(y => y is Frugivore).Map(y => y));
var carnivore = new Segment<Animal, Animal>(x => x.Cast<Carnivore>().Safe().Map(y => { y!.Eat = "Meat"; return y; }).Cast<Animal>());
var frugivore = new Segment<Animal, Animal>(x => x.Cast<Frugivore>().Safe().Map(y => { y!.Eat = "Fruit"; return y; }).Cast<Animal>());

var pipeline = new PipelineBuilder()
.Source([new Animal("Bird"), new Carnivore("Dog")])
Expand Down
Loading

0 comments on commit 65d0bc4

Please sign in to comment.