/** * A closure that maps a given pipe item to a different type. */ import {Awaitable} from './types' export type PipeOperator = (subject: T) => T2 /** * A closure that maps a given pipe item to an item of the same type. */ export type ReflexivePipeOperator = (subject: T) => T /** * A condition or condition-resolving function for pipe methods. */ export type PipeCondition = boolean | ((subject: T) => boolean) /** * A class for writing chained/conditional operations in a data-flow manner. * * This is useful when you need to do a series of operations on an object, perhaps conditionally. * * @example * Say we have a Collection of items, and want to apply some transformations and filtering based on arguments: * * ```typescript * const collection = collect([1, 2, 3, 4, 5, 6, 7, 8, 9]) * * function transform(collection, evensOnly = false, returnEntireCollection = false) { * return Pipe.wrap(collection) * .when(evensOnly, coll => { * return coll.filter(x => !(x % 2)) * }) * .unless(returnEntireCollection, coll => { * return coll.take(3) * }) * .tap(coll => { * return coll.map(x => x * 2)) * }) * .get() * } * * transform(collection) // => Collection[2, 4, 6] * * transform(collection, true) // => Collection[4, 8, 12] * * transform(collection, false, true) // => Collection[2, 4, 6, 8, 10, 12, 14, 16, 18] * ``` */ export class Pipe { /** * Return a new Pipe containing the given subject. * @param subject */ static wrap(subject: subjectType): Pipe { return new Pipe(subject) } constructor( /** * The item being operated on. */ private subject: T, ) {} /** * Apply the given operator to the item in the pipe, and return a new pipe with the result. * * @example * ```typescript * Pipe.wrap(2) * .tap(x => x * 4) * .get() // => 8 * ``` * * @param op */ tap(op: PipeOperator): Pipe { return new Pipe(op(this.subject)) } /** * Like tap, but always returns the original pipe. * @param op */ peek(op: PipeOperator): this { op(this.subject) return this } /** * If `check` is truthy, apply the given operator to the item in the pipe and return the result. * Otherwise, just return the current pipe unchanged. * * @param check * @param op */ when(check: PipeCondition, op: ReflexivePipeOperator): Pipe { if ( (typeof check === 'function' && check(this.subject)) || check ) { return Pipe.wrap(op(this.subject)) } return this } /** * If `check` is falsy, apply the given operator to the item in the pipe and return the result. * Otherwise, just return the current pipe unchanged. * * @param check * @param op */ unless(check: PipeCondition, op: ReflexivePipeOperator): Pipe { if ( (typeof check === 'function' && check(this.subject)) || check ) { return this } return Pipe.wrap(op(this.subject)) } /** * Alias of `unless()`. * @param check * @param op */ whenNot(check: PipeCondition, op: ReflexivePipeOperator): Pipe { return this.unless(check, op) } /** * Get the item in the pipe. * * @example * ```typescript * Pipe.wrap(4).get() // => 4 * ``` */ get(): T { return this.subject } /** * Get an AsyncPipe with the current item in the pipe. */ async(): AsyncPipe { return AsyncPipe.wrap(this.subject) } } /** * A subject function that yields the value in the AsyncPipe. */ export type AsyncPipeResolver = () => Awaitable /** * A closure that maps a given pipe item to a different type. */ export type AsyncPipeOperator = (subject: T) => Awaitable export type PromisePipeOperator = (subject: T, resolve: (val: T2) => unknown, reject: (err: Error) => unknown) => Awaitable /** * A closure that maps a given pipe item to an item of the same type. */ export type ReflexiveAsyncPipeOperator = (subject: T) => Awaitable /** * A condition or condition-resolving function for pipe methods. */ export type AsyncPipeCondition = boolean | ((subject: T) => Awaitable) /** * An asynchronous version of the Pipe helper. */ export class AsyncPipe { /** * Get an AsyncPipe with the given value in it. * @param subject */ static wrap(subject: subjectType): AsyncPipe { return new AsyncPipe(() => subject) } constructor( /** The current value resolver of the pipe. */ private subject: AsyncPipeResolver, ) {} /** * Apply a transformative operator to the pipe. * @param op */ tap(op: AsyncPipeOperator): AsyncPipe { return new AsyncPipe(async () => op(await this.subject())) } /** * Apply a transformative operator to the pipe, wrapping it * in a Promise and passing the resolve/reject callbacks to the * closure. * @param op */ promise(op: PromisePipeOperator): AsyncPipe { return new AsyncPipe(() => { return new Promise((res, rej) => { (async () => this.subject())() .then(subject => { op(subject, res, rej) }) }) }) } /** * Apply an operator to the pipe, but return the reference * to the current pipe. The operator is resolved when the * overall pipe is resolved. * @param op */ peek(op: AsyncPipeOperator): AsyncPipe { return new AsyncPipe(async () => { const subject = await this.subject() await op(subject) return subject }) } /** * Apply an operator to the pipe, if the check condition passes. * @param check * @param op */ when(check: AsyncPipeCondition, op: ReflexiveAsyncPipeOperator): AsyncPipe { return new AsyncPipe(async () => { let subject if ( typeof check === 'function' ) { check = await check(subject = await this.subject()) } subject = subject ?? await this.subject() if ( check ) { return ((await op(subject)) ?? subject) as T } return subject as T }) } /** * Apply an operator to the pipe, if the check condition fails. * @param check * @param op */ unless(check: AsyncPipeCondition, op: ReflexiveAsyncPipeOperator): AsyncPipe { if ( typeof check === 'function' ) { return this.when(async (subject: T) => !(await check(subject)), op) } return this.when(!check, op) } /** * Alias of `unless()`. * @param check * @param op */ whenNot(check: AsyncPipeCondition, op: ReflexiveAsyncPipeOperator): AsyncPipe { return this.unless(check, op) } /** * Get the transformed value from the pipe. */ async resolve(): Promise { return this.subject() } /** * Resolve the value and return it in a sync `Pipe` instance. */ async sync(): Promise> { return Pipe.wrap(await this.subject()) } /** Get the transformed value from the pipe. Allows awaiting the pipe directly. */ then(): Promise { return this.resolve() } }