From 7c474041ede2d120625fd839abc5b19f383c5660 Mon Sep 17 00:00:00 2001 From: Diogo Biazus Date: Fri, 1 Mar 2024 09:35:40 -0500 Subject: [PATCH 1/6] Implement catchError so we can take the input and error from a failed composable and recover from it --- src/composable/composable.ts | 25 +++++++++++++++++++++++++ src/composable/index.test.ts | 34 +++++++++++++++++++++++++++++++++- 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/src/composable/composable.ts b/src/composable/composable.ts index 0bd3baa2..50136de8 100644 --- a/src/composable/composable.ts +++ b/src/composable/composable.ts @@ -184,6 +184,30 @@ function map( > } +/** + * Creates a new function that will try to recover from a resulting Failure. When the given function succeeds, its result is returned without changes. + * @example + * import { cf as C } from 'domain-functions' + * + * const increment = C.composable(({ id }: { id: number }) => id + 1) + * const negativeOnError = C.catchError(increment, (result, originalInput) => ( + * originalInput.id * -1 + * )) + */ +function catchError( + fn: T, + catcher: ( + err: Omit, + ...originalInput: Parameters + ) => UnpackResult>, +) { + return (async (...args) => { + const res = await fn(...args) + if (res.success) return success(res.data) + return composable(catcher)(res, ...(args as any)) + }) as T +} + /** * Creates a new function that will apply a transformation over a resulting Failure from the given function. When the given function succeeds, its result is returned without changes. * @example @@ -212,6 +236,7 @@ function mapError( export { all, + catchError, collect, composable, error, diff --git a/src/composable/index.test.ts b/src/composable/index.test.ts index 8216dd43..ff2f8600 100644 --- a/src/composable/index.test.ts +++ b/src/composable/index.test.ts @@ -2,7 +2,7 @@ import { assertEquals, describe, it } from '../test-prelude.ts' import { map, mapError, pipe, sequence } from './index.ts' import type { Composable, ErrorWithMessage, Result } from './index.ts' import { Equal, Expect } from './types.test.ts' -import { all, collect, composable } from './composable.ts' +import { all, catchError, collect, composable } from './composable.ts' const voidFn = composable(() => {}) const toString = composable((a: unknown) => `${a}`) @@ -383,3 +383,35 @@ describe('mapError', () => { }) }) +describe('catchError', () => { + it('receives an error as input to another composable', async () => { + const fn = catchError(faultyAdd, (_error, a, b) => a + b) + const res = await fn(1, 2) + + type _FN = Expect< + Equal number>> + > + type _R = Expect>> + + assertEquals(res, { + success: true, + data: 3, + errors: [], + }) + }) + + it('fails when catcher fail', async () => { + const fn = catchError(faultyAdd, () => { + throw new Error('Catcher also has problems') + }) + const res = await fn(1, 2) + + type _FN = Expect< + Equal number>> + > + type _R = Expect>> + + assertEquals(res.success, false) + assertEquals(res.errors![0].message, 'Catcher also has problems') + }) +}) From 671a4d11877bcf6d56ceebcc9e3a67482e0cbb30 Mon Sep 17 00:00:00 2001 From: Diogo Biazus Date: Fri, 1 Mar 2024 09:41:30 -0500 Subject: [PATCH 2/6] Export catch error on cf namespace --- src/composable/index.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/composable/index.ts b/src/composable/index.ts index 5793cd5e..166d9479 100644 --- a/src/composable/index.ts +++ b/src/composable/index.ts @@ -1,3 +1,12 @@ export type { Composable, Result, ErrorWithMessage } from './types.ts' export { toErrorWithMessage } from './errors.ts' -export { composable, pipe, map, mapError, sequence, all, collect } from './composable.ts' +export { + catchError, + composable, + pipe, + map, + mapError, + sequence, + all, + collect, +} from './composable.ts' From f596f9624196b2933ae13dfc606d9be8fb865b36 Mon Sep 17 00:00:00 2001 From: Diogo Biazus Date: Fri, 5 Apr 2024 16:28:17 -0400 Subject: [PATCH 3/6] Add cast so we typecheck in deno 1.42.1 --- src/domain-functions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain-functions.ts b/src/domain-functions.ts index 78d53ae0..4ce82905 100644 --- a/src/domain-functions.ts +++ b/src/domain-functions.ts @@ -168,7 +168,7 @@ function pipe( ...fns: T ): DomainFunction>> { const last = (ls: T[]): T => ls[ls.length - 1] - return map(sequence(...fns), last) + return map(sequence(...fns), last) as DomainFunction>> } /** From 9a2d6fb2cb7bd009466e458bb8c81f7be921e1a3 Mon Sep 17 00:00:00 2001 From: Diogo Biazus Date: Fri, 5 Apr 2024 16:42:05 -0400 Subject: [PATCH 4/6] Enable catcher with different types --- src/composable/composable.ts | 10 +++++----- src/composable/index.test.ts | 18 +++++++++++++++++- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/composable/composable.ts b/src/composable/composable.ts index 50136de8..f1c0797b 100644 --- a/src/composable/composable.ts +++ b/src/composable/composable.ts @@ -194,18 +194,18 @@ function map( * originalInput.id * -1 * )) */ -function catchError( - fn: T, +function catchError( + fn: Composable, catcher: ( err: Omit, ...originalInput: Parameters - ) => UnpackResult>, + ) => R, ) { - return (async (...args) => { + return (async (...args: Parameters) => { const res = await fn(...args) if (res.success) return success(res.data) return composable(catcher)(res, ...(args as any)) - }) as T + }) as Composable<(...args: Parameters) => ReturnType | R> } /** diff --git a/src/composable/index.test.ts b/src/composable/index.test.ts index ff2f8600..de79c879 100644 --- a/src/composable/index.test.ts +++ b/src/composable/index.test.ts @@ -384,7 +384,23 @@ describe('mapError', () => { }) describe('catchError', () => { - it('receives an error as input to another composable', async () => { + it('changes the type to accomodate catcher return type', async () => { + const fn = catchError(faultyAdd, () => null) + const res = await fn(1, 2) + + type _FN = Expect< + Equal number | null>> + > + type _R = Expect>> + + assertEquals(res, { + success: true, + data: null, + errors: [], + }) + }) + + it('receives an error as input to another function and returns a new composable', async () => { const fn = catchError(faultyAdd, (_error, a, b) => a + b) const res = await fn(1, 2) From f92f925882b5874493c225a947220098ee88a777 Mon Sep 17 00:00:00 2001 From: Diogo Biazus Date: Fri, 5 Apr 2024 16:57:23 -0400 Subject: [PATCH 5/6] Handle unions between [] types and any[] --- src/composable/composable.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/composable/composable.ts b/src/composable/composable.ts index f1c0797b..3716290b 100644 --- a/src/composable/composable.ts +++ b/src/composable/composable.ts @@ -205,7 +205,15 @@ function catchError( const res = await fn(...args) if (res.success) return success(res.data) return composable(catcher)(res, ...(args as any)) - }) as Composable<(...args: Parameters) => ReturnType | R> + }) as Composable< + ( + ...args: Parameters + ) => ReturnType extends any[] + ? R extends never[] + ? ReturnType + : ReturnType | R + : ReturnType | R + > } /** From 645597540de73a2b797c22040a4a6d6de05ed30b Mon Sep 17 00:00:00 2001 From: Diogo Biazus Date: Sat, 6 Apr 2024 10:57:17 -0400 Subject: [PATCH 6/6] Change catchError API so it receives the list of errors --- src/composable/composable.ts | 7 ++----- src/composable/index.test.ts | 6 ++++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/composable/composable.ts b/src/composable/composable.ts index 3716290b..aaf43f92 100644 --- a/src/composable/composable.ts +++ b/src/composable/composable.ts @@ -196,15 +196,12 @@ function map( */ function catchError( fn: Composable, - catcher: ( - err: Omit, - ...originalInput: Parameters - ) => R, + catcher: (err: Failure['errors'], ...originalInput: Parameters) => R, ) { return (async (...args: Parameters) => { const res = await fn(...args) if (res.success) return success(res.data) - return composable(catcher)(res, ...(args as any)) + return composable(catcher)(res.errors, ...(args as any)) }) as Composable< ( ...args: Parameters diff --git a/src/composable/index.test.ts b/src/composable/index.test.ts index de79c879..2782ecaf 100644 --- a/src/composable/index.test.ts +++ b/src/composable/index.test.ts @@ -400,8 +400,10 @@ describe('catchError', () => { }) }) - it('receives an error as input to another function and returns a new composable', async () => { - const fn = catchError(faultyAdd, (_error, a, b) => a + b) + it('receives the list of errors as input to another function and returns a new composable', async () => { + const fn = catchError(faultyAdd, (errors, a, b) => + errors.length > 1 ? NaN : a + b, + ) const res = await fn(1, 2) type _FN = Expect<