import { Failure } from '@ephox/bedrock-common'; import { Arr, Fun, type Result } from '@ephox/katamari'; import * as AsyncActions from '../pipe/AsyncActions'; import * as GeneralActions from '../pipe/GeneralActions'; import { type DieFn, type NextFn, Pipe, type RunFn } from '../pipe/Pipe'; import { addLogging, type GuardFn } from './Guard'; import { Pipeline } from './Pipeline'; import { Step } from './Step'; import { addLogEntry, TestLogs } from './TestLogs'; export interface Chain { runChain: RunFn; } export type ChainGuard = GuardFn; const on = (f: (value: T, next: NextFn, die: DieFn, logs: TestLogs) => void): Chain => { const runChain = Pipe((input: T, next: NextFn, die: DieFn, logs: TestLogs) => { f(input, (v: U, newLogs) => { next(v, newLogs); }, (err, newLogs) => die(err, newLogs), logs); }); return { runChain }; }; const control = (chain: Chain, guard: ChainGuard): Chain => on((input: T, next: NextFn, die: DieFn, logs: TestLogs) => { guard(chain.runChain, input, (v: V, newLogs: TestLogs) => { next(v, newLogs); }, die, logs); }); const mapper = (fx: (value: T) => U): Chain => on((input: T, next: NextFn, die: DieFn, logs: TestLogs) => { next(fx(input), logs); }); const identity = mapper(Fun.identity); const binder = (fx: (input: T) => Result): Chain => on((input: T, next: NextFn, die: DieFn, logs: TestLogs) => { fx(input).fold((err) => { die(err, logs); }, (v) => { next(v, logs); }); }); const op = (fx: (value: T) => void): Chain => on((input: T, next: NextFn, die: DieFn, logs: TestLogs) => { fx(input); next(input, logs); }); const async = (fx: (input: T, next: (v: U) => void, die: (err) => void) => void): Chain => on((v, n, d, logs) => fx(v, (v) => n(v, logs), (err) => d(err, logs))); const inject = (value: U): Chain => on((_input: T, next: NextFn, die: DieFn, logs: TestLogs) => { next(value, logs); }); const injectThunked = (f: () => U): Chain => on((_input: T, next: NextFn, die: DieFn, logs: TestLogs) => { next(f(), logs); }); const extract = (chain: Chain): Step => ({ runStep: chain.runChain }); const fromChains = (chains: Chain[]): Chain => { const cs = Arr.map(chains, extract); return on((value, next, die, initLogs) => { Pipeline.async(value, cs, (v, newLogs) => next(v, newLogs), die, initLogs); }); }; const fromChainsWith = (initial: T, chains: Chain[]): Chain => fromChains( [ inject(initial) ].concat(chains) ); const fromIsolatedChains = (chains: Chain[]): Chain => { const cs = Arr.map(chains, extract); return on((value, next, die, initLogs) => { Pipeline.async(value, cs, (_v, newLogs) => { // Ignore the output value and use the original value instead next(value, newLogs); }, die, initLogs); }); }; const fromIsolatedChainsWith = (initial: T, chains: Chain[]): Chain => fromIsolatedChains( [ inject(initial) ].concat(chains) ); // Find the first chain which doesn't fail, and use its value. Fails if no chain passes. const exists = (chains: Chain[]): Chain => { const cs = Arr.map(chains, extract); let index = 0; const attempt = (value: T, next: NextFn, die: DieFn, initLogs: TestLogs): void => { let replacementDie = die; if (index + 1 < cs.length) { replacementDie = () => { index += 1; attempt(value, next, die, initLogs); }; } Pipeline.runStep(value, cs[index], next, replacementDie, initLogs); }; return on(attempt); }; const fromParent = (parent: Chain, chains: Chain[]): Chain => on((cvalue: T, cnext: NextFn, cdie: DieFn, clogs: TestLogs) => { Pipeline.async(cvalue, [ extract(parent) ], (value: U, parentLogs: TestLogs) => { const cs = Arr.map(chains, (c) => Step.raw((_, next, die, logs) => { // Replace _ with value c.runChain(value, next, die, logs); })); Pipeline.async(cvalue, cs, (_, finalLogs) => { // Ignore all the values and use the original cnext(value, finalLogs); }, cdie, parentLogs); }, cdie, clogs); }); /** * @deprecated Use isolate() instead * TODO: remove */ const asStep = (initial: U, chains: Chain[]): Step => Step.raw((initValue, next, die, logs) => { const cs = Arr.map(chains, extract); Pipeline.async( initial, cs, // Ignore all the values and use the original (_v, ls) => { next(initValue, ls); }, die, logs ); }); /** * Wrap a Chain into an "isolated" Step, with its own local state. * The state of the outer Step is passed-through. * Use the functions in ChainSequence to compose multiple Chains. * * @param initial * @param chain */ const isolate = (initial: U, chain: Chain): Step => Step.raw((initValue, next, die, logs) => { Pipeline.runStep( initial, extract(chain), // Ignore all the values and use the original (_v, ls) => { next(initValue, ls); }, die, logs ); }); // Convenience functions const debugging = op(GeneralActions.debug); const log = (message: string): Chain => on((input: T, next: NextFn, die: DieFn, logs: TestLogs) => { // eslint-disable-next-line no-console console.log(message); next(input, addLogEntry(logs, message)); }); const label = (label: string, chain: Chain): Chain => control(chain, addLogging(label)); const wait = (amount: number): Chain => on((input: T, next: NextFn, die: DieFn, logs: TestLogs) => { AsyncActions.delay(amount)(() => next(input, logs), die); }); const pipeline = (chains: Chain[], onSuccess: NextFn, onFailure: DieFn, initLogs?: TestLogs): void => { Pipeline.async({}, Arr.map(chains, extract), (output, logs) => { onSuccess(output, logs); }, onFailure, TestLogs.getOrInit(initLogs)); }; const runStepsOnValue = (getSteps: (value: I) => Step[]): Chain => Chain.on((input: I, next, die, initLogs) => { const steps = getSteps(input); Pipeline.async(input, steps, (stepsOutput, newLogs) => next(stepsOutput, newLogs), die, initLogs); }); const predicate = (p: (value: T) => boolean): Chain => on((input, next, die, logs) => p(input) ? next(input, logs) : die('predicate did not succeed', logs)); const toPromise = (c: Chain) => (a: A): Promise => new Promise((resolve, reject) => { c.runChain(a, (b, _logs) => { // TODO: What to do with logs? We lose them. resolve(b); }, (err, logs) => { reject(Failure.prepFailure(err, logs)); }, TestLogs.init() ); }); const fromPromise = (f: (a: A) => Promise): Chain => Chain.async((input, next, die) => { f(input).then(next, die); }); export const Chain = { on, op, async, control, mapper, identity, binder, runStepsOnValue, inject, injectThunked, fromChains, fromChainsWith, fromIsolatedChains, fromIsolatedChainsWith, exists, fromParent, asStep, isolate, wait, debugging, log, label, toPromise, fromPromise, pipeline, predicate };