Skip to content

Commit

Permalink
Merge pull request #133 from seasonedcc/catchError
Browse files Browse the repository at this point in the history
catchError combinator to recover from failed composable
  • Loading branch information
gustavoguichard authored Apr 8, 2024
2 parents 8df93c1 + 6455975 commit dafc5cf
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 3 deletions.
30 changes: 30 additions & 0 deletions src/composable/composable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,35 @@ function map<T extends Composable, R>(
>
}

/**
* 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<T extends Fn, R>(
fn: Composable<T>,
catcher: (err: Failure['errors'], ...originalInput: Parameters<T>) => R,
) {
return (async (...args: Parameters<T>) => {
const res = await fn(...args)
if (res.success) return success(res.data)
return composable(catcher)(res.errors, ...(args as any))
}) as Composable<
(
...args: Parameters<T>
) => ReturnType<T> extends any[]
? R extends never[]
? ReturnType<T>
: ReturnType<T> | R
: ReturnType<T> | R
>
}

/**
* 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
Expand Down Expand Up @@ -212,6 +241,7 @@ function mapError<T extends Composable, R>(

export {
all,
catchError,
collect,
composable,
error,
Expand Down
52 changes: 51 additions & 1 deletion src/composable/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`)
Expand Down Expand Up @@ -383,3 +383,53 @@ describe('mapError', () => {
})
})

describe('catchError', () => {
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<typeof fn, Composable<(a: number, b: number) => number | null>>
>
type _R = Expect<Equal<typeof res, Result<number | null>>>

assertEquals(res, {
success: true,
data: null,
errors: [],
})
})

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<
Equal<typeof fn, Composable<(a: number, b: number) => number>>
>
type _R = Expect<Equal<typeof res, Result<number>>>

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<typeof fn, Composable<(a: number, b: number) => number>>
>
type _R = Expect<Equal<typeof res, Result<number>>>

assertEquals(res.success, false)
assertEquals(res.errors![0].message, 'Catcher also has problems')
})
})
11 changes: 10 additions & 1 deletion src/composable/index.ts
Original file line number Diff line number Diff line change
@@ -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'
2 changes: 1 addition & 1 deletion src/domain-functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ function pipe<T extends DomainFunction[]>(
...fns: T
): DomainFunction<Last<UnpackAll<T>>> {
const last = <T>(ls: T[]): T => ls[ls.length - 1]
return map(sequence(...fns), last)
return map(sequence(...fns), last) as DomainFunction<Last<UnpackAll<T>>>
}

/**
Expand Down

0 comments on commit dafc5cf

Please sign in to comment.