Skip to content

Commit

Permalink
refactor substitute core
Browse files Browse the repository at this point in the history
  • Loading branch information
notanengineercom committed Oct 7, 2022
1 parent 8514d14 commit a2bb99f
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 190 deletions.
63 changes: 10 additions & 53 deletions src/Substitute.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,25 @@
import { inspect, InspectOptions } from 'util'
import { DisabledSubstituteObject, ObjectSubstitute } from './Transformations'
import { SubstituteNode } from './SubstituteNode'

import { SubstituteBase } from './SubstituteBase'
import { createSubstituteProxy } from './SubstituteProxy'
import { Recorder } from './Recorder'
import { DisabledSubstituteObject, ObjectSubstitute, OmitProxyMethods } from './Transformations'

export type SubstituteOf<T extends Object> = ObjectSubstitute<OmitProxyMethods<T>, T> & T
type Instantiable<T> = { [SubstituteBase.instance]?: T }

export class Substitute extends SubstituteBase {
private _proxy: Substitute
private _recorder: Recorder = new Recorder()
private _context: { disableAssertions: boolean } = { disableAssertions: false }

constructor() {
super()
this._proxy = createSubstituteProxy(
this,
{
get: (target, _property, _, node) => {
if (target.context.disableAssertions) node.disableAssertions()
}
// apply: (target, _, args, __, proxy) => {
// const rootProperty = proxy.get(target, '()', proxy) TODO: Implement to support callable interfaces
// return Reflect.apply(rootProperty, rootProperty, args)
// }
}
)
}
export type SubstituteOf<T> = ObjectSubstitute<T> & T
type InstantiableSubstitute = SubstituteOf<unknown> & { [SubstituteNode.instance]?: SubstituteNode }

export class Substitute {
static for<T>(): SubstituteOf<T> {
const substitute = new this()
const substitute = SubstituteNode.createRoot()
return substitute.proxy as unknown as SubstituteOf<T>
}

static disableFor<T extends SubstituteOf<unknown> & Instantiable<Substitute>>(substituteProxy: T): DisabledSubstituteObject<T> {
const substitute = substituteProxy[SubstituteBase.instance]
static disableFor<T extends InstantiableSubstitute>(substituteProxy: T): DisabledSubstituteObject<T> {
const substitute = substituteProxy[SubstituteNode.instance]

const disableProxy = <
TParameters extends unknown[],
TReturnType extends unknown
>(reflection: (...args: TParameters) => TReturnType): typeof reflection => (...args) => {
substitute.context.disableAssertions = true
substitute.rootContext.substituteMethodsEnabled = false
const reflectionResult = reflection(...args)
substitute.context.disableAssertions = false
substitute.rootContext.substituteMethodsEnabled = true
return reflectionResult
}

Expand All @@ -59,23 +35,4 @@ export class Substitute extends SubstituteBase {
}
}) as DisabledSubstituteObject<T>
}

public get proxy() {
return this._proxy
}

public get recorder() {
return this._recorder
}

public get context() {
return this._context
}

protected printableForm(_: number, options: InspectOptions): string {
const records = inspect(this.recorder, options)

const instanceName = 'Substitute' // Substitute<FooThing>
return instanceName + ' {' + records + '\n}'
}
}
35 changes: 0 additions & 35 deletions src/SubstituteBase.ts

This file was deleted.

139 changes: 101 additions & 38 deletions src/SubstituteNode.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,79 @@
import { inspect, InspectOptions } from 'util'
import { inspect, InspectOptions, types } from 'util'

import { PropertyType, isSubstitutionMethod, isAssertionMethod, AssertionMethod, SubstitutionMethod, textModifier, ConfigurationMethod, isSubstituteMethod } from './Utilities'
import { SubstituteException } from './SubstituteException'
import { RecordedArguments } from './RecordedArguments'
import { SubstituteNodeBase } from './SubstituteNodeBase'
import { SubstituteBase } from './SubstituteBase'
import { createSubstituteProxy } from './SubstituteProxy'
import { ClearType } from './Transformations'
import { RecordedArguments } from './RecordedArguments'
import { ClearType as ClearTypeMap, PropertyType as PropertyTypeMap, isAssertionMethod, isSubstituteMethod, isSubstitutionMethod, textModifier } from './Utilities'
import { SubstituteException } from './SubstituteException'
import type { FilterFunction, SubstituteContext, SubstitutionMethod, ClearType, PropertyType } from './Types'

const instance = Symbol('Substitute:Instance')
type SpecialProperty = typeof instance | typeof inspect.custom | 'then'
type RootContext = { substituteMethodsEnabled: boolean }

type SubstituteContext = SubstitutionMethod | AssertionMethod | ConfigurationMethod | 'none'
const clearTypeToFilterMap: Record<ClearType, (node: SubstituteNode) => boolean> = {
const clearTypeToFilterMap: Record<ClearType, FilterFunction<SubstituteNode>> = {
all: () => true,
receivedCalls: node => !node.hasContext,
substituteValues: node => node.isSubstitution
}

export class SubstituteNode extends SubstituteNodeBase<SubstituteNode> {
export class SubstituteNode extends SubstituteNodeBase {
private _proxy: SubstituteNode
private _propertyType: PropertyType = PropertyType.property
private _rootContext: RootContext

private _propertyType: PropertyType = PropertyTypeMap.Property
private _accessorType: 'get' | 'set' = 'get'
private _recordedArguments: RecordedArguments = RecordedArguments.none()

private _context: SubstituteContext = 'none'
private _disabledAssertions: boolean = false
private _disabledSubstituteMethods: boolean = false

constructor(property: PropertyKey, parent: SubstituteNode | SubstituteBase) {
super(property, parent)
this._proxy = createSubstituteProxy(
private constructor(key: PropertyKey, parent?: SubstituteNode) {
super(key, parent)
if (this.isRoot()) this._rootContext = { substituteMethodsEnabled: true }
if (this.isIntermediateNode()) this._rootContext = this.root.rootContext
this._proxy = new Proxy(
this,
{
get: (node, _, __, nextNode) => {
if (node.isAssertion) nextNode.executeAssertion()
get: function (target, property) {
if (target.isSpecialProperty(property)) return target.evaluateSpecialProperty(property)
const newNode = SubstituteNode.createChild(property, target)
if (target.isRoot() && !target.rootContext.substituteMethodsEnabled) newNode.disableSubstituteMethods()
if (target.isIntermediateNode() && target.isAssertion) newNode.executeAssertion()
return newNode.read()
},
set: (node, _, __, ___, nextNode) => {
if (node.isAssertion) nextNode.executeAssertion()
set: function (target, property, value) {
const newNode = SubstituteNode.createChild(property, target)
newNode.write(value)
if (target.isAssertion) newNode.executeAssertion()
return true
},
apply: (node, _, rawArguments) => {
node.handleMethod(rawArguments)
if (node.context === 'clearSubstitute') return node.clear()
return node.parent?.isAssertion ?? false ? node.executeAssertion() : node.read()
apply: function (target, _thisArg, rawArguments) {
target.handleMethod(rawArguments)
if (target.hasContext) target.handleSpecialContext()
return (target.parent?.isAssertion ?? false) ? target.executeAssertion() : target.read()
}
}
)
}

public static instance: typeof instance = instance

public static createRoot(): SubstituteNode {
return new this('*Substitute<Root>')
}

public static createChild(key: PropertyKey, parent: SubstituteNode): SubstituteNode {
return new this(key, parent)
}

public get proxy() {
return this._proxy
}

public get rootContext() {
return this._rootContext
}

get context(): SubstituteContext {
return this._context
}
Expand Down Expand Up @@ -80,20 +106,20 @@ export class SubstituteNode extends SubstituteNodeBase<SubstituteNode> {
return this._recordedArguments
}

public get disabledAssertions() {
return this._disabledAssertions
public get disabledSubstituteMethods() {
return this._disabledSubstituteMethods
}

public assignContext(context: SubstituteContext): void {
this._context = context
}

public disableAssertions() {
this._disabledAssertions = true
public disableSubstituteMethods() {
this._disabledSubstituteMethods = true
}

public read(): SubstituteNode | void | never {
if (this.parent?.isSubstitution ?? false) return
if ((this.parent?.isSubstitution ?? false) || this.context === 'clearSubstitute') return
if (this.isAssertion) return this.proxy

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

public clear() {
const clearType: ClearType = this.recordedArguments.value[0] ?? 'all'
const filter = clearTypeToFilterMap[clearType] as (node: SubstituteNodeBase) => boolean
this.root.recorder.clearRecords(filter)
const clearType: ClearType = this.recordedArguments.value[0] ?? ClearTypeMap.All
const filter = clearTypeToFilterMap[clearType]
this.recorder.clearRecords(filter)
}

public executeSubstitution(contextArguments: RecordedArguments) {
Expand All @@ -122,7 +148,7 @@ export class SubstituteNode extends SubstituteNodeBase<SubstituteNode> {
case 'throws':
throw substitutionValue
case 'mimicks':
const argumentsToApply = this.propertyType === PropertyType.property ? [] : contextArguments.value
const argumentsToApply = this.propertyType === PropertyTypeMap.Property ? [] : contextArguments.value
return substitutionValue(...argumentsToApply)
case 'resolves':
return Promise.resolve(substitutionValue)
Expand All @@ -136,8 +162,8 @@ export class SubstituteNode extends SubstituteNodeBase<SubstituteNode> {
}

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

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

public handleMethod(rawArguments: any[]): void {
this._propertyType = PropertyType.method
this._propertyType = PropertyTypeMap.Method
this._recordedArguments = RecordedArguments.from(rawArguments)
if (!isSubstituteMethod(this.property)) return
this.tryToAssignContext()
}

private tryToAssignContext() {
if (!isSubstituteMethod(this.property)) return
if (this.isIntermediateNode() && isSubstitutionMethod(this.property)) return this.parent.assignContext(this.property)
if (this.disabledAssertions || !this.isHead()) return

if (this.disabledSubstituteMethods) return
this.assignContext(this.property)
}

private handleSpecialContext(): void {
if (this.context === 'clearSubstitute') return this.clear()
if (this.context === 'didNotReceive') this._recordedArguments = RecordedArguments.from([0])
}

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

protected printableForm(_: number, options: InspectOptions): string {
private isSpecialProperty(property: PropertyKey): property is SpecialProperty {
return property === SubstituteNode.instance || property === inspect.custom || property === 'then'
}

private evaluateSpecialProperty(property: SpecialProperty) {
switch (property) {
case SubstituteNode.instance:
return this
case inspect.custom:
return this.printableForm.bind(this)
case 'then':
return
default:
throw SubstituteException.generic(`Evaluation of special property ${property} is not implemented`)
}
}

public [inspect.custom](...args: [_: number, options: InspectOptions]): string {
return types.isProxy(this) ? this[inspect.custom](...args) : this.printableForm(...args)
}

private printableForm(_: number, options: InspectOptions): string {
return this.isRoot() ? this.printRootNode(options) : this.printNode(options)
}

private printRootNode(options: InspectOptions): string {
const records = inspect(this.recorder, options)
const instanceName = '*Substitute<Root>' // Substitute<FooThing>
return instanceName + ' {' + records + '\n}'
}

private printNode(options: InspectOptions): string {
const hasContext = this.hasContext
const args = inspect(this.recordedArguments, options)
const label = this.isSubstitution
Expand Down
Loading

0 comments on commit a2bb99f

Please sign in to comment.