Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
gustavoguichard committed Jun 26, 2024
1 parent e6b5760 commit 1ad27da
Show file tree
Hide file tree
Showing 17 changed files with 271 additions and 66 deletions.
26 changes: 18 additions & 8 deletions src/combinators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,17 @@ function pipe<
Fns extends [(...args: any[]) => any, ...Array<(...args: any[]) => any>],
>(
...fns: Fns
): PipeReturn<CanComposeInSequence<Fns>> {
): PipeReturn<CanComposeInSequence<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<Composables<Fns>>
>
}

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

/**
* Composes functions to run in parallel returning a tuple of all results when all are successful.
*
Expand All @@ -96,8 +100,10 @@ function pipe<
function all<Fns extends Array<(...args: any[]) => any>>(
...fns: Fns
): Composable<
(...args: Parameters<NonNullable<CanComposeInParallel<Fns>[0]>>) => {
[k in keyof Fns]: UnpackData<Composable<Fns[k]>>
(
...args: Parameters<NonNullable<CanComposeInParallel<Composables<Fns>>[0]>>
) => {
[k in keyof Fns]: UnpackData<Composables<Fns>[k]>
}
> {
const callable = (async (...args) => {
Expand All @@ -109,8 +115,12 @@ function all<Fns extends Array<(...args: any[]) => any>>(

return success((results as Success[]).map(({ data }) => data))
}) as Composable<
(...args: Parameters<NonNullable<CanComposeInParallel<Fns>[0]>>) => {
[k in keyof Fns]: UnpackData<Composable<Fns[k]>>
(
...args: Parameters<
NonNullable<CanComposeInParallel<Composables<Fns>>[0]>
>
) => {
[k in keyof Fns]: UnpackData<Composables<Fns>[k]>
}
>
callable.kind = 'composable' as const
Expand Down Expand Up @@ -176,7 +186,7 @@ function sequence<
Fns extends [(...args: any[]) => any, ...Array<(...args: any[]) => any>],
>(
...fns: Fns
): SequenceReturn<CanComposeInSequence<Fns>> {
): SequenceReturn<CanComposeInSequence<Composables<Fns>>> {
const callable = (async (...args) => {
const [head, ...tail] = fns

Expand All @@ -190,7 +200,7 @@ function sequence<
result.push(res.data)
}
return success(result)
}) as SequenceReturn<CanComposeInSequence<Fns>>
}) as SequenceReturn<CanComposeInSequence<Composables<Fns>>>
callable.kind = 'composable' as const
return callable
}
Expand Down
84 changes: 52 additions & 32 deletions src/constructors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
Failure,
ParserSchema,
Success,
UnpackData,
} from './types.ts'

/**
Expand Down Expand Up @@ -88,38 +89,6 @@ function fromSuccess<O, P extends any[]>(
}) as (...args: P) => Promise<O>
}

/**
* Creates a composable with unknown input and context that uses schemas to parse them into known types.
* This allows you to code the function with arbitrary types knowinng that they will be enforced in runtime.
* Very useful when piping data coming from any external source into your composables.
* After giving the input and context schemas, you can pass a handler function that takes type safe input and context. That function is gonna catch any errors and always return a Result.
* @param inputSchema the schema for the input
* @param contextSchema the schema for the context
* @returns a handler function that takes type safe input and context
* @example
* const safeFunction = withSchema(
* z.object({ greeting: z.string() }),
* z.object({
* user: z.object({ name: z.string() })
* }),
* )
* const safeGreet = safeFunction(({ greeting }, { user }) => ({
* message: `${greeting} ${user.name}`
* })
*/
function withSchema<I, C>(
inputSchema?: ParserSchema<I>,
contextSchema?: ParserSchema<C>,
): <Output>(
hander: (input: I, context: C) => Output,
) => ComposableWithSchema<Output> {
return (handler) =>
applySchema(
inputSchema,
contextSchema,
)(composable(handler)) as ComposableWithSchema<any>
}

/**
* Takes a composable and creates a composable withSchema that will assert the input and context types according to the given schemas.
* @param fn a composable function
Expand All @@ -146,6 +115,7 @@ function applySchema<ParsedInput, ParsedContext>(
inputSchema?: ParserSchema<ParsedInput>,
contextSchema?: ParserSchema<ParsedContext>,
) {
// TODO: Accept plain functions and equalize with withSchema
return <R, Input, Context>(
fn: Composable<(input: Input, context: Context) => R>,
): ApplySchemaReturn<ParsedInput, ParsedContext, typeof fn> => {
Expand All @@ -171,6 +141,56 @@ function applySchema<ParsedInput, ParsedContext>(
}
}

/**
* Creates a composable with unknown input and context that uses schemas to parse them into known types.
* This allows you to code the function with arbitrary types knowinng that they will be enforced in runtime.
* Very useful when piping data coming from any external source into your composables.
* After giving the input and context schemas, you can pass a handler function that takes type safe input and context. That function is gonna catch any errors and always return a Result.
* @param inputSchema the schema for the input
* @param contextSchema the schema for the context
* @returns a handler function that takes type safe input and context
* @example
* const safeFunction = withSchema(
* z.object({ greeting: z.string() }),
* z.object({
* user: z.object({ name: z.string() })
* }),
* )
* const safeGreet = safeFunction(({ greeting }, { user }) => ({
* message: `${greeting} ${user.name}`
* })
*/
function withSchema<I, C>(
inputSchema?: ParserSchema<I>,
contextSchema?: ParserSchema<C>,
): <Fn extends (input: I, context: C) => unknown>(
fn: Fn,
) => ComposableWithSchema<UnpackData<Composable<Fn>>> {
return ((fn) => {
const callable = (input?: unknown, context?: unknown) => {
const ctxResult = (contextSchema ?? alwaysUnknownSchema).safeParse(
context,
)
const result = (inputSchema ?? alwaysUnknownSchema).safeParse(input)

if (!result.success || !ctxResult.success) {
const inputErrors = result.success ? [] : result.error.issues.map(
(error) => new InputError(error.message, error.path as string[]),
)
const ctxErrors = ctxResult.success ? [] : ctxResult.error.issues.map(
(error) => new ContextError(error.message, error.path as string[]),
)
return Promise.resolve(failure([...inputErrors, ...ctxErrors]))
}
return composable(fn)(result.data as I, ctxResult.data as C)
}
;(callable as any).kind = 'composable' as const
return callable
}) as <Fn extends (input: I, context: C) => any>(
fn: Fn,
) => ComposableWithSchema<UnpackData<Composable<Fn>>>
}

const alwaysUnknownSchema: ParserSchema<unknown> = {
safeParse: (data: unknown) => ({ success: true, data }),
}
Expand Down
16 changes: 10 additions & 6 deletions src/context/combinators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@ function pipe<Fns extends Array<(...args: any[]) => any>>(
): PipeReturn<Composables<Fns>> {
const callable =
((input: any, context: any) =>
A.pipe(...applyContextToList(fns, context) as any)(input)) as PipeReturn<
A.pipe(...applyContextToList(fns, context) as [
Composable,
...Composable[],
])(input)) as PipeReturn<
Composables<Fns>
>
;(callable as any).kind = 'composable' as const
Expand All @@ -65,11 +68,12 @@ function pipe<Fns extends Array<(...args: any[]) => any>>(
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>>
const callable = ((input: any, context: any) =>
A.sequence(
...applyContextToList(fns, context) as [Composable, ...Composable[]],
)(
input,
)) as SequenceReturn<Composables<Fns>>
;(callable as any).kind = 'composable' as const
return callable
}
Expand Down
4 changes: 0 additions & 4 deletions src/context/tests/types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,10 +137,6 @@ namespace PipeReturn {
}

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<
Expand Down
3 changes: 1 addition & 2 deletions src/context/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,7 @@ type BranchContext<
Resolver extends (
...args: any[]
) => Composable | null | Promise<Composable | null>,
> = Awaited<ReturnType<Resolver>> extends Composable<any>
? CommonContext<
> = Awaited<ReturnType<Resolver>> extends Composable<any> ? CommonContext<
[SourceComposable, NonNullable<Awaited<ReturnType<Resolver>>>]
>
: GetContext<Parameters<SourceComposable>>
Expand Down
1 change: 0 additions & 1 deletion src/internal/types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,6 @@ namespace Prettify {

namespace ApplyArgumentsToFns {
type WithEmpty = Expect<Equal<Internal.ApplyArgumentsToFns<[], [string]>, []>>
type _ = Internal.ApplyArgumentsToFns<[() => 1], [string]>
type WithSingle = Expect<
Equal<
Internal.ApplyArgumentsToFns<[Composable<() => 1>], [string]>,
Expand Down
19 changes: 19 additions & 0 deletions src/tests/all.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,25 @@ describe('all', () => {
assertEquals(res, success<[number, string, undefined]>([3, '1', undefined]))
})

it('accepts plain functions', async () => {
const fn = all(
(a: number, b: number) => a + b,
(a: unknown) => `${a}`,
() => {},
)
const res = await fn(1, 2)

type _FN = Expect<
Equal<
typeof fn,
Composable<(a: number, b: number) => [number, string, void]>
>
>
// type _R = Expect<Equal<typeof res, Result<[number, string, void]>>>>

assertEquals(res, success<[number, string, void]>([3, '1', undefined]))
})

it('handles optional arguments', async () => {
const fn = all(optionalAdd, toString, voidFn)

Expand Down
21 changes: 20 additions & 1 deletion src/tests/branch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ import {
success,
withSchema,
} from '../index.ts'
import type { Composable, ComposableWithSchema, UnpackData } from '../types.ts'
import type {
Composable,
ComposableWithSchema,
Result,
UnpackData,
} from '../types.ts'
import { assertEquals, assertIsError, describe, it, z } from './prelude.ts'

describe('branch', () => {
Expand All @@ -24,6 +29,20 @@ describe('branch', () => {
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)
const fn = branch(a, (a) => a === 3 ? composable(b) : null)
const res = await fn(1, 2)

type _FN = Expect<
Equal<typeof fn, Composable<(a: number, b: number) => number | string>>
>
type _R = Expect<Equal<typeof res, Result<number | string>>>

assertEquals(res, success('2'))
})

it('should enable conditionally choosing the next Composable with the output of first one', async () => {
const a = composable((id: number) => ({
id: id + 2,
Expand Down
15 changes: 15 additions & 0 deletions src/tests/catch-failure.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,21 @@ describe('catchFailure', () => {
assertEquals(res, success(null))
})

it('accepts plain functions', async () => {
const fn = catchFailure((a: number, b: number) => {
if (a === 1) throw new Error('a is 1')
return a + b
}, () => 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(null))
})

it('returns original type when catcher returns empty list', async () => {
const getList = composable(() => [1, 2, 3])
const fn = catchFailure(getList, () => [])
Expand Down
13 changes: 2 additions & 11 deletions src/tests/collect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,10 @@ const faultyAdd = composable((a: number, b: number) => {
return a + b
})

// TODO: accept plain functions
describe('collect', () => {
it('collects the results of an object of Composables into a result with same format', async () => {
const fn: Composable<
(
args_0: number,
args_1: number,
) => {
add: number
string: string
void: void
}
> = collect({ add: add, string: toString, void: voidFn })

const fn = collect({ add: add, string: toString, void: voidFn })
const res = await fn(1, 2)

type _FN = Expect<
Expand Down
Loading

0 comments on commit 1ad27da

Please sign in to comment.