|
| 1 | +import { Argument, AllArguments } from './Arguments' |
| 2 | + |
| 3 | +type ArgumentsClass = 'primitives-only' | 'with-single-argument' | 'all-arguments' |
| 4 | +export class RecordedArguments { |
| 5 | + private _argumentsClass: ArgumentsClass |
| 6 | + private _value?: any[] |
| 7 | + private readonly _hasNoArguments: boolean = false |
| 8 | + |
| 9 | + private constructor(rawArguments?: any[]) { |
| 10 | + if (typeof rawArguments === 'undefined') { |
| 11 | + this._hasNoArguments = true |
| 12 | + return this |
| 13 | + } |
| 14 | + |
| 15 | + this._argumentsClass = this.classifyArguments(rawArguments) |
| 16 | + this._value = rawArguments |
| 17 | + } |
| 18 | + |
| 19 | + static from(rawArguments: any[]): RecordedArguments { |
| 20 | + return new this(rawArguments) |
| 21 | + } |
| 22 | + |
| 23 | + static noArguments(): RecordedArguments { |
| 24 | + return new this() |
| 25 | + } |
| 26 | + |
| 27 | + static sort<T extends { arguments: RecordedArguments }>(objectWithArguments: T[]): T[] { |
| 28 | + return objectWithArguments.sort((a, b) => { |
| 29 | + const aClass = a.arguments.argumentsClass |
| 30 | + const bClass = b.arguments.argumentsClass |
| 31 | + |
| 32 | + if (aClass === bClass) return 0 |
| 33 | + if (aClass === 'primitives-only') return -1 |
| 34 | + if (bClass === 'primitives-only') return 1 |
| 35 | + |
| 36 | + if (aClass === 'with-single-argument') return -1 |
| 37 | + if (bClass === 'with-single-argument') return 1 |
| 38 | + |
| 39 | + if (aClass === 'all-arguments') return -1 |
| 40 | + return 1 |
| 41 | + }) |
| 42 | + } |
| 43 | + |
| 44 | + get argumentsClass(): ArgumentsClass { |
| 45 | + return this._argumentsClass |
| 46 | + } |
| 47 | + |
| 48 | + get value(): any[] | undefined { |
| 49 | + return this._value |
| 50 | + } |
| 51 | + |
| 52 | + get hasNoArguments(): boolean { |
| 53 | + return this._hasNoArguments |
| 54 | + } |
| 55 | + |
| 56 | + public match(other: RecordedArguments) { |
| 57 | + if (this.hasNoArguments && other.hasNoArguments) return true |
| 58 | + if (this.argumentsClass === 'all-arguments' || other.argumentsClass === 'all-arguments') return true |
| 59 | + if (this.value.length !== this.value.length) return false |
| 60 | + |
| 61 | + return this.value.every((argument, index) => this.areArgumentsEqual(argument, other.value[index])) |
| 62 | + } |
| 63 | + |
| 64 | + private classifyArguments(rawArguments: any[]): ArgumentsClass { |
| 65 | + const allPrimitives = rawArguments.every(arg => !(arg instanceof Argument || arg instanceof AllArguments)) |
| 66 | + if (allPrimitives) return 'primitives-only' |
| 67 | + |
| 68 | + const hasSingleArgument = rawArguments.some(arg => arg instanceof Argument) |
| 69 | + if (hasSingleArgument) return 'with-single-argument' |
| 70 | + |
| 71 | + return 'all-arguments' |
| 72 | + } |
| 73 | + |
| 74 | + private areArgumentsEqual(a: any, b: any): boolean { |
| 75 | + if (a instanceof Argument && b instanceof Argument) return false |
| 76 | + if (a instanceof Argument) return a.matches(b) |
| 77 | + if (b instanceof Argument) return b.matches(a) |
| 78 | + return this.areArgumentsDeepEqual(a, b) |
| 79 | + } |
| 80 | + |
| 81 | + private areArgumentsDeepEqual(a: any, b: any, previousSeenObjects?: WeakSet<object>): boolean { |
| 82 | + const seenObjects = previousSeenObjects ?? new WeakSet() |
| 83 | + |
| 84 | + const seenA = seenObjects.has(a) |
| 85 | + const seenB = seenObjects.has(b) |
| 86 | + const aIsNonNullObject = typeof a === 'object' && a !== null |
| 87 | + const bIsNonNullObject = typeof b === 'object' && b !== null |
| 88 | + |
| 89 | + if (!seenA && aIsNonNullObject) seenObjects.add(a) |
| 90 | + if (!seenB && bIsNonNullObject) seenObjects.add(b) |
| 91 | + |
| 92 | + const aEqualsB = a === b |
| 93 | + if ((seenA && seenB) || aEqualsB) return aEqualsB |
| 94 | + |
| 95 | + if (aIsNonNullObject && bIsNonNullObject) { |
| 96 | + if (a.constructor !== b.constructor) return false |
| 97 | + const objectAKeys = Object.keys(a) |
| 98 | + if (objectAKeys.length !== Object.keys(b).length) return false |
| 99 | + return objectAKeys.every(key => this.areArgumentsDeepEqual(a[key], b[key], seenObjects)) |
| 100 | + } |
| 101 | + |
| 102 | + return aEqualsB |
| 103 | + } |
| 104 | +} |
0 commit comments