1
1
import { Matcher , MatchResult } from './matcher' ;
2
2
import { AbsentMatch } from './private/matchers/absent' ;
3
+ import { sortKeyComparator } from './private/sorting' ;
4
+ import { SparseMatrix } from './private/sparse-matrix' ;
3
5
import { getType } from './private/type' ;
4
6
5
7
/**
@@ -196,18 +198,53 @@ class ArrayMatch extends Matcher {
196
198
message : `Expected type array but received ${ getType ( actual ) } ` ,
197
199
} ) ;
198
200
}
199
- if ( ! this . subsequence && this . pattern . length !== actual . length ) {
200
- return new MatchResult ( actual ) . recordFailure ( {
201
+
202
+ return this . subsequence ? this . testSubsequence ( actual ) : this . testFullArray ( actual ) ;
203
+ }
204
+
205
+ private testFullArray ( actual : Array < any > ) : MatchResult {
206
+ const result = new MatchResult ( actual ) ;
207
+
208
+ let i = 0 ;
209
+ for ( ; i < this . pattern . length && i < actual . length ; i ++ ) {
210
+ const patternElement = this . pattern [ i ] ;
211
+ const matcher = Matcher . isMatcher ( patternElement )
212
+ ? patternElement
213
+ : new LiteralMatch ( this . name , patternElement , { partialObjects : this . partialObjects } ) ;
214
+
215
+ const innerResult = matcher . test ( actual [ i ] ) ;
216
+ result . compose ( `${ i } ` , innerResult ) ;
217
+ }
218
+
219
+ if ( i < this . pattern . length ) {
220
+ result . recordFailure ( {
201
221
matcher : this ,
202
- path : [ ] ,
203
- message : `Expected array of length ${ this . pattern . length } but received ${ actual . length } ` ,
222
+ message : `Not enough elements in array (expecting ${ this . pattern . length } , got ${ actual . length } )` ,
223
+ path : [ ` ${ i } ` ] ,
204
224
} ) ;
205
225
}
226
+ if ( i < actual . length ) {
227
+ result . recordFailure ( {
228
+ matcher : this ,
229
+ message : `Too many elements in array (expecting ${ this . pattern . length } , got ${ actual . length } )` ,
230
+ path : [ `${ i } ` ] ,
231
+ } ) ;
232
+ }
233
+
234
+ return result ;
235
+ }
236
+
237
+ private testSubsequence ( actual : Array < any > ) : MatchResult {
238
+ const result = new MatchResult ( actual ) ;
239
+
240
+ // For subsequences, there is a lot of testing and backtracking that happens
241
+ // here, keep track of it all so we can report in a sensible amount of
242
+ // detail on what we did if the match happens to fail.
206
243
207
244
let patternIdx = 0 ;
208
245
let actualIdx = 0 ;
246
+ const matches = new SparseMatrix < MatchResult > ( ) ;
209
247
210
- const result = new MatchResult ( actual ) ;
211
248
while ( patternIdx < this . pattern . length && actualIdx < actual . length ) {
212
249
const patternElement = this . pattern [ patternIdx ] ;
213
250
@@ -216,30 +253,59 @@ class ArrayMatch extends Matcher {
216
253
: new LiteralMatch ( this . name , patternElement , { partialObjects : this . partialObjects } ) ;
217
254
218
255
const matcherName = matcher . name ;
219
- if ( this . subsequence && ( matcherName == 'absent' || matcherName == 'anyValue' ) ) {
256
+ if ( matcherName == 'absent' || matcherName == 'anyValue' ) {
220
257
// array subsequence matcher is not compatible with anyValue() or absent() matcher. They don't make sense to be used together.
221
258
throw new Error ( `The Matcher ${ matcherName } () cannot be nested within arrayWith()` ) ;
222
259
}
223
260
224
261
const innerResult = matcher . test ( actual [ actualIdx ] ) ;
262
+ matches . set ( patternIdx , actualIdx , innerResult ) ;
225
263
226
- if ( ! this . subsequence || ! innerResult . hasFailed ( ) ) {
227
- result . compose ( `[${ actualIdx } ]` , innerResult ) ;
264
+ actualIdx ++ ;
265
+ if ( innerResult . isSuccess ) {
266
+ result . compose ( `${ actualIdx } ` , innerResult ) ; // Record any captures
228
267
patternIdx ++ ;
229
- actualIdx ++ ;
230
- } else {
231
- actualIdx ++ ;
232
268
}
233
269
}
234
270
235
- for ( ; patternIdx < this . pattern . length ; patternIdx ++ ) {
236
- const pattern = this . pattern [ patternIdx ] ;
237
- const element = ( Matcher . isMatcher ( pattern ) || typeof pattern === 'object' ) ? ' ' : ` [${ pattern } ] ` ;
238
- result . recordFailure ( {
239
- matcher : this ,
240
- path : [ ] ,
241
- message : `Missing element${ element } at pattern index ${ patternIdx } ` ,
242
- } ) ;
271
+ // If we haven't matched all patterns:
272
+ // - Report on each one that did match on where it matched (perhaps it was wrong)
273
+ // - Report the closest match for the failing one
274
+ if ( patternIdx < this . pattern . length ) {
275
+ // Succeeded Pattern Index
276
+ for ( let spi = 0 ; spi < patternIdx ; spi ++ ) {
277
+ const foundMatch = matches . row ( spi ) . find ( ( [ , r ] ) => r . isSuccess ) ;
278
+ if ( ! foundMatch ) { continue ; } // Should never fail but let's be defensive
279
+
280
+ const [ index ] = foundMatch ;
281
+
282
+ result . compose ( `${ index } ` , new MatchResult ( actual [ index ] ) . recordFailure ( {
283
+ matcher : this ,
284
+ message : `arrayWith pattern ${ spi } matched here` ,
285
+ path : [ ] ,
286
+ cost : 0 , // This is an informational message so it would be unfair to assign it cost
287
+ } ) ) ;
288
+ }
289
+
290
+ const failedMatches = matches . row ( patternIdx ) ;
291
+ failedMatches . sort ( sortKeyComparator ( ( [ i , r ] ) => [ r . failCost , i ] ) ) ;
292
+ if ( failedMatches . length > 0 ) {
293
+ const [ index , innerResult ] = failedMatches [ 0 ] ;
294
+ result . recordFailure ( {
295
+ matcher : this ,
296
+ message : `Could not match arrayWith pattern ${ patternIdx } . This is the closest match` ,
297
+ path : [ `${ index } ` ] ,
298
+ cost : 0 , // Informational message
299
+ } ) ;
300
+ result . compose ( `${ index } ` , innerResult ) ;
301
+ } else {
302
+ // The previous matcher matched at the end of the pattern and we didn't even get to try anything
303
+ result . recordFailure ( {
304
+ matcher : this ,
305
+ message : `Could not match arrayWith pattern ${ patternIdx } . No more elements to try` ,
306
+ path : [ `${ actual . length } ` ] ,
307
+ } ) ;
308
+ }
243
309
}
244
310
245
311
return result ;
@@ -288,8 +354,8 @@ class ObjectMatch extends Matcher {
288
354
if ( ! ( a in this . pattern ) ) {
289
355
result . recordFailure ( {
290
356
matcher : this ,
291
- path : [ `/ ${ a } ` ] ,
292
- message : ' Unexpected key' ,
357
+ path : [ a ] ,
358
+ message : ` Unexpected key ${ a } ` ,
293
359
} ) ;
294
360
}
295
361
}
@@ -299,16 +365,16 @@ class ObjectMatch extends Matcher {
299
365
if ( ! ( patternKey in actual ) && ! ( patternVal instanceof AbsentMatch ) ) {
300
366
result . recordFailure ( {
301
367
matcher : this ,
302
- path : [ `/ ${ patternKey } ` ] ,
303
- message : `Missing key '${ patternKey } ' among { ${ Object . keys ( actual ) . join ( ',' ) } } ` ,
368
+ path : [ patternKey ] ,
369
+ message : `Missing key '${ patternKey } '` ,
304
370
} ) ;
305
371
continue ;
306
372
}
307
373
const matcher = Matcher . isMatcher ( patternVal ) ?
308
374
patternVal :
309
375
new LiteralMatch ( this . name , patternVal , { partialObjects : this . partial } ) ;
310
376
const inner = matcher . test ( actual [ patternKey ] ) ;
311
- result . compose ( `/ ${ patternKey } ` , inner ) ;
377
+ result . compose ( patternKey , inner ) ;
312
378
}
313
379
314
380
return result ;
@@ -324,35 +390,38 @@ class SerializedJson extends Matcher {
324
390
} ;
325
391
326
392
public test ( actual : any ) : MatchResult {
327
- const result = new MatchResult ( actual ) ;
328
393
if ( getType ( actual ) !== 'string' ) {
329
- result . recordFailure ( {
394
+ return new MatchResult ( actual ) . recordFailure ( {
330
395
matcher : this ,
331
396
path : [ ] ,
332
397
message : `Expected JSON as a string but found ${ getType ( actual ) } ` ,
333
398
} ) ;
334
- return result ;
335
399
}
336
400
let parsed ;
337
401
try {
338
402
parsed = JSON . parse ( actual ) ;
339
403
} catch ( err ) {
340
404
if ( err instanceof SyntaxError ) {
341
- result . recordFailure ( {
405
+ return new MatchResult ( actual ) . recordFailure ( {
342
406
matcher : this ,
343
407
path : [ ] ,
344
408
message : `Invalid JSON string: ${ actual } ` ,
345
409
} ) ;
346
- return result ;
347
410
} else {
348
411
throw err ;
349
412
}
350
413
}
351
414
352
415
const matcher = Matcher . isMatcher ( this . pattern ) ? this . pattern : new LiteralMatch ( this . name , this . pattern ) ;
353
416
const innerResult = matcher . test ( parsed ) ;
354
- result . compose ( `(${ this . name } )` , innerResult ) ;
355
- return result ;
417
+ if ( innerResult . hasFailed ( ) ) {
418
+ innerResult . recordFailure ( {
419
+ matcher : this ,
420
+ path : [ ] ,
421
+ message : 'Encoded JSON value does not match' ,
422
+ } ) ;
423
+ }
424
+ return innerResult ;
356
425
}
357
426
}
358
427
0 commit comments