UNPKG

chai-latte

Version:

Build expressive & readable fluent interface libraries.

150 lines (124 loc) 4.96 kB
import { getPrototypeChain } from './lib/getPrototypeChain'; import { ConfigurableCallback } from './lib/ConfigurableCallback'; import { Expression, RegisteredAPI } from './register'; export const combine = (...expressions: RegisteredAPI<any>[][]) => { const sharedArgs = []; const combinedFluentAPI: any = {}; // share argument between all functions in the subtree. Array.from(ConfigurableCallback.configByCallback).forEach(([_, configurableCallback]) => { configurableCallback.args = sharedArgs; }); expressions.forEach((expression, i) => { expression.forEach((registeredAPI) => { registeredAPI.expression.setExpressionIdx(i); }); }); expressions.forEach((expression, i) => { expression.forEach((registeredAPI) => { mergeSubtree(registeredAPI.api, combinedFluentAPI); }); }); return combinedFluentAPI; } const mergeSubtree = (source, target) => { const isSourceObj = typeof source === 'object'; const isTargetObj = typeof target === 'object'; if (isSourceObj && isTargetObj) { // console.log('merging {} with {}', source, target); return mergeObjectWithObject(source, target); } const isSourceFn = typeof source === 'function'; const isTargetFn = typeof target === 'function'; if (isSourceFn && isTargetFn) { // console.log('merging callbacks', source, target) return mergeCallbackWithCallback(source, target); } if (isSourceObj && isTargetFn) { // console.log('merging {} with callback', source, target); return mergeObjectWithFunctionProps(source, target); } if (isSourceFn && isTargetObj) { // console.log('merging callback with {}', source, target); return mergeFunctionWithObject(source, target); } throw new Error(`Case handled src:${typeof source} target:${typeof target}`) }; const parentTargetByTarget = new Map<any, any>(); const mergeObjectWithObject = (source, target) => { const [[sourceProp, sourceSubtree]] = Object.entries(source); const doesKeyExist = !!target[sourceProp]; parentTargetByTarget.set(target[sourceProp], { parent: target, prop: sourceProp }); if (!doesKeyExist) { target[sourceProp] = sourceSubtree; parentTargetByTarget.set(sourceSubtree, { parent: target, prop: sourceProp }); return; } mergeSubtree(sourceSubtree, target[sourceProp]); return target; } const mergeCallbackWithCallback = (source, target) => { const targetCallbackBuilder = ConfigurableCallback.getBuilderOf(target); const srcCallbackBuilder = ConfigurableCallback.getBuilderOf(source); if (!srcCallbackBuilder || !targetCallbackBuilder) { return; } srcCallbackBuilder.returnByArg.forEach((returned, arg) => { if (!targetCallbackBuilder.returnByArg.get(arg)) { targetCallbackBuilder.setArgOrigin(arg, srcCallbackBuilder); targetCallbackBuilder.returnByArg.set(arg, returned); } else { mergeSubtree(returned, targetCallbackBuilder.returnByArg.get(arg)) } }); // port parent class api to all its extends subclasses; targetCallbackBuilder.returnByArg.forEach((retuned, arg) => { const argProtoChain = getPrototypeChain(arg); argProtoChain.forEach((proto) => { if (targetCallbackBuilder.returnByArg.has(proto)) { mergeSubtree(targetCallbackBuilder.returnByArg.get(proto), retuned); } }) }) // console.log('srcCallbackBuilder.returnByArg', srcCallbackBuilder.returnByArg) // console.log('targetCallbackBuilder.returnByArg', targetCallbackBuilder.returnByArg) return target; } const mergeObjectWithFunctionProps = (source, target) => { ensureExpressionsAreNotOveridden(source, target); const targetConfig = ConfigurableCallback.getBuilderOf(target); Object.entries(source).forEach(([srcKey, srcVal]) => { parentTargetByTarget.set(targetConfig.props[srcKey], { parent: targetConfig.props, prop: srcKey }) if (targetConfig.props[srcKey]) { return mergeSubtree(srcVal, targetConfig.props[srcKey]); } targetConfig.props[srcKey] = srcVal; parentTargetByTarget.set(srcVal, { parent: targetConfig.props, prop: srcKey }) }); return target; } const mergeFunctionWithObject = (source, target) => { ensureExpressionsAreNotOveridden(source, target); const { parent, prop } = parentTargetByTarget.get(target); parent[prop] = source; parentTargetByTarget.set(parent[prop], { parent: parent, prop: prop, }); return mergeObjectWithFunctionProps(target, source); } const ensureExpressionsAreNotOveridden = (source, target) => { const isSrcFinal = Expression.isFinalCallback(source); const isTargetFinal = Expression.isFinalCallback(target); if (isSrcFinal || isTargetFinal) { // should not override a preexisting API. // ex: defining two expressions like the.guy('xyz') and the.guy('xyz').are.incompatible(); // will cause a conflict, the latter overrides the former. throw new Error('Incompatible Fluent API'); } }