diff --git a/API.md b/API.md index f3142de..4993e9a 100644 --- a/API.md +++ b/API.md @@ -36,9 +36,9 @@ - [Success](#success-1) - [UnpackData](#unpackdata) - [Combinators with Context](#combinators-with-context) - - [context.branch](#contextbranch) - - [context.pipe](#contextpipe) - - [context.sequence](#contextsequence) + - [withContext.branch](#withcontextbranch) + - [withContext.pipe](#withcontextpipe) + - [withContext.sequence](#withcontextsequence) - [Serialization](#serialization) - [serialize](#serialize) - [serializeError](#serializeerror) @@ -459,7 +459,7 @@ The most common use case is to log failures to the console or to an external ser ```ts const traceToConsole = trace((result, ...args) => { - if(!context.result.success) { + if(!result.success) { console.trace("Composable Failure ", result, ...args) } }) @@ -748,15 +748,15 @@ The context is a concept of an argument that is passed to every functions of a s However in sequential compositions, we need a set of special combinators that will forward the context - the second parameter - to every function in the composition. -Use the sequential combinators from the namespace `context` to get this behavior. +Use the sequential combinators from the namespace `withContext` to get this behavior. -For a deeper explanation check the [`context` docs](./context.md). +For a deeper explanation check the [context docs](./context.md). -## context.branch +## withContext.branch It is the same as `branch` but it will forward the context to the next composable. ```ts -import { context } from 'composable-functions' +import { withContext } from 'composable-functions' const getIdOrEmail = (data: { id?: number, email?: string }) => { return data.id ?? data.email @@ -773,38 +773,39 @@ const findUserByEmail = (email: string, ctx: { user: User }) => { } return db.users.find } -const findUserByIdOrEmail = context.branch( +const findUserByIdOrEmail = withContext.branch( getIdOrEmail, (data) => (typeof data === "number" ? findUserById : findUserByEmail), ) const result = await findUserByIdOrEmail({ id: 1 }, { user: { admin: true } }) ``` -## context.pipe + +## withContext.pipe Similar to `pipe` but it will forward the context to the next composable. ```ts -import { context } from 'composable-functions' +import { withContext } from 'composable-functions' const a = (aNumber: number, ctx: { user: User }) => String(aNumber) const b = (aString: string, ctx: { user: User }) => aString == '1' const c = (aBoolean: boolean, ctx: { user: User }) => aBoolean && ctx.user.admin -const d = context.pipe(a, b, c) +const d = withContext.pipe(a, b, c) const result = await d(1, { user: { admin: true } }) ``` -## context.sequence +## withContext.sequence Similar to `sequence` but it will forward the context to the next composable. ```ts -import { context } from 'composable-functions' +import { withContext } from 'composable-functions' const a = (aNumber: number, ctx: { user: User }) => String(aNumber) const b = (aString: string, ctx: { user: User }) => aString === '1' const c = (aBoolean: boolean, ctx: { user: User }) => aBoolean && ctx.user.admin -const d = context.sequence(a, b, c) +const d = withContext.sequence(a, b, c) const result = await d(1, { user: { admin: true } }) ``` diff --git a/context.md b/context.md index 90ebf0f..cf5e282 100644 --- a/context.md +++ b/context.md @@ -13,12 +13,12 @@ The currently authenticated user would have to be propagated every time there is To avoid such awkwardness we use context: ```tsx -import { context } from 'composable-functions' +import { withContext } from 'composable-functions' const dangerousFunction = async (input: string, { user } : { user: { name: string, admin: boolean } }) => { // do something that only the admin can do } -const carryUser = context.pipe(gatherInput, dangerousFunction) +const carryUser = withContext.pipe(gatherInput, dangerousFunction) ``` ## Composing with context @@ -27,15 +27,15 @@ These combinators are useful for composing functions with context. Note that the ### `pipe` -The context.pipe function allows you to compose multiple functions in a sequence, forwarding the context to each function in the chain. +The `withContext.pipe` function allows you to compose multiple functions in a sequence, forwarding the context to each function in the chain. ```ts -import { context } from 'composable-functions' +import { withContext } from 'composable-functions' const a = (str: string, ctx: { user: User }) => str === '1' const b = (bool: boolean, ctx: { user: User }) => bool && ctx.user.admin -const pipeline = context.pipe(a, b) +const pipeline = withContext.pipe(a, b) const result = await pipeline('1', { user: { admin: true } }) /* @@ -48,15 +48,15 @@ result = { ``` ### `sequence` -The context.sequence function works similarly to pipe, but it returns a tuple containing the result of each function in the sequence. +The `withContext.sequence` function works similarly to pipe, but it returns a tuple containing the result of each function in the sequence. ```ts -import { context } from 'composable-functions' +import { withContext } from 'composable-functions' const a = (str: string, ctx: { user: User }) => str === '1' const b = (bool: boolean, ctx: { user: User }) => bool && ctx.user.admin -const sequence = context.sequence(a, b) +const sequence = withContext.sequence(a, b) const result = await sequence('1', { user: { admin: true } }) /* @@ -70,15 +70,15 @@ result = { ### `branch` -The context.branch function adds conditional logic to your compositions, forwarding the context to each branch as needed. +The `withContext.branch` function adds conditional logic to your compositions, forwarding the context to each branch as needed. ```ts -import { composable, context } from 'composable-functions' +import { withContext } from 'composable-functions' 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) +const incrementUntilEven = withContext.branch(adminIncrement, adminMakeItEven) const result = await incrementUntilEven(1, { user: { admin: true } }) /* diff --git a/migrating-df.md b/migrating-df.md index e9158fb..b35f520 100644 --- a/migrating-df.md +++ b/migrating-df.md @@ -8,7 +8,7 @@ This document will guide you through the migration process. - 🛡️ Enhanced Type Safety: Enjoy robust **type-safety during function composition**. The improved type-checking mechanisms prevent incompatible functions from being composed, reducing runtime errors and improving code reliability. - 🤌 Simplified Function Creation: **No need to define schemas**. Create composable functions easily and efficiently without the overhead of schema definitions. Work with plain functions in every combinator. - 🕵🏽 Runtime Validation: Use the [`applySchema`](./API.md#applyschema) function for optional runtime validation of inputs and context. This provides flexibility to enforce data integrity when needed without mandating it for every function. Assuming you have a big chain of composables you can use [`applySchema`](./API.md#applyschema) to run your runtime validation only once **avoiding unnecessary processing**. -- 🔀 Flexible Compositions: The new combinators, such as [`context.pipe`](./API.md#contextpipe), [`context.sequence`](./API.md#contextsequence), and [`context.branch`](./API.md#contextbranch), offer powerful ways to manage **typed context** which are contextual information across your compositions. +- 🔀 Flexible Compositions: The new combinators, such as [`withContext.pipe`](./API.md#withcontextpipe), [`withContext.sequence`](./API.md#withcontextsequence), and [`withContext.branch`](./API.md#withcontextbranch), offer powerful ways to manage **typed context** which are contextual information across your compositions. - 🛠️ Incremental Migration: Seamlessly migrate your existing codebase incrementally. **Both `domain-functions` and `composable-functions` can coexist**, allowing you to transition module by module. - 🛟 Enhanced Combinators: New and improved combinators like [`map`](./API.md#map), [`mapParameters`](./API.md#mapparameters), [`mapErrors`](./API.md#maperrors) and [`catchFailure`](./API.md#catchfailure) provide more control over error handling and transformation, making your **code more resilient**. @@ -89,16 +89,16 @@ The `environment` we used to have in domain-functions is now called `context` an When it comes to sequential compositions, however, we need special combinators to preserve the context so they work as the domain-functions' combinators. -Use the sequential combinators from the namespace `context` to keep this familiar behavior. +Use the sequential combinators from the namespace `withContext` to keep this familiar behavior. ```ts -import { context } from 'composable-functions' +import { withContext } from 'composable-functions' -const result = context.pipe(fn1, fn2)(input, ctx) -// same for `context.sequence` and `context.branch` +const result = withContext.pipe(fn1, fn2)(input, ctx) +// same for `withContext.sequence` and `withContext.branch` ``` -**Note**: The `pipe`, `sequence`, and `branch` outside of the `context` namespace will not keep the context through the composition. +**Note**: The `pipe`, `sequence`, and `branch` outside of the `withContext` namespace will not keep the context through the composition. ## Modified combinators ### map @@ -266,13 +266,13 @@ if (result.errors.some(isInputError)) { | `all(df1, df2)` | `all(fn1, fn2)` | | `collect(df1, df2)` | `collect(fn1, fn2)` | | `merge(df1, df2)` | `map(all(fn1, fn2), mergeObjects)` | -| `branch(df1, (res) => res ? null : df2)` | `context.branch(fn1, (res) => res ? null : fn2)` | +| `branch(df1, (res) => res ? null : df2)` | `withContext.branch(fn1, (res) => res ? null : fn2)` | | -- | `branch(fn1, (res) => res ? null : fn2)` without context | -| `pipe(df1, df2)` | `context.pipe(fn1, fn2)` | +| `pipe(df1, df2)` | `withContext.pipe(fn1, fn2)` | | -- | `pipe(fn1, fn2)` without context | -| `sequence(df1, df2)` | `context.sequence(fn1, fn2)` | +| `sequence(df1, df2)` | `withContext.sequence(fn1, fn2)` | | -- | `sequence(fn1, fn2)` without context | -| `collectSequence({ name: nameDf, age: ageDf })` | `map(context.sequence(nameDf, ageDf), ([name, age]) => ({ name, age }))` | +| `collectSequence({ name: nameDf, age: ageDf })` | `map(withContext.sequence(nameDf, ageDf), ([name, age]) => ({ name, age }))` | | `map(df, (o) => ({ result: o }))` | `map(fn, (o) => ({ result: o }))` | | -- | `map(fn, (o, ...args) => ({ result: o, args }))` | | `first(df1, df2)` | -- * read docs above | diff --git a/src/context/combinators.ts b/src/context/combinators.ts index 59bc599..b211885 100644 --- a/src/context/combinators.ts +++ b/src/context/combinators.ts @@ -20,11 +20,11 @@ function applyContextToList< * @example * * ```ts - * import { context } from 'composable-functions' + * import { withContext } from 'composable-functions' * * const a = (aNumber: number) => String(aNumber) * const b = (aString: string) => aString === '1' - * const d = context.pipe(a, b) + * const d = withContext.pipe(a, b) * // ^? ComposableWithSchema * ``` */ @@ -45,16 +45,16 @@ function pipe( } /** - * Works like `context.pipe` but it will collect the output of every function in a tuple. + * Works like `withContext.pipe` but it will collect the output of every function in a tuple. * * @example * * ```ts - * import { context } from 'composable-functions' + * import { withContext } from 'composable-functions' * * const a = (aNumber: number) => String(aNumber) * const b = (aString: string) => aString === '1' - * const aComposable = context.sequence(a, b) + * const aComposable = withContext.sequence(a, b) * // ^? ComposableWithSchema<[string, boolean]> * ``` */ diff --git a/src/context/context.ts b/src/context/context.ts new file mode 100644 index 0000000..0afb9fa --- /dev/null +++ b/src/context/context.ts @@ -0,0 +1,13 @@ +import { branch, pipe, sequence } from './index.ts' + +/** + * @deprecated use `import { withContext } from 'composable-functions'` instead + */ +const context = { + branch, + pipe, + sequence, +} + +// deno-lint-ignore verbatim-module-syntax +export { context } diff --git a/src/context/environment.ts b/src/context/environment.ts index 997c602..f7b47f4 100644 --- a/src/context/environment.ts +++ b/src/context/environment.ts @@ -1,7 +1,7 @@ import { branch, pipe, sequence } from './index.ts' /** - * @deprecated use `import { context } from 'composable-functions'` instead + * @deprecated use `import { withContext } from 'composable-functions'` instead */ const environment = { branch, diff --git a/src/context/tests/branch.test.ts b/src/context/tests/branch.test.ts index f6b2d38..ea8c0a2 100644 --- a/src/context/tests/branch.test.ts +++ b/src/context/tests/branch.test.ts @@ -3,10 +3,10 @@ import { all, applySchema, composable, - context, failure, InputError, success, + withContext, } from '../../index.ts' import type { Composable, @@ -23,7 +23,7 @@ describe('branch', () => { ({ id }: { id: number }, context: number) => id - 1 + context, ) - const c = context.branch(a, () => Promise.resolve(b)) + const c = withContext.branch(a, () => Promise.resolve(b)) type _R = Expect< Equal< typeof c, @@ -40,7 +40,7 @@ describe('branch', () => { }) const b = ({ id }: { id: number }, context: number) => id - 1 + context - const c = context.branch(a, () => Promise.resolve(b)) + const c = withContext.branch(a, () => Promise.resolve(b)) type _R = Expect< Equal< typeof c, @@ -62,7 +62,7 @@ describe('branch', () => { })) const b = applySchema(z.object({ id: z.number() }))(({ id }) => id - 1) - const c = context.branch(a, () => Promise.resolve(b)) + const c = withContext.branch(a, () => Promise.resolve(b)) type _R = Expect>> assertEquals(await c({ id: 1 }), success(2)) @@ -75,7 +75,10 @@ describe('branch', () => { })) const b = applySchema(z.object({ id: z.number() }))(({ id }) => String(id)) const c = applySchema(z.object({ id: z.number() }))(({ id }) => id * 2) - const d = context.branch(a, (output) => output.next === 'multiply' ? c : b) + const d = withContext.branch( + a, + (output) => output.next === 'multiply' ? c : b, + ) type _R = Expect>> assertEquals(await d({ id: 1 }), success(6)) @@ -87,7 +90,7 @@ describe('branch', () => { next: 'multiply', })) const b = applySchema(z.object({ id: z.number() }))(({ id }) => String(id)) - const d = context.branch(a, (output) => { + const d = withContext.branch(a, (output) => { type _Check = Expect>> return output.next === 'multiply' ? null : b }) @@ -112,7 +115,7 @@ describe('branch', () => { next: 'multiply', }) const b = ({ id }: { id: number }) => String(id) - const d = context.branch(a, (output) => { + const d = withContext.branch(a, (output) => { type _Check = Expect>> return output.next === 'multiply' ? null : b }) @@ -136,7 +139,7 @@ describe('branch', () => { ({ inp }: { inp: number }, { ctx }: { ctx: number }) => inp + ctx, ) - const c = context.branch(a, () => b) + const c = withContext.branch(a, () => b) type _R = Expect< Equal< typeof c, @@ -152,7 +155,7 @@ describe('branch', () => { id: id + 2, })) const b = composable(({ id }: { id: number }) => id - 1) - const c = context.branch(a, () => b) + const c = withContext.branch(a, () => b) type _R = Expect>> assertEquals( @@ -166,7 +169,7 @@ describe('branch', () => { id: String(id), })) const b = applySchema(z.object({ id: z.number() }))(({ id }) => id - 1) - const c = context.branch(a, () => b) + const c = withContext.branch(a, () => b) type _R = Expect>> assertEquals( @@ -180,7 +183,7 @@ describe('branch', () => { id: id + 2, })) const b = composable(({ id }: { id: number }) => id - 1) - const c = context.branch(a, (_) => { + const c = withContext.branch(a, (_) => { throw new Error('condition function failed') // deno-lint-ignore no-unreachable return b @@ -200,8 +203,8 @@ describe('branch', () => { const b = composable(({ id }: { id: number }) => id - 1) const c = composable((n: number, ctx: number) => ctx + n * 2) const d = all( - context.pipe( - context.branch(a, () => b), + withContext.pipe( + withContext.branch(a, () => b), c, ), a, diff --git a/src/context/tests/pipe.test.ts b/src/context/tests/pipe.test.ts index f66e634..61aa83e 100644 --- a/src/context/tests/pipe.test.ts +++ b/src/context/tests/pipe.test.ts @@ -2,11 +2,11 @@ import { assertEquals, describe, it, z } from './prelude.ts' import { applySchema, composable, - context, ContextError, failure, InputError, success, + withContext, } from '../../index.ts' import type { Composable, ComposableWithSchema } from '../../index.ts' import type { Internal } from '../../internal/types.ts' @@ -18,7 +18,7 @@ describe('pipe', () => { })) const b = applySchema(z.object({ id: z.number() }))(({ id }) => id - 1) - const c = context.pipe(a, b) + const c = withContext.pipe(a, b) type _R = Expect>> assertEquals(await c({ id: 1 }), success(2)) @@ -36,7 +36,7 @@ describe('pipe', () => { z.object({ ctx: z.number() }), )(({ inp }, { ctx }) => inp + ctx) - const c = context.pipe(a, b) + const c = withContext.pipe(a, b) type _R = Expect>> assertEquals(await c(undefined, { ctx: 1 }), success(4)) @@ -55,7 +55,7 @@ describe('pipe', () => { ctxParser, )(({ inp }, { ctx }) => inp + ctx) - const c = context.pipe(a, b) + const c = withContext.pipe(a, b) type _R = Expect>> assertEquals( @@ -78,7 +78,7 @@ describe('pipe', () => { z.object({ ctx: z.number() }), )(({ inp }, { ctx }) => inp + ctx) - const c = context.pipe(a, b) + const c = withContext.pipe(a, b) type _R = Expect>> assertEquals( @@ -93,7 +93,7 @@ describe('pipe', () => { ({ inp }: { inp: number }, { ctx }: { ctx: number }) => inp + ctx, ) - const c = context.pipe(a, b) + const c = withContext.pipe(a, b) type _R = Expect< Equal< typeof c, @@ -115,7 +115,7 @@ describe('pipe', () => { ({ aBoolean }) => !aBoolean, ) - const d = context.pipe(a, b, c) + const d = withContext.pipe(a, b, c) type _R = Expect>> assertEquals(await d({ aNumber: 1 }), success(false)) @@ -123,7 +123,7 @@ describe('pipe', () => { it('fails to compose functions with third mandatory parameter', async () => { const add = composable((a: number, ctx: number) => a + ctx) - const fn = context.pipe( + const fn = withContext.pipe( add, composable((x: number, _ctx: number, _makeItFail: boolean) => x), ) @@ -138,7 +138,7 @@ describe('pipe', () => { it('fails to compose incompatible functions', async () => { const add = composable((a: number, ctx: number) => a + ctx) - const fn = context.pipe( + const fn = withContext.pipe( add, composable((x: string) => x), ) @@ -151,7 +151,7 @@ describe('pipe', () => { it('compose using context when piped functions requires a second parameter', async () => { const add = composable((a: number, ctx: number) => a + ctx) - const fn = context.pipe(add, add) + const fn = withContext.pipe(add, add) const res = await fn(1, 2) @@ -163,7 +163,7 @@ describe('pipe', () => { it('accepts plain functions', async () => { const add = (a: number, ctx: number) => a + ctx - const fn = context.pipe(add, add) + const fn = withContext.pipe(add, add) const res = await fn(1, 2) diff --git a/src/context/tests/sequence.test.ts b/src/context/tests/sequence.test.ts index 0b1261e..7435635 100644 --- a/src/context/tests/sequence.test.ts +++ b/src/context/tests/sequence.test.ts @@ -2,11 +2,11 @@ import { assertEquals, describe, it, z } from './prelude.ts' import { applySchema, composable, - context, ContextError, failure, InputError, success, + withContext, } from '../../index.ts' import type { Composable } from '../../index.ts' @@ -19,7 +19,7 @@ describe('sequence', () => { result: id - 1, })) - const c = context.sequence(a, b) + const c = withContext.sequence(a, b) type _R = Expect< Equal< typeof c, @@ -50,7 +50,7 @@ describe('sequence', () => { z.object({ ctx: z.number() }), )(({ inp }, { ctx }) => ({ result: inp + ctx })) - const c = context.sequence(a, b) + const c = withContext.sequence(a, b) type _R = Expect< Equal< typeof c, @@ -85,7 +85,7 @@ describe('sequence', () => { ctxParser, )(({ inp }, { ctx }) => inp + ctx) - const c = context.sequence(a, b) + const c = withContext.sequence(a, b) type _R = Expect< Equal< typeof c, @@ -115,7 +115,7 @@ describe('sequence', () => { z.object({ ctx: z.number() }), )(({ inp }, { ctx }) => inp + ctx) - const c = context.sequence(a, b) + const c = withContext.sequence(a, b) type _R = Expect< Equal< typeof c, @@ -143,7 +143,7 @@ describe('sequence', () => { z.object({ ctx: z.number() }), )(({ inp }, { ctx }) => inp + ctx) - const c = context.sequence(a, b) + const c = withContext.sequence(a, b) type _R = Expect< Equal< typeof c, @@ -172,7 +172,7 @@ describe('sequence', () => { }), ) - const d = context.sequence(a, b, c) + const d = withContext.sequence(a, b, c) type _R = Expect< Equal< typeof d, @@ -204,7 +204,7 @@ describe('sequence', () => { it('should properly type the context', async () => { const a = composable((a: number, b: number) => a + b) const b = composable((a: number, b: number) => `${a} + ${b}`) - const c = context.sequence(a, b) + const c = withContext.sequence(a, b) type _R = Expect< Equal [number, string]>> > @@ -215,7 +215,7 @@ describe('sequence', () => { 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) + const c = withContext.sequence(a, b) type _R = Expect< Equal [number, string]>> > diff --git a/src/index.ts b/src/index.ts index 5803b11..f7c03d6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -65,4 +65,5 @@ export type { // FUNCTIONS WITH CONTEXT export { environment } from './context/environment.ts' -export * as context from './context/index.ts' +export { context } from './context/context.ts' +export * as withContext from './context/index.ts'