@rimbu/deep 0.12.1 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/match.ts CHANGED
@@ -25,7 +25,7 @@ export namespace Match {
25
25
  * Determines the various allowed match types for given type `T`.
26
26
  * @typeparam T - the input value type
27
27
  * @typeparam C - utility type
28
- * @typeparam P - the parant type
28
+ * @typeparam P - the parent type
29
29
  * @typeparam R - the root object type
30
30
  */
31
31
  export type Entry<T, C, P, R> = IsAnyFunc<T> extends true
@@ -36,7 +36,15 @@ export namespace Match {
36
36
  Match.WithResult<T, P, R, Match.Obj<T, C, P, R>>
37
37
  : IsArray<T> extends true
38
38
  ? // determine allowed match values for array or tuple
39
- Match.Arr<T, C, P, R> | Match.Func<T, P, R, Match.Arr<T, C, P, R>>
39
+ | Match.Arr<T, C, P, R>
40
+ | Match.Entry<T[number & keyof T], C[number & keyof C], P, R>[]
41
+ | Match.Func<
42
+ T,
43
+ P,
44
+ R,
45
+ | Match.Arr<T, C, P, R>
46
+ | Match.Entry<T[number & keyof T], C[number & keyof C], P, R>[]
47
+ >
40
48
  : // only accept values with same interface
41
49
  Match.WithResult<T, P, R, { [K in keyof C]: C[K & keyof T] }>;
42
50
 
@@ -44,7 +52,7 @@ export namespace Match {
44
52
  * The type that determines allowed matchers for objects.
45
53
  * @typeparam T - the input value type
46
54
  * @typeparam C - utility type
47
- * @typeparam P - the parant type
55
+ * @typeparam P - the parent type
48
56
  * @typeparam R - the root object type
49
57
  */
50
58
  export type Obj<T, C, P, R> =
@@ -65,20 +73,31 @@ export namespace Match {
65
73
  * The type that determines allowed matchers for arrays/tuples.
66
74
  * @typeparam T - the input value type
67
75
  * @typeparam C - utility type
68
- * @typeparam P - the parant type
76
+ * @typeparam P - the parent type
69
77
  * @typeparam R - the root object type
70
78
  */
71
79
  export type Arr<T, C, P, R> =
72
80
  | C
73
81
  | Match.CompoundForArr<T, C, P, R>
74
- | (Match.TupIndices<T, C, R> & { [K in Match.CompoundType]?: never });
82
+ | Match.TraversalForArr<T, C, R>
83
+ | (Match.TupIndices<T, C, R> & {
84
+ [K in Match.CompoundType | Match.ArrayTraversalType]?: never;
85
+ });
75
86
 
87
+ /**
88
+ * A type that either directly results in result type `S` or is a function taking the value, parent, and root values, and
89
+ * returns a value of type `S`.
90
+ * @typeparam T - the input value type
91
+ * @typeparam P - the parent type
92
+ * @typeparam R - the root object type
93
+ * @typeparam S - the result type
94
+ */
76
95
  export type WithResult<T, P, R, S> = S | Match.Func<T, P, R, S>;
77
96
 
78
97
  /**
79
98
  * Type used to determine the allowed function types. Always includes booleans.
80
99
  * @typeparam T - the input value type
81
- * @typeparam P - the parant type
100
+ * @typeparam P - the parent type
82
101
  * @typeparam R - the root object type
83
102
  * @typeparam S - the allowed return value type
84
103
  */
@@ -103,6 +122,11 @@ export namespace Match {
103
122
  */
104
123
  export type CompoundType = 'every' | 'some' | 'none' | 'single';
105
124
 
125
+ /**
126
+ * Keys used to indicate an array match traversal.
127
+ */
128
+ export type ArrayTraversalType = `${CompoundType}Item`;
129
+
106
130
  /**
107
131
  * Compount matcher for objects, can only be an array staring with a compound type keyword.
108
132
  * @typeparam T - the input value type
@@ -123,15 +147,31 @@ export namespace Match {
123
147
  * @typeparam R - the root object type
124
148
  */
125
149
  export type CompoundForArr<T, C, P, R> = {
126
- [K in CompoundType]: {
127
- [K2 in CompoundType]?: K2 extends K ? Match.Entry<T, C, P, R>[] : never;
150
+ [K in Match.CompoundType]: {
151
+ [K2 in Match.CompoundType]?: K2 extends K
152
+ ? Match.Entry<T, C, P, R>[]
153
+ : never;
154
+ };
155
+ }[Match.CompoundType];
156
+
157
+ /**
158
+ * Defines an object containing exactly one `TraversalType` key, having a matcher for the array element type.
159
+ * @typeparam T - the input value type
160
+ * @typeparam C - utility type
161
+ * @typeparam R - the root object type
162
+ */
163
+ export type TraversalForArr<T, C, R> = {
164
+ [K in Match.ArrayTraversalType]: {
165
+ [K2 in Match.ArrayTraversalType]?: K2 extends K
166
+ ? Match.Entry<T[number & keyof T], C[number & keyof C], T, R>
167
+ : never;
128
168
  };
129
- }[CompoundType];
169
+ }[Match.ArrayTraversalType];
130
170
 
131
171
  /**
132
- * Utility type for collecting errors
172
+ * Utility type for collecting match failure reasons
133
173
  */
134
- export type ErrorCollector = string[] | undefined;
174
+ export type FailureLog = string[];
135
175
  }
136
176
 
137
177
  /**
@@ -140,7 +180,7 @@ export namespace Match {
140
180
  * @typeparam C - utility type
141
181
  * @param source - the value to match (should be a plain object)
142
182
  * @param matcher - a matcher object or a function taking the matcher API and returning a match object
143
- * @param errorCollector - (optional) a string array that can be passed to collect reasons why the match failed
183
+ * @param failureLog - (optional) a string array that can be passed to collect reasons why the match failed
144
184
  * @example
145
185
  * ```ts
146
186
  * const input = { a: 1, b: { c: true, d: 'a' } }
@@ -156,9 +196,9 @@ export namespace Match {
156
196
  export function match<T, C extends Partial<T> = Partial<T>>(
157
197
  source: T,
158
198
  matcher: Match<T, C>,
159
- errorCollector: Match.ErrorCollector = undefined
199
+ failureLog?: Match.FailureLog
160
200
  ): boolean {
161
- return matchEntry(source, source, source, matcher as any, errorCollector);
201
+ return matchEntry(source, source, source, matcher as any, failureLog);
162
202
  }
163
203
 
164
204
  /**
@@ -169,7 +209,7 @@ function matchEntry<T, C, P, R>(
169
209
  parent: P,
170
210
  root: R,
171
211
  matcher: Match.Entry<T, C, P, R>,
172
- errorCollector: Match.ErrorCollector
212
+ failureLog?: Match.FailureLog
173
213
  ): boolean {
174
214
  if (Object.is(source, matcher)) {
175
215
  // value and target are exactly the same, always will be true
@@ -179,7 +219,7 @@ function matchEntry<T, C, P, R>(
179
219
  if (matcher === null || matcher === undefined) {
180
220
  // these matchers can only be direct matches, and previously it was determined that
181
221
  // they are not equal
182
- errorCollector?.push(
222
+ failureLog?.push(
183
223
  `value ${JSON.stringify(source)} did not match matcher ${matcher}`
184
224
  );
185
225
 
@@ -191,7 +231,7 @@ function matchEntry<T, C, P, R>(
191
231
  const result = Object.is(source, matcher);
192
232
 
193
233
  if (!result) {
194
- errorCollector?.push(
234
+ failureLog?.push(
195
235
  `both value and matcher are functions, but they do not have the same reference`
196
236
  );
197
237
  }
@@ -207,7 +247,7 @@ function matchEntry<T, C, P, R>(
207
247
  // function resulted in a direct match result
208
248
 
209
249
  if (!matcherResult) {
210
- errorCollector?.push(
250
+ failureLog?.push(
211
251
  `function matcher returned false for value ${JSON.stringify(source)}`
212
252
  );
213
253
  }
@@ -216,22 +256,22 @@ function matchEntry<T, C, P, R>(
216
256
  }
217
257
 
218
258
  // function resulted in a value that needs to be further matched
219
- return matchEntry(source, parent, root, matcherResult, errorCollector);
259
+ return matchEntry(source, parent, root, matcherResult, failureLog);
220
260
  }
221
261
 
222
262
  if (isPlainObj(source)) {
223
263
  // source ia a plain object, can be partially matched
224
- return matchPlainObj(source, parent, root, matcher as any, errorCollector);
264
+ return matchPlainObj(source, parent, root, matcher as any, failureLog);
225
265
  }
226
266
 
227
267
  if (Array.isArray(source)) {
228
268
  // source is an array
229
- return matchArr(source, root, matcher as any, errorCollector);
269
+ return matchArr(source, parent, root, matcher as any, failureLog);
230
270
  }
231
271
 
232
272
  // already determined above that the source and matcher are not equal
233
273
 
234
- errorCollector?.push(
274
+ failureLog?.push(
235
275
  `value ${JSON.stringify(
236
276
  source
237
277
  )} does not match given matcher ${JSON.stringify(matcher)}`
@@ -245,9 +285,10 @@ function matchEntry<T, C, P, R>(
245
285
  */
246
286
  function matchArr<T extends any[], C, P, R>(
247
287
  source: T,
288
+ parent: P,
248
289
  root: R,
249
290
  matcher: Match.Arr<T, C, P, R>,
250
- errorCollector: Match.ErrorCollector
291
+ failureLog?: Match.FailureLog
251
292
  ): boolean {
252
293
  if (Array.isArray(matcher)) {
253
294
  // directly compare array contents
@@ -256,7 +297,7 @@ function matchArr<T extends any[], C, P, R>(
256
297
  if (length !== matcher.length) {
257
298
  // if lengths not equal, arrays are not equal
258
299
 
259
- errorCollector?.push(
300
+ failureLog?.push(
260
301
  `array lengths are not equal: value length ${source.length} !== matcher length ${matcher.length}`
261
302
  );
262
303
 
@@ -266,10 +307,12 @@ function matchArr<T extends any[], C, P, R>(
266
307
  // loop over arrays, matching every value
267
308
  let index = -1;
268
309
  while (++index < length) {
269
- if (!Object.is(source[index], matcher[index])) {
310
+ if (
311
+ !matchEntry(source[index], source, root, matcher[index], failureLog)
312
+ ) {
270
313
  // item did not match, return false
271
314
 
272
- errorCollector?.push(
315
+ failureLog?.push(
273
316
  `index ${index} does not match with value ${JSON.stringify(
274
317
  source[index]
275
318
  )} and matcher ${matcher[index]}`
@@ -283,6 +326,81 @@ function matchArr<T extends any[], C, P, R>(
283
326
  return true;
284
327
  }
285
328
 
329
+ // matcher is plain object
330
+
331
+ if (`every` in matcher) {
332
+ return matchCompound(
333
+ source,
334
+ parent,
335
+ root,
336
+ ['every', ...(matcher.every as any)],
337
+ failureLog
338
+ );
339
+ }
340
+ if (`some` in matcher) {
341
+ return matchCompound(
342
+ source,
343
+ parent,
344
+ root,
345
+ ['some', ...(matcher.some as any)],
346
+ failureLog
347
+ );
348
+ }
349
+ if (`none` in matcher) {
350
+ return matchCompound(
351
+ source,
352
+ parent,
353
+ root,
354
+ ['none', ...(matcher.none as any)],
355
+ failureLog
356
+ );
357
+ }
358
+ if (`single` in matcher) {
359
+ return matchCompound(
360
+ source,
361
+ parent,
362
+ root,
363
+ ['single', ...(matcher.single as any)],
364
+ failureLog
365
+ );
366
+ }
367
+ if (`someItem` in matcher) {
368
+ return matchTraversal(
369
+ source,
370
+ root,
371
+ 'someItem',
372
+ matcher.someItem as any,
373
+ failureLog
374
+ );
375
+ }
376
+ if (`everyItem` in matcher) {
377
+ return matchTraversal(
378
+ source,
379
+ root,
380
+ 'everyItem',
381
+ matcher.everyItem as any,
382
+ failureLog
383
+ );
384
+ }
385
+ if (`noneItem` in matcher) {
386
+ return matchTraversal(
387
+ source,
388
+ root,
389
+ 'noneItem',
390
+ matcher.noneItem as any,
391
+ failureLog
392
+ );
393
+ }
394
+ if (`singleItem` in matcher) {
395
+ return matchTraversal(
396
+ source,
397
+ root,
398
+ 'singleItem',
399
+ matcher.singleItem as any,
400
+ failureLog
401
+ );
402
+ }
403
+
286
404
  // matcher is plain object with index keys
287
405
 
288
406
  for (const index in matcher as any) {
@@ -291,7 +409,7 @@ function matchArr<T extends any[], C, P, R>(
291
409
  if (!(index in source)) {
292
410
  // source does not have item at given index
293
411
 
294
- errorCollector?.push(
412
+ failureLog?.push(
295
413
  `index ${index} does not exist in source ${JSON.stringify(
296
414
  source
297
415
  )} but should match matcher ${JSON.stringify(matcherAtIndex)}`
@@ -306,13 +424,13 @@ function matchArr<T extends any[], C, P, R>(
306
424
  source,
307
425
  root,
308
426
  matcherAtIndex,
309
- errorCollector
427
+ failureLog
310
428
  );
311
429
 
312
430
  if (!result) {
313
431
  // item did not match
314
432
 
315
- errorCollector?.push(
433
+ failureLog?.push(
316
434
  `index ${index} does not match with value ${JSON.stringify(
317
435
  (source as any)[index]
318
436
  )} and matcher ${JSON.stringify(matcherAtIndex)}`
@@ -335,11 +453,11 @@ function matchPlainObj<T, C, P, R>(
335
453
  parent: P,
336
454
  root: R,
337
455
  matcher: Match.Obj<T, C, P, R>,
338
- errorCollector: Match.ErrorCollector
456
+ failureLog?: Match.FailureLog
339
457
  ): boolean {
340
458
  if (Array.isArray(matcher)) {
341
459
  // the matcher is of compound type
342
- return matchCompound(source, parent, root, matcher as any, errorCollector);
460
+ return matchCompound(source, parent, root, matcher as any, failureLog);
343
461
  }
344
462
 
345
463
  // partial object props matcher
@@ -348,7 +466,7 @@ function matchPlainObj<T, C, P, R>(
348
466
  if (!(key in source)) {
349
467
  // the source does not have the given key
350
468
 
351
- errorCollector?.push(
469
+ failureLog?.push(
352
470
  `key ${key} is specified in matcher but not present in value ${JSON.stringify(
353
471
  source
354
472
  )}`
@@ -363,11 +481,11 @@ function matchPlainObj<T, C, P, R>(
363
481
  source,
364
482
  root,
365
483
  matcher[key],
366
- errorCollector
484
+ failureLog
367
485
  );
368
486
 
369
487
  if (!result) {
370
- errorCollector?.push(
488
+ failureLog?.push(
371
489
  `key ${key} does not match in value ${JSON.stringify(
372
490
  (source as any)[key]
373
491
  )} with matcher ${JSON.stringify(matcher[key])}`
@@ -389,7 +507,7 @@ function matchCompound<T, C, P, R>(
389
507
  parent: P,
390
508
  root: R,
391
509
  compound: [Match.CompoundType, ...Match.Entry<T, C, P, R>[]],
392
- errorCollector: string[] | undefined
510
+ failureLog?: Match.FailureLog
393
511
  ): boolean {
394
512
  // first item indicates compound match type
395
513
  const matchType = compound[0];
@@ -410,11 +528,11 @@ function matchCompound<T, C, P, R>(
410
528
  parent,
411
529
  root,
412
530
  compound[index] as Entry,
413
- errorCollector
531
+ failureLog
414
532
  );
415
533
 
416
534
  if (!result) {
417
- errorCollector?.push(
535
+ failureLog?.push(
418
536
  `in compound "every": match at index ${index} failed`
419
537
  );
420
538
 
@@ -432,11 +550,11 @@ function matchCompound<T, C, P, R>(
432
550
  parent,
433
551
  root,
434
552
  compound[index] as Entry,
435
- errorCollector
553
+ failureLog
436
554
  );
437
555
 
438
556
  if (result) {
439
- errorCollector?.push(
557
+ failureLog?.push(
440
558
  `in compound "none": match at index ${index} succeeded`
441
559
  );
442
560
 
@@ -456,12 +574,12 @@ function matchCompound<T, C, P, R>(
456
574
  parent,
457
575
  root,
458
576
  compound[index] as Entry,
459
- errorCollector
577
+ failureLog
460
578
  );
461
579
 
462
580
  if (result) {
463
581
  if (onePassed) {
464
- errorCollector?.push(
582
+ failureLog?.push(
465
583
  `in compound "single": multiple matches succeeded`
466
584
  );
467
585
 
@@ -473,7 +591,7 @@ function matchCompound<T, C, P, R>(
473
591
  }
474
592
 
475
593
  if (!onePassed) {
476
- errorCollector?.push(`in compound "single": no matches succeeded`);
594
+ failureLog?.push(`in compound "single": no matches succeeded`);
477
595
  }
478
596
 
479
597
  return onePassed;
@@ -486,7 +604,7 @@ function matchCompound<T, C, P, R>(
486
604
  parent,
487
605
  root,
488
606
  compound[index] as Entry,
489
- errorCollector
607
+ failureLog
490
608
  );
491
609
 
492
610
  if (result) {
@@ -494,9 +612,87 @@ function matchCompound<T, C, P, R>(
494
612
  }
495
613
  }
496
614
 
497
- errorCollector?.push(`in compound "some": no matches succeeded`);
615
+ failureLog?.push(`in compound "some": no matches succeeded`);
616
+
617
+ return false;
618
+ }
619
+ }
620
+ }
621
+
622
+ function matchTraversal<T extends any[], C extends any[], R>(
623
+ source: T,
624
+ root: R,
625
+ matchType: Match.ArrayTraversalType,
626
+ matcher: Match.Entry<T[keyof T], C[keyof C], T, R>,
627
+ failureLog?: Match.FailureLog
628
+ ): boolean {
629
+ let index = -1;
630
+ const length = source.length;
631
+
632
+ switch (matchType) {
633
+ case 'someItem': {
634
+ while (++index < length) {
635
+ if (matchEntry(source[index], source, root, matcher, failureLog)) {
636
+ return true;
637
+ }
638
+ }
639
+
640
+ failureLog?.push(
641
+ `in array traversal "someItem": no items matched given matcher`
642
+ );
498
643
 
499
644
  return false;
500
645
  }
646
+ case 'everyItem': {
647
+ while (++index < length) {
648
+ if (!matchEntry(source[index], source, root, matcher, failureLog)) {
649
+ failureLog?.push(
650
+ `in array traversal "everyItem": at least one item did not match given matcher`
651
+ );
652
+ return false;
653
+ }
654
+ }
655
+
656
+ return true;
657
+ }
658
+ case 'noneItem': {
659
+ while (++index < length) {
660
+ if (matchEntry(source[index], source, root, matcher, failureLog)) {
661
+ failureLog?.push(
662
+ `in array traversal "noneItem": at least one item matched given matcher`
663
+ );
664
+ return false;
665
+ }
666
+ }
667
+
668
+ return true;
669
+ }
670
+ case 'singleItem': {
671
+ let singleMatched = false;
672
+
673
+ while (++index < length) {
674
+ if (matchEntry(source[index], source, root, matcher, failureLog)) {
675
+ if (singleMatched) {
676
+ failureLog?.push(
677
+ `in array traversal "singleItem": more than one item matched given matcher`
678
+ );
679
+
680
+ return false;
681
+ }
682
+
683
+ singleMatched = true;
684
+ }
685
+ }
686
+
687
+ if (!singleMatched) {
688
+ failureLog?.push(
689
+ `in array traversal "singleItem": no item matched given matcher`
690
+ );
691
+
692
+ return false;
693
+ }
694
+
695
+ return true;
696
+ }
501
697
  }
502
698
  }