Skip to content

Commit a2bb99f

Browse files
refactor substitute core
1 parent 8514d14 commit a2bb99f

File tree

5 files changed

+152
-190
lines changed

5 files changed

+152
-190
lines changed

src/Substitute.ts

Lines changed: 10 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,25 @@
1-
import { inspect, InspectOptions } from 'util'
1+
import { DisabledSubstituteObject, ObjectSubstitute } from './Transformations'
2+
import { SubstituteNode } from './SubstituteNode'
23

3-
import { SubstituteBase } from './SubstituteBase'
4-
import { createSubstituteProxy } from './SubstituteProxy'
5-
import { Recorder } from './Recorder'
6-
import { DisabledSubstituteObject, ObjectSubstitute, OmitProxyMethods } from './Transformations'
7-
8-
export type SubstituteOf<T extends Object> = ObjectSubstitute<OmitProxyMethods<T>, T> & T
9-
type Instantiable<T> = { [SubstituteBase.instance]?: T }
10-
11-
export class Substitute extends SubstituteBase {
12-
private _proxy: Substitute
13-
private _recorder: Recorder = new Recorder()
14-
private _context: { disableAssertions: boolean } = { disableAssertions: false }
15-
16-
constructor() {
17-
super()
18-
this._proxy = createSubstituteProxy(
19-
this,
20-
{
21-
get: (target, _property, _, node) => {
22-
if (target.context.disableAssertions) node.disableAssertions()
23-
}
24-
// apply: (target, _, args, __, proxy) => {
25-
// const rootProperty = proxy.get(target, '()', proxy) TODO: Implement to support callable interfaces
26-
// return Reflect.apply(rootProperty, rootProperty, args)
27-
// }
28-
}
29-
)
30-
}
4+
export type SubstituteOf<T> = ObjectSubstitute<T> & T
5+
type InstantiableSubstitute = SubstituteOf<unknown> & { [SubstituteNode.instance]?: SubstituteNode }
316

7+
export class Substitute {
328
static for<T>(): SubstituteOf<T> {
33-
const substitute = new this()
9+
const substitute = SubstituteNode.createRoot()
3410
return substitute.proxy as unknown as SubstituteOf<T>
3511
}
3612

37-
static disableFor<T extends SubstituteOf<unknown> & Instantiable<Substitute>>(substituteProxy: T): DisabledSubstituteObject<T> {
38-
const substitute = substituteProxy[SubstituteBase.instance]
13+
static disableFor<T extends InstantiableSubstitute>(substituteProxy: T): DisabledSubstituteObject<T> {
14+
const substitute = substituteProxy[SubstituteNode.instance]
3915

4016
const disableProxy = <
4117
TParameters extends unknown[],
4218
TReturnType extends unknown
4319
>(reflection: (...args: TParameters) => TReturnType): typeof reflection => (...args) => {
44-
substitute.context.disableAssertions = true
20+
substitute.rootContext.substituteMethodsEnabled = false
4521
const reflectionResult = reflection(...args)
46-
substitute.context.disableAssertions = false
22+
substitute.rootContext.substituteMethodsEnabled = true
4723
return reflectionResult
4824
}
4925

@@ -59,23 +35,4 @@ export class Substitute extends SubstituteBase {
5935
}
6036
}) as DisabledSubstituteObject<T>
6137
}
62-
63-
public get proxy() {
64-
return this._proxy
65-
}
66-
67-
public get recorder() {
68-
return this._recorder
69-
}
70-
71-
public get context() {
72-
return this._context
73-
}
74-
75-
protected printableForm(_: number, options: InspectOptions): string {
76-
const records = inspect(this.recorder, options)
77-
78-
const instanceName = 'Substitute' // Substitute<FooThing>
79-
return instanceName + ' {' + records + '\n}'
80-
}
8138
}

src/SubstituteBase.ts

Lines changed: 0 additions & 35 deletions
This file was deleted.

src/SubstituteNode.ts

Lines changed: 101 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,79 @@
1-
import { inspect, InspectOptions } from 'util'
1+
import { inspect, InspectOptions, types } from 'util'
22

3-
import { PropertyType, isSubstitutionMethod, isAssertionMethod, AssertionMethod, SubstitutionMethod, textModifier, ConfigurationMethod, isSubstituteMethod } from './Utilities'
4-
import { SubstituteException } from './SubstituteException'
5-
import { RecordedArguments } from './RecordedArguments'
63
import { SubstituteNodeBase } from './SubstituteNodeBase'
7-
import { SubstituteBase } from './SubstituteBase'
8-
import { createSubstituteProxy } from './SubstituteProxy'
9-
import { ClearType } from './Transformations'
4+
import { RecordedArguments } from './RecordedArguments'
5+
import { ClearType as ClearTypeMap, PropertyType as PropertyTypeMap, isAssertionMethod, isSubstituteMethod, isSubstitutionMethod, textModifier } from './Utilities'
6+
import { SubstituteException } from './SubstituteException'
7+
import type { FilterFunction, SubstituteContext, SubstitutionMethod, ClearType, PropertyType } from './Types'
8+
9+
const instance = Symbol('Substitute:Instance')
10+
type SpecialProperty = typeof instance | typeof inspect.custom | 'then'
11+
type RootContext = { substituteMethodsEnabled: boolean }
1012

11-
type SubstituteContext = SubstitutionMethod | AssertionMethod | ConfigurationMethod | 'none'
12-
const clearTypeToFilterMap: Record<ClearType, (node: SubstituteNode) => boolean> = {
13+
const clearTypeToFilterMap: Record<ClearType, FilterFunction<SubstituteNode>> = {
1314
all: () => true,
1415
receivedCalls: node => !node.hasContext,
1516
substituteValues: node => node.isSubstitution
1617
}
1718

18-
export class SubstituteNode extends SubstituteNodeBase<SubstituteNode> {
19+
export class SubstituteNode extends SubstituteNodeBase {
1920
private _proxy: SubstituteNode
20-
private _propertyType: PropertyType = PropertyType.property
21+
private _rootContext: RootContext
22+
23+
private _propertyType: PropertyType = PropertyTypeMap.Property
2124
private _accessorType: 'get' | 'set' = 'get'
2225
private _recordedArguments: RecordedArguments = RecordedArguments.none()
2326

2427
private _context: SubstituteContext = 'none'
25-
private _disabledAssertions: boolean = false
28+
private _disabledSubstituteMethods: boolean = false
2629

27-
constructor(property: PropertyKey, parent: SubstituteNode | SubstituteBase) {
28-
super(property, parent)
29-
this._proxy = createSubstituteProxy(
30+
private constructor(key: PropertyKey, parent?: SubstituteNode) {
31+
super(key, parent)
32+
if (this.isRoot()) this._rootContext = { substituteMethodsEnabled: true }
33+
if (this.isIntermediateNode()) this._rootContext = this.root.rootContext
34+
this._proxy = new Proxy(
3035
this,
3136
{
32-
get: (node, _, __, nextNode) => {
33-
if (node.isAssertion) nextNode.executeAssertion()
37+
get: function (target, property) {
38+
if (target.isSpecialProperty(property)) return target.evaluateSpecialProperty(property)
39+
const newNode = SubstituteNode.createChild(property, target)
40+
if (target.isRoot() && !target.rootContext.substituteMethodsEnabled) newNode.disableSubstituteMethods()
41+
if (target.isIntermediateNode() && target.isAssertion) newNode.executeAssertion()
42+
return newNode.read()
3443
},
35-
set: (node, _, __, ___, nextNode) => {
36-
if (node.isAssertion) nextNode.executeAssertion()
44+
set: function (target, property, value) {
45+
const newNode = SubstituteNode.createChild(property, target)
46+
newNode.write(value)
47+
if (target.isAssertion) newNode.executeAssertion()
48+
return true
3749
},
38-
apply: (node, _, rawArguments) => {
39-
node.handleMethod(rawArguments)
40-
if (node.context === 'clearSubstitute') return node.clear()
41-
return node.parent?.isAssertion ?? false ? node.executeAssertion() : node.read()
50+
apply: function (target, _thisArg, rawArguments) {
51+
target.handleMethod(rawArguments)
52+
if (target.hasContext) target.handleSpecialContext()
53+
return (target.parent?.isAssertion ?? false) ? target.executeAssertion() : target.read()
4254
}
4355
}
4456
)
4557
}
4658

59+
public static instance: typeof instance = instance
60+
61+
public static createRoot(): SubstituteNode {
62+
return new this('*Substitute<Root>')
63+
}
64+
65+
public static createChild(key: PropertyKey, parent: SubstituteNode): SubstituteNode {
66+
return new this(key, parent)
67+
}
68+
4769
public get proxy() {
4870
return this._proxy
4971
}
5072

73+
public get rootContext() {
74+
return this._rootContext
75+
}
76+
5177
get context(): SubstituteContext {
5278
return this._context
5379
}
@@ -80,20 +106,20 @@ export class SubstituteNode extends SubstituteNodeBase<SubstituteNode> {
80106
return this._recordedArguments
81107
}
82108

83-
public get disabledAssertions() {
84-
return this._disabledAssertions
109+
public get disabledSubstituteMethods() {
110+
return this._disabledSubstituteMethods
85111
}
86112

87113
public assignContext(context: SubstituteContext): void {
88114
this._context = context
89115
}
90116

91-
public disableAssertions() {
92-
this._disabledAssertions = true
117+
public disableSubstituteMethods() {
118+
this._disabledSubstituteMethods = true
93119
}
94120

95121
public read(): SubstituteNode | void | never {
96-
if (this.parent?.isSubstitution ?? false) return
122+
if ((this.parent?.isSubstitution ?? false) || this.context === 'clearSubstitute') return
97123
if (this.isAssertion) return this.proxy
98124

99125
const mostSuitableSubstitution = this.getMostSuitableSubstitution()
@@ -108,9 +134,9 @@ export class SubstituteNode extends SubstituteNodeBase<SubstituteNode> {
108134
}
109135

110136
public clear() {
111-
const clearType: ClearType = this.recordedArguments.value[0] ?? 'all'
112-
const filter = clearTypeToFilterMap[clearType] as (node: SubstituteNodeBase) => boolean
113-
this.root.recorder.clearRecords(filter)
137+
const clearType: ClearType = this.recordedArguments.value[0] ?? ClearTypeMap.All
138+
const filter = clearTypeToFilterMap[clearType]
139+
this.recorder.clearRecords(filter)
114140
}
115141

116142
public executeSubstitution(contextArguments: RecordedArguments) {
@@ -122,7 +148,7 @@ export class SubstituteNode extends SubstituteNodeBase<SubstituteNode> {
122148
case 'throws':
123149
throw substitutionValue
124150
case 'mimicks':
125-
const argumentsToApply = this.propertyType === PropertyType.property ? [] : contextArguments.value
151+
const argumentsToApply = this.propertyType === PropertyTypeMap.Property ? [] : contextArguments.value
126152
return substitutionValue(...argumentsToApply)
127153
case 'resolves':
128154
return Promise.resolve(substitutionValue)
@@ -136,8 +162,8 @@ export class SubstituteNode extends SubstituteNodeBase<SubstituteNode> {
136162
}
137163

138164
public executeAssertion(): void | never {
139-
const siblings = [...this.getAllSiblings().filter(n => !n.hasContext && n.accessorType === this.accessorType)]
140165
if (!this.isIntermediateNode()) throw new Error('Not possible')
166+
const siblings = [...this.getAllSiblings().filter(n => !n.hasContext && n.accessorType === this.accessorType)]
141167

142168
const expectedCount = this.parent.recordedArguments.value[0] ?? undefined
143169
const finiteExpectation = expectedCount !== undefined
@@ -168,14 +194,20 @@ export class SubstituteNode extends SubstituteNodeBase<SubstituteNode> {
168194
}
169195

170196
public handleMethod(rawArguments: any[]): void {
171-
this._propertyType = PropertyType.method
197+
this._propertyType = PropertyTypeMap.Method
172198
this._recordedArguments = RecordedArguments.from(rawArguments)
173-
if (!isSubstituteMethod(this.property)) return
199+
this.tryToAssignContext()
200+
}
174201

202+
private tryToAssignContext() {
203+
if (!isSubstituteMethod(this.property)) return
175204
if (this.isIntermediateNode() && isSubstitutionMethod(this.property)) return this.parent.assignContext(this.property)
176-
if (this.disabledAssertions || !this.isHead()) return
177-
205+
if (this.disabledSubstituteMethods) return
178206
this.assignContext(this.property)
207+
}
208+
209+
private handleSpecialContext(): void {
210+
if (this.context === 'clearSubstitute') return this.clear()
179211
if (this.context === 'didNotReceive') this._recordedArguments = RecordedArguments.from([0])
180212
}
181213

@@ -188,7 +220,38 @@ export class SubstituteNode extends SubstituteNodeBase<SubstituteNode> {
188220
return sortedNodes[0]
189221
}
190222

191-
protected printableForm(_: number, options: InspectOptions): string {
223+
private isSpecialProperty(property: PropertyKey): property is SpecialProperty {
224+
return property === SubstituteNode.instance || property === inspect.custom || property === 'then'
225+
}
226+
227+
private evaluateSpecialProperty(property: SpecialProperty) {
228+
switch (property) {
229+
case SubstituteNode.instance:
230+
return this
231+
case inspect.custom:
232+
return this.printableForm.bind(this)
233+
case 'then':
234+
return
235+
default:
236+
throw SubstituteException.generic(`Evaluation of special property ${property} is not implemented`)
237+
}
238+
}
239+
240+
public [inspect.custom](...args: [_: number, options: InspectOptions]): string {
241+
return types.isProxy(this) ? this[inspect.custom](...args) : this.printableForm(...args)
242+
}
243+
244+
private printableForm(_: number, options: InspectOptions): string {
245+
return this.isRoot() ? this.printRootNode(options) : this.printNode(options)
246+
}
247+
248+
private printRootNode(options: InspectOptions): string {
249+
const records = inspect(this.recorder, options)
250+
const instanceName = '*Substitute<Root>' // Substitute<FooThing>
251+
return instanceName + ' {' + records + '\n}'
252+
}
253+
254+
private printNode(options: InspectOptions): string {
192255
const hasContext = this.hasContext
193256
const args = inspect(this.recordedArguments, options)
194257
const label = this.isSubstitution

0 commit comments

Comments
 (0)