diff --git a/Streamistry.Core/Fluent/AggregatorBuilder.cs b/Streamistry.Core/Fluent/AggregatorBuilder.cs index 28dbe20..dfb2809 100644 --- a/Streamistry.Core/Fluent/AggregatorBuilder.cs +++ b/Streamistry.Core/Fluent/AggregatorBuilder.cs @@ -15,17 +15,17 @@ public AggregatorBuilder(IPipeBuilder upstream) => Upstream = upstream; public SpecializedAggregatorBuilder AsMax() - => new SpecializedAggregatorBuilder(Upstream, typeof(Max<>), [typeof(TInput)]); + => new (Upstream, typeof(Max<>), [typeof(TInput)]); public SpecializedAggregatorBuilder AsMin() - => new SpecializedAggregatorBuilder(Upstream, typeof(Min<>), [typeof(TInput)]); + => new (Upstream, typeof(Min<>), [typeof(TInput)]); public SpecializedAggregatorBuilder AsAverage() - => new SpecializedAggregatorBuilder(Upstream, typeof(Average<,>), [typeof(TInput), typeof(TOutput)]); + => new (Upstream, typeof(Average<,>), [typeof(TInput), typeof(TOutput)]); public SpecializedAggregatorBuilder AsMedian() - => new SpecializedAggregatorBuilder(Upstream, typeof(Median<,>), [typeof(TInput), typeof(TOutput)]); + => new (Upstream, typeof(Median<,>), [typeof(TInput), typeof(TOutput)]); public SpecializedAggregatorBuilder AsSum() - => new SpecializedAggregatorBuilder(Upstream, typeof(Sum<,>), [typeof(TInput), typeof(TOutput)]); + => new (Upstream, typeof(Sum<,>), [typeof(TInput), typeof(TOutput)]); public SpecializedAggregatorBuilder AsCount() - => new SpecializedAggregatorBuilder(Upstream, typeof(Count<,>), [typeof(TInput), typeof(TOutput)]); + => new (Upstream, typeof(Count<,>), [typeof(TInput), typeof(TOutput)]); } public class SpecializedAggregatorBuilder : PipeElementBuilder diff --git a/Streamistry.Core/Fluent/ParserBuilder.cs b/Streamistry.Core/Fluent/ParserBuilder.cs index 2a90321..13952d2 100644 --- a/Streamistry.Core/Fluent/ParserBuilder.cs +++ b/Streamistry.Core/Fluent/ParserBuilder.cs @@ -4,6 +4,7 @@ using System.Reflection.PortableExecutable; using System.Text; using System.Threading.Tasks; +using System.Reflection; using Streamistry.Pipes.Parsers; namespace Streamistry.Fluent; @@ -33,8 +34,8 @@ IDualRoute IBuilder.OnBuildPipeElement() public class ParserBuilder { - protected IPipeBuilder Upstream { get; } - protected IFormatProvider? FormatProvider { get; set; } + public IPipeBuilder Upstream { get; } + public IFormatProvider? FormatProvider { get; protected set; } public ParserBuilder(IPipeBuilder upstream) => Upstream = upstream; @@ -61,10 +62,22 @@ public SpecializedParserBuilder(IPipeBuilder upstream, Type type, IForma => (Type, FormatProvider) = (type, formatProvider); public override IChainablePort OnBuildPipeElement() - => (IChainablePort)Activator.CreateInstance( + => IsFormatProviderEnabled(Type) + ? (IChainablePort)Activator.CreateInstance( + Type + , Upstream.BuildPipeElement() + , FormatProvider)! + : (IChainablePort)Activator.CreateInstance( Type , Upstream.BuildPipeElement() - , FormatProvider)!; + )!; + + private bool IsFormatProviderEnabled(Type type) + { + var ctors = type.GetConstructors(BindingFlags.Public | BindingFlags.Instance) + .Where(x => x.GetParameters().Length <= 2); + return ctors.Any(x => x.GetParameters().Length == 2); + } IDualRoute IBuilder.BuildPipeElement() => base.BuildPipeElement().Pipe is IDualRoute dual ? dual : throw new InvalidCastException(); diff --git a/Streamistry.Json.Testing/Fluent/ArrayParserTests.cs b/Streamistry.Json.Testing/Fluent/ArrayParserTests.cs new file mode 100644 index 0000000..a86cc3d --- /dev/null +++ b/Streamistry.Json.Testing/Fluent/ArrayParserTests.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Nodes; +using System.Threading.Tasks; +using NUnit.Framework; +using Streamistry.Fluent; +using Streamistry.Testability; +using Streamistry.Json.Fluent; + +namespace Streamistry.Json.Testing.Fluent; +public class ArrayParserTests +{ + [Test] + public void ParseAsJsonArray_ValidEntry_Successful() + { + var pipeline = new PipelineBuilder() + .Source([ + $"[{JsonTests.JsonFirst}, {JsonTests.JsonSecond}, {JsonTests.JsonThird}]", + $"[{JsonTests.JsonFirst}, {JsonTests.JsonThird}]", + $"[{JsonTests.JsonThird}]" + ]) + .Parse() + .AsJsonArray() + .Checkpoint(out var parser) + .Build(); + + var outputs = parser.GetOutputs(pipeline.Start); + Assert.That(outputs, Has.Length.EqualTo(3)); + Assert.That(outputs, Has.All.Not.Null); + Assert.That(outputs, Has.All.TypeOf()); + } +} diff --git a/Streamistry.Json.Testing/Fluent/ArrayPathPluckerTests.cs b/Streamistry.Json.Testing/Fluent/ArrayPathPluckerTests.cs new file mode 100644 index 0000000..b942226 --- /dev/null +++ b/Streamistry.Json.Testing/Fluent/ArrayPathPluckerTests.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Nodes; +using System.Threading.Tasks; +using NUnit.Framework; +using Streamistry.Fluent; +using Streamistry.Testability; +using Streamistry.Json.Fluent; + +namespace Streamistry.Json.Testing.Fluent; +public class ArrayPathPluckerTests +{ + [Test] + public void Pluck_ValidSinglePath_ExistingValue() + { + var pipeline = new PipelineBuilder() + .Source([ + (JsonArray)JsonNode.Parse( + $"[{JsonTests.JsonFirst}, {JsonTests.JsonSecond}, {JsonTests.JsonThird}]" + )! + ]) + .Pluck("$[1].user.contact.email") + .Checkpoint(out var pluck) + .Build(); + + var outputs = pluck.GetOutputs(pipeline.Start); + Assert.That(outputs, Has.Length.EqualTo(1)); + Assert.That(outputs[0], Does.Contain("nikola.tesla@blueorigin.com")); + } +} diff --git a/Streamistry.Json.Testing/Fluent/ArraySplitterTests.cs b/Streamistry.Json.Testing/Fluent/ArraySplitterTests.cs new file mode 100644 index 0000000..48ed852 --- /dev/null +++ b/Streamistry.Json.Testing/Fluent/ArraySplitterTests.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Nodes; +using System.Threading.Tasks; +using NUnit.Framework; +using Streamistry.Fluent; +using Streamistry.Testability; +using Streamistry.Json.Fluent; + +namespace Streamistry.Json.Testing.Fluent; +public class ArraySplitterTests +{ + [Test] + public void Split_ValidEntry_Successful() + { + var pipeline = new PipelineBuilder() + .Source([ + $"[{JsonTests.JsonFirst}, {JsonTests.JsonSecond}, {JsonTests.JsonThird}]", + $"[{JsonTests.JsonFirst}, {JsonTests.JsonThird}]", + ]) + .Parse() + .AsJsonArray() + .Split() + .Checkpoint(out var splitter) + .Build(); + + var outputs = splitter.GetOutputs(pipeline.Start); + Assert.That(outputs, Has.Length.EqualTo(5)); + Assert.That(outputs, Has.All.Not.Null); + Assert.That(outputs, Has.All.TypeOf()); + } +} diff --git a/Streamistry.Json.Testing/Fluent/ObjectParserTests.cs b/Streamistry.Json.Testing/Fluent/ObjectParserTests.cs new file mode 100644 index 0000000..e224853 --- /dev/null +++ b/Streamistry.Json.Testing/Fluent/ObjectParserTests.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Nodes; +using System.Threading.Tasks; +using NUnit.Framework; +using Streamistry.Fluent; +using Streamistry.Testability; +using Streamistry.Json.Fluent; + +namespace Streamistry.Json.Testing.Fluent; +public class ObjectParserTests +{ + [Test] + public void ParseAsJsonObject_ValidEntries_Successful() + { + var pipeline = new PipelineBuilder() + .Source([JsonTests.JsonFirst, JsonTests.JsonSecond, JsonTests.JsonThird]) + .Parse() + .AsJsonObject() + .Checkpoint(out var parser) + .Build(); + + var outputs = parser.GetOutputs(pipeline.Start); + Assert.That(outputs, Has.Length.EqualTo(3)); + Assert.That(outputs, Has.All.Not.Null); + Assert.That(outputs, Has.All.TypeOf()); + } +} diff --git a/Streamistry.Json.Testing/Fluent/PathPluckerTests.cs b/Streamistry.Json.Testing/Fluent/PathPluckerTests.cs new file mode 100644 index 0000000..3c6aef4 --- /dev/null +++ b/Streamistry.Json.Testing/Fluent/PathPluckerTests.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Nodes; +using System.Threading.Tasks; +using NUnit.Framework; +using Streamistry.Fluent; +using Streamistry.Testability; +using Streamistry.Json.Fluent; + +namespace Streamistry.Json.Testing.Fluent; +public class PathPluckerTests +{ + [Test] + [TestCase(JsonTests.JsonFirst, "albert.einstein@gmail.com")] + [TestCase(JsonTests.JsonThird, null)] + public void Pluck_ValidPath_ExistingValue(string jsonString, string? email) + { + var pipeline = new PipelineBuilder() + .Source([(JsonObject)JsonNode.Parse(jsonString)!]) + .Pluck("$.user.contact.email") + .Checkpoint(out var pluck) + .Build(); + + var outputs = pluck.GetOutputs(pipeline.Start); + Assert.That(outputs, Has.Length.EqualTo(1)); + Assert.That(outputs, Does.Contain(email)); + } + + [Test] + [TestCase(JsonTests.JsonThird, null)] + public void Pluck_NonExistingPath_Null(string jsonString, string? email) + { + var pipeline = new PipelineBuilder() + .Source([(JsonObject)JsonNode.Parse(jsonString)!]) + .Pluck("$.user.contact.email") + .Checkpoint(out var pluck) + .Build(); + + var outputs = pluck.GetOutputs(pipeline.Start); + Assert.That(outputs, Has.Length.EqualTo(1)); + Assert.That(outputs, Does.Contain(email)); + } +} diff --git a/Streamistry.Json.Testing/Fluent/ValueMapperTests.cs b/Streamistry.Json.Testing/Fluent/ValueMapperTests.cs new file mode 100644 index 0000000..968f729 --- /dev/null +++ b/Streamistry.Json.Testing/Fluent/ValueMapperTests.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Nodes; +using System.Threading.Tasks; +using NUnit.Framework; +using Streamistry.Fluent; +using Streamistry.Testability; +using Streamistry.Json.Fluent; +using System.Globalization; + +namespace Streamistry.Json.Testing.Fluent; +public class ValueMapperTests +{ + [Test] + public void AsJsonValue_ValidEntry_Successful() + { + var pipeline = new PipelineBuilder() + .Source([new DateOnly(1879, 3, 14), new DateOnly(1856, 7, 10), new DateOnly(1903, 12, 28)]) + .AsJsonValue() + .Checkpoint(out var mapper) + .Build(); + + var outputs = mapper.GetOutputs(pipeline.Start); + Assert.Multiple(() => { + Assert.That(outputs, Has.Length.EqualTo(3)); + Assert.That(outputs, Has.All.Not.Null); + Assert.That(outputs, Has.All.AssignableTo()); + }); + Assert.That(outputs.Select(x => x!.ToJsonString()), Has.One.EqualTo("\"1879-03-14\"")); + } + + [Test] + public void AsJsonValue_ValidEntryWithCustomFunction_Successful() + { + var pipeline = new PipelineBuilder() + .Source([new DateOnly(1879, 3, 14), new DateOnly(1856, 7, 10), new DateOnly(1903, 12, 28)]) + .AsJsonValue(x => $"{x.ToString("MMMM", CultureInfo.InvariantCulture)} {x.Year}") + .Checkpoint(out var mapper) + .Build(); + + var outputs = mapper.GetOutputs(pipeline.Start); + Assert.Multiple(() => { + Assert.That(outputs, Has.Length.EqualTo(3)); + Assert.That(outputs, Has.All.Not.Null); + Assert.That(outputs, Has.All.AssignableTo()); + }); + Assert.That(outputs.Select(x => x!.ToJsonString()), Has.One.EqualTo("\"March 1879\"")); + } +} diff --git a/Streamistry.Json/ArraySplitter.cs b/Streamistry.Json/ArraySplitter.cs index 9da7ab6..4dcc354 100644 --- a/Streamistry.Json/ArraySplitter.cs +++ b/Streamistry.Json/ArraySplitter.cs @@ -9,7 +9,7 @@ namespace Streamistry.Json; internal class ArraySplitter : Splitter { - public ArraySplitter(IChainablePipe upstream) + public ArraySplitter(IChainablePort upstream) : base(upstream, x => [..Split(x)]) { } diff --git a/Streamistry.Json/Fluent/ArrayPathPluckerBuilder.cs b/Streamistry.Json/Fluent/ArrayPathPluckerBuilder.cs new file mode 100644 index 0000000..4da452c --- /dev/null +++ b/Streamistry.Json/Fluent/ArrayPathPluckerBuilder.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Nodes; +using System.Threading.Tasks; +using Streamistry.Fluent; + +namespace Streamistry.Json.Fluent; +public class ArrayPathPluckerBuilder : PipeElementBuilder +{ + protected string Path { get; set; } + + public ArrayPathPluckerBuilder(IPipeBuilder upstream, string path) + : base(upstream) + => (Path) = (path); + + public override IChainablePort OnBuildPipeElement() + => new PathArrayPlucker( + Upstream.BuildPipeElement() + , Path ?? throw new InvalidOperationException() + ); +} diff --git a/Streamistry.Json/Fluent/ArraySplitterBuilder.cs b/Streamistry.Json/Fluent/ArraySplitterBuilder.cs new file mode 100644 index 0000000..d6e9a52 --- /dev/null +++ b/Streamistry.Json/Fluent/ArraySplitterBuilder.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.Json.Nodes; +using System.Threading.Tasks; +using Streamistry.Fluent; + +namespace Streamistry.Json.Fluent; +public class ArraySplitterBuilder : PipeElementBuilder +{ + public ArraySplitterBuilder(IPipeBuilder upstream) + : base(upstream) + { } + + public override IChainablePort OnBuildPipeElement() + => new ArraySplitter( + Upstream.BuildPipeElement() + ); +} diff --git a/Streamistry.Json/Fluent/FluentJsonExtensions.cs b/Streamistry.Json/Fluent/FluentJsonExtensions.cs new file mode 100644 index 0000000..43383c5 --- /dev/null +++ b/Streamistry.Json/Fluent/FluentJsonExtensions.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Nodes; +using System.Threading.Tasks; +using Streamistry.Fluent; + +namespace Streamistry.Json.Fluent; + +public static class BasePipeBuilderExtension +{ + public static ValueMapperBuilder AsJsonValue(this BasePipeBuilder builder, Func? toString = null) + => new(builder, toString); + + public static PathPluckerBuilder Pluck(this BasePipeBuilder builder, string path) + => new(builder, path); + + public static ArrayPathPluckerBuilder Pluck(this BasePipeBuilder builder, string path) + => new(builder, path); + + public static ArraySplitterBuilder Split(this BasePipeBuilder builder) + => new(builder); +} + +public static class ParserBuilderExtension +{ + public static SpecializedParserBuilder AsJsonObject(this ParserBuilder builder) + => new(builder.Upstream, typeof(ObjectParser), null); + + public static SpecializedParserBuilder AsJsonArray(this ParserBuilder builder) + => new(builder.Upstream, typeof(ArrayParser), null); +} diff --git a/Streamistry.Json/Fluent/PathPluckerBuilder.cs b/Streamistry.Json/Fluent/PathPluckerBuilder.cs new file mode 100644 index 0000000..2c9cbca --- /dev/null +++ b/Streamistry.Json/Fluent/PathPluckerBuilder.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using System.Text.Json.Nodes; +using System.Threading.Tasks; +using Streamistry.Fluent; + +namespace Streamistry.Json.Fluent; +public class PathPluckerBuilder : PipeElementBuilder +{ + protected string Path { get; set; } + + public PathPluckerBuilder(IPipeBuilder upstream, string path) + : base(upstream) + => (Path) = (path); + + public override IChainablePort OnBuildPipeElement() + => new PathPlucker( + Upstream.BuildPipeElement() + , Path ?? throw new InvalidOperationException() + ); +} diff --git a/Streamistry.Json/Fluent/ValueMapperBuilder.cs b/Streamistry.Json/Fluent/ValueMapperBuilder.cs new file mode 100644 index 0000000..d5ec691 --- /dev/null +++ b/Streamistry.Json/Fluent/ValueMapperBuilder.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Nodes; +using System.Threading.Tasks; +using Streamistry.Fluent; + +namespace Streamistry.Json.Fluent; +public class ValueMapperBuilder : MapperBuilder +{ + public ValueMapperBuilder(IPipeBuilder upstream, Func? toString) + : base(upstream, value => toString is null ? JsonValue.Create(value)! : JsonValue.Create(toString.Invoke(value))) + { } +} diff --git a/Streamistry.Json/ObjectParser.cs b/Streamistry.Json/ObjectParser.cs index c00dfe0..5c6e985 100644 --- a/Streamistry.Json/ObjectParser.cs +++ b/Streamistry.Json/ObjectParser.cs @@ -14,7 +14,6 @@ public ObjectParser(IChainablePipe upstream) : base(upstream, new ParserDelegate(TryParse)) { } - private static bool TryParse(string? text, [NotNullWhen(true)] out JsonObject? value) { value = null; diff --git a/Streamistry.Json/ObjectPropertyAppender.cs b/Streamistry.Json/ObjectPropertyAppender.cs index babafad..9e68ae9 100644 --- a/Streamistry.Json/ObjectPropertyAppender.cs +++ b/Streamistry.Json/ObjectPropertyAppender.cs @@ -53,7 +53,7 @@ private static void AddOrReplaceProperty(JsonNode root, string jsonPath, string private static void CreatePathAndAddProperty(JsonNode root, string jsonPath, string propertyName, JsonNode value) { var segments = jsonPath.TrimStart('$').Split('.', StringSplitOptions.RemoveEmptyEntries); - JsonNode current = root; + var current = root; foreach (var segment in segments) { diff --git a/Streamistry.Json/PathPlucker.cs b/Streamistry.Json/PathPlucker.cs index 12f6a3b..3aa4b2b 100644 --- a/Streamistry.Json/PathPlucker.cs +++ b/Streamistry.Json/PathPlucker.cs @@ -9,11 +9,11 @@ namespace Streamistry.Json; public abstract class BaseJsonPathPlucker : Mapper where TJson : JsonNode { - public BaseJsonPathPlucker(IChainablePipe upstream, string path) + public BaseJsonPathPlucker(IChainablePort upstream, string path) : this(path, upstream) { } - protected BaseJsonPathPlucker(string path, IChainablePipe? upstream = null) + protected BaseJsonPathPlucker(string path, IChainablePort? upstream = null) : base((x) => GetValue(x, JsonPath.Parse(path)), upstream) { } @@ -34,18 +34,18 @@ public PathPlucker(string path) : this(path, null) { } - public PathPlucker(IChainablePipe upstream, string path) + public PathPlucker(IChainablePort upstream, string path) : this(path, upstream) { } - public PathPlucker(string path, IChainablePipe? upstream = null) + public PathPlucker(string path, IChainablePort? upstream = null) : base(path, upstream) { } } -public class JsonPathArrayPlucker : BaseJsonPathPlucker +public class PathArrayPlucker : BaseJsonPathPlucker { - public JsonPathArrayPlucker(IChainablePipe upstream, string path) + public PathArrayPlucker(IChainablePort upstream, string path) : base(upstream, path) { } } diff --git a/Streamistry.Testing/SplitterTests.cs b/Streamistry.Testing/SplitterTests.cs index c2d5084..86e1b88 100644 --- a/Streamistry.Testing/SplitterTests.cs +++ b/Streamistry.Testing/SplitterTests.cs @@ -9,7 +9,7 @@ namespace Streamistry.Testing; public class SplitterTests { - private static readonly string[] EmptyArray = Array.Empty(); + private static string[] EmptyArray { get; } = []; [Test] public void Emit_Splitter_Successful() @@ -18,7 +18,9 @@ public void Emit_Splitter_Successful() Assert.Multiple(() => { Assert.That(splitter.EmitAndGetOutput("foo;bar;quark"), Is.EqualTo("quark")); - Assert.That(splitter.EmitAndGetManyOutputs("foo;bar;quark"), Is.EqualTo(new string[] { "foo", "bar", "quark" })); + + var expected = new string[] { "foo", "bar", "quark" }; + Assert.That(splitter.EmitAndGetManyOutputs("foo;bar;quark"), Is.EqualTo(expected)); }); }