Skip to content

Commit

Permalink
feat: fluent API includes json-oriented features (#110)
Browse files Browse the repository at this point in the history
  • Loading branch information
Seddryck authored Sep 29, 2024
1 parent 2654dd0 commit d42c8bf
Show file tree
Hide file tree
Showing 18 changed files with 378 additions and 21 deletions.
12 changes: 6 additions & 6 deletions Streamistry.Core/Fluent/AggregatorBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,17 @@ public AggregatorBuilder(IPipeBuilder<TInput> upstream)
=> Upstream = upstream;

public SpecializedAggregatorBuilder<TInput, TAccumulate, TOutput> AsMax()
=> new SpecializedAggregatorBuilder<TInput, TAccumulate, TOutput>(Upstream, typeof(Max<>), [typeof(TInput)]);
=> new (Upstream, typeof(Max<>), [typeof(TInput)]);
public SpecializedAggregatorBuilder<TInput, TAccumulate, TOutput> AsMin()
=> new SpecializedAggregatorBuilder<TInput, TAccumulate, TOutput>(Upstream, typeof(Min<>), [typeof(TInput)]);
=> new (Upstream, typeof(Min<>), [typeof(TInput)]);
public SpecializedAggregatorBuilder<TInput, TAccumulate, TOutput> AsAverage()
=> new SpecializedAggregatorBuilder<TInput, TAccumulate, TOutput>(Upstream, typeof(Average<,>), [typeof(TInput), typeof(TOutput)]);
=> new (Upstream, typeof(Average<,>), [typeof(TInput), typeof(TOutput)]);
public SpecializedAggregatorBuilder<TInput, TAccumulate, TOutput> AsMedian()
=> new SpecializedAggregatorBuilder<TInput, TAccumulate, TOutput>(Upstream, typeof(Median<,>), [typeof(TInput), typeof(TOutput)]);
=> new (Upstream, typeof(Median<,>), [typeof(TInput), typeof(TOutput)]);
public SpecializedAggregatorBuilder<TInput, TAccumulate, TOutput> AsSum()
=> new SpecializedAggregatorBuilder<TInput, TAccumulate, TOutput>(Upstream, typeof(Sum<,>), [typeof(TInput), typeof(TOutput)]);
=> new (Upstream, typeof(Sum<,>), [typeof(TInput), typeof(TOutput)]);
public SpecializedAggregatorBuilder<TInput, TAccumulate, TOutput> AsCount()
=> new SpecializedAggregatorBuilder<TInput, TAccumulate, TOutput>(Upstream, typeof(Count<,>), [typeof(TInput), typeof(TOutput)]);
=> new (Upstream, typeof(Count<,>), [typeof(TInput), typeof(TOutput)]);
}

public class SpecializedAggregatorBuilder<TInput, TAccumulate, TOutput> : PipeElementBuilder<TInput, TOutput>
Expand Down
21 changes: 17 additions & 4 deletions Streamistry.Core/Fluent/ParserBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -33,8 +34,8 @@ IDualRoute IBuilder<IDualRoute>.OnBuildPipeElement()

public class ParserBuilder<TInput>
{
protected IPipeBuilder<TInput> Upstream { get; }
protected IFormatProvider? FormatProvider { get; set; }
public IPipeBuilder<TInput> Upstream { get; }
public IFormatProvider? FormatProvider { get; protected set; }

public ParserBuilder(IPipeBuilder<TInput> upstream)
=> Upstream = upstream;
Expand All @@ -61,10 +62,22 @@ public SpecializedParserBuilder(IPipeBuilder<TInput> upstream, Type type, IForma
=> (Type, FormatProvider) = (type, formatProvider);

public override IChainablePort<TOutput> OnBuildPipeElement()
=> (IChainablePort<TOutput>)Activator.CreateInstance(
=> IsFormatProviderEnabled(Type)
? (IChainablePort<TOutput>)Activator.CreateInstance(
Type
, Upstream.BuildPipeElement()
, FormatProvider)!
: (IChainablePort<TOutput>)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<IDualRoute>.BuildPipeElement()
=> base.BuildPipeElement().Pipe is IDualRoute dual ? dual : throw new InvalidCastException();
Expand Down
34 changes: 34 additions & 0 deletions Streamistry.Json.Testing/Fluent/ArrayParserTests.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.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<JsonArray>());
}
}
32 changes: 32 additions & 0 deletions Streamistry.Json.Testing/Fluent/ArrayPathPluckerTests.cs
Original file line number Diff line number Diff line change
@@ -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<string>("$[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("[email protected]"));
}
}
34 changes: 34 additions & 0 deletions Streamistry.Json.Testing/Fluent/ArraySplitterTests.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.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<JsonObject>());
}
}
30 changes: 30 additions & 0 deletions Streamistry.Json.Testing/Fluent/ObjectParserTests.cs
Original file line number Diff line number Diff line change
@@ -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<JsonObject>());
}
}
45 changes: 45 additions & 0 deletions Streamistry.Json.Testing/Fluent/PathPluckerTests.cs
Original file line number Diff line number Diff line change
@@ -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, "[email protected]")]
[TestCase(JsonTests.JsonThird, null)]
public void Pluck_ValidPath_ExistingValue(string jsonString, string? email)
{
var pipeline = new PipelineBuilder()
.Source([(JsonObject)JsonNode.Parse(jsonString)!])
.Pluck<string>("$.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<string>("$.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));
}
}
51 changes: 51 additions & 0 deletions Streamistry.Json.Testing/Fluent/ValueMapperTests.cs
Original file line number Diff line number Diff line change
@@ -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<JsonValue>());
});
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<JsonValue>());
});
Assert.That(outputs.Select(x => x!.ToJsonString()), Has.One.EqualTo("\"March 1879\""));
}
}
2 changes: 1 addition & 1 deletion Streamistry.Json/ArraySplitter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
namespace Streamistry.Json;
internal class ArraySplitter : Splitter<JsonArray, JsonObject>
{
public ArraySplitter(IChainablePipe<JsonArray> upstream)
public ArraySplitter(IChainablePort<JsonArray> upstream)
: base(upstream, x => [..Split(x)])
{ }

Expand Down
23 changes: 23 additions & 0 deletions Streamistry.Json/Fluent/ArrayPathPluckerBuilder.cs
Original file line number Diff line number Diff line change
@@ -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<TOutput> : PipeElementBuilder<JsonArray, TOutput>
{
protected string Path { get; set; }

public ArrayPathPluckerBuilder(IPipeBuilder<JsonArray> upstream, string path)
: base(upstream)
=> (Path) = (path);

public override IChainablePort<TOutput> OnBuildPipeElement()
=> new PathArrayPlucker<TOutput>(
Upstream.BuildPipeElement()
, Path ?? throw new InvalidOperationException()
);
}
21 changes: 21 additions & 0 deletions Streamistry.Json/Fluent/ArraySplitterBuilder.cs
Original file line number Diff line number Diff line change
@@ -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<JsonArray, JsonObject>
{
public ArraySplitterBuilder(IPipeBuilder<JsonArray> upstream)
: base(upstream)
{ }

public override IChainablePort<JsonObject> OnBuildPipeElement()
=> new ArraySplitter(
Upstream.BuildPipeElement()
);
}
33 changes: 33 additions & 0 deletions Streamistry.Json/Fluent/FluentJsonExtensions.cs
Original file line number Diff line number Diff line change
@@ -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<TInput> AsJsonValue<TInput>(this BasePipeBuilder<TInput> builder, Func<TInput, string>? toString = null)
=> new(builder, toString);

public static PathPluckerBuilder<TOutput> Pluck<TOutput>(this BasePipeBuilder<JsonObject> builder, string path)
=> new(builder, path);

public static ArrayPathPluckerBuilder<TOutput> Pluck<TOutput>(this BasePipeBuilder<JsonArray> builder, string path)
=> new(builder, path);

public static ArraySplitterBuilder Split(this BasePipeBuilder<JsonArray> builder)
=> new(builder);
}

public static class ParserBuilderExtension
{
public static SpecializedParserBuilder<TInput, JsonObject> AsJsonObject<TInput>(this ParserBuilder<TInput> builder)
=> new(builder.Upstream, typeof(ObjectParser), null);

public static SpecializedParserBuilder<TInput, JsonArray> AsJsonArray<TInput>(this ParserBuilder<TInput> builder)
=> new(builder.Upstream, typeof(ArrayParser), null);
}
Loading

0 comments on commit d42c8bf

Please sign in to comment.