generated from wazzamatazz/csharp-repo-template
-
Notifications
You must be signed in to change notification settings - Fork 0
/
TimeSeriesExtractorOptions.cs
410 lines (386 loc) · 16.6 KB
/
TimeSeriesExtractorOptions.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Text.Json;
using Json.Pointer;
namespace Jaahas.Json {
/// <summary>
/// Options for <see cref="TimeSeriesExtractor"/>.
/// </summary>
public class TimeSeriesExtractorOptions : IValidatableObject {
/// <summary>
/// Specifies a JSON Pointer that the <see cref="TimeSeriesExtractor"/> should start
/// processing data from.
/// </summary>
/// <remarks>
/// If <see cref="StartAt"/> is <see langword="null"/> start from the root of the JSON
/// document.
/// </remarks>
public JsonPointerLiteral? StartAt { get; set; }
/// <summary>
/// The template to use when generating keys for extracted values.
/// </summary>
/// <remarks>
///
/// <para>
/// If <see cref="Template"/> is <see langword="null"/> or white space, <see cref="TimeSeriesExtractorConstants.DefaultTemplate"/>
/// will be used.
/// </para>
///
/// <para>
/// Templates can contain placholders, in the format <c>{property_name}</c>, where
/// <c>property_name</c> is the name of a property on the JSON object that is being
/// processed. The placeholder for the JSON Pointer path of the property being processed
/// (without the leading <c>/</c>) is <c>{$prop}</c>.
/// </para>
///
/// <para>
/// For example, consider the following JSON:
/// </para>
///
/// <code lang="JSON">
/// {
/// "deviceId": 1,
/// "temperature": 21.7,
/// "pressure": 1001.2
/// }
/// </code>
///
/// <para>
/// Given a <see cref="Template"/> value of <c>devices/{deviceId}/{$prop}</c>, the key
/// generated for the <c>pressure</c> property will be <c>devices/1/pressure</c>.
/// </para>
///
/// <para>
/// Use the <see cref="IncludeProperty"/> delegate to ignore JSON properties that are
/// not required or are used only for metadata purposes, and the <see cref="GetTemplateReplacement"/>
/// delegate to define default replacement values for placeholders that are not found in
/// the JSON object.
/// </para>
///
/// <para>
/// Important: When <see cref="Recursive"/> mode is enabled, placeholders behave differently.
/// See the documentation for the <see cref="Recursive"/> property for more information.
/// </para>
///
/// </remarks>
public string Template { get; set; } = TimeSeriesExtractorConstants.DefaultTemplate;
/// <summary>
/// A delegate that accepts a placeholder name referenced in the <see cref="Template"/> and
/// returns the default replacement for that placeholder.
/// </summary>
/// <remarks>
/// The default replacement for a given placeholder is only used if a replacement cannot
/// be identified from the JSON that is being parsed.
/// </remarks>
public Func<string, string?>? GetTemplateReplacement { get; set; }
/// <summary>
/// Specifies if the <see cref="TimeSeriesExtractor"/> will emit a value for a JSON
/// property even if it cannot find replacement values for all placeholders in the
/// <see cref="Template"/>.
/// </summary>
public bool AllowUnresolvedTemplateReplacements { get; set; } = true;
/// <summary>
/// The JSON Pointer to the property that defines the timestamp for the samples
/// extracted from the JSON.
/// </summary>
/// <remarks>
///
/// <para>
/// If <see cref="TimestampProperty"/> is <see langword="null"/>, the configured
/// timestamp property can not be found in the JSON document, or the property does not
/// represent a <see cref="DateTimeOffset"/> value, <see cref="GetDefaultTimestamp"/>
/// will be used the retrieve the sample time.
/// </para>
///
/// <para>
/// By default, timestamps can be specified as a string value that can be directly parsed
/// to <see cref="DateTimeOffset"/>, or as a number value that represents the number of
/// milliseconds since 1 January 1970 UTC. The default parsing rules can be overridden
/// by specifying a value for the <see cref="TimestampParser"/> property.
/// </para>
///
/// </remarks>
/// <seealso cref="TimestampParser"/>
public JsonPointerLiteral? TimestampProperty { get; set; } = JsonPointer.Parse(TimeSeriesExtractorConstants.DefaultTimestampProperty);
/// <summary>
/// A delegate that overrides the default timestamp parser.
/// </summary>
/// <remarks>
///
/// <para>
/// By default, timestamps can be specified as a string value that can be directly parsed
/// to <see cref="DateTimeOffset"/>, or as a number value that represents the number of
/// milliseconds since 1 January 1970 UTC.
/// </para>
///
/// <para>
/// Specify a value for this property if you need to customise timestamp parsing e.g. if
/// the timestamp is specified as the number of whole seconds since 1 Januaty 1970.
/// </para>
///
/// </remarks>
public Func<JsonElement, DateTimeOffset?>? TimestampParser { get; set; }
/// <summary>
/// A delegate that will retrieve the default sample timestamp to use if a timestamp
/// property cannot be identified on a JSON object.
/// </summary>
/// <remarks>
/// Specify <see langword="null"/> to use <see cref="DateTimeOffset.UtcNow"/> at the
/// moment that the JSON is parsed as the default sample timestamp.
/// </remarks>
public Func<DateTimeOffset>? GetDefaultTimestamp { get; set; }
/// <summary>
/// When both <see cref="AllowNestedTimestamps"/> and <see cref="Recursive"/> are <see langword="true"/>,
/// the <see cref="TimestampProperty"/> will be resolved at every level of the JSON document
/// hierarchy instead of being resolved once against the root element only.
/// </summary>
/// <remarks>
///
/// <para>
/// This option is useful when processing JSON documents that contain multiple samples,
/// with each sample specifying its own timestamp.
/// </para>
///
/// <para>
/// For example, consider the following JSON:
/// </para>
///
/// <code lang="JSON">
/// {
/// "data": {
/// "device-1": {
/// "time": "2023-12-01T00:00:00Z",
/// "temperature": 21.7
/// },
/// "device-2": {
/// "time": "2023-12-01T00:30:00Z",
/// "temperature": 22.1
/// }
/// }
/// }
/// </code>
///
/// <para>
/// By setting <see cref="AllowNestedTimestamps"/> and <see cref="Recursive"/> to <see langword="true"/>
/// and using <c>/time</c> as the <see cref="TimestampProperty"/>, each <c>temperature</c>
/// sample will be assigned the timestamp from its sibling <c>time</c> property.
/// </para>
///
/// </remarks>
public bool AllowNestedTimestamps { get; set; }
/// <summary>
/// A delegate that is used to determine if a JSON element should be processed by the time
/// series extractor.
/// </summary>
public JsonPointerMatchDelegate? CanProcessElement { get; set; }
/// <summary>
/// When <see langword="true"/>, JSON properties that contain other objects or arrays will
/// be processed recursively, instead of treating the properties as string values.
/// </summary>
/// <remarks>
///
/// <para>
/// <see cref="PathSeparator"/> is used to separate hierarchy levels when recursively
/// processing objects.
/// </para>
///
/// <para>
/// Consider the following JSON:
/// </para>
///
/// <code lang="JSON">
/// {
/// "deviceId": 1,
/// "measurements": {
/// "temperature": 21.7,
/// "pressure": 1001.2
/// }
/// }
/// </code>
///
/// <para>
/// Given a key template of <c>devices/{deviceId}/{$prop}</c>, <see cref="IncludeProperty"/>
/// configured to skip <c>deviceId</c>, recursive processing enabled, and a path
/// separator of <c>/</c>, values for the following keys under the <c>measurements</c>
/// property will be emitted:
/// </para>
///
/// <list type="bullet">
/// <item>
/// <description><c>devices/1/measurements/temperature</c></description>
/// </item>
/// <item>
/// <description><c>devices/1/measurements/pressure</c></description>
/// </item>
/// </list>
///
/// <para>
/// When processing an array rather than an object, the array index will be used as part
/// of the key. For example, consider the following JSON:
/// </para>
///
/// <code lang="JSON">
/// {
/// "deviceId": 1,
/// "measurements": [
/// 21.7,
/// 1001.2
/// ]
/// }
/// </code>
///
/// <para>
/// Using the same options as the previous example, values for the following keys will
/// be emitted:
/// </para>
///
/// <list type="bullet">
/// <item>
/// <description><c>devices/1/measurements/0</c></description>
/// </item>
/// <item>
/// <description><c>devices/1/measurements/1</c></description>
/// </item>
/// </list>
///
/// <para>
/// Note that, when running in recursive mode, template placeholder replacements behave
/// differently:
/// </para>
///
/// <list type="bullet">
/// <item>
/// <description>
/// When a template includes a reference to another property (e.g. <c>deviceId</c> in
/// <c>{deviceId}/{$prop}</c>), all instances of that property from the root object
/// to the current element will be used in the replacement, using the <see cref="PathSeparator"/>
/// to join them together. For example, if a parent object has a <c>deviceId</c> of
/// <c>1</c> and a child object has a <c>deviceId</c> of <c>A</c>, the <c>{deviceId}</c>
/// placeholder in the key template will be replaced with <c>1/A</c> when it is applied
/// to a property on the child object.
/// </description>
/// </item>
/// <item>
/// <description>
/// The <c>{$prop}</c> placeholder is replaced with the names of every property that
/// was visited in order to arrive at the current property from the root object (e.g.
/// <c>measurements/acceleration/X</c>). If you only require the local, unqualified
/// property name in your generated keys, you can use the <c>{$prop-local}</c> placeholder
/// instead.
/// </description>
/// </item>
/// </list>
///
/// </remarks>
public bool Recursive { get; set; }
/// <summary>
/// The maximum allowed recursion depth when <see cref="Recursive"/> is <see langword="true"/>.
/// </summary>
/// <remarks>
///
/// <para>
/// When the recursion depth limit is reached and the current JSON element is an array or
/// an object, a sample will be emitted that contains the serialized JSON element as its
/// value instead of recursing into the object or array.
/// </para>
///
/// <para>
/// A <see cref="MaxDepth"/> of less than one specifies that there is no recursion limit.
/// </para>
///
/// </remarks>
public int MaxDepth { get; set; } = TimeSeriesExtractorConstants.DefaultMaxDepth;
/// <summary>
/// When <see cref="Recursive"/> is <see langword="true"/>, <see cref="PathSeparator"/> is
/// used to separate hierarchy levels when generating sample keys for nested objects and arrays.
/// </summary>
[Required]
public string PathSeparator { get; set; } = TimeSeriesExtractorConstants.DefaultPathSeparator;
/// <summary>
/// When <see cref="Recursive"/> is <see langword="true"/>, setting <see cref="IncludeArrayIndexesInSampleKeys"/>
/// to <see langword="false"/> will omit array indexes from the keys generated for extracted
/// samples.
/// </summary>
/// <remarks>
///
/// <para>
/// This property is <see langword="true"/> by default. It can be useful to set it to
/// <see langword="false"/> when a JSON document contains multiple samples for the same
/// device or instrument, with each sample defining its own timestamp.
/// </para>
///
/// <para>
/// For example, consider the following JSON:
/// </para>
///
/// <code lang="JSON">
/// {
/// "data": {
/// "device-1": [
/// {
/// "time": "2023-12-01T00:00:00Z",
/// "temperature": 21.7
/// },
/// {
/// "time": "2023-12-01T00:30:00Z",
/// "temperature": 22.1
/// }
/// ]
/// }
/// }
/// </code>
///
/// <para>
/// When <see cref="Recursive"/> and <see cref="AllowNestedTimestamps"/> are <see langword="true"/>,
/// and <see cref="IncludeArrayIndexesInSampleKeys"/> is <see langword="false"/>, multiple
/// samples are emitted using <c>data/device-1/temperature</c> as their sample key. If
/// <see cref="IncludeArrayIndexesInSampleKeys"/> was <see langword="true"/>, the sample
/// keys would be <c>data/device-1/0/temperature</c> and <c>data/device-1/1/temperature</c>.
/// </para>
///
/// <para>
/// Use the <see cref="AllowNestedTimestamps"/> property to allow sample timestamps to
/// be defined in nested objects.
/// </para>
///
/// </remarks>
/// <seealso cref="AllowNestedTimestamps"/>
public bool IncludeArrayIndexesInSampleKeys { get; set; } = true;
/// <summary>
/// Creates a new <see cref="TimeSeriesExtractorOptions"/> instance.
/// </summary>
public TimeSeriesExtractorOptions() { }
/// <summary>
/// Creates a new <see cref="TimeSeriesExtractorOptions"/> instance from an existing
/// instance.
/// </summary>
/// <param name="existing">
/// The existing instance.
/// </param>
public TimeSeriesExtractorOptions(TimeSeriesExtractorOptions? existing) {
if (existing == null) {
return;
}
AllowNestedTimestamps = existing.AllowNestedTimestamps;
AllowUnresolvedTemplateReplacements = existing.AllowUnresolvedTemplateReplacements;
CanProcessElement = existing.CanProcessElement;
GetDefaultTimestamp = existing.GetDefaultTimestamp;
GetTemplateReplacement = existing.GetTemplateReplacement;
IncludeArrayIndexesInSampleKeys = existing.IncludeArrayIndexesInSampleKeys;
MaxDepth = existing.MaxDepth;
PathSeparator = existing.PathSeparator;
Recursive = existing.Recursive;
StartAt = existing.StartAt;
Template = existing.Template;
TimestampParser = existing.TimestampParser;
TimestampProperty = existing.TimestampProperty;
}
/// <inheritdoc/>
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) {
if (string.IsNullOrWhiteSpace(Template)) {
yield return new ValidationResult($"The template cannot be null or white space.", new[] { nameof(Template) });
}
}
}
}