diff --git a/Streamistry.Core/Pipes/Mappers/JsonPathPlucker.cs b/Streamistry.Core/Pipes/Mappers/JsonPathPlucker.cs new file mode 100644 index 0000000..793656b --- /dev/null +++ b/Streamistry.Core/Pipes/Mappers/JsonPathPlucker.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Text; +using System.Text.Json.Nodes; +using System.Threading.Tasks; +using Json.Path; + +namespace Streamistry.Pipes.Mappers; +public abstract class BaseJsonPathPlucker : Mapper where TJson : JsonNode +{ + public BaseJsonPathPlucker(IChainablePipe upstream, string path) + : base(upstream, (x) => GetValue(x, JsonPath.Parse(path))) + { } + + protected static T? GetValue(TJson? node, JsonPath path) + { + var matches = path.Evaluate(node).Matches; + if (matches.Count==0) + return default; + if (matches[0].Value.TryGetValue(out var value)) + return value; + return default; + } +} + +public class JsonPathPlucker : BaseJsonPathPlucker +{ + public JsonPathPlucker(IChainablePipe upstream, string path) + : base(upstream, path) + { } +} + +public class JsonPathArrayPlucker : BaseJsonPathPlucker +{ + public JsonPathArrayPlucker(IChainablePipe upstream, string path) + : base(upstream, path) + { } +} diff --git a/Streamistry.Core/Pipes/Parsers/JsonArrayParser.cs b/Streamistry.Core/Pipes/Parsers/JsonArrayParser.cs new file mode 100644 index 0000000..10550f4 --- /dev/null +++ b/Streamistry.Core/Pipes/Parsers/JsonArrayParser.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Nodes; +using System.Threading.Tasks; + +namespace Streamistry.Pipes.Parsers; +public class JsonArrayParser : StringParser +{ + public JsonArrayParser(IChainablePipe upstream) + : base(upstream, TryParse) + { } + + private static bool TryParse(string? text, out JsonArray? value) + { + value = null; + if (text == null) + return true; + + try + { + value = (JsonArray)JsonNode.Parse(text)!; + return true; + } + catch + { return false; } + } +} diff --git a/Streamistry.Core/Pipes/Parsers/JsonObjectParser.cs b/Streamistry.Core/Pipes/Parsers/JsonObjectParser.cs new file mode 100644 index 0000000..a1e0dde --- /dev/null +++ b/Streamistry.Core/Pipes/Parsers/JsonObjectParser.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Threading.Tasks; + +namespace Streamistry.Pipes.Parsers; +public class JsonObjectParser : StringParser +{ + public JsonObjectParser(IChainablePipe upstream) + : base(upstream, new ParserDelegate(TryParse)) + { } + + private static bool TryParse(string? text, out JsonObject? value) + { + value = null; + if (text == null) + return true; + try + { + value = (JsonObject)JsonNode.Parse(text)!; + return true; + } + catch + { return false; } + } +} diff --git a/Streamistry.Core/Pipes/Splitters/JsonArraySplitter.cs b/Streamistry.Core/Pipes/Splitters/JsonArraySplitter.cs new file mode 100644 index 0000000..69b8149 --- /dev/null +++ b/Streamistry.Core/Pipes/Splitters/JsonArraySplitter.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection.Metadata.Ecma335; +using System.Text; +using System.Text.Json.Nodes; +using System.Threading.Tasks; + +namespace Streamistry.Pipes.Splitters; +internal class JsonArraySplitter : Splitter +{ + public JsonArraySplitter(IChainablePipe upstream) + : base(upstream, x => [..Split(x)]) + { } + + private static IEnumerable? Split(JsonArray? array) + { + if (array is null) + yield return null; + else + foreach (var item in array!) + yield return (JsonObject?)item; + } +} diff --git a/Streamistry.Core/Streamistry.csproj b/Streamistry.Core/Streamistry.csproj index 51dcd55..3b160ae 100644 --- a/Streamistry.Core/Streamistry.csproj +++ b/Streamistry.Core/Streamistry.csproj @@ -13,6 +13,7 @@ + diff --git a/Streamistry.Testing/JsonTests.cs b/Streamistry.Testing/JsonTests.cs new file mode 100644 index 0000000..b595ff5 --- /dev/null +++ b/Streamistry.Testing/JsonTests.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using NUnit.Framework; +using Streamistry.Pipes.Sinks; +using Streamistry.Pipes.Sources; +using Streamistry.Pipes.Mappers; +using System.Text.Json.Nodes; +using Streamistry.Pipes.Parsers; +using Streamistry.Pipes.Splitters; + +namespace Streamistry.Testing; +public class JsonTests +{ + private const string JsonFirst = @" + { + ""user"": { + ""name"": ""Albert Einstein"", + ""age"": 30, + ""contact"": { + ""email"": ""albert.einstein@gmail.com"", + ""phone"": ""123-456-7890"" + } + } + }"; + + private const string JsonSecond = @" + { + ""user"": { + ""name"": ""Nikola Tesla"", + ""age"": 35, + ""contact"": { + ""email"": ""nikola.tesla@blueorigin.com"", + ""phone"": ""321-654-0987"" + } + } + }"; + + private const string JsonThird = @" + { + ""user"": { + ""name"": ""John von Neumann"", + ""age"": 72, + ""contact"": { + ""phone"": ""456-123-0987"" + } + } + }"; + + [Test] + [TestCase(JsonFirst, "albert.einstein@gmail.com")] + [TestCase(JsonThird, null)] + public void JsonPathPlucker_ValidPath_ExistingValue(string jsonString, string? email) + { + var pipeline = new Pipeline(); + var plucker = new JsonPathPlucker(pipeline, "$.user.contact.email"); + var sink = new MemorySink(plucker); + plucker.Emit((JsonObject)JsonNode.Parse(jsonString)!); + + Assert.That(sink.State, Has.Count.EqualTo(1)); + Assert.That(sink.State.First(), Is.EqualTo(email)); + } + + [Test] + public void JsonObjectParser_ValidString_ExistingValue() + { + var source = new EnumerableSource([JsonFirst, JsonSecond, JsonThird]); + var pipeline = new Pipeline(source); + var parser = new JsonObjectParser(source); + var plucker = new JsonPathPlucker(parser, "$.user.contact.email"); + var sink = new MemorySink(plucker); + pipeline.Start(); + + Assert.That(sink.State, Has.Count.EqualTo(3)); + Assert.That(sink.State.First(), Is.EqualTo("albert.einstein@gmail.com")); + Assert.That(sink.State.Last(), Is.Null); + } + + [Test] + public void JsonArrayParser_ValidString_ExistingValue() + { + var array = $"[{JsonFirst}, {JsonSecond}, {JsonThird}]"; + var source = new EnumerableSource([array]); + var pipeline = new Pipeline(source); + var parser = new JsonArrayParser(source); + var splitter = new JsonArraySplitter(parser); + var plucker = new JsonPathPlucker(splitter, "$.user.contact.email"); + var sink = new MemorySink(plucker); + pipeline.Start(); + + Assert.That(sink.State, Has.Count.EqualTo(3)); + Assert.That(sink.State.First(), Is.EqualTo("albert.einstein@gmail.com")); + Assert.That(sink.State.Last(), Is.Null); + } +}