Skip to content

Commit

Permalink
Merge pull request #167 from seasonedcc/with-context
Browse files Browse the repository at this point in the history
Swap `context` namespace for `withContext` for better clarity
  • Loading branch information
gustavoguichard authored Jul 19, 2024
2 parents 4719c4f + 86391dc commit f562a24
Show file tree
Hide file tree
Showing 10 changed files with 97 additions and 79 deletions.
31 changes: 16 additions & 15 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}
})
Expand Down Expand Up @@ -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
Expand All @@ -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 } })
```
Expand Down
22 changes: 11 additions & 11 deletions context.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 } })
/*
Expand All @@ -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 } })
/*
Expand All @@ -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 } })
/*
Expand Down
20 changes: 10 additions & 10 deletions migrating-df.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**.

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 |
Expand Down
10 changes: 5 additions & 5 deletions src/context/combinators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean>
* ```
*/
Expand All @@ -45,16 +45,16 @@ function pipe<Fns extends Function[]>(
}

/**
* 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]>
* ```
*/
Expand Down
13 changes: 13 additions & 0 deletions src/context/context.ts
Original file line number Diff line number Diff line change
@@ -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 }
2 changes: 1 addition & 1 deletion src/context/environment.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
31 changes: 17 additions & 14 deletions src/context/tests/branch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import {
all,
applySchema,
composable,
context,
failure,
InputError,
success,
withContext,
} from '../../index.ts'
import type {
Composable,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -53,7 +53,7 @@ describe('branch', () => {

it('will enforce noImplicitAny', () => {
// @ts-expect-error: implicit any
const _fn = context.branch((a) => a, () => null)
const _fn = withContext.branch((a) => a, () => null)
})

it('should pipe a composable with a function that returns a composable with schema', async () => {
Expand All @@ -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<Equal<typeof c, ComposableWithSchema<number>>>

assertEquals(await c({ id: 1 }), success(2))
Expand All @@ -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<Equal<typeof d, ComposableWithSchema<number | string>>>

assertEquals(await d({ id: 1 }), success(6))
Expand All @@ -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<Equal<typeof output, UnpackData<typeof a>>>
return output.next === 'multiply' ? null : b
})
Expand All @@ -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<Equal<typeof output, ReturnType<typeof a>>>
return output.next === 'multiply' ? null : b
})
Expand All @@ -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,
Expand All @@ -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<Equal<typeof c, ComposableWithSchema<number>>>

assertEquals(
Expand All @@ -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<Equal<typeof c, ComposableWithSchema<number>>>

assertEquals(
Expand All @@ -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
Expand All @@ -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,
Expand Down
Loading

0 comments on commit f562a24

Please sign in to comment.