diff --git a/README.md b/README.md
index 33a55b9..1089f23 100644
--- a/README.md
+++ b/README.md
@@ -92,12 +92,12 @@ new TimeSeriesExtractorOptions() {
}
```
-If you have a known list of properties to include or exclude, you can use one of the `TimeSeriesExtractor.CreatePropertyMatcher` overloads to create a compatible delegate that can be assigned to the `IncludeProperty` property. For example:
+If you have a known list of properties to include or exclude, you can use one of the `TimeSeriesExtractor.CreateJsonPointerMatchDelegate` method overloads to create a compatible delegate that can be assigned to the `IncludeProperty` property. For example:
```csharp
-var matcher = TimeSeriesExtractor.CreatePropertyMatcher(
- propertiesToInclude: new[] { "/temperature", "/pressure", "/humidity" },
- propertiesToExclude: null
+var matcher = TimeSeriesExtractor.CreateJsonPointerMatchDelegate(
+ pointersToInclude: new[] { "/temperature", "/pressure", "/humidity" },
+ pointersToExclude: null
);
var options = new TimeSeriesExtractorOptions() {
@@ -105,6 +105,59 @@ var options = new TimeSeriesExtractorOptions() {
}
```
+### Pattern Matching using Wildcards
+
+`TimeSeriesExtractor.CreateJsonPointerMatchDelegate` supports using single- and multi-character wildcards (`?` and `*` respectively) in JSON pointers when the `allowWildcards` parameter is `true`. Note that the pattern must still be a valid JSON Pointer.
+
+Example: include all properties that are descendents of `/data`:
+
+```csharp
+var matcher = TimeSeriesExtractor.CreateJsonPointerMatchDelegate(
+ pointersToInclude: new[] { "/data/*" },
+ pointersToExclude: null,
+ allowWildcards: true
+);
+
+var options = new TimeSeriesExtractorOptions() {
+ IncludeProperty = matcher
+}
+```
+
+
+### Pattern Matching using MQTT-style Match Expressions
+
+In addition to matching using wildcard characters, `TimeSeriesExtractor.CreateJsonPointerMatchDelegate` also supports using MQTT-style match expressions in JSON pointers when the `allowWildcards` parameter is `true`.
+
+Example 1: include all properties that are descendents of `/data/instrument-1`:
+
+```csharp
+var matcher = TimeSeriesExtractor.CreateJsonPointerMatchDelegate(
+ pointersToInclude: new[] { "/data/instrument-1/#" },
+ pointersToExclude: null,
+ allowWildcards: true
+);
+
+var options = new TimeSeriesExtractorOptions() {
+ IncludeProperty = matcher
+}
+```
+
+Example 2: include all `temperature` properties that are grandchildren of `/data`:
+
+```csharp
+var matcher = TimeSeriesExtractor.CreateJsonPointerMatchDelegate(
+ pointersToInclude: new[] { "/data/+/temperature" },
+ pointersToExclude: null,
+ allowWildcards: true
+);
+
+var options = new TimeSeriesExtractorOptions() {
+ IncludeProperty = matcher
+}
+```
+
+Note that, if the JSON pointer includes single- or multi-character wildcards, regular pattern matching will always be used instead of MQTT-style match expressions.
+
## Data Sample Keys
diff --git a/build/version.json b/build/version.json
index d9f2ce6..63349f0 100644
--- a/build/version.json
+++ b/build/version.json
@@ -1,6 +1,6 @@
{
"Major": 0,
- "Minor": 13,
+ "Minor": 14,
"Patch": 0,
"PreRelease": ""
}
diff --git a/src/JsonTimeSeriesExtractor/TimeSeriesExtractor.cs b/src/JsonTimeSeriesExtractor/TimeSeriesExtractor.cs
index d67946c..e661590 100644
--- a/src/JsonTimeSeriesExtractor/TimeSeriesExtractor.cs
+++ b/src/JsonTimeSeriesExtractor/TimeSeriesExtractor.cs
@@ -41,52 +41,106 @@ public sealed partial class TimeSeriesExtractor {
private static Regex GetSampleKeyTemplateMatcher() => s_sampleKeyTemplateMatcher;
#endif
+ ///
+ /// Single-level wildcard character in a JSON Pointer path.
+ ///
+ public const string SingleLevelWildcard = "+";
+
+ ///
+ /// Multi-level wildcard character in a JSON Pointer path.
+ ///
+ ///
+ /// Multi-level wildcards are only valid in the final segment of a JSON Pointer path.
+ ///
+ public const string MultiLevelWildcard = "#";
+
+ ///
+ /// Single-character wildcard character in a JSON Pointer path.
+ ///
+ public const string SingleCharacterWildcard = "?";
+
+ ///
+ /// Multi-character wildcard character in a JSON Pointer path.
+ ///
+ public const string MultiCharacterWildcard = "*";
+
+
///
/// Creates a property matcher function compatible with
- /// that includes and/or excludes the specified properties.
+ /// that includes and/or excludes properties matching the specified pointers.
///
- ///
+ ///
/// The JSON pointers to properties to include. If not , only
/// properties that match an entry in this list will be included. Otherwise, properties
- /// will be included unless they match an entry in .
+ /// will be included unless they match an entry in .
///
- ///
+ ///
/// The JSON pointers to properties to exclude.
///
+ ///
+ /// Specifies if MQTT-style wildcard patterns are allowed in the specified JSON pointer
+ /// paths.
+ ///
///
/// A function that returns if a
/// should be generated for the specified property or otherwise.
///
- public static Func CreatePropertyMatcher(IEnumerable? propertiesToInclude, IEnumerable? propertiesToExclude) {
- return (pointer) => {
- // Apply exclusion rules first.
- if (propertiesToExclude != null) {
- foreach (var exclude in propertiesToExclude) {
- if (exclude == null) {
- continue;
- }
+ ///
+ ///
+ ///
+ /// When wildcard patterns are enabled via the parameter,
+ /// segments in instances can specify either pattern match
+ /// wildcards (i.e. ? for a single-character wildcard, and * for a
+ /// multi-character wildcard) or MQTT-style wildcard characters (i.e. + for a
+ /// single-level wildcard, and # for a multi-level wildcard).
+ ///
+ ///
+ ///
+ /// The two matching styles are mutually exclusive; if a pointer path contains single-
+ /// or multi-character wildcard characters the path is assumed to be a pattern match,
+ /// and MQTT-style wildcards are treated as literal characters. For example,
+ /// /foo/+/bar is treated as an MQTT-style match, but /foo/+/* is treated
+ /// as a regular pattern match.
+ ///
+ ///
+ ///
+ /// In an MQTT-style match expression, the multi-level wildcard character is only valid
+ /// in the final segment of the pointer path. For example, /foo/bar/# is a valid
+ /// MQTT match expression, but /foo/#/bar is not.
+ ///
+ ///
+ ///
+ /// In an MQTT-style match expression, you cannot specify both wildcard and non-wildcard
+ /// characters in the same pointer segment. For example, /foo/bar+/baz is not a
+ /// valid MQTT match expression and will be interpreted as a literal JSON Pointer path.
+ ///
+ ///
+ ///
+ public static Func CreateJsonPointerMatchDelegate(IEnumerable? pointersToInclude, IEnumerable? pointersToExclude, bool allowWildcards = false) {
+ Predicate? includePredicate = null;
+ Predicate? excludePredicate = null;
- if (exclude.Equals(pointer)) {
- return false;
- }
- }
- }
+ if (pointersToInclude != null) {
+ includePredicate = CreateJsonPointerMatchDelegate(pointersToInclude, allowWildcards);
+ }
- // If inclusion rules are specified, only include properties that match.
- if (propertiesToInclude != null) {
- foreach (var include in propertiesToInclude) {
- if (include == null) {
- continue;
- }
+ if (pointersToExclude != null) {
+ excludePredicate = CreateJsonPointerMatchDelegate(pointersToExclude, allowWildcards);
+ }
- if (include.Equals(pointer)) {
- return true;
- }
- }
+ if (includePredicate == null && excludePredicate == null) {
+ return _ => true;
+ }
+
+ return pointer => {
+ if (excludePredicate != null && excludePredicate.Invoke(pointer)) {
return false;
}
- // Fallback to including all properties.
+ if (includePredicate != null) {
+ return includePredicate.Invoke(pointer);
+ }
+
return true;
};
}
@@ -94,32 +148,197 @@ public static Func CreatePropertyMatcher(IEnumerable
/// Creates a property matcher function compatible with
- /// that includes and/or excludes the specified properties.
+ /// that includes and/or excludes properties matching the specified pointers.
///
- ///
- /// The JSON pointers to properties to include. If not , only
+ ///
+ /// The JSON pointers for properties to include. If not , only
/// properties that match an entry in this list will be included. Otherwise, properties
- /// will be included unless they match an entry in .
+ /// will be included unless they match an entry in .
///
- ///
+ ///
/// The JSON pointers to properties to exclude.
///
+ ///
+ /// Specifies if MQTT-style wildcard patterns are allowed in the specified JSON pointer
+ /// paths.
+ ///
///
/// A function that returns if a
/// should be generated for the specified property or otherwise.
///
///
- /// Any entry in or
+ /// Any entry in or
/// is .
///
///
- /// Any entry in or
+ /// Any entry in or
/// is not a valid JSON pointer.
///
- public static Func CreatePropertyMatcher(IEnumerable? propertiesToInclude, IEnumerable? propertiesToExclude) {
- var includes = propertiesToInclude?.Select(JsonPointer.Parse)?.ToArray();
- var excludes = propertiesToExclude?.Select(JsonPointer.Parse)?.ToArray();
- return CreatePropertyMatcher(includes, excludes);
+ ///
+ ///
+ ///
+ /// When wildcard patterns are enabled via the parameter,
+ /// segments in instances can specify either pattern match
+ /// wildcards (i.e. ? for a single-character wildcard, and * for a
+ /// multi-character wildcard) or MQTT-style wildcard characters (i.e. + for a
+ /// single-level wildcard, and # for a multi-level wildcard).
+ ///
+ ///
+ ///
+ /// The two matching styles are mutually exclusive; if a pointer path contains single-
+ /// or multi-character wildcard characters the path is assumed to be a pattern match,
+ /// and MQTT-style wildcards are treated as literal characters. For example,
+ /// /foo/+/bar is treated as an MQTT-style match, but /foo/+/* is treated
+ /// as a regular pattern match.
+ ///
+ ///
+ ///
+ /// In an MQTT-style match expression, the multi-level wildcard character is only valid
+ /// in the final segment of the pointer path. For example, /foo/bar/# is a valid
+ /// MQTT match expression, but /foo/#/bar is not.
+ ///
+ ///
+ ///
+ /// In an MQTT-style match expression, you cannot specify both wildcard and non-wildcard
+ /// characters in the same pointer segment. For example, /foo/bar+/baz is not a
+ /// valid MQTT match expression and will be interpreted as a literal JSON Pointer path.
+ ///
+ ///
+ ///
+ public static Func CreateJsonPointerMatchDelegate(IEnumerable? pointersToInclude, IEnumerable? pointersToExclude, bool allowWildcards = false) {
+ var includes = pointersToInclude?.Select(JsonPointer.Parse)?.ToArray();
+ var excludes = pointersToExclude?.Select(JsonPointer.Parse)?.ToArray();
+ return CreateJsonPointerMatchDelegate(includes, excludes, allowWildcards);
+ }
+
+
+ ///
+ /// Creates a predicate that tests if a JSON pointer matches against any of the specified JSON pointers.
+ ///
+ ///
+ /// The JSON pointers to match against.
+ ///
+ ///
+ /// Specifies if pattern match or MQTT-style match expressions are allowed in the
+ /// .
+ ///
+ ///
+ /// A predicate that returns if the specified JSON pointer matches
+ /// any of the .
+ ///
+ private static Predicate CreateJsonPointerMatchDelegate(IEnumerable matchPointers, bool allowWildcards) {
+ if (!allowWildcards) {
+ // No wildcards: return a simple predicate that checks for equality.
+ return pointer => matchPointers.Any(x => x != null && x.Equals(pointer));
+ }
+
+ var pointersWithWildcardStatus = matchPointers.Select(x => {
+ var containsPatternMatchWildcard = ContainsPatternMatchWildcard(x);
+ return new {
+ Pointer = x,
+ ContainsPatternMatchExpression = containsPatternMatchWildcard,
+ ContainsMqttMatchExpression = !containsPatternMatchWildcard && (ContainsSingleLevelMqttWildcard(x) || ContainsMultiLevelMqttWildcard(x))
+ };
+ }).ToArray();
+
+ if (pointersWithWildcardStatus.All(x => !x.ContainsPatternMatchExpression && !x.ContainsMqttMatchExpression)) {
+ // No wildcards are present: return a simple predicate that checks for equality.
+ return pointer => matchPointers.Any(x => x != null && x.Equals(pointer));
+ }
+
+ // Wildcards are present: return a more complex predicate that checks for wildcard matches.
+ var predicates = new List>();
+
+ foreach (var matchPointer in pointersWithWildcardStatus) {
+ if (matchPointer == null) {
+ continue;
+ }
+
+ if (!matchPointer.ContainsPatternMatchExpression && !matchPointer.ContainsMqttMatchExpression) {
+ // Pointer does not contain wildcards: add a simple predicate that checks for equality.
+ predicates.Add(pointer => matchPointer.Equals(pointer));
+ continue;
+ }
+
+ // Pointer contains wildcards: add a predicate that checks for wildcard matches.
+
+ if (matchPointer.ContainsPatternMatchExpression) {
+ // Use pattern match wildcards.
+#if NETCOREAPP
+ var pattern = Regex.Escape(matchPointer.Pointer.ToString())
+ .Replace(@"\*", ".*", StringComparison.Ordinal)
+ .Replace(@"\?", ".", StringComparison.Ordinal);
+#else
+ var pattern = Regex.Escape(matchPointer.Pointer.ToString())
+ .Replace(@"\*", ".*")
+ .Replace(@"\?", ".");
+#endif
+
+ var regex = new Regex($"^{pattern}$", RegexOptions.IgnoreCase | RegexOptions.Singleline, TimeSpan.FromSeconds(1));
+ predicates.Add(pointer => regex.IsMatch(pointer.ToString()));
+ continue;
+ }
+
+ // For each pointer segment, we'll check if that segment is a single-level or
+ // multi-level wildcard.
+ var matchSegments = matchPointer.Pointer.Segments.Reverse().Select((x, i) => new {
+ Segment = x,
+ IsSingleLevelWildcard = x.Value.Equals(SingleLevelWildcard, StringComparison.Ordinal),
+ // Multi-level wildcard is only valid in the final segment, which is at index
+ // 0 in our reversed segment list.
+ IsMultiLevelWildcard = i == 0 && x.Value.Equals(MultiLevelWildcard, StringComparison.Ordinal)
+ }).Reverse().ToArray();
+
+ predicates.Add(pointer => {
+ if (pointer.Segments.Length < matchSegments.Length) {
+ // The pointer has fewer segments than the match pattern; no match.
+ return false;
+ }
+
+ if (pointer.Segments.Length > matchSegments.Length) {
+ // The pointer has more segments than the match pattern; no match unless
+ // the last match segment is a multi-level wildcard.
+ return matchSegments[matchSegments.Length - 1].IsMultiLevelWildcard;
+ }
+
+ for (var i = 0; i < pointer.Segments.Length; i++) {
+ var pointerSegment = pointer.Segments[i];
+ var matchSegment = matchSegments[i];
+
+ if (matchSegment.IsSingleLevelWildcard) {
+ // Single-level wildcard: match any segment.
+ continue;
+ }
+
+ if (matchSegment.IsMultiLevelWildcard) {
+ // Multi-level wildcard: match all remaining pointer segments.
+ break;
+ }
+
+ // Check for an exact match between the current pointer segment and match
+ // segment.
+ if (!pointerSegment.Equals(matchSegment.Segment)) {
+ return false;
+ }
+ }
+
+ return true;
+ });
+ }
+
+ return predicates.Count == 0
+ ? _ => true
+ : pointer => predicates.Any(x => x.Invoke(pointer));
+
+ bool ContainsPatternMatchWildcard(JsonPointer p) {
+ var s = p.ToString();
+ return s.Contains(SingleCharacterWildcard) || s.Contains(MultiCharacterWildcard);
+ };
+
+ bool ContainsSingleLevelMqttWildcard(JsonPointer p) => p.Segments.Any(x => x.Value.Equals(SingleLevelWildcard, StringComparison.Ordinal));
+
+ bool ContainsMultiLevelMqttWildcard(JsonPointer p) => p.Segments[p.Segments.Length - 1].Value.Equals(MultiLevelWildcard, StringComparison.Ordinal);
+
}
@@ -287,11 +506,6 @@ int currentRecursionDepth
) {
var pointer = JsonPointer.Create(context.ElementStack.Where(x => x.Key != null).Reverse().Select(x => PointerSegment.Create(x.Key!)));
- // Check if this property should be included.
- if (!context.IncludeElement.Invoke(pointer)) {
- yield break;
- }
-
var currentElement = context.ElementStack.Peek();
if (!context.Options.Recursive || (context.Options.MaxDepth > 0 && currentRecursionDepth >= context.Options.MaxDepth)) {
@@ -299,27 +513,12 @@ int currentRecursionDepth
// depth; build a sample with the current element. When we have exceeded the
// maximum recursion depth, the value will be the serialized JSON of the element
// if the element is an object or an array.
- string key = null!;
- var error = false;
-
- try {
- key = BuildSampleKeyFromTemplate(
- context.Options,
- pointer,
- context.ElementStack,
- context.IsDefaultSampleKeyTemplate
- );
- }
- catch (InvalidOperationException) {
- error = true;
- }
-
- if (error) {
+ var sample = BuildSample(pointer, currentElement.Element);
+ if (!sample.HasValue) {
yield break;
}
- var timestamp = context.TimestampStack.Peek();
- yield return BuildSampleFromJsonValue(timestamp.Timestamp, timestamp.Source, key, currentElement.Element);
+ yield return sample.Value;
}
else {
// We are doing recursive processing and have not exceeded the maximum recursion
@@ -376,6 +575,11 @@ int currentRecursionDepth
}
TimeSeriesSample? BuildSample(JsonPointer pointer, JsonElement element) {
+ // Check if this element should be included.
+ if (!context.IncludeElement.Invoke(pointer)) {
+ return null;
+ }
+
try {
var key = BuildSampleKeyFromTemplate(
context.Options,
diff --git a/test/JsonTimeSeriesExtractor.Tests/JsonTimeSeriesExtractorTests.cs b/test/JsonTimeSeriesExtractor.Tests/JsonTimeSeriesExtractorTests.cs
index 4957b49..673878a 100644
--- a/test/JsonTimeSeriesExtractor.Tests/JsonTimeSeriesExtractorTests.cs
+++ b/test/JsonTimeSeriesExtractor.Tests/JsonTimeSeriesExtractorTests.cs
@@ -1,5 +1,4 @@
using System;
-using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
@@ -176,15 +175,10 @@ public void ShouldExcludeSpecifiedProperties() {
var samples = TimeSeriesExtractor.GetSamples(json, new TimeSeriesExtractorOptions() {
Template = TestContext.TestName + "/{MacAddress}/{DataFormat}/{$prop}",
TimestampProperty = JsonPointer.Parse("/" + nameof(deviceSample.Timestamp)),
- IncludeProperty = prop => {
- if (prop.ToString().Equals("/" + nameof(deviceSample.DataFormat))) {
- return false;
- }
- if (prop.ToString().Equals("/" + nameof(deviceSample.MacAddress))) {
- return false;
- }
- return true;
- }
+ IncludeProperty = TimeSeriesExtractor.CreateJsonPointerMatchDelegate(null, new[] {
+ $"/{nameof(deviceSample.DataFormat)}",
+ $"/{nameof(deviceSample.MacAddress)}"
+ })
}).ToArray();
Assert.AreEqual(11, samples.Length);
@@ -218,18 +212,11 @@ public void ShouldIncludeSpecifiedProperties() {
var samples = TimeSeriesExtractor.GetSamples(json, new TimeSeriesExtractorOptions() {
Template = TestContext.TestName + "/{MacAddress}/{DataFormat}/{$prop}",
TimestampProperty = JsonPointer.Parse("/" + nameof(deviceSample.Timestamp)),
- IncludeProperty = prop => {
- if (prop.ToString().Equals("/" + nameof(deviceSample.Temperature))) {
- return true;
- }
- if (prop.ToString().Equals("/" + nameof(deviceSample.Humidity))) {
- return true;
- }
- if (prop.ToString().Equals("/" + nameof(deviceSample.Pressure))) {
- return true;
- }
- return false;
- }
+ IncludeProperty = TimeSeriesExtractor.CreateJsonPointerMatchDelegate(new[] {
+ $"/{nameof(deviceSample.Temperature)}",
+ $"/{nameof(deviceSample.Humidity)}",
+ $"/{nameof(deviceSample.Pressure)}"
+ }, null)
}).ToArray();
Assert.AreEqual(3, samples.Length);
@@ -239,6 +226,170 @@ public void ShouldIncludeSpecifiedProperties() {
}
+ [TestMethod]
+ public void ShouldIncludePropertiesUsingMqttMultiLevelMatch() {
+ var deviceSample = new {
+ Data = new {
+ Timestamp = DateTimeOffset.Parse("2021-05-28T17:41:09.7031076+03:00"),
+ SignalStrength = -75,
+ DataFormat = 5,
+ Temperature = 19.3,
+ Humidity = 37.905,
+ Pressure = 1013.35,
+ Acceleration = new {
+ X = -0.872,
+ Y = 0.512,
+ Z = -0.04
+ },
+ BatteryVoltage = 3.085,
+ TxPower = 4,
+ MovementCounter = 5,
+ MeasurementSequence = 34425,
+ MacAddress = "AB:CD:EF:01:23:45"
+ }
+ };
+
+ var json = JsonSerializer.Serialize(deviceSample);
+
+ var samples = TimeSeriesExtractor.GetSamples(json, new TimeSeriesExtractorOptions() {
+ Recursive = true,
+ TimestampProperty = JsonPointer.Parse($"/{nameof(deviceSample.Data)}/{nameof(deviceSample.Data.Timestamp)}"),
+ IncludeProperty = TimeSeriesExtractor.CreateJsonPointerMatchDelegate(new[] {
+ $"/{nameof(deviceSample.Data)}/{nameof(deviceSample.Data.Acceleration)}/#",
+ }, null, allowWildcards: true)
+ }).ToArray();
+
+ Assert.AreEqual(3, samples.Length);
+ Assert.IsTrue(samples.All(x => x.Timestamp.UtcDateTime.Equals(deviceSample.Data.Timestamp.UtcDateTime)));
+ Assert.IsTrue(samples.All(x => x.TimestampSource == TimestampSource.Document));
+ Assert.IsTrue(samples.All(x => x.Key.StartsWith("Data/Acceleration/")));
+ }
+
+
+ [TestMethod]
+ public void ShouldIncludePropertiesUsingMqttSingleLevelMatch() {
+ var deviceSample = new {
+ Data = new {
+ Timestamp = DateTimeOffset.Parse("2021-05-28T17:41:09.7031076+03:00"),
+ SignalStrength = -75,
+ DataFormat = 5,
+ Temperature = 19.3,
+ Humidity = 37.905,
+ Pressure = 1013.35,
+ Acceleration = new {
+ X = -0.872,
+ Y = 0.512,
+ Z = -0.04
+ },
+ BatteryVoltage = 3.085,
+ TxPower = 4,
+ MovementCounter = 5,
+ MeasurementSequence = 34425,
+ MacAddress = "AB:CD:EF:01:23:45"
+ }
+ };
+
+ var json = JsonSerializer.Serialize(deviceSample);
+
+ var samples = TimeSeriesExtractor.GetSamples(json, new TimeSeriesExtractorOptions() {
+ Recursive = true,
+ TimestampProperty = JsonPointer.Parse($"/{nameof(deviceSample.Data)}/{nameof(deviceSample.Data.Timestamp)}"),
+ IncludeProperty = TimeSeriesExtractor.CreateJsonPointerMatchDelegate(new[] {
+ $"/+/+/X",
+ }, null, allowWildcards: true)
+ }).ToArray();
+
+ Assert.AreEqual(1, samples.Length);
+ var sample = samples[0];
+
+ Assert.AreEqual(deviceSample.Data.Timestamp.UtcDateTime, sample.Timestamp.UtcDateTime);
+ Assert.AreEqual(TimestampSource.Document, sample.TimestampSource);
+ Assert.AreEqual("Data/Acceleration/X", sample.Key);
+ }
+
+
+ [TestMethod]
+ public void ShouldIncludePropertiesUsingMultiCharacterPatternMatch() {
+ var deviceSample = new {
+ Data = new {
+ Timestamp = DateTimeOffset.Parse("2021-05-28T17:41:09.7031076+03:00"),
+ SignalStrength = -75,
+ DataFormat = 5,
+ Temperature = 19.3,
+ Humidity = 37.905,
+ Pressure = 1013.35,
+ Acceleration = new {
+ X = -0.872,
+ Y = 0.512,
+ Z = -0.04
+ },
+ BatteryVoltage = 3.085,
+ TxPower = 4,
+ MovementCounter = 5,
+ MeasurementSequence = 34425,
+ MacAddress = "AB:CD:EF:01:23:45"
+ }
+ };
+
+ var json = JsonSerializer.Serialize(deviceSample);
+
+ var samples = TimeSeriesExtractor.GetSamples(json, new TimeSeriesExtractorOptions() {
+ Recursive = true,
+ TimestampProperty = JsonPointer.Parse($"/{nameof(deviceSample.Data)}/{nameof(deviceSample.Data.Timestamp)}"),
+ IncludeProperty = TimeSeriesExtractor.CreateJsonPointerMatchDelegate(new[] {
+ $"/*/X",
+ }, null, allowWildcards: true)
+ }).ToArray();
+
+ Assert.AreEqual(1, samples.Length);
+ var sample = samples[0];
+
+ Assert.AreEqual(deviceSample.Data.Timestamp.UtcDateTime, sample.Timestamp.UtcDateTime);
+ Assert.AreEqual(TimestampSource.Document, sample.TimestampSource);
+ Assert.AreEqual("Data/Acceleration/X", sample.Key);
+ }
+
+
+ [TestMethod]
+ public void ShouldIncludePropertiesUsingSingleCharacterPatternMatch() {
+ var deviceSample = new {
+ Data = new {
+ Timestamp = DateTimeOffset.Parse("2021-05-28T17:41:09.7031076+03:00"),
+ SignalStrength = -75,
+ DataFormat = 5,
+ Temperature = 19.3,
+ Humidity = 37.905,
+ Pressure = 1013.35,
+ Acceleration = new {
+ X = -0.872,
+ Y = 0.512,
+ Z = -0.04
+ },
+ BatteryVoltage = 3.085,
+ TxPower = 4,
+ MovementCounter = 5,
+ MeasurementSequence = 34425,
+ MacAddress = "AB:CD:EF:01:23:45"
+ }
+ };
+
+ var json = JsonSerializer.Serialize(deviceSample);
+
+ var samples = TimeSeriesExtractor.GetSamples(json, new TimeSeriesExtractorOptions() {
+ Recursive = true,
+ TimestampProperty = JsonPointer.Parse($"/{nameof(deviceSample.Data)}/{nameof(deviceSample.Data.Timestamp)}"),
+ IncludeProperty = TimeSeriesExtractor.CreateJsonPointerMatchDelegate(new[] {
+ $"/{nameof(deviceSample.Data)}/{nameof(deviceSample.Data.Acceleration)}/?",
+ }, null, allowWildcards: true)
+ }).ToArray();
+
+ Assert.AreEqual(3, samples.Length);
+ Assert.IsTrue(samples.All(x => x.Timestamp.UtcDateTime.Equals(deviceSample.Data.Timestamp.UtcDateTime)));
+ Assert.IsTrue(samples.All(x => x.TimestampSource == TimestampSource.Document));
+ Assert.IsTrue(samples.All(x => x.Key.StartsWith("Data/Acceleration/")));
+ }
+
+
[TestMethod]
public void ShouldParseTopLevelArray() {
var deviceSamples = new[] {