Skip to content

Commit

Permalink
feat: Combinators accept any function instead of only composables
Browse files Browse the repository at this point in the history
  • Loading branch information
gustavoguichard committed Jun 26, 2024
1 parent f46ee1b commit e6b5760
Show file tree
Hide file tree
Showing 6 changed files with 128 additions and 83 deletions.
53 changes: 36 additions & 17 deletions src/combinators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ function all<Fns extends Array<(...args: any[]) => any>>(
[k in keyof Fns]: UnpackData<Composable<Fns[k]>>
}
> {
return (async (...args) => {
const callable = (async (...args) => {
const results = await Promise.all(fns.map((fn) => composable(fn)(...args)))

if (results.some(({ success }) => success === false)) {
Expand All @@ -113,6 +113,8 @@ function all<Fns extends Array<(...args: any[]) => any>>(
[k in keyof Fns]: UnpackData<Composable<Fns[k]>>
}
>
callable.kind = 'composable' as const
return callable
}

/**
Expand Down Expand Up @@ -175,7 +177,7 @@ function sequence<
>(
...fns: Fns
): SequenceReturn<CanComposeInSequence<Fns>> {
return (async (...args) => {
const callable = (async (...args) => {
const [head, ...tail] = fns

const res = await composable(head)(...args)
Expand All @@ -189,6 +191,8 @@ function sequence<
}
return success(result)
}) as SequenceReturn<CanComposeInSequence<Fns>>
callable.kind = 'composable' as const
return callable
}

/**
Expand All @@ -214,12 +218,14 @@ function map<Fn extends (...args: any[]) => any, O>(
...originalInput: Parameters<Fn>
) => O | Promise<O>,
): Composable<(...args: Parameters<Fn>) => O> {
return (async (...args) => {
const callable = (async (...args) => {
const result = await composable(fn)(...args)
if (!result.success) return failure(result.errors)

return composable(mapper)(result.data, ...args)
}) as Composable<(...args: Parameters<Fn>) => O>
callable.kind = 'composable' as const
return callable
}

/**
Expand All @@ -244,11 +250,13 @@ function mapParameters<
fn: Fn,
mapper: (...args: NewParameters) => Promise<MapperOutput> | MapperOutput,
): MapParametersReturn<Composable<Fn>, NewParameters, MapperOutput> {
return (async (...args) => {
const callable = (async (...args) => {
const output = await composable(mapper)(...args)
if (!output.success) return failure(output.errors)
return composable(fn)(...output.data)
}) as MapParametersReturn<Composable<Fn>, NewParameters, MapperOutput>
callable.kind = 'composable' as const
return callable
}

/**
Expand Down Expand Up @@ -279,7 +287,7 @@ function catchFailure<
: Awaited<ReturnType<C>> | UnpackData<Composable<Fn>>
: Awaited<ReturnType<C>> | UnpackData<Composable<Fn>>
> {
return (async (...args: Parameters<Fn>) => {
const callable = (async (...args: Parameters<Fn>) => {
const res = await composable(fn)(...args)
if (res.success) return success(res.data)
return composable(catcher)(res.errors, ...(args as never))
Expand All @@ -291,6 +299,8 @@ function catchFailure<
: Awaited<ReturnType<C>> | UnpackData<Composable<Fn>>
: Awaited<ReturnType<C>> | UnpackData<Composable<Fn>>
>
callable.kind = 'composable' as const
return callable
}

/**
Expand All @@ -311,7 +321,7 @@ function mapErrors<Fn extends (...args: any[]) => any>(
fn: Fn,
mapper: (err: Error[]) => Error[] | Promise<Error[]>,
): Composable<Fn> {
return (async (...args) => {
const callable = (async (...args) => {
const res = await composable(fn)(...args)
if (res.success) return success(res.data)
const mapped = await composable(mapper)(res.errors)
Expand All @@ -321,6 +331,8 @@ function mapErrors<Fn extends (...args: any[]) => any>(
return failure(mapped.errors)
}
}) as Composable<Fn>
callable.kind = 'composable' as const
return callable
}

/**
Expand Down Expand Up @@ -349,12 +361,16 @@ function trace(
): <Fn extends (...args: any[]) => any>(
fn: Fn,
) => Composable<Fn> {
return ((fn) => async (...args) => {
const originalResult = await composable(fn)(...args)
const traceResult = await composable(traceFn)(originalResult, ...args)
if (traceResult.success) return originalResult
return ((fn) => {
const callable = async (...args: any) => {
const originalResult = await composable(fn)(...args)
const traceResult = await composable(traceFn)(originalResult, ...args)
if (traceResult.success) return originalResult

return failure(traceResult.errors)
return failure(traceResult.errors)
}
callable.kind = 'composable' as const
return callable
}) as <Fn extends (...args: any[]) => any>(
fn: Fn,
) => Composable<Fn>
Expand Down Expand Up @@ -382,24 +398,27 @@ function trace(
* ```
*/
function branch<
SourceComposable extends Composable,
SourceComposable extends (...args: any[]) => any,
Resolver extends (
o: UnpackData<SourceComposable>,
o: UnpackData<Composable<SourceComposable>>,
) => Composable | null | Promise<Composable | null>,
>(
cf: SourceComposable,
// TODO: Accept any function as resolver
resolver: Resolver,
): BranchReturn<SourceComposable, Resolver> {
return (async (...args: Parameters<SourceComposable>) => {
const result = await cf(...args)
): BranchReturn<Composable<SourceComposable>, Resolver> {
const callable = (async (...args: Parameters<SourceComposable>) => {
const result = await composable(cf)(...args)
if (!result.success) return result

return composable(async () => {
const nextComposable = await resolver(result.data)
if (typeof nextComposable !== 'function') return result.data
return fromSuccess(nextComposable)(result.data)
})()
}) as BranchReturn<SourceComposable, Resolver>
}) as BranchReturn<Composable<SourceComposable>, Resolver>
;(callable as any).kind = 'composable' as const
return callable
}

export {
Expand Down
4 changes: 3 additions & 1 deletion src/constructors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ function applySchema<ParsedInput, ParsedContext>(
return <R, Input, Context>(
fn: Composable<(input: Input, context: Context) => R>,
): ApplySchemaReturn<ParsedInput, ParsedContext, typeof fn> => {
return ((input?: unknown, context?: unknown) => {
const callable = ((input?: unknown, context?: unknown) => {
const ctxResult = (contextSchema ?? alwaysUnknownSchema).safeParse(
context,
)
Expand All @@ -166,6 +166,8 @@ function applySchema<ParsedInput, ParsedContext>(
}
return fn(result.data as Input, ctxResult.data as Context)
}) as ApplySchemaReturn<ParsedInput, ParsedContext, typeof fn>
;(callable as any).kind = 'composable' as const
return callable
}
}

Expand Down
53 changes: 38 additions & 15 deletions src/context/combinators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,18 @@ import * as A from '../combinators.ts'
import { composable, fromSuccess } from '../constructors.ts'
import type { BranchReturn, PipeReturn, SequenceReturn } from './types.ts'

type Composables<Fns extends Array<(...args: any[]) => any>> = {
[K in keyof Fns]: Composable<Fns[K]>
}

function applyContextToList<
Fns extends Array<(input: unknown, context: unknown) => unknown>,
>(fns: Fns, context: unknown) {
return fns.map((fn) => (input) => fn(input, context)) as [Composable]
return fns.map((fn) => {
const callable = ((input) => fn(input, context)) as Composable
callable.kind = 'composable' as const
return callable
}) as Composable[]
}

/**
Expand All @@ -27,9 +35,16 @@ function applyContextToList<
* // ^? ComposableWithSchema<{ aBoolean: boolean }>
* ```
*/
function pipe<Fns extends Composable[]>(...fns: Fns): PipeReturn<Fns> {
return ((input: any, context: any) =>
A.pipe(...applyContextToList(fns, context))(input)) as PipeReturn<Fns>
function pipe<Fns extends Array<(...args: any[]) => any>>(
...fns: Fns
): PipeReturn<Composables<Fns>> {
const callable =
((input: any, context: any) =>
A.pipe(...applyContextToList(fns, context) as any)(input)) as PipeReturn<
Composables<Fns>
>
;(callable as any).kind = 'composable' as const
return callable
}

/**
Expand All @@ -47,36 +62,44 @@ function pipe<Fns extends Composable[]>(...fns: Fns): PipeReturn<Fns> {
* ```
*/

function sequence<Fns extends Composable[]>(...fns: Fns): SequenceReturn<Fns> {
return ((input: any, context: any) =>
A.sequence(...applyContextToList(fns, context))(
input,
)) as SequenceReturn<Fns>
function sequence<Fns extends Array<(...args: any[]) => any>>(
...fns: Fns
): SequenceReturn<Composables<Fns>> {
const callable =
((input: any, context: any) =>
A.sequence(...applyContextToList(fns, context) as any)(
input,
)) as SequenceReturn<Composables<Fns>>
;(callable as any).kind = 'composable' as const
return callable
}

/**
* Like branch but preserving the context parameter.
*/
function branch<
SourceComposable extends Composable,
SourceComposable extends (...args: any[]) => any,
Resolver extends (
o: UnpackData<SourceComposable>,
o: UnpackData<Composable<SourceComposable>>,
) => Composable | null | Promise<Composable | null>,
>(
cf: SourceComposable,
// TODO: Accept any function as resolver
resolver: Resolver,
): BranchReturn<SourceComposable, Resolver> {
return (async (...args: Parameters<SourceComposable>) => {
): BranchReturn<Composable<SourceComposable>, Resolver> {
const callable = (async (...args: Parameters<SourceComposable>) => {
const [input, context] = args
const result = await cf(input, context)
const result = await composable(cf)(input, context)
if (!result.success) return result

return composable(async () => {
const nextFn = await resolver(result.data)
if (typeof nextFn !== 'function') return result.data
return fromSuccess(nextFn)(result.data, context)
})()
}) as BranchReturn<SourceComposable, Resolver>
}) as BranchReturn<Composable<SourceComposable>, Resolver>
;(callable as any).kind = 'composable' as const
return callable
}

export { branch, pipe, sequence }
48 changes: 24 additions & 24 deletions src/context/tests/branch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,30 +59,30 @@ describe('branch', () => {
assertEquals(await d({ id: 1 }), success(6))
})

// it('should not pipe if the predicate returns null', async () => {
// const a = withSchema(z.object({ id: z.number() }))(({ id }) => ({
// id: id + 2,
// next: 'multiply',
// }))
// const b = withSchema(z.object({ id: z.number() }))(({ id }) => String(id))
// const d = context.branch(a, (output) => {
// type _Check = Expect<Equal<typeof output, UnpackData<typeof a>>>
// return output.next === 'multiply' ? null : b
// })
// type _R = Expect<
// Equal<
// typeof d,
// Composable<
// (
// input?: unknown,
// context?: unknown,
// ) => string | { id: number; next: string }
// >
// >
// >

// assertEquals(await d({ id: 1 }), success({ id: 3, next: 'multiply' }))
// })
it('should not pipe if the predicate returns null', async () => {
const a = withSchema(z.object({ id: z.number() }))(({ id }) => ({
id: id + 2,
next: 'multiply',
}))
const b = withSchema(z.object({ id: z.number() }))(({ id }) => String(id))
const d = context.branch(a, (output) => {
type _Check = Expect<Equal<typeof output, UnpackData<typeof a>>>
return output.next === 'multiply' ? null : b
})
type _R = Expect<
Equal<
typeof d,
Composable<
(
input?: unknown,
context?: unknown,
) => string | { id: number; next: string }
>
>
>

assertEquals(await d({ id: 1 }), success({ id: 3, next: 'multiply' }))
})

it('should use the same context in all composed functions', async () => {
const a = composable((_input: unknown, { ctx }: { ctx: number }) => ({
Expand Down
49 changes: 24 additions & 25 deletions src/context/tests/types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,31 +136,30 @@ namespace PipeReturn {
>
}

// namespace BranchReturn {
// type _ = Subject.BranchReturn<
// Composable<(a: number, e?: unknown) => number>,
// (a: number) => Composable<(a: number, e: number) => string>
// >
// // TODO: Fix this test
// // type testCommonCtx = Expect<
// // Equal<
// // Subject.BranchReturn<
// // Composable<(a: number, e?: unknown) => number>,
// // (a: number) => Composable<(a: number, e: number) => string>
// // >,
// // Composable<(a: number, e: number) => string>
// // >
// // >
// // type test = Expect<
// // Equal<
// // Subject.BranchReturn<
// // Composable<(a?: unknown, e?: unknown) => number>,
// // (a: number) => null | Composable<(a?: unknown, e?: unknown) => string>
// // >,
// // Composable<(a?: unknown, e?: unknown) => string | number>
// // >
// // >
// }
namespace BranchReturn {
type _ = Subject.BranchReturn<
Composable<(a: number, e?: unknown) => number>,
(a: number) => Composable<(a: number, e: number) => string>
>
type testCommonCtx = Expect<
Equal<
Subject.BranchReturn<
Composable<(a: number, e?: unknown) => number>,
(a: number) => Composable<(a: number, e: number) => string>
>,
Composable<(a: number, e: number) => string>
>
>
type test = Expect<
Equal<
Subject.BranchReturn<
Composable<(a?: unknown, e?: unknown) => number>,
(a: number) => null | Composable<(a?: unknown, e?: unknown) => string>
>,
Composable<(a?: unknown, e?: unknown) => string | number>
>
>
}

namespace GetContext {
type test1 = Expect<
Expand Down
4 changes: 3 additions & 1 deletion src/context/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,9 @@ type BranchContext<
...args: any[]
) => Composable | null | Promise<Composable | null>,
> = Awaited<ReturnType<Resolver>> extends Composable<any>
? CommonContext<[SourceComposable, Awaited<ReturnType<Resolver>>]>
? CommonContext<
[SourceComposable, NonNullable<Awaited<ReturnType<Resolver>>>]
>
: GetContext<Parameters<SourceComposable>>

type BranchReturn<
Expand Down

0 comments on commit e6b5760

Please sign in to comment.