src/interceptors/decorator.ts
import { ImmutableArray, isArray } from '../utils/index.js'
import { callInfo } from '../common/index.js'
import {
IDecorator, IStrategy, IStrategyInfo,
propertyType, ICallInfo, ISubstitute,
ICallChainParams
} from './types/index.js'
export class Decorator implements IDecorator {
public wrap(strategyInfo: IStrategyInfo): void {
strategyInfo = this.copyStrategy(strategyInfo)
const strategyStore = strategyInfo.substitute ?
this.defineWrapStrategies() :
this.defineNonWrapStrategies()
const strategy = strategyStore[strategyInfo.type]
strategy(strategyInfo)
}
private defineNonWrapStrategies(): IStrategy {
const result = <IStrategy>{}
result[propertyType.method] = (strategyInfo: IStrategyInfo) => {
const value = strategyInfo.source[strategyInfo.name] as Function
strategyInfo.destination[strategyInfo.name] = function () {
const args = Array.prototype.slice.call(arguments, 0)
return value.apply(this, args)
}
}
result[propertyType.getter] = (strategyInfo : IStrategyInfo) => {
const { configurable, enumerable } = strategyInfo.descriptor ?
strategyInfo.descriptor : { configurable: true, enumerable: true }
Object.defineProperty(strategyInfo.destination, strategyInfo.name, {
get: this.defineGetter(strategyInfo),
configurable,
enumerable
})
}
result[propertyType.setter] = (strategyInfo : IStrategyInfo) => {
const { configurable, enumerable } = strategyInfo.descriptor ?
strategyInfo.descriptor : { configurable: true, enumerable: true }
Object.defineProperty(strategyInfo.destination, strategyInfo.name, {
set: this.defineSetter(strategyInfo),
configurable,
enumerable
})
}
result[propertyType.fullProperty] = (strategyInfo : IStrategyInfo) => {
const { configurable, enumerable } = strategyInfo.descriptor ?
strategyInfo.descriptor : { configurable: true, enumerable: true }
Object.defineProperty(strategyInfo.destination, strategyInfo.name, {
get: this.defineGetter(strategyInfo),
set: this.defineSetter(strategyInfo),
configurable,
enumerable
})
}
result[propertyType.field] = result[propertyType.fullProperty]
return result
}
private defineWrapStrategies(): IStrategy {
const result = <IStrategy>{}
const createCallChainFromList = this.createCallChainFromList.bind(this)
const defineWrapGetter = this.defineWrapGetter.bind(this)
const defineWrapSetter = this.defineWrapSetter.bind(this)
const defineGetter = this.defineGetter.bind(this)
const defineSetter = this.defineSetter.bind(this)
result[propertyType.method] = (strategyInfo: IStrategyInfo) => {
const value = strategyInfo.source[strategyInfo.name] as Function
strategyInfo.destination[strategyInfo.name] = function () {
const args = Array.prototype.slice.call(arguments, 0)
const delegate = (args?: any[]) => {
if (!args || !isArray(args)) {
args = [args]
}
return value.apply(this, args)
}
return createCallChainFromList({
delegate,
strategyInfo,
args: ImmutableArray.createImmutable(args),
wrapperContext: this,
})
}
}
result[propertyType.getter] = (strategyInfo: IStrategyInfo) => {
Object.defineProperty(strategyInfo.destination, strategyInfo.name, {
get: defineWrapGetter(strategyInfo),
configurable: true,
enumerable: strategyInfo.descriptor ? strategyInfo.descriptor.enumerable : true
})
}
result[propertyType.setter] = (strategyInfo: IStrategyInfo) => {
Object.defineProperty(strategyInfo.destination, strategyInfo.name, {
set: defineWrapSetter(strategyInfo),
configurable: true,
enumerable: strategyInfo.descriptor ? strategyInfo.descriptor.enumerable : true
})
}
result[propertyType.fullProperty] = (strategyInfo: IStrategyInfo) => {
const getter = strategyInfo.substitute!.type === callInfo.any ||
strategyInfo.substitute!.type === callInfo.getterSetter ||
strategyInfo.substitute!.type === callInfo.getter ||
strategyInfo.substitute!.type === callInfo.field ?
defineWrapGetter(strategyInfo) : defineGetter(strategyInfo)
const setter = strategyInfo.substitute!.type === callInfo.any ||
strategyInfo.substitute!.type === callInfo.getterSetter ||
strategyInfo.substitute!.type === callInfo.setter ||
strategyInfo.substitute!.type === callInfo.field ?
defineWrapSetter(strategyInfo) : defineSetter(strategyInfo)
Object.defineProperty(strategyInfo.destination, strategyInfo.name, {
get: getter,
set: setter,
configurable: true,
enumerable: strategyInfo.descriptor ? strategyInfo.descriptor.enumerable : true
})
}
result[propertyType.field] = result[propertyType.fullProperty]
return result
}
private defineWrapSetter(strategyInfo : IStrategyInfo) {
const createCallChainFromList = this.createCallChainFromList.bind(this)
const defineSetter = this.defineSetter.bind(this)
return function (value: {}) {
const delegate = defineSetter(strategyInfo).bind(this)
return createCallChainFromList({
delegate,
strategyInfo,
args: ImmutableArray.createImmutable([value]),
wrapperContext: this,
callType: callInfo.setter,
})
}
}
private defineWrapGetter(strategyInfo : IStrategyInfo) {
const createCallChainFromList = this.createCallChainFromList.bind(this)
const defineGetter = this.defineGetter.bind(this)
return function () {
const delegate = defineGetter(strategyInfo).bind(this)
return createCallChainFromList({
delegate,
strategyInfo,
args: ImmutableArray.createImmutable([]),
wrapperContext: this,
callType : callInfo.getter,
})
}
}
private defineGetter(strategyInfo : IStrategyInfo) {
return function () {
return strategyInfo.contextName ? this[strategyInfo.contextName][strategyInfo.name]
: strategyInfo.source[strategyInfo.name]
}
}
private defineSetter(strategyInfo : IStrategyInfo) {
return function (argValue: any) {
if (strategyInfo.contextName) {
this[strategyInfo.contextName][strategyInfo.name] = argValue
} else {
strategyInfo.source[strategyInfo.name] = argValue
}
}
}
private createCallChainFromList(info : ICallChainParams) {
const mainCallInfo = this.createCallInfo(info)
this.createCallAction(mainCallInfo, info, info.strategyInfo.substitute!.next)
return info.strategyInfo.substitute!.wrapper.call(info.wrapperContext, mainCallInfo)
}
private createCallAction(
callInfo: ICallInfo, info: ICallChainParams, substitute?: ISubstitute) {
if (!substitute) {
return
}
const createCallAction = this.createCallAction.bind(this)
const childCallInfo = this.createCallInfo(info)
const wrapper = substitute.wrapper.bind(info.wrapperContext)
callInfo.next = result => {
childCallInfo.result = result
createCallAction(childCallInfo, info, substitute.next)
return wrapper(childCallInfo) as {}
}
}
private createCallInfo(info: ICallChainParams): ICallInfo {
const getter = <() => {}>info.delegate
const setter = <(value: any) => void>info.delegate
return {
source: info.strategyInfo.source,
name : info.strategyInfo.name,
args : info.args.value,
type : info.callType || info.strategyInfo.substitute!.type,
invoke: info.delegate,
get: info.callType === callInfo.getter ? getter : undefined,
set: info.callType === callInfo.setter ? setter : undefined
}
}
private copyStrategy(strategyInfo: IStrategyInfo): IStrategyInfo {
return {
type: strategyInfo.type,
descriptor: strategyInfo.descriptor,
substitute: strategyInfo.substitute,
name: strategyInfo.name,
source: strategyInfo.source,
destination: strategyInfo.destination,
contextName: strategyInfo.contextName
}
}
}