From c848cacfc77ee8279764d029f4f82df2bece89df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20L=2E=20Charlier?= Date: Thu, 26 Sep 2024 23:36:46 +0200 Subject: [PATCH] feat: filters for null and non-null values (#106) * feat: filters for null and non-null values * refactor: review nullability of many methods * ci: calculate coverage on the tested library --- Streamistry.Core/Aggregator.cs | 10 +-- Streamistry.Core/ChainablePipe.cs | 6 +- Streamistry.Core/DualRouterPipe.cs | 2 +- Streamistry.Core/ExceptionMapper.cs | 10 +-- Streamistry.Core/ExceptionRouterPipe.cs | 8 +-- Streamistry.Core/Filter.cs | 65 ++++++++++++++++--- Streamistry.Core/Fluent/AggregatorBuilder.cs | 12 ++-- Streamistry.Core/Fluent/BasePipeBuilder.cs | 21 ++++-- Streamistry.Core/Fluent/FilterBuilder.cs | 29 ++++++++- Streamistry.Core/Fluent/MapperBuilder.cs | 4 +- Streamistry.Core/Fluent/PluckerBuilder.cs | 4 +- Streamistry.Core/Fluent/Segment.cs | 4 +- Streamistry.Core/Fluent/SplitterBuilder.cs | 4 +- Streamistry.Core/IChainablePipe.cs | 6 +- Streamistry.Core/IProcessablePipe.cs | 2 +- Streamistry.Core/Mapper.cs | 10 +-- Streamistry.Core/OutputPort.cs | 8 +-- Streamistry.Core/Pipes/Aggregators/Average.cs | 4 +- Streamistry.Core/Pipes/Aggregators/Max.cs | 6 +- Streamistry.Core/Pipes/Aggregators/Median.cs | 4 +- Streamistry.Core/Pipes/Aggregators/Min.cs | 6 +- Streamistry.Core/Pipes/Aggregators/Sum.cs | 4 +- Streamistry.Core/Pipes/Mappers/Caster.cs | 14 ++-- Streamistry.Core/Pipes/Mappers/Plucker.cs | 16 ++--- Streamistry.Core/Pipes/Mappers/Translator.cs | 8 +-- Streamistry.Core/SingleRouterPipe.cs | 6 +- Streamistry.Core/Splitter.cs | 12 ++-- Streamistry.Core/TryRouterPipe.cs | 4 +- Streamistry.Json/PathPlucker.cs | 6 +- Streamistry.Json/RestResponder.cs | 11 ++-- Streamistry.Json/ValueMapper.cs | 4 +- .../Fluent/PipelineBuilderTests.cs | 37 +++++++++++ Streamistry.Testing/SplitterTests.cs | 6 +- appveyor.yml | 4 +- 34 files changed, 236 insertions(+), 121 deletions(-) diff --git a/Streamistry.Core/Aggregator.cs b/Streamistry.Core/Aggregator.cs index d04b17b..327a67b 100644 --- a/Streamistry.Core/Aggregator.cs +++ b/Streamistry.Core/Aggregator.cs @@ -18,12 +18,12 @@ namespace Streamistry; /// The type of the elements in the output stream, determined by applying a selection function to the accumulated state. public class Aggregator : SingleRouterPipe { - public Func Accumulator { get; } - public Func Selector { get; } + public Func Accumulator { get; } + public Func Selector { get; } public TAccumulate? State { get; set; } private TAccumulate? Seed { get; } - public Aggregator(IChainablePort? upstream, Func accumulator, Func selector, TAccumulate? seed = default, Expression>>? completion = null) + public Aggregator(IChainablePort? upstream, Func accumulator, Func selector, TAccumulate? seed = default, Expression>>? completion = null) : base(upstream) { (Accumulator, Selector, State, Seed) = (accumulator, selector, seed, seed); @@ -32,9 +32,9 @@ public Aggregator(IChainablePort? upstream, Func downstream, Action? completion) + public void RegisterDownstream(Action downstream, Action? completion) { RegisterDownstream(downstream); RegisterOnCompleted(completion); @@ -39,10 +39,10 @@ public void RegisterDownstream(Action downstream, Action? completion) public void RegisterOnCompleted(Action? action) => Completion += action; - public void RegisterDownstream(Action action) + public void RegisterDownstream(Action action) => Main.RegisterDownstream(action); - public void UnregisterDownstream(Action downstream) + public void UnregisterDownstream(Action downstream) => Main.UnregisterDownstream(downstream); protected void PushDownstream(T? obj) diff --git a/Streamistry.Core/DualRouterPipe.cs b/Streamistry.Core/DualRouterPipe.cs index e7f1103..28ea4fc 100644 --- a/Streamistry.Core/DualRouterPipe.cs +++ b/Streamistry.Core/DualRouterPipe.cs @@ -21,7 +21,7 @@ public DualRouterPipe(IChainablePort? upstream) } [Meter] - public abstract void Emit(TInput? obj); + public abstract void Emit(TInput obj); public void Bind(IChainablePort input) { diff --git a/Streamistry.Core/ExceptionMapper.cs b/Streamistry.Core/ExceptionMapper.cs index b49e77f..f7ba853 100644 --- a/Streamistry.Core/ExceptionMapper.cs +++ b/Streamistry.Core/ExceptionMapper.cs @@ -9,22 +9,22 @@ namespace Streamistry; public class ExceptionMapper : ExceptionRouterPipe { - public Func Function { get; init; } + public Func Function { get; init; } - protected ExceptionMapper(Func function, IChainablePort ? upstream) + protected ExceptionMapper(Func function, IChainablePort ? upstream) : base(upstream) { Function = function; } - public ExceptionMapper(IChainablePort upstream, Func function) + public ExceptionMapper(IChainablePort upstream, Func function) : this(function, upstream) { } - public ExceptionMapper(Func function) + public ExceptionMapper(Func function) : this(function, null) { } - protected override TOutput? Invoke(TInput? obj) + protected override TOutput Invoke(TInput obj) => Function.Invoke(obj); } diff --git a/Streamistry.Core/ExceptionRouterPipe.cs b/Streamistry.Core/ExceptionRouterPipe.cs index fb35334..44c28ee 100644 --- a/Streamistry.Core/ExceptionRouterPipe.cs +++ b/Streamistry.Core/ExceptionRouterPipe.cs @@ -14,7 +14,7 @@ public ExceptionRouterPipe(IChainablePort? upstream) { } [Trace] - public override void Emit(TInput? obj) + public override void Emit(TInput obj) { if (TryCatchInvoke(obj, out var value, out var exception)) PushDownstream(value); @@ -23,9 +23,9 @@ public override void Emit(TInput? obj) } [Trace] - protected virtual bool TryCatchInvoke(TInput? obj, out TOutput? value, out Exception? ex) + protected virtual bool TryCatchInvoke(TInput obj, out TOutput value, out Exception? ex) { - value = default; + value = default!; ex = null; try { @@ -40,5 +40,5 @@ protected virtual bool TryCatchInvoke(TInput? obj, out TOutput? value, out Excep } [Trace] - protected abstract TOutput? Invoke(TInput? obj); + protected abstract TOutput Invoke(TInput obj); } diff --git a/Streamistry.Core/Filter.cs b/Streamistry.Core/Filter.cs index eebc8e7..34a3916 100644 --- a/Streamistry.Core/Filter.cs +++ b/Streamistry.Core/Filter.cs @@ -13,31 +13,78 @@ namespace Streamistry; /// The output stream is composed of elements that satisfy the predicate; elements that do not satisfy the predicate are excluded from the downstream stream. /// /// The type of the elements in both the input and output streams. -public class Filter : BaseSingleRouterPipe +public class Filter : BaseFilter { - public Func Predicate { get; init; } - - public Filter(Func predicate) + public Filter(Func predicate) : this(predicate, null) { } - public Filter(IChainablePort upstream, Func predicate) + public Filter(IChainablePort upstream, Func predicate) : this(predicate, upstream) { } - public Filter(Func predicate, IChainablePort? upstream = null) - : base(upstream) + public Filter(Func predicate, IChainablePort? upstream = null) + : base(predicate, upstream) { Predicate = predicate; } - public override void Emit(TInput? obj) + [Meter] + public override void Emit(TInput obj) { if (Invoke(obj)) PushDownstream(obj); } +} + +public class FilterNull : BaseFilter +{ + public FilterNull(IChainablePort? upstream = null) + : base((x) => x is null, upstream) + { } + + [Meter] + public override void Emit(TInput? obj) + { + if (Invoke(obj)) + PushDownstream(default); + } +} + +public class FilterNotNull : BaseFilter + where TOutput : notnull +{ + public FilterNotNull(IChainablePort? upstream = null) + : base((x) => x is not null, upstream) + { } + + [Meter] + public override void Emit(TInput obj) + { + if (Invoke(obj)) + PushDownstream(obj is TOutput output ? output : throw new InvalidCastException()); + } +} + +public abstract class BaseFilter : BaseSingleRouterPipe +{ + public Func Predicate { get; init; } + + public BaseFilter(Func predicate) + : this(predicate, null) + { } + + public BaseFilter(IChainablePort upstream, Func predicate) + : this(predicate, upstream) + { } + + public BaseFilter(Func predicate, IChainablePort? upstream = null) + : base(upstream) + { + Predicate = predicate; + } [Trace] - protected bool Invoke(TInput? input) + protected bool Invoke(TInput input) => Predicate.Invoke(input); } diff --git a/Streamistry.Core/Fluent/AggregatorBuilder.cs b/Streamistry.Core/Fluent/AggregatorBuilder.cs index 0865f7f..28dbe20 100644 --- a/Streamistry.Core/Fluent/AggregatorBuilder.cs +++ b/Streamistry.Core/Fluent/AggregatorBuilder.cs @@ -45,21 +45,21 @@ public override IChainablePort OnBuildPipeElement() public class UniversalAggregatorBuilder : PipeElementBuilder { - protected Func? Accumulator { get; } - protected Func? Selector { get; set; } = x => (TOutput?)Convert.ChangeType(x, typeof(TOutput)); - protected TAccumulate? Seed { get; set; } = default; + protected Func? Accumulator { get; } + protected Func? Selector { get; set; } = x => (TOutput)Convert.ChangeType(x, typeof(TOutput))!; + protected TAccumulate Seed { get; set; } = default!; - public UniversalAggregatorBuilder(IPipeBuilder upstream, Func accumulator) + public UniversalAggregatorBuilder(IPipeBuilder upstream, Func accumulator) : base(upstream) => (Accumulator) = (accumulator); - public UniversalAggregatorBuilder WithSelector(Func? selector) + public UniversalAggregatorBuilder WithSelector(Func? selector) { Selector = selector; return this; } - public UniversalAggregatorBuilder WithSeed(TAccumulate? seed) + public UniversalAggregatorBuilder WithSeed(TAccumulate seed) { Seed = seed; return this; diff --git a/Streamistry.Core/Fluent/BasePipeBuilder.cs b/Streamistry.Core/Fluent/BasePipeBuilder.cs index 762cf71..35000e5 100644 --- a/Streamistry.Core/Fluent/BasePipeBuilder.cs +++ b/Streamistry.Core/Fluent/BasePipeBuilder.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Data.SqlTypes; using System.Linq; using System.Linq.Expressions; using System.Text; @@ -31,24 +32,29 @@ public BasePipeBuilder Checkpoint(out IChainablePort port) public SinkBuilder Sink() => new(this); - public MapperBuilder Map(Func? function) + public MapperBuilder Map(Func? function) => new(this, function); - public FilterBuilder Filter(Func? function) + public FilterBuilder Filter(Func? function) => new(this, function); - public PluckerBuilder Pluck(Expression> expr) + public FilterNullBuilder IsNull() + => new(this); + public FilterNotNullBuilder IsNotNull() + where TNext : notnull + => new(this); + public PluckerBuilder Pluck(Expression> expr) => new(this, expr); public CasterBuilder Cast() => new(this); public ConstantBuilder Constant(TNext value) => new(this, value); - public SplitterBuilder Split(Func? function) + public SplitterBuilder Split(Func? function) => new(this, function); - public UniversalAggregatorBuilder Aggregate(Func accumulator) + public UniversalAggregatorBuilder Aggregate(Func accumulator) => new(this, accumulator); - public UniversalAggregatorBuilder Aggregate(Func accumulator) + public UniversalAggregatorBuilder Aggregate(Func accumulator) => new(this, accumulator); - public UniversalAggregatorBuilder Aggregate(Func accumulator) + public UniversalAggregatorBuilder Aggregate(Func accumulator) => new(this, accumulator); public AggregatorBuilder Aggregate() => new(this); @@ -61,3 +67,4 @@ public ParserBuilder Parse() public BinderBuilder Bind(Segment segment) => new(this, segment); } + diff --git a/Streamistry.Core/Fluent/FilterBuilder.cs b/Streamistry.Core/Fluent/FilterBuilder.cs index 90651fa..ca90c91 100644 --- a/Streamistry.Core/Fluent/FilterBuilder.cs +++ b/Streamistry.Core/Fluent/FilterBuilder.cs @@ -7,9 +7,9 @@ namespace Streamistry.Fluent; public class FilterBuilder : PipeElementBuilder, IPipeBuilder { - protected Func? Function { get; } + protected Func? Function { get; } - public FilterBuilder(IPipeBuilder upstream, Func? function) + public FilterBuilder(IPipeBuilder upstream, Func? function) :base(upstream) => (Function) = (function); @@ -19,3 +19,28 @@ public override IChainablePort OnBuildPipeElement() , Function ?? throw new InvalidOperationException() ); } + +public class FilterNullBuilder : PipeElementBuilder, IPipeBuilder +{ + public FilterNullBuilder(IPipeBuilder upstream) + : base(upstream) + { } + + public override IChainablePort OnBuildPipeElement() + => new FilterNull( + Upstream.BuildPipeElement()! + ); +} + +public class FilterNotNullBuilder : PipeElementBuilder, IPipeBuilder + where TOutput: notnull +{ + public FilterNotNullBuilder(IPipeBuilder upstream) + : base(upstream) + { } + + public override IChainablePort OnBuildPipeElement() + => new FilterNotNull( + Upstream.BuildPipeElement() + ); +} diff --git a/Streamistry.Core/Fluent/MapperBuilder.cs b/Streamistry.Core/Fluent/MapperBuilder.cs index a583f48..0270b3b 100644 --- a/Streamistry.Core/Fluent/MapperBuilder.cs +++ b/Streamistry.Core/Fluent/MapperBuilder.cs @@ -8,10 +8,10 @@ namespace Streamistry.Fluent; public class MapperBuilder : PipeElementBuilder, ISafeBuilder> { - protected Func? Function { get; set; } + protected Func? Function { get; set; } private bool IsSafe { get; set; } = false; - public MapperBuilder(IPipeBuilder upstream, Func? function) + public MapperBuilder(IPipeBuilder upstream, Func? function) : base(upstream) => (Function) = (function); diff --git a/Streamistry.Core/Fluent/PluckerBuilder.cs b/Streamistry.Core/Fluent/PluckerBuilder.cs index 19da13a..ee19e49 100644 --- a/Streamistry.Core/Fluent/PluckerBuilder.cs +++ b/Streamistry.Core/Fluent/PluckerBuilder.cs @@ -9,9 +9,9 @@ namespace Streamistry.Fluent; public class PluckerBuilder : PipeElementBuilder { - protected Expression> Expr { get; set; } + protected Expression> Expr { get; set; } - public PluckerBuilder(IPipeBuilder upstream, Expression> expr) + public PluckerBuilder(IPipeBuilder upstream, Expression> expr) : base(upstream) => (Expr) = (expr); diff --git a/Streamistry.Core/Fluent/Segment.cs b/Streamistry.Core/Fluent/Segment.cs index 75cef9b..33c9ae0 100644 --- a/Streamistry.Core/Fluent/Segment.cs +++ b/Streamistry.Core/Fluent/Segment.cs @@ -41,10 +41,10 @@ public IChainablePipe Pipe public IBindablePipe GetTarget() => Target ?? throw new InvalidOperationException(); - public void RegisterDownstream(Action downstream) + public void RegisterDownstream(Action downstream) => Target ??= (IBindablePipe)downstream.Target!; - public void UnregisterDownstream(Action downstream) + public void UnregisterDownstream(Action downstream) => Target ??= (IBindablePipe)downstream.Target!; diff --git a/Streamistry.Core/Fluent/SplitterBuilder.cs b/Streamistry.Core/Fluent/SplitterBuilder.cs index adc2cc9..2353a1e 100644 --- a/Streamistry.Core/Fluent/SplitterBuilder.cs +++ b/Streamistry.Core/Fluent/SplitterBuilder.cs @@ -7,9 +7,9 @@ namespace Streamistry.Fluent; public class SplitterBuilder : PipeElementBuilder { - protected Func? Function { get; set; } + protected Func? Function { get; set; } - public SplitterBuilder(IPipeBuilder upstream, Func? function) + public SplitterBuilder(IPipeBuilder upstream, Func? function) : base(upstream) => (Function) = (function); diff --git a/Streamistry.Core/IChainablePipe.cs b/Streamistry.Core/IChainablePipe.cs index 66d6444..09fbfde 100644 --- a/Streamistry.Core/IChainablePipe.cs +++ b/Streamistry.Core/IChainablePipe.cs @@ -7,7 +7,7 @@ namespace Streamistry; public interface IChainablePipe : IChainablePort, IChainablePipe { - void RegisterDownstream(Action downstream, Action? complete); + void RegisterDownstream(Action downstream, Action? complete); } public interface IChainablePipe : IObservablePipe @@ -18,8 +18,8 @@ public interface IChainablePipe : IObservablePipe public interface IChainablePort : IChainablePort { - void RegisterDownstream(Action action); - void UnregisterDownstream(Action downstream); + void RegisterDownstream(Action action); + void UnregisterDownstream(Action downstream); } public interface IChainablePort diff --git a/Streamistry.Core/IProcessablePipe.cs b/Streamistry.Core/IProcessablePipe.cs index 1d713db..f8a8aba 100644 --- a/Streamistry.Core/IProcessablePipe.cs +++ b/Streamistry.Core/IProcessablePipe.cs @@ -14,5 +14,5 @@ public interface IBindablePipe public interface IProcessablePipe { - void Emit(T? obj); + void Emit(T obj); } diff --git a/Streamistry.Core/Mapper.cs b/Streamistry.Core/Mapper.cs index a9a40b5..4b0b8a3 100644 --- a/Streamistry.Core/Mapper.cs +++ b/Streamistry.Core/Mapper.cs @@ -16,23 +16,23 @@ namespace Streamistry; /// The type of the elements in the output stream after the function is applied. public class Mapper : SingleRouterPipe { - public Func Function { get; init; } + public Func Function { get; init; } - protected Mapper(Func function, IChainablePort? upstream) + protected Mapper(Func function, IChainablePort? upstream) : base(upstream) { Function = function; } - public Mapper(IChainablePort upstream, Func function) + public Mapper(IChainablePort upstream, Func function) : this(function, upstream) { } - public Mapper(Func function) + public Mapper(Func function) : this(function, null) { } [Trace] - protected override TOutput? Invoke(TInput? obj) + protected override TOutput Invoke(TInput obj) => Function.Invoke(obj); } diff --git a/Streamistry.Core/OutputPort.cs b/Streamistry.Core/OutputPort.cs index b6ce8a0..14d4854 100644 --- a/Streamistry.Core/OutputPort.cs +++ b/Streamistry.Core/OutputPort.cs @@ -17,17 +17,17 @@ public class OutputPort : IChainablePort public OutputPort(IChainablePipe pipe, string name) => (Name, Pipe) = (name, pipe); - public void RegisterDownstream(Action downstream) + public void RegisterDownstream(Action downstream) => Downstream += downstream; - public void UnregisterDownstream(Action downstream) + public void UnregisterDownstream(Action downstream) => Downstream -= downstream; public void PushDownstream(T? obj) => Downstream?.Invoke(obj); - public Action[] GetDownstreamInvocations() - => Downstream?.GetInvocationList().Cast>().ToArray() ?? Array.Empty>(); + public Action[] GetDownstreamInvocations() + => Downstream?.GetInvocationList().Cast>().ToArray() ?? Array.Empty>(); } public class MainOutputPort : OutputPort diff --git a/Streamistry.Core/Pipes/Aggregators/Average.cs b/Streamistry.Core/Pipes/Aggregators/Average.cs index ce8bd1b..6022f47 100644 --- a/Streamistry.Core/Pipes/Aggregators/Average.cs +++ b/Streamistry.Core/Pipes/Aggregators/Average.cs @@ -20,8 +20,8 @@ public AverageState Append(T? value) return this; } - public readonly T? Select() - => count > T.Zero ? total / count : default; + public readonly T Select() + => count > T.Zero ? total / count : default!; } public class Average : Aggregator, T> where T : INumber diff --git a/Streamistry.Core/Pipes/Aggregators/Max.cs b/Streamistry.Core/Pipes/Aggregators/Max.cs index 58fd4d3..519114e 100644 --- a/Streamistry.Core/Pipes/Aggregators/Max.cs +++ b/Streamistry.Core/Pipes/Aggregators/Max.cs @@ -11,7 +11,7 @@ namespace Streamistry.Pipes.Aggregators; public struct MaxState() where T : INumber { private bool IsEmpty { get; set; } = true; - private T? Value { get; set; } = default; + private T Value { get; set; } = default!; public MaxState Append(T? value) { @@ -21,8 +21,8 @@ public MaxState Append(T? value) return this; } - public readonly T? Select() - => IsEmpty ? default : Value; + public readonly T Select() + => IsEmpty ? default! : Value; } public class Max : Aggregator, TInput> where TInput : INumber diff --git a/Streamistry.Core/Pipes/Aggregators/Median.cs b/Streamistry.Core/Pipes/Aggregators/Median.cs index 61eaaf3..9821f7c 100644 --- a/Streamistry.Core/Pipes/Aggregators/Median.cs +++ b/Streamistry.Core/Pipes/Aggregators/Median.cs @@ -23,10 +23,10 @@ public MedianState Append(T? value) return this; } - public T? Select() + public T Select() { if (list is null || count == 0) - return default; + return default!; if (!IsOrdered) list = list.OrderBy(x => x); diff --git a/Streamistry.Core/Pipes/Aggregators/Min.cs b/Streamistry.Core/Pipes/Aggregators/Min.cs index 8a83b8e..928979d 100644 --- a/Streamistry.Core/Pipes/Aggregators/Min.cs +++ b/Streamistry.Core/Pipes/Aggregators/Min.cs @@ -11,7 +11,7 @@ namespace Streamistry.Pipes.Aggregators; public struct MinState() where T : INumber { private bool IsEmpty { get; set; } = true; - private T? Value { get; set; } = default; + private T Value { get; set; } = default!; public MinState Append(T? value) { @@ -21,8 +21,8 @@ public MinState Append(T? value) return this; } - public readonly T? Select() - => IsEmpty ? default : Value; + public readonly T Select() + => IsEmpty ? default! : Value; } public class Min : Aggregator, TInput> where TInput : INumber diff --git a/Streamistry.Core/Pipes/Aggregators/Sum.cs b/Streamistry.Core/Pipes/Aggregators/Sum.cs index c2deec5..09d1534 100644 --- a/Streamistry.Core/Pipes/Aggregators/Sum.cs +++ b/Streamistry.Core/Pipes/Aggregators/Sum.cs @@ -16,7 +16,7 @@ public Sum(IChainablePipe upstream, Expression>>? completion = null, IChainablePipe? upstream = null) - : base(upstream, (x, y) => x is null || y is null ? default : x += y, (x) => x, seed, completion) + : base(upstream, (x, y) => x is null || y is null ? default! : x += y, (x) => x, seed, completion) { } } @@ -32,6 +32,6 @@ public Sum(IChainablePipe upstream, Expression>>? completion = null, IChainablePipe? upstream = null) : base(upstream - , (x, y) => x is null || y is null ? default : x += TOutput.CreateChecked(y), (x) => x, seed, completion) + , (x, y) => x is null || y is null ? default! : x += TOutput.CreateChecked(y), (x) => x, seed, completion) { } } diff --git a/Streamistry.Core/Pipes/Mappers/Caster.cs b/Streamistry.Core/Pipes/Mappers/Caster.cs index c184deb..8afb1a8 100644 --- a/Streamistry.Core/Pipes/Mappers/Caster.cs +++ b/Streamistry.Core/Pipes/Mappers/Caster.cs @@ -88,10 +88,10 @@ public InternalCaster(Func? implicitOperator) ImplicitOperator = implicitOperator; } - public TOutput? Cast(TInput? input) + public TOutput Cast(TInput input) { if (input is null) - return default; + return default!; if (ImplicitOperator is not null) return ImplicitOperator.Invoke(input); @@ -135,7 +135,7 @@ public SafeCaster(IChainablePort? upstream) CasterHelper.GetExplicitOperator()); } - protected override bool TryInvoke(TInput? obj, [NotNullWhen(true)] out TOutput? value) + protected override bool TryInvoke(TInput obj, [NotNullWhen(true)] out TOutput? value) => Caster.TryCast(obj, out value); private class InternalCaster @@ -149,11 +149,11 @@ public InternalCaster(Func? implicitOperator, Func : Mapper { - public Plucker(Expression> expr) + public Plucker(Expression> expr) : this(expr, null) { } - public Plucker(IChainablePort upstream, Expression> expr) + public Plucker(IChainablePort upstream, Expression> expr) : this(expr, upstream) { } - protected Plucker(Expression> expr, IChainablePort? upstream = null) + protected Plucker(Expression> expr, IChainablePort? upstream = null) : base(x => RetrieveProperty(x, expr), upstream) { } - private static TOutput? RetrieveProperty(TInput? input, Expression> lambda) + private static TOutput RetrieveProperty(TInput input, Expression> lambda) { if (input is null) - return default; + return default!; - return (TOutput?)GetNestedPropertyValue(input, lambda); + return (TOutput)GetNestedPropertyValue(input, lambda); } - public static object? GetNestedPropertyValue(object target, LambdaExpression lambda) + public static object GetNestedPropertyValue(object target, LambdaExpression lambda) { MemberExpression? expr; if (lambda.Body is not MemberExpression) @@ -57,6 +57,6 @@ protected Plucker(Expression> expr, IChainablePort : Mapper, IPreparableP public Translator(IChainablePipe upstream , IChainablePipe> dictionary) - : this(upstream, dictionary, x => default) + : this(upstream, dictionary, x => default!) { } public Translator(IChainablePipe upstream , IChainablePipe> dictionary - , Func notFound) + , Func notFound) : base(upstream, notFound) { dictionary.RegisterDownstream(EmitDictionayEntry, CompleteDictionayEntry); @@ -36,12 +36,12 @@ public void EmitDictionayEntry(KeyValuePair kv) public virtual void CompleteDictionayEntry() => Preparation?.Invoke(); - protected override TOutput? Invoke(TInput? value) + protected override TOutput Invoke(TInput value) { if (value is not null && Store.TryGetValue(value, out var output)) return output; else - return base.Invoke(value); + return base.Invoke(value!); } public void RegisterOnPrepared(Action downstream) diff --git a/Streamistry.Core/SingleRouterPipe.cs b/Streamistry.Core/SingleRouterPipe.cs index d011540..3a57709 100644 --- a/Streamistry.Core/SingleRouterPipe.cs +++ b/Streamistry.Core/SingleRouterPipe.cs @@ -17,7 +17,7 @@ protected BaseSingleRouterPipe(IChainablePort? upstream) } [Meter] - public abstract void Emit(TInput? obj); + public abstract void Emit(TInput obj); public void Bind(IChainablePort input) { @@ -38,12 +38,12 @@ protected SingleRouterPipe(IChainablePort? upstream) { } [Meter] - public override void Emit(TInput? obj) + public override void Emit(TInput obj) { var value = Invoke(obj); PushDownstream(value); } [Trace] - protected abstract TOutput? Invoke(TInput? obj); + protected abstract TOutput Invoke(TInput obj); } diff --git a/Streamistry.Core/Splitter.cs b/Streamistry.Core/Splitter.cs index cc5cf27..9be43c4 100644 --- a/Streamistry.Core/Splitter.cs +++ b/Streamistry.Core/Splitter.cs @@ -15,24 +15,24 @@ namespace Streamistry; /// The type of the elements in the output stream after the function is applied. public class Splitter : BaseSingleRouterPipe { - public Func Function { get; init; } + public Func Function { get; init; } - public Splitter(IChainablePort upstream, Func function) + public Splitter(IChainablePort upstream, Func function) : this(function, upstream) { } - public Splitter(Func function) + public Splitter(Func function) : this(function, null) { } - protected Splitter(Func function, IChainablePort? upstream = null) + protected Splitter(Func function, IChainablePort? upstream = null) : base(upstream) { Function = function; } [Meter] - public override void Emit(TInput? obj) + public override void Emit(TInput obj) { var results = Invoke(obj); if (results is null) @@ -43,6 +43,6 @@ public override void Emit(TInput? obj) } [Trace] - protected TOutput[]? Invoke(TInput? obj) + protected TOutput[]? Invoke(TInput obj) => Function.Invoke(obj); } diff --git a/Streamistry.Core/TryRouterPipe.cs b/Streamistry.Core/TryRouterPipe.cs index 418145d..05aeb78 100644 --- a/Streamistry.Core/TryRouterPipe.cs +++ b/Streamistry.Core/TryRouterPipe.cs @@ -14,7 +14,7 @@ public TryRouterPipe(IChainablePort? upstream) { } [Meter] - public override void Emit(TInput? obj) + public override void Emit(TInput obj) { if (TryInvoke(obj, out var value)) PushDownstream(value); @@ -23,5 +23,5 @@ public override void Emit(TInput? obj) } [Trace] - protected abstract bool TryInvoke(TInput? obj, [NotNullWhen(true)] out TOutput? value); + protected abstract bool TryInvoke(TInput obj, [NotNullWhen(true)] out TOutput? value); } diff --git a/Streamistry.Json/PathPlucker.cs b/Streamistry.Json/PathPlucker.cs index 192ad2c..12f6a3b 100644 --- a/Streamistry.Json/PathPlucker.cs +++ b/Streamistry.Json/PathPlucker.cs @@ -17,14 +17,14 @@ protected BaseJsonPathPlucker(string path, IChainablePipe? upstream = nul : base((x) => GetValue(x, JsonPath.Parse(path)), upstream) { } - protected static T? GetValue(TJson? node, JsonPath path) + protected static T GetValue(TJson? node, JsonPath path) { var matches = path.Evaluate(node).Matches; if (matches.Count==0) - return default; + return default!; if (matches[0].Value.TryGetValue(out var value)) return value; - return default; + return default!; } } diff --git a/Streamistry.Json/RestResponder.cs b/Streamistry.Json/RestResponder.cs index 1680cf1..58761ca 100644 --- a/Streamistry.Json/RestResponder.cs +++ b/Streamistry.Json/RestResponder.cs @@ -10,10 +10,10 @@ using Streamistry.Pipes.Parsers; namespace Streamistry.Json; -public class RestResponder : TryRouterPipe, IProcessablePipe where TOutput : JsonNode +public class RestResponder : TryRouterPipe, IProcessablePipe where TOutput : JsonNode { protected HttpClient Client { get; } - protected Func UrlBuiler { get; } + protected Func UrlBuiler { get; } public RestResponder(HttpClient client, Func urlBuilder) : this(client, urlBuilder, null) @@ -23,7 +23,7 @@ public RestResponder(IChainablePort upstream, HttpClient client, Func urlBuilder, IChainablePort? upstream = null) + protected RestResponder(HttpClient client, Func urlBuilder, IChainablePort? upstream = null) : base(upstream) { Client = client; @@ -31,7 +31,7 @@ protected RestResponder(HttpClient client, Func urlBuilder, ICh } [Meter] - protected override bool TryInvoke(TInput? obj, [NotNullWhen(true)] out TOutput? value) + protected override bool TryInvoke(TInput obj, [NotNullWhen(true)] out TOutput? value) { value = null; var url = UrlBuiler.Invoke(obj); @@ -63,7 +63,4 @@ protected virtual string ConvertHttpContentToString(HttpContent content) return reader.ReadToEnd(); } } - - - } diff --git a/Streamistry.Json/ValueMapper.cs b/Streamistry.Json/ValueMapper.cs index 0dfd88f..5fa7b51 100644 --- a/Streamistry.Json/ValueMapper.cs +++ b/Streamistry.Json/ValueMapper.cs @@ -8,8 +8,8 @@ namespace Streamistry.Json; public class ValueMapper : Mapper { - public ValueMapper(IChainablePort upstream, Func? toString = null) - : base(upstream, value => toString is null ? JsonValue.Create(value) : JsonValue.Create(toString.Invoke(value))) + public ValueMapper(IChainablePort upstream, Func? toString = null) + : base(upstream, value => toString is null ? JsonValue.Create(value)! : JsonValue.Create(toString.Invoke(value))) { } } diff --git a/Streamistry.Testing/Fluent/PipelineBuilderTests.cs b/Streamistry.Testing/Fluent/PipelineBuilderTests.cs index f11b970..62b3e7e 100644 --- a/Streamistry.Testing/Fluent/PipelineBuilderTests.cs +++ b/Streamistry.Testing/Fluent/PipelineBuilderTests.cs @@ -694,4 +694,41 @@ public void Build_WithMoreThanTwoUpstreamsUnion_Success() Assert.That(union.GetOutputs(pipeline.Start), Has.Length.EqualTo(2)); } + + [Test] + public void Build_WithNullAndNotNull_Success() + { + var isNull = new Segment(x => x.IsNull().Constant(new Animal("Dragon"))); + var isNotNull = new Segment(x => x.IsNotNull().Cast().Safe().Map(y => { y!.Eat = "Meat"; return y; }).Cast()); + + var pipeline = new PipelineBuilder() + .Source([new Animal("Bird"), null, new Carnivore("Dog"), new Frugivore("Parrot")]) + .Branch(isNull, isNotNull) + .Union().Checkpoint(out var union) + .Build(); + + var outputs = union.GetOutputs(pipeline.Start); + Assert.That(outputs, Has.Length.EqualTo(2)); + Assert.That(outputs.Select(x => x!.Name), Does.Contain("Dragon")); + Assert.That(outputs.Select(x => x!.Name), Does.Contain("Dog")); + } + + [Test] + public void Build_WithNullAndNotNullValueType_Success() + { + var isNull = new Segment(x => x.IsNull().Constant(0)); + var isNotNull = new Segment(x => x.IsNotNull()); + + var pipeline = new PipelineBuilder() + .Source(new int?[] {1, null, 3}) + .Branch(isNull, isNotNull) + .Union().Checkpoint(out var union) + .Build(); + + var outputs = union.GetOutputs(pipeline.Start); + Assert.That(outputs, Has.Length.EqualTo(3)); + Assert.That(outputs, Does.Contain(1)); + Assert.That(outputs, Does.Contain(0)); + Assert.That(outputs, Does.Contain(3)); + } } diff --git a/Streamistry.Testing/SplitterTests.cs b/Streamistry.Testing/SplitterTests.cs index a88d5fb..c2d5084 100644 --- a/Streamistry.Testing/SplitterTests.cs +++ b/Streamistry.Testing/SplitterTests.cs @@ -9,10 +9,12 @@ namespace Streamistry.Testing; public class SplitterTests { + private static readonly string[] EmptyArray = Array.Empty(); + [Test] public void Emit_Splitter_Successful() { - var splitter = new Splitter(x => x?.Split(';') ?? null); + var splitter = new Splitter(x => x?.Split(';') ?? EmptyArray); Assert.Multiple(() => { Assert.That(splitter.EmitAndGetOutput("foo;bar;quark"), Is.EqualTo("quark")); @@ -23,7 +25,7 @@ public void Emit_Splitter_Successful() [Test] public void Emit_NullSplitter_Successful() { - var splitter = new Splitter(x => x?.Split(';') ?? null); + var splitter = new Splitter(x => x?.Split(';') ?? EmptyArray); Assert.Multiple(() => { Assert.That(splitter.EmitAndGetOutput(string.Empty), Is.EqualTo(string.Empty)); diff --git a/appveyor.yml b/appveyor.yml index c010cd2..258f85e 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -52,9 +52,9 @@ build_script: test_script: - pwsh: | $ErrorActionPreference = "Stop" - dotnet test Streamistry.Testing -c Release /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:Threshold=10 /p:ThresholdType=line /p:CoverletOutput=../.coverage/coverage.Streamistry.xml --test-adapter-path:. --logger:Appveyor --no-build --nologo + dotnet test Streamistry.Testing -c Release /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:Include="[Streamistry]*" /p:Threshold=10 /p:ThresholdType=line /p:CoverletOutput=../.coverage/coverage.Streamistry.xml --test-adapter-path:. --logger:Appveyor --no-build --nologo $globalTestResult = $LastExitCode - dotnet test Streamistry.Json.Testing -c Release /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:Threshold=10 /p:ThresholdType=line /p:CoverletOutput=../.coverage/coverage.Streamistry.Json.xml --test-adapter-path:. --logger:Appveyor --no-build --nologo + dotnet test Streamistry.Json.Testing -c Release /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:Include="[Streamistry.Json]*" /p:Threshold=10 /p:ThresholdType=line /p:CoverletOutput=../.coverage/coverage.Streamistry.Json.xml --test-adapter-path:. --logger:Appveyor --no-build --nologo $globalTestResult = $LastExitCode if($globalTestResult -ne 0) { $host.SetShouldExit($globalTestResult) }