diff --git a/src/Task/Task.ts b/src/Task/Task.ts index 17d6e668..4a1fa3df 100644 --- a/src/Task/Task.ts +++ b/src/Task/Task.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-misused-promises, @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-use-before-define */ -import { constant, identity, range } from "../util" +import { constant, identity, range, Validation } from "../util" export type Reject = (error: E) => void export type Resolve = (result: S) => void @@ -574,6 +574,20 @@ export const tapChain = ( task: Task, ): Task => chain(result => fn(result).forward(result), task) +/** + * Run a function on a successful value which can fail the task or modify the type. + * @param fn A function will return a Validation on the value. + * @param task The task to tap on succcess. + */ +export const validate = ( + fn: (value: S) => Validation, + task: Task, +): Task => + chain((value: S) => { + const result = fn(value) + return result.success ? of(result.value) : fail(result.error) + }, task) + /** * Given a task, map the failure error to a Task. * @alias recoverWith @@ -890,6 +904,12 @@ export class Task implements PromiseLike { return tapChain(fn, this) } + public validate( + fn: (value: S) => Validation, + ): Task { + return validate(fn, this) + } + public mapError(fn: (error: E) => E2): Task { return mapError(fn, this) } diff --git a/src/Task/__tests__/validate.spec.ts b/src/Task/__tests__/validate.spec.ts new file mode 100644 index 00000000..64f55837 --- /dev/null +++ b/src/Task/__tests__/validate.spec.ts @@ -0,0 +1,57 @@ +import { failedValidation, successfulValidation, Validation } from "../../util" +import { fail, succeed } from "../Task" +import { ERROR_RESULT } from "./util" + +enum GT4Brand { + _ = "", +} +type GT4 = GT4Brand & number + +const isGT4 = (value: number): value is GT4 => value > 4 + +const isGT4Validator = (value: number): Validation => { + if (isGT4(value)) { + return successfulValidation(value) + } + + return failedValidation(`${value.toString()} <= 4`) +} + +describe("validate", () => { + test("should call validate successfully", () => { + const resolve = jest.fn() + const reject = jest.fn() + + succeed(5) + .validate(isGT4Validator) + .map((val: GT4) => val) + .fork(reject, resolve) + + expect(reject).not.toBeCalled() + expect(resolve).toBeCalledWith(5) + }) + + test("should call validate when failing", () => { + const resolve = jest.fn() + const reject = jest.fn() + + succeed(3) + .validate(isGT4Validator) + .mapError((err: string) => err) + .fork(reject, resolve) + + expect(reject).toBeCalledWith("3 <= 4") + expect(resolve).not.toBeCalled() + }) + + test("should call not validate when given a failure", () => { + const resolve = jest.fn() + const reject = jest.fn() + const validate = jest.fn() + + fail(ERROR_RESULT).validate(validate).fork(reject, resolve) + + expect(validate).not.toBeCalled() + expect(reject).toBeCalledWith(ERROR_RESULT) + }) +}) diff --git a/src/index.ts b/src/index.ts index 1cc5658a..073f918e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,3 +4,12 @@ import Task, { ExternalTask, LoopBreak, LoopContinue } from "./Task/index" export * from "./util" export { RemoteData, Task, ExternalTask, LoopContinue, LoopBreak, Subscription } + +/** + * Given a function that returns a task, return a new function that + * returns a promise instead. + * @param fn A function which returns a promise + */ +export const wrapTaskCreator = ( + fn: (...args: Args) => Task, +) => (...args: Args): Promise => fn(...args).toPromise() diff --git a/src/util.ts b/src/util.ts index a6f24704..2781b0f9 100644 --- a/src/util.ts +++ b/src/util.ts @@ -49,3 +49,17 @@ Array.prototype.chain_ = function ( ): U { return fn(this) } + +export type Validation = + | { success: true; value: S } + | { success: false; error: E } + +export const successfulValidation = (value: S): Validation => ({ + success: true, + value, +}) + +export const failedValidation = (error: E): Validation => ({ + success: false, + error, +})