Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Accept Plain functions in every combinator #155

Merged
merged 8 commits into from
Jun 27, 2024
170 changes: 109 additions & 61 deletions src/combinators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type {
UnpackData,
} from './types.ts'
import { composable, failure, fromSuccess, success } from './constructors.ts'
import type { Internal } from './internal/types.ts'

/**
* Merges a list of objects into a single object.
Expand Down Expand Up @@ -62,12 +63,14 @@ function mergeObjects<T extends unknown[] = unknown[]>(
* // ^? Composable<({ aNumber }: { aNumber: number }) => { aBoolean: boolean }>
* ```
*/
function pipe<Fns extends [Composable, ...Composable[]]>(
function pipe<
Fns extends [(...args: any[]) => any, ...Array<(...args: any[]) => any>],
>(
...fns: Fns
): PipeReturn<CanComposeInSequence<Fns>> {
): PipeReturn<CanComposeInSequence<Internal.Composables<Fns>>> {
const last = <T extends any[]>(arr: T): Last<T> => arr.at(-1)
return map(sequence(...fns), last as never) as PipeReturn<
CanComposeInSequence<Fns>
CanComposeInSequence<Internal.Composables<Fns>>
>
}

Expand All @@ -91,26 +94,36 @@ function pipe<Fns extends [Composable, ...Composable[]]>(
* // ^? Composable<(id: number) => [string, number, boolean]>
* ```
*/
function all<Fns extends Composable[]>(
function all<Fns extends Array<(...args: any[]) => any>>(
...fns: Fns
): Composable<
(...args: Parameters<NonNullable<CanComposeInParallel<Fns>[0]>>) => {
[k in keyof Fns]: UnpackData<Fns[k]>
(
...args: Parameters<
NonNullable<CanComposeInParallel<Internal.Composables<Fns>>[0]>
>
) => {
[k in keyof Fns]: UnpackData<Internal.Composables<Fns>[k]>
}
> {
return (async (...args) => {
const results = await Promise.all(fns.map((fn) => fn(...args)))
const callable = (async (...args) => {
const results = await Promise.all(fns.map((fn) => composable(fn)(...args)))

if (results.some(({ success }) => success === false)) {
return failure(results.map(({ errors }) => errors).flat())
}

return success((results as Success[]).map(({ data }) => data))
}) as Composable<
(...args: Parameters<NonNullable<CanComposeInParallel<Fns>[0]>>) => {
[k in keyof Fns]: UnpackData<Fns[k]>
(
...args: Parameters<
NonNullable<CanComposeInParallel<Internal.Composables<Fns>>[0]>
>
) => {
[k in keyof Fns]: UnpackData<Internal.Composables<Fns>[k]>
}
>
callable.kind = 'composable' as const
return callable
}

/**
Expand All @@ -127,27 +140,34 @@ function all<Fns extends Composable[]>(
* // ^? Composable<() => { a: string, b: number }>
* ```
*/
function collect<Fns extends Record<string, Composable>>(
function collect<Fns extends Record<string, (...args: any[]) => any>>(
fns: Fns,
): Composable<
(
...args: Parameters<
Exclude<CanComposeInParallel<RecordToTuple<Fns>>[0], undefined>
Exclude<
CanComposeInParallel<RecordToTuple<Internal.Composables<Fns>>>[0],
undefined
>
>
) => {
[key in keyof Fns]: UnpackData<Fns[key]>
[key in keyof Fns]: UnpackData<Composable<Fns[key]>>
}
> {
const fnsWithKey = Object.entries(fns).map(([key, cf]) =>
map(cf, (result) => ({ [key]: result }))
)
return map(all(...(fnsWithKey as any)), mergeObjects) as Composable<
const allFns = all(...(fnsWithKey as any)) as Composable
return map(allFns, mergeObjects) as Composable<
(
...args: Parameters<
Exclude<CanComposeInParallel<RecordToTuple<Fns>>[0], undefined>
Exclude<
CanComposeInParallel<RecordToTuple<Internal.Composables<Fns>>>[0],
undefined
>
>
) => {
[key in keyof Fns]: UnpackData<Fns[key]>
[key in keyof Fns]: UnpackData<Composable<Fns[key]>>
}
>
}
Expand All @@ -167,23 +187,27 @@ function collect<Fns extends Record<string, Composable>>(
* ```
*/

function sequence<Fns extends [Composable, ...Composable[]]>(
function sequence<
Fns extends [(...args: any[]) => any, ...Array<(...args: any[]) => any>],
>(
...fns: Fns
): SequenceReturn<CanComposeInSequence<Fns>> {
return (async (...args) => {
): SequenceReturn<CanComposeInSequence<Internal.Composables<Fns>>> {
const callable = (async (...args) => {
const [head, ...tail] = fns

const res = await head(...args)
const res = await composable(head)(...args)
if (!res.success) return failure(res.errors)

const result = [res.data]
for await (const fn of tail) {
const res = await fn(result.at(-1))
const res = await composable(fn)(result.at(-1))
if (!res.success) return failure(res.errors)
result.push(res.data)
}
return success(result)
}) as SequenceReturn<CanComposeInSequence<Fns>>
}) as SequenceReturn<CanComposeInSequence<Internal.Composables<Fns>>>
callable.kind = 'composable' as const
return callable
}

/**
Expand All @@ -202,19 +226,21 @@ function sequence<Fns extends [Composable, ...Composable[]]>(
* // result === '1 -> 2'
* ```
*/
function map<Fn extends Composable, O>(
function map<Fn extends (...args: any[]) => any, O>(
fn: Fn,
mapper: (
res: UnpackData<Fn>,
res: UnpackData<Composable<Fn>>,
...originalInput: Parameters<Fn>
) => O | Promise<O>,
): Composable<(...args: Parameters<Fn>) => O> {
return async (...args) => {
const result = await fn(...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 @@ -232,18 +258,20 @@ function map<Fn extends Composable, O>(
* ```
*/
function mapParameters<
Fn extends Composable,
Fn extends (...args: any[]) => any,
NewParameters extends unknown[],
const MapperOutput extends Parameters<Fn>,
const MapperOutput extends Parameters<Composable<Fn>>,
>(
fn: Fn,
mapper: (...args: NewParameters) => Promise<MapperOutput> | MapperOutput,
): MapParametersReturn<Fn, NewParameters, MapperOutput> {
return async (...args) => {
): MapParametersReturn<Composable<Fn>, NewParameters, MapperOutput> {
const callable = (async (...args) => {
const output = await composable(mapper)(...args)
if (!output.success) return failure(output.errors)
return fn(...output.data)
}
return composable(fn)(...output.data)
}) as MapParametersReturn<Composable<Fn>, NewParameters, MapperOutput>
callable.kind = 'composable' as const
return callable
}

/**
Expand All @@ -261,7 +289,7 @@ function mapParameters<
* ```
*/
function catchFailure<
Fn extends Composable,
Fn extends (...args: any[]) => any,
C extends (err: Error[], ...originalInput: Parameters<Fn>) => any,
>(
fn: Fn,
Expand All @@ -270,15 +298,24 @@ function catchFailure<
(
...args: Parameters<Fn>
) => Awaited<ReturnType<C>> extends never[]
? UnpackData<Fn> extends any[] ? UnpackData<Fn>
: Awaited<ReturnType<C>> | UnpackData<Fn>
: Awaited<ReturnType<C>> | UnpackData<Fn>
? UnpackData<Composable<Fn>> extends any[] ? UnpackData<Composable<Fn>>
: Awaited<ReturnType<C>> | UnpackData<Composable<Fn>>
: Awaited<ReturnType<C>> | UnpackData<Composable<Fn>>
> {
return async (...args: Parameters<Fn>) => {
const res = await fn(...args)
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))
}
}) as Composable<
(
...args: Parameters<Fn>
) => Awaited<ReturnType<C>> extends never[]
? UnpackData<Composable<Fn>> extends any[] ? UnpackData<Composable<Fn>>
: Awaited<ReturnType<C>> | UnpackData<Composable<Fn>>
: Awaited<ReturnType<C>> | UnpackData<Composable<Fn>>
>
callable.kind = 'composable' as const
return callable
}

/**
Expand All @@ -295,20 +332,22 @@ function catchFailure<
* }))
* ```
*/
function mapErrors<P extends unknown[], Output>(
fn: Composable<(...args: P) => Output>,
function mapErrors<Fn extends (...args: any[]) => any>(
fn: Fn,
mapper: (err: Error[]) => Error[] | Promise<Error[]>,
): Composable<(...args: P) => Output> {
return async (...args) => {
const res = await fn(...args)
): Composable<Fn> {
const callable = (async (...args) => {
const res = await composable(fn)(...args)
if (res.success) return success(res.data)
const mapped = await composable(mapper)(res.errors)
if (mapped.success) {
return failure(mapped.data)
} else {
return failure(mapped.errors)
}
}
}) as Composable<Fn>
callable.kind = 'composable' as const
return callable
}

/**
Expand All @@ -334,16 +373,22 @@ function trace(
result: Result<unknown>,
...originalInput: unknown[]
) => Promise<void> | void,
): <P extends unknown[], Output>(
fn: Composable<(...args: P) => Output>,
) => Composable<(...args: P) => Output> {
return (fn) => async (...args) => {
const originalResult = await fn(...args)
const traceResult = await composable(traceFn)(originalResult, ...args)
if (traceResult.success) return originalResult
): <Fn extends (...args: any[]) => any>(
fn: Fn,
) => Composable<Fn> {
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 All @@ -368,24 +413,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: Make resolver accept plain functions
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
Loading
Loading