Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Swap context namespace for withContext for better clarity #167

Merged
merged 1 commit into from
Jul 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading