From a40510dcfc86130d468ccaf94fb859b6a62941bb Mon Sep 17 00:00:00 2001 From: Guga Guichard Date: Wed, 26 Jun 2024 18:46:10 -0300 Subject: [PATCH] feat: Combinators accept any function instead of only composables --- src/combinators.ts | 88 +++++++++++++++++++++--------- src/constructors.ts | 70 +++++++++++++----------- src/context/combinators.ts | 52 +++++++++++++----- src/context/tests/branch.test.ts | 68 +++++++++++++++-------- src/context/tests/pipe.test.ts | 12 ++++ src/context/tests/sequence.test.ts | 11 ++++ src/context/tests/types.test.ts | 45 +++++++-------- src/context/types.ts | 5 +- src/internal/types.test.ts | 1 - src/internal/types.ts | 10 +++- src/tests/all.test.ts | 19 +++++++ src/tests/branch.test.ts | 22 +++++++- src/tests/catch-failure.test.ts | 15 +++++ src/tests/collect.test.ts | 40 ++++++++++---- src/tests/constructors.test.ts | 53 ++++++++++++++++++ src/tests/map-errors.test.ts | 16 ++++++ src/tests/map-parameters.test.ts | 18 ++++++ src/tests/map.test.ts | 12 ++++ src/tests/pipe.test.ts | 12 ++++ src/tests/sequence.test.ts | 12 ++++ src/tests/trace.test.ts | 13 ++++- 21 files changed, 455 insertions(+), 139 deletions(-) diff --git a/src/combinators.ts b/src/combinators.ts index 2e60148f..50dc494c 100644 --- a/src/combinators.ts +++ b/src/combinators.ts @@ -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. @@ -66,10 +67,10 @@ function pipe< Fns extends [(...args: any[]) => any, ...Array<(...args: any[]) => any>], >( ...fns: Fns -): PipeReturn> { +): PipeReturn>> { const last = (arr: T): Last => arr.at(-1) return map(sequence(...fns), last as never) as PipeReturn< - CanComposeInSequence + CanComposeInSequence> > } @@ -96,11 +97,15 @@ function pipe< function all any>>( ...fns: Fns ): Composable< - (...args: Parameters[0]>>) => { - [k in keyof Fns]: UnpackData> + ( + ...args: Parameters< + NonNullable>[0]> + > + ) => { + [k in keyof Fns]: UnpackData[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)) { @@ -109,10 +114,16 @@ function all any>>( return success((results as Success[]).map(({ data }) => data)) }) as Composable< - (...args: Parameters[0]>>) => { - [k in keyof Fns]: UnpackData> + ( + ...args: Parameters< + NonNullable>[0]> + > + ) => { + [k in keyof Fns]: UnpackData[k]> } > + callable.kind = 'composable' as const + return callable } /** @@ -134,7 +145,10 @@ function collect any>>( ): Composable< ( ...args: Parameters< - Exclude>[0], undefined> + Exclude< + CanComposeInParallel>>[0], + undefined + > > ) => { [key in keyof Fns]: UnpackData> @@ -147,7 +161,10 @@ function collect any>>( return map(allFns, mergeObjects) as Composable< ( ...args: Parameters< - Exclude>[0], undefined> + Exclude< + CanComposeInParallel>>[0], + undefined + > > ) => { [key in keyof Fns]: UnpackData> @@ -174,8 +191,8 @@ function sequence< Fns extends [(...args: any[]) => any, ...Array<(...args: any[]) => any>], >( ...fns: Fns -): SequenceReturn> { - return (async (...args) => { +): SequenceReturn>> { + const callable = (async (...args) => { const [head, ...tail] = fns const res = await composable(head)(...args) @@ -188,7 +205,9 @@ function sequence< result.push(res.data) } return success(result) - }) as SequenceReturn> + }) as SequenceReturn>> + callable.kind = 'composable' as const + return callable } /** @@ -214,12 +233,14 @@ function map any, O>( ...originalInput: Parameters ) => O | Promise, ): Composable<(...args: Parameters) => 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) => O> + callable.kind = 'composable' as const + return callable } /** @@ -244,11 +265,13 @@ function mapParameters< fn: Fn, mapper: (...args: NewParameters) => Promise | MapperOutput, ): MapParametersReturn, 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, NewParameters, MapperOutput> + callable.kind = 'composable' as const + return callable } /** @@ -279,7 +302,7 @@ function catchFailure< : Awaited> | UnpackData> : Awaited> | UnpackData> > { - return (async (...args: Parameters) => { + const callable = (async (...args: Parameters) => { const res = await composable(fn)(...args) if (res.success) return success(res.data) return composable(catcher)(res.errors, ...(args as never)) @@ -291,6 +314,8 @@ function catchFailure< : Awaited> | UnpackData> : Awaited> | UnpackData> > + callable.kind = 'composable' as const + return callable } /** @@ -311,7 +336,7 @@ function mapErrors any>( fn: Fn, mapper: (err: Error[]) => Error[] | Promise, ): Composable { - 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) @@ -321,6 +346,8 @@ function mapErrors any>( return failure(mapped.errors) } }) as Composable + callable.kind = 'composable' as const + return callable } /** @@ -349,12 +376,16 @@ function trace( ): any>( fn: Fn, ) => Composable { - 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 any>( fn: Fn, ) => Composable @@ -382,16 +413,17 @@ function trace( * ``` */ function branch< - SourceComposable extends Composable, + SourceComposable extends (...args: any[]) => any, Resolver extends ( - o: UnpackData, + o: UnpackData>, ) => Composable | null | Promise, >( cf: SourceComposable, + // TODO: Make resolver accept plain functions resolver: Resolver, -): BranchReturn { - return (async (...args: Parameters) => { - const result = await cf(...args) +): BranchReturn, Resolver> { + const callable = (async (...args: Parameters) => { + const result = await composable(cf)(...args) if (!result.success) return result return composable(async () => { @@ -399,7 +431,9 @@ function branch< if (typeof nextComposable !== 'function') return result.data return fromSuccess(nextComposable)(result.data) })() - }) as BranchReturn + }) as BranchReturn, Resolver> + ;(callable as any).kind = 'composable' as const + return callable } export { diff --git a/src/constructors.ts b/src/constructors.ts index 383e8a4c..8cb26627 100644 --- a/src/constructors.ts +++ b/src/constructors.ts @@ -7,6 +7,7 @@ import type { Failure, ParserSchema, Success, + UnpackData, } from './types.ts' /** @@ -88,38 +89,6 @@ function fromSuccess( }) as (...args: P) => Promise } -/** - * 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( - inputSchema?: ParserSchema, - contextSchema?: ParserSchema, -): ( - hander: (input: I, context: C) => Output, -) => ComposableWithSchema { - return (handler) => - applySchema( - inputSchema, - contextSchema, - )(composable(handler)) as ComposableWithSchema -} - /** * 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 @@ -146,10 +115,11 @@ function applySchema( inputSchema?: ParserSchema, contextSchema?: ParserSchema, ) { + // TODO: Accept plain functions and equalize with withSchema return ( fn: Composable<(input: Input, context: Context) => R>, ): ApplySchemaReturn => { - return ((input?: unknown, context?: unknown) => { + const callable = ((input?: unknown, context?: unknown) => { const ctxResult = (contextSchema ?? alwaysUnknownSchema).safeParse( context, ) @@ -166,9 +136,43 @@ function applySchema( } return fn(result.data as Input, ctxResult.data as Context) }) as ApplySchemaReturn + ;(callable as any).kind = 'composable' as const + return callable } } +/** + * 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( + inputSchema?: ParserSchema, + contextSchema?: ParserSchema, +): unknown>( + fn: Fn, +) => ComposableWithSchema>> { + return (handler) => + applySchema( + inputSchema, + contextSchema, + )(composable(handler)) as ComposableWithSchema +} + const alwaysUnknownSchema: ParserSchema = { safeParse: (data: unknown) => ({ success: true, data }), } diff --git a/src/context/combinators.ts b/src/context/combinators.ts index 5ff9d6a3..ca7cdabf 100644 --- a/src/context/combinators.ts +++ b/src/context/combinators.ts @@ -2,11 +2,16 @@ import type { Composable, UnpackData } from '../types.ts' import * as A from '../combinators.ts' import { composable, fromSuccess } from '../constructors.ts' import type { BranchReturn, PipeReturn, SequenceReturn } from './types.ts' +import type { Internal } from '../internal/types.ts' 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) => composable(fn)(input, context)) as Composable + callable.kind = 'composable' as const + return callable + }) as Composable[] } /** @@ -27,9 +32,19 @@ function applyContextToList< * // ^? ComposableWithSchema<{ aBoolean: boolean }> * ``` */ -function pipe(...fns: Fns): PipeReturn { - return ((input: any, context: any) => - A.pipe(...applyContextToList(fns, context))(input)) as PipeReturn +function pipe any>>( + ...fns: Fns +): PipeReturn> { + const callable = + ((input: any, context: any) => + A.pipe(...applyContextToList(fns, context) as [ + Composable, + ...Composable[], + ])(input)) as PipeReturn< + Internal.Composables + > + ;(callable as any).kind = 'composable' as const + return callable } /** @@ -47,28 +62,35 @@ function pipe(...fns: Fns): PipeReturn { * ``` */ -function sequence(...fns: Fns): SequenceReturn { - return ((input: any, context: any) => - A.sequence(...applyContextToList(fns, context))( +function sequence any>>( + ...fns: Fns +): SequenceReturn> { + const callable = ((input: any, context: any) => + A.sequence( + ...applyContextToList(fns, context) as [Composable, ...Composable[]], + )( input, - )) as SequenceReturn + )) as SequenceReturn> + ;(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, + o: UnpackData>, ) => Composable | null | Promise, >( cf: SourceComposable, + // TODO: Make resolver accept plain functions resolver: Resolver, -): BranchReturn { - return (async (...args: Parameters) => { +): BranchReturn, Resolver> { + const callable = (async (...args: Parameters) => { 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 () => { @@ -76,7 +98,9 @@ function branch< if (typeof nextFn !== 'function') return result.data return fromSuccess(nextFn)(result.data, context) })() - }) as BranchReturn + }) as BranchReturn, Resolver> + ;(callable as any).kind = 'composable' as const + return callable } export { branch, pipe, sequence } diff --git a/src/context/tests/branch.test.ts b/src/context/tests/branch.test.ts index 04ff69e4..e14acdc8 100644 --- a/src/context/tests/branch.test.ts +++ b/src/context/tests/branch.test.ts @@ -34,6 +34,26 @@ describe('branch', () => { assertEquals(await c({ id: 1 }, 0), success(2)) }) + it('accepts plain functions', async () => { + 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 c = context.branch(a, () => Promise.resolve(b)) + type _R = Expect< + Equal< + typeof c, + Composable<(input: { id: number }, context: number) => number> + > + > + + assertEquals(await c({ id: 1 }, 0), success(2)) + }) + it('should pipe a composable with a function that returns a composable with schema', async () => { const a = withSchema(z.object({ id: z.number() }))(({ id }) => ({ id: id + 2, @@ -59,30 +79,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>> - // 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>> + 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 }) => ({ diff --git a/src/context/tests/pipe.test.ts b/src/context/tests/pipe.test.ts index 0ff4b8e6..6cb812d3 100644 --- a/src/context/tests/pipe.test.ts +++ b/src/context/tests/pipe.test.ts @@ -160,4 +160,16 @@ describe('pipe', () => { > assertEquals(res, success(5)) }) + + it('accepts plain functions', async () => { + const add = (a: number, ctx: number) => a + ctx + const fn = context.pipe(add, add) + + const res = await fn(1, 2) + + type _FN = Expect< + Equal number>> + > + assertEquals(res, success(5)) + }) }) diff --git a/src/context/tests/sequence.test.ts b/src/context/tests/sequence.test.ts index 27397672..0726b427 100644 --- a/src/context/tests/sequence.test.ts +++ b/src/context/tests/sequence.test.ts @@ -211,4 +211,15 @@ describe('sequence', () => { assertEquals(await c(1, 2), success<[number, string]>([3, '3 + 2'])) }) + + it('accepts plain functions', async () => { + const a = (a: number, b: number) => a + b + const b = (a: number, b: number) => `${a} + ${b}` + const c = context.sequence(a, b) + type _R = Expect< + Equal [number, string]>> + > + + assertEquals(await c(1, 2), success<[number, string]>([3, '3 + 2'])) + }) }) diff --git a/src/context/tests/types.test.ts b/src/context/tests/types.test.ts index 1e46de9d..59b131c4 100644 --- a/src/context/tests/types.test.ts +++ b/src/context/tests/types.test.ts @@ -136,31 +136,26 @@ 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 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< diff --git a/src/context/types.ts b/src/context/types.ts index 98b47f5e..fbbb8816 100644 --- a/src/context/types.ts +++ b/src/context/types.ts @@ -96,8 +96,9 @@ type BranchContext< Resolver extends ( ...args: any[] ) => Composable | null | Promise, -> = Awaited> extends Composable - ? CommonContext<[SourceComposable, Awaited>]> +> = Awaited> extends Composable ? CommonContext< + [SourceComposable, NonNullable>>] + > : GetContext> type BranchReturn< diff --git a/src/internal/types.test.ts b/src/internal/types.test.ts index d154e05d..c5f36e95 100644 --- a/src/internal/types.test.ts +++ b/src/internal/types.test.ts @@ -253,7 +253,6 @@ namespace Prettify { namespace ApplyArgumentsToFns { type WithEmpty = Expect, []>> - type _ = Internal.ApplyArgumentsToFns<[() => 1], [string]> type WithSingle = Expect< Equal< Internal.ApplyArgumentsToFns<[Composable<() => 1>], [string]>, diff --git a/src/internal/types.ts b/src/internal/types.ts index 989fb34d..1cad934a 100644 --- a/src/internal/types.ts +++ b/src/internal/types.ts @@ -1,6 +1,6 @@ // deno-lint-ignore-file no-namespace -import { Composable } from '../types.ts' +import type { Composable } from '../types.ts' namespace Internal { export type IncompatibleArguments = { @@ -154,6 +154,14 @@ namespace Internal { ? B extends Record ? Prettify : FailToCompose : FailToCompose + + export type Composables< + Fns extends + | Record any> + | Array<(...args: any[]) => any>, + > = { + [K in keyof Fns]: Composable any>> + } } export type { Internal } diff --git a/src/tests/all.test.ts b/src/tests/all.test.ts index 17df0b0e..e053610a 100644 --- a/src/tests/all.test.ts +++ b/src/tests/all.test.ts @@ -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>>> + + assertEquals(res, success<[number, string, void]>([3, '1', undefined])) + }) + it('handles optional arguments', async () => { const fn = all(optionalAdd, toString, voidFn) diff --git a/src/tests/branch.test.ts b/src/tests/branch.test.ts index e4f1ee58..18f5045c 100644 --- a/src/tests/branch.test.ts +++ b/src/tests/branch.test.ts @@ -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', () => { @@ -24,6 +29,21 @@ 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) + // TODO: Make resolver accept plain functions + const fn = branch(a, (a) => a === 3 ? composable(b) : null) + const res = await fn(1, 2) + + type _FN = Expect< + Equal number | string>> + > + type _R = Expect>> + + 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, diff --git a/src/tests/catch-failure.test.ts b/src/tests/catch-failure.test.ts index b9e59204..f0f85501 100644 --- a/src/tests/catch-failure.test.ts +++ b/src/tests/catch-failure.test.ts @@ -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 number | null>> + > + type _R = Expect>> + + assertEquals(res, success(null)) + }) + it('returns original type when catcher returns empty list', async () => { const getList = composable(() => [1, 2, 3]) const fn = catchFailure(getList, () => []) diff --git a/src/tests/collect.test.ts b/src/tests/collect.test.ts index d6013367..dfa57f7e 100644 --- a/src/tests/collect.test.ts +++ b/src/tests/collect.test.ts @@ -20,17 +20,37 @@ const faultyAdd = composable((a: number, b: number) => { 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< + Equal< + typeof fn, + Composable< + ( + a: number, + b: number, + ) => { + add: number + string: string + void: void + } + > + > + > + type _R = Expect< + Equal> + > + + assertEquals(res, success({ add: 3, string: '1', void: undefined })) + }) + + it('accepts plain functions', async () => { + const fn = collect({ + add: (a: number, b: number) => a + b, + string: (a: unknown) => String(a), + void: () => {}, + }) const res = await fn(1, 2) type _FN = Expect< diff --git a/src/tests/constructors.test.ts b/src/tests/constructors.test.ts index 0874ab57..bfabfc1c 100644 --- a/src/tests/constructors.test.ts +++ b/src/tests/constructors.test.ts @@ -61,6 +61,17 @@ describe('composable', () => { type _FN = Expect any>>> }) + it('accepts another composable and avoids nesting it', async () => { + const a = composable(() => 'hey') + const fn = composable(a) + const res = await fn() + + type _FN = Expect 'hey'>>> + type _R = Expect>> + + assertEquals(res, success('hey')) + }) + it('infers the types of async functions', async () => { const fn = composable(asyncAdd) const res = await fn(1, 2) @@ -199,6 +210,24 @@ describe('withSchema', () => { }) }) + it('accepts a composable', async () => { + const handler = withSchema()(composable(() => 'no input!')) + type _R = Expect>> + + assertEquals(await handler(), success('no input!')) + }) + + it('defaults non-declared input to unknown', async () => { + const handler = withSchema()((args) => args) + type _R = Expect>> + + assertEquals(await handler('some input'), { + success: true, + data: 'some input', + errors: [], + }) + }) + it('uses zod parsers to parse the input and context and call the schema function', async () => { const parser = z.object({ id: z.preprocess(Number, z.number()) }) const ctxParser = z.object({ uid: z.preprocess(Number, z.number()) }) @@ -406,6 +435,30 @@ describe('applySchema', () => { assertEquals(result, success('a')) }) + // TODO: Accept plain functions and equalize with withSchema + // it('accepts a plain function', async () => { + // const inputSchema = z.object({ id: z.preprocess(Number, z.number()) }) + // const ctxSchema = z.object({ uid: z.preprocess(Number, z.number()) }) + + // const handler = applySchema( + // inputSchema, + // ctxSchema, + // )( + // composable( + // ({ id }: { id: number }, { uid }: { uid: number }) => + // [id, uid] as const, + // ), + // ) + // type _R = Expect< + // Equal> + // > + + // assertEquals( + // await handler({ id: 1 }, { uid: 2 }), + // success<[number, number]>([1, 2]), + // ) + // }); + it('fails to compose when there is an object schema with incompatible properties', async () => { const inputSchema = z.object({ x: z.string() }) diff --git a/src/tests/map-errors.test.ts b/src/tests/map-errors.test.ts index 70ed4f16..171c1097 100644 --- a/src/tests/map-errors.test.ts +++ b/src/tests/map-errors.test.ts @@ -25,6 +25,22 @@ describe('mapErrors', () => { assertEquals(res, success(4)) }) + it('accepts plain functions', async () => { + const fn = mapErrors((a: number, b: number) => { + if (a === 1) throw new Error('a is 1') + return a + b + }, (errors) => errors.map(cleanError)) + const res = await fn(1, 2) + + type _FN = Expect< + Equal number>> + > + type _R = Expect>> + + assertEquals(res.success, false) + assertEquals(res.errors[0].message, 'a is 1!!!') + }) + it('maps over the error results of a Composable function', async () => { const fn = mapErrors(faultyAdd, (errors) => errors.map(cleanError)) const res = await fn(1, 2) diff --git a/src/tests/map-parameters.test.ts b/src/tests/map-parameters.test.ts index 0615d441..476a1e6e 100644 --- a/src/tests/map-parameters.test.ts +++ b/src/tests/map-parameters.test.ts @@ -28,6 +28,24 @@ describe('mapParameters', () => { assertEquals(res, success(3)) }) + it('accepts plain functions', async () => { + const fn = mapParameters( + (a: number, b: number) => a + b, + ({ a, b }: { a: number; b: number }) => [a, b], + ) + const res = await fn({ a: 1, b: 2 }) + + type _FN = Expect< + Equal< + typeof fn, + Composable<(d: { a: number; b: number }) => number> + > + > + type _R = Expect>> + + assertEquals(res, success(3)) + }) + it('maps with an async function', async () => { const fn = mapParameters( add, diff --git a/src/tests/map.test.ts b/src/tests/map.test.ts index 29039113..8d75fdbe 100644 --- a/src/tests/map.test.ts +++ b/src/tests/map.test.ts @@ -22,6 +22,18 @@ describe('map', () => { assertEquals(res, success(true)) }) + it('accepts plain functions', async () => { + const fn = map((a: number, b: number) => a + b, (a) => a + 1 === 4) + const res = await fn(1, 2) + + type _FN = Expect< + Equal boolean>> + > + type _R = Expect>> + + assertEquals(res, success(true)) + }) + it('maps with an async function', async () => { const fn = map(add, (a) => Promise.resolve(a + 1 === 4)) const res = await fn(1, 2) diff --git a/src/tests/pipe.test.ts b/src/tests/pipe.test.ts index 059bdacb..a0e365c6 100644 --- a/src/tests/pipe.test.ts +++ b/src/tests/pipe.test.ts @@ -26,6 +26,18 @@ describe('pipe', () => { assertEquals(res, success('3')) }) + it('accepts plain functions', async () => { + const fn = pipe((a: number, b: number) => a + b, toString) + const res = await fn(1, 2) + + type _FN = Expect< + Equal string>> + > + type _R = Expect>> + + assertEquals(res, success('3')) + }) + it('type checks and composes async functions', async () => { const asyncProduceToIncrement = composable(() => Promise.resolve({ toIncrement: 1, someOtherProperty: 'test' }) diff --git a/src/tests/sequence.test.ts b/src/tests/sequence.test.ts index 71f0d3ed..ae82cf79 100644 --- a/src/tests/sequence.test.ts +++ b/src/tests/sequence.test.ts @@ -26,6 +26,18 @@ describe('sequence', () => { assertEquals(res, success<[number, string]>([3, '3'])) }) + it('accepts plain functions', async () => { + const fn = sequence((a: number, b: number) => a + b, toString) + const res = await fn(1, 2) + + type _FN = Expect< + Equal [number, string]>> + > + type _R = Expect>> + + assertEquals(res, success<[number, string]>([3, '3'])) + }) + it('type checks and composes async functions', async () => { const asyncProduceToIncrement = composable(() => Promise.resolve({ toIncrement: 1, someOtherProperty: 'test' }) diff --git a/src/tests/trace.test.ts b/src/tests/trace.test.ts index 371a494e..ec328a89 100644 --- a/src/tests/trace.test.ts +++ b/src/tests/trace.test.ts @@ -6,7 +6,7 @@ import { trace, withSchema, } from '../index.ts' -import type { Composable, ComposableWithSchema } from '../index.ts' +import type { Composable, ComposableWithSchema, Result } from '../index.ts' describe('trace', () => { it('converts trace exceptions to failures', async () => { @@ -27,6 +27,17 @@ describe('trace', () => { assertIsError(result.errors[0], Error, 'Problem in tracing') }) + it('accepts plain functions as composable', async () => { + const a = (id: number) => id + 1 + const b = trace((result) => console.log(result.success))(a) + const res = await b(1) + + type _FN = Expect number>>> + type _R = Expect>> + + assertEquals(res, success(2)) + }) + it('converts trace exceptions to failures', async () => { const a = composable(({ id }: { id: number }) => id + 1)