Skip to content

Commit

Permalink
Merge pull request #159 from seasonedcc/branch-resolver
Browse files Browse the repository at this point in the history
Accept plain functions in `branch` Resolver
  • Loading branch information
diogob committed Jun 27, 2024
2 parents c942b4a + f0ad2f6 commit 7018aed
Show file tree
Hide file tree
Showing 11 changed files with 92 additions and 68 deletions.
22 changes: 8 additions & 14 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,15 +233,9 @@ Use `branch` to add conditional logic to your compositions.
It receives a composable and a predicate function that should return the next composable to be executed based on the previous function's output, like `pipe`.

```ts
const getIdOrEmail = (data: { id?: number, email?: string }) => {
return data.id ?? data.email
}
const findUserById = composable((id: number) => {
return db.users.find({ id })
})
const findUserByEmail = composable((email: string) => {
return db.users.find({ email })
})
const getIdOrEmail = (data: { id?: number, email?: string }) => data.id ?? data.email
const findUserById = (id: number) => db.users.find({ id })
const findUserByEmail = (email: string) => db.users.find({ email })
const findUserByIdOrEmail = branch(
getIdOrEmail,
(data) => (typeof data === "number" ? findUserById : findUserByEmail),
Expand All @@ -260,7 +254,7 @@ For the example above, the result will be:
If you don't want to pipe when a certain condition is matched, you can return `null` like so:
```ts
const a = () => 'a'
const b = composable(() => 'b')
const b = () => 'b'
const fn = branch(a, (data) => data === 'a' ? null : b)
// ^? Composable<() => 'a' | 'b'>
```
Expand Down Expand Up @@ -792,18 +786,18 @@ import { context } from 'composable-functions'
const getIdOrEmail = (data: { id?: number, email?: string }) => {
return data.id ?? data.email
}
const findUserById = composable((id: number, ctx: { user: User }) => {
const findUserById = (id: number, ctx: { user: User }) => {
if (!ctx.user.admin) {
throw new Error('Unauthorized')
}
return db.users.find({ id })
})
const findUserByEmail = composable((email: string, ctx: { user: User }) => {
}
const findUserByEmail = (email: string, ctx: { user: User }) => {
if (!ctx.user.admin) {
throw new Error('Unauthorized')
}
return db.users.find
})
}
const findUserByIdOrEmail = context.branch(
getIdOrEmail,
(data) => (typeof data === "number" ? findUserById : findUserByEmail),
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ const add = composable((a: number, b: number) => a + b)
// ^? Composable<(a: number, b: number) => number>
```

Or we can use combinators that evaluate to both plain functions and `Composable` into another `Composable`:
Or we can use combinators work with both plain functions and `Composable` to create other composables:

```typescript
import { composable, pipe } from 'composable-functions'
Expand Down
3 changes: 1 addition & 2 deletions context.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,8 @@ The context.branch function adds conditional logic to your compositions, forward
```ts
import { composable, context } from 'composable-functions'

const adminIncrement = composable((a: number, { user }: { user: { admin: boolean } }) =>
const adminIncrement = (a: number, { user }: { user: { admin: boolean } }) =>
user.admin ? a + 1 : a
)
const adminMakeItEven = (sum: number) => sum % 2 != 0 ? adminIncrement : null
const incrementUntilEven = context.branch(adminIncrement, adminMakeItEven)

Expand Down
27 changes: 13 additions & 14 deletions src/combinators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ function mergeObjects<T extends unknown[] = unknown[]>(
* ```
*/
function pipe<
Fns extends [(...args: any[]) => any, ...Array<(...args: any[]) => any>],
Fns extends [Internal.AnyFn, ...Internal.AnyFn[]],
>(
...fns: Fns
): PipeReturn<CanComposeInSequence<Internal.Composables<Fns>>> {
Expand Down Expand Up @@ -90,7 +90,7 @@ function pipe<
* // ^? Composable<(id: number) => [string, number, boolean]>
* ```
*/
function all<Fns extends Array<(...args: any[]) => any>>(
function all<Fns extends Internal.AnyFn[]>(
...fns: Fns
): Composable<
(
Expand Down Expand Up @@ -136,7 +136,7 @@ function all<Fns extends Array<(...args: any[]) => any>>(
* // ^? Composable<() => { a: string, b: number }>
* ```
*/
function collect<Fns extends Record<string, (...args: any[]) => any>>(
function collect<Fns extends Record<string, Internal.AnyFn>>(
fns: Fns,
): Composable<
(
Expand Down Expand Up @@ -184,7 +184,7 @@ function collect<Fns extends Record<string, (...args: any[]) => any>>(
*/

function sequence<
Fns extends [(...args: any[]) => any, ...Array<(...args: any[]) => any>],
Fns extends [Internal.AnyFn, ...Internal.AnyFn[]],
>(
...fns: Fns
): SequenceReturn<CanComposeInSequence<Internal.Composables<Fns>>> {
Expand Down Expand Up @@ -222,7 +222,7 @@ function sequence<
* // result === '1 -> 2'
* ```
*/
function map<Fn extends (...args: any[]) => any, O>(
function map<Fn extends Internal.AnyFn, O>(
fn: Fn,
mapper: (
res: UnpackData<Composable<Fn>>,
Expand Down Expand Up @@ -254,7 +254,7 @@ function map<Fn extends (...args: any[]) => any, O>(
* ```
*/
function mapParameters<
Fn extends (...args: any[]) => any,
Fn extends Internal.AnyFn,
NewParameters extends unknown[],
const MapperOutput extends Parameters<Composable<Fn>>,
>(
Expand Down Expand Up @@ -285,7 +285,7 @@ function mapParameters<
* ```
*/
function catchFailure<
Fn extends (...args: any[]) => any,
Fn extends Internal.AnyFn,
C extends (err: Error[], ...originalInput: Parameters<Fn>) => any,
>(
fn: Fn,
Expand Down Expand Up @@ -328,7 +328,7 @@ function catchFailure<
* }))
* ```
*/
function mapErrors<Fn extends (...args: any[]) => any>(
function mapErrors<Fn extends Internal.AnyFn>(
fn: Fn,
mapper: (err: Error[]) => Error[] | Promise<Error[]>,
): Composable<Fn> {
Expand Down Expand Up @@ -369,7 +369,7 @@ function trace(
result: Result<unknown>,
...originalInput: unknown[]
) => Promise<void> | void,
): <Fn extends (...args: any[]) => any>(
): <Fn extends Internal.AnyFn>(
fn: Fn,
) => Composable<Fn> {
return ((fn) => {
Expand All @@ -382,7 +382,7 @@ function trace(
}
callable.kind = 'composable' as const
return callable
}) as <Fn extends (...args: any[]) => any>(
}) as <Fn extends Internal.AnyFn>(
fn: Fn,
) => Composable<Fn>
}
Expand All @@ -409,13 +409,12 @@ function trace(
* ```
*/
function branch<
SourceComposable extends (...args: any[]) => any,
SourceComposable extends Internal.AnyFn,
Resolver extends (
o: UnpackData<Composable<SourceComposable>>,
) => Composable | null | Promise<Composable | null>,
) => Internal.AnyFn | null | Promise<Internal.AnyFn | null>,
>(
cf: SourceComposable,
// TODO: Make resolver accept plain functions
resolver: Resolver,
): BranchReturn<Composable<SourceComposable>, Resolver> {
const callable = (async (...args: Parameters<SourceComposable>) => {
Expand All @@ -425,7 +424,7 @@ function branch<
return composable(async () => {
const nextComposable = await resolver(result.data)
if (typeof nextComposable !== 'function') return result.data
return fromSuccess(nextComposable)(result.data)
return fromSuccess(composable(nextComposable))(result.data)
})()
}) as BranchReturn<Composable<SourceComposable>, Resolver>
;(callable as any).kind = 'composable' as const
Expand Down
7 changes: 4 additions & 3 deletions src/constructors.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { mapErrors } from './combinators.ts'
import { ContextError, ErrorList, InputError } from './errors.ts'
import type { Internal } from './internal/types.ts'
import type {
ApplySchemaReturn,
Composable,
Expand Down Expand Up @@ -40,10 +41,10 @@ function toError(maybeError: unknown): Error {
*/
function composable<T extends Function>(
fn: T,
): Composable<T extends (...args: any[]) => any ? T : never> {
): Composable<T extends Internal.AnyFn ? T : never> {
if ('kind' in fn && fn.kind === 'composable') {
return fn as unknown as Composable<
T extends (...args: any[]) => any ? T : never
T extends Internal.AnyFn ? T : never
>
}
const callable = async (...args: any[]) => {
Expand All @@ -59,7 +60,7 @@ function composable<T extends Function>(
}
}
callable.kind = 'composable' as const
return callable as Composable<T extends (...args: any[]) => any ? T : never>
return callable as Composable<T extends Internal.AnyFn ? T : never>
}

/**
Expand Down
11 changes: 5 additions & 6 deletions src/context/combinators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ function applyContextToList<
* // ^? ComposableWithSchema<{ aBoolean: boolean }>
* ```
*/
function pipe<Fns extends Array<(...args: any[]) => any>>(
function pipe<Fns extends Internal.AnyFn[]>(
...fns: Fns
): PipeReturn<Internal.Composables<Fns>> {
const callable =
Expand Down Expand Up @@ -62,7 +62,7 @@ function pipe<Fns extends Array<(...args: any[]) => any>>(
* ```
*/

function sequence<Fns extends Array<(...args: any[]) => any>>(
function sequence<Fns extends Internal.AnyFn[]>(
...fns: Fns
): SequenceReturn<Internal.Composables<Fns>> {
const callable = ((input: any, context: any) =>
Expand All @@ -79,13 +79,12 @@ function sequence<Fns extends Array<(...args: any[]) => any>>(
* Like branch but preserving the context parameter.
*/
function branch<
SourceComposable extends (...args: any[]) => any,
SourceComposable extends Internal.AnyFn,
Resolver extends (
o: UnpackData<Composable<SourceComposable>>,
) => Composable | null | Promise<Composable | null>,
) => Internal.AnyFn | null | Promise<Internal.AnyFn | null>,
>(
cf: SourceComposable,
// TODO: Make resolver accept plain functions
resolver: Resolver,
): BranchReturn<Composable<SourceComposable>, Resolver> {
const callable = (async (...args: Parameters<SourceComposable>) => {
Expand All @@ -96,7 +95,7 @@ function branch<
return composable(async () => {
const nextFn = await resolver(result.data)
if (typeof nextFn !== 'function') return result.data
return fromSuccess(nextFn)(result.data, context)
return fromSuccess(composable(nextFn))(result.data, context)
})()
}) as BranchReturn<Composable<SourceComposable>, Resolver>
;(callable as any).kind = 'composable' as const
Expand Down
27 changes: 23 additions & 4 deletions src/context/tests/branch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,7 @@ describe('branch', () => {
const a = ({ id }: { id: number }, context: number) => ({
id: id + 2 + context,
})
// TODO: Make resolver accept plain functions
const b = composable(
({ id }: { id: number }, context: number) => id - 1 + context,
)
const b = ({ id }: { id: number }, context: number) => id - 1 + context

const c = context.branch(a, () => Promise.resolve(b))
type _R = Expect<
Expand Down Expand Up @@ -104,6 +101,28 @@ describe('branch', () => {
assertEquals(await d({ id: 1 }), success({ id: 3, next: 'multiply' }))
})

it('should not pipe if plain function predicate returns null', async () => {
const a = (id: number) => ({
id: id + 2,
next: 'multiply',
})
const b = ({ id }: { id: number }) => String(id)
const d = context.branch(a, (output) => {
type _Check = Expect<Equal<typeof output, ReturnType<typeof a>>>
return output.next === 'multiply' ? null : b
})
type _R = Expect<
Equal<
typeof d,
Composable<
(i: number, c?: unknown) => string | { id: number; next: string }
>
>
>

assertEquals(await d(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 }) => ({
inp: ctx + 2,
Expand Down
12 changes: 6 additions & 6 deletions src/context/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,17 +95,17 @@ type BranchContext<
SourceComposable extends Composable,
Resolver extends (
...args: any[]
) => Composable | null | Promise<Composable | null>,
> = Awaited<ReturnType<Resolver>> extends Composable<any> ? CommonContext<
[SourceComposable, NonNullable<Awaited<ReturnType<Resolver>>>]
) => Internal.AnyFn | null | Promise<Internal.AnyFn | null>,
> = Awaited<ReturnType<Resolver>> extends Internal.AnyFn ? CommonContext<
[SourceComposable, Composable<NonNullable<Awaited<ReturnType<Resolver>>>>]
>
: GetContext<Parameters<SourceComposable>>

type BranchReturn<
SourceComposable extends Composable,
Resolver extends (
...args: any[]
) => Composable | null | Promise<Composable | null>,
) => Internal.AnyFn | null | Promise<Internal.AnyFn | null>,
> = CanComposeInSequence<
[SourceComposable, Composable<Resolver>]
> extends Composable[]
Expand All @@ -124,8 +124,8 @@ type BranchReturn<
>
) => null extends Awaited<ReturnType<Resolver>> ?
| UnpackData<SourceComposable>
| UnpackData<Extract<Awaited<ReturnType<Resolver>>, Composable>>
: UnpackData<Extract<Awaited<ReturnType<Resolver>>, Composable>>
| UnpackData<Composable<NonNullable<Awaited<ReturnType<Resolver>>>>>
: UnpackData<Composable<NonNullable<Awaited<ReturnType<Resolver>>>>>
>
: CanComposeInSequence<[SourceComposable, Awaited<ReturnType<Resolver>>]>
: CanComposeInSequence<[SourceComposable, Composable<Resolver>]>
Expand Down
8 changes: 4 additions & 4 deletions src/internal/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ namespace Internal {
argument2: B
}

export type AnyFn = (...args: any[]) => any

export type Prettify<T> =
& {
[K in keyof T]: T[K]
Expand Down Expand Up @@ -156,11 +158,9 @@ namespace Internal {
: FailToCompose<A, B>

export type Composables<
Fns extends
| Record<string, (...args: any[]) => any>
| Array<(...args: any[]) => any>,
Fns extends Record<string, AnyFn> | Array<AnyFn>,
> = {
[K in keyof Fns]: Composable<Extract<Fns[K], (...args: any[]) => any>>
[K in keyof Fns]: Composable<Extract<Fns[K], AnyFn>>
}
}

Expand Down
21 changes: 17 additions & 4 deletions src/tests/branch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ import { assertEquals, assertIsError, describe, it, z } from './prelude.ts'

describe('branch', () => {
it('should pipe a composable with arbitrary types', async () => {
const a = composable(({ id }: { id: number }) => ({
const a = ({ id }: { id: number }) => ({
id: id + 2,
}))
})
const b = composable(({ id }: { id: number }) => id - 1)

const c = branch(a, () => Promise.resolve(b))
Expand All @@ -29,11 +29,24 @@ describe('branch', () => {
assertEquals(await c({ id: 1 }), success(2))
})

it('accepts plain resolver functions', async () => {
const a = ({ id }: { id: number }) => ({
id: id + 2,
})
const b = ({ id }: { id: number }) => id - 1

const c = branch(a, () => Promise.resolve(b))
type _R = Expect<
Equal<typeof c, Composable<(input: { id: number }) => number>>
>

assertEquals(await c({ id: 1 }), success(2))
})

it('accepts plain functions', async () => {
const a = (a: number, b: number) => a + b
const b = (a: number) => String(a - 1)
// TODO: Make resolver accept plain functions
const fn = branch(a, (a) => a === 3 ? composable(b) : null)
const fn = branch(a, (a) => a === 3 ? b : null)
const res = await fn(1, 2)

type _FN = Expect<
Expand Down
Loading

0 comments on commit 7018aed

Please sign in to comment.