Skip to content

Commit

Permalink
Merge pull request #152 from seasonedcc/rename-env-to-context
Browse files Browse the repository at this point in the history
  • Loading branch information
gustavoguichard authored Jun 19, 2024
2 parents 382a3f7 + 1631352 commit 0c30eb0
Show file tree
Hide file tree
Showing 30 changed files with 542 additions and 503 deletions.
104 changes: 52 additions & 52 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@
- [inputFromSearch](#inputfromsearch)
- [Error Constructors and Handlers](#error-constructors-and-handlers)
- [ErrorList](#errorlist)
- [EnvironmentError](#environmenterror)
- [ContextError](#contexterror)
- [InputError](#inputerror)
- [isEnvironmentError](#isenvironmenterror)
- [isContextError](#iscontexterror)
- [isInputError](#isinputerror)
- [Type-safe runtime utilities](#type-safe-runtime-utilities)
- [mergeObjects](#mergeobjects)
Expand All @@ -36,10 +36,10 @@
- [Result](#result)
- [Success](#success-1)
- [UnpackData](#unpackdata)
- [Combinators with Environment](#combinators-with-environment)
- [environment.branch](#environmentbranch)
- [environment.pipe](#environmentpipe)
- [environment.sequence](#environmentsequence)
- [Combinators with Context](#combinators-with-context)
- [context.branch](#contextbranch)
- [context.pipe](#contextpipe)
- [context.sequence](#contextsequence)
- [Serialization](#serialization)
- [serialize](#serialize)
- [serializeError](#serializeerror)
Expand All @@ -48,7 +48,7 @@
# Constructors

## applySchema
It takes a composable and schemas for the input and environment, and returns the same composable with the schemas applied. So the types will be asserted at runtime.
It takes a composable and schemas for the input and context, and returns the same composable with the schemas applied. So the types will be asserted at runtime.

It is useful when dealing with external data, such as API requests, where you want to ensure the data is in the correct shape before processing it.

Expand All @@ -68,7 +68,7 @@ const safeFunction = applySchema(
)(fn)

type Test = typeof safeFunction
// ^? Composable<(input?: unknown, env?: unknown) => { message: string }>
// ^? Composable<(input?: unknown, ctx?: unknown) => { message: string }>
```
## composable
Expand Down Expand Up @@ -151,7 +151,7 @@ expect(result).toEqual({
```

## withSchema
It creates a composable with unknown input and environment types, and applies the schemas to them so the arguments are assured at runtime.
It creates a composable with unknown input and context types, and applies the schemas to them so the arguments are assured at runtime.

See `applySchema` above for more information.

Expand All @@ -164,14 +164,14 @@ const result = await runtimeSafeAdd(1, 2)
console.log(result.success ? result.data : result.errors)
```

If there are input or environment errors, they will be returned in the `errors` property of the result.
If there are input or context errors, they will be returned in the `errors` property of the result.
```ts
const result = await runtimeSafeAdd('1', null)
// {
// success: false,
// errors: [
// new InputError('Expected number, received string'),
// new EnvironmentError('Expected number, received null')
// new ContextError('Expected number, received null')
// ],
// }
```
Expand Down Expand Up @@ -417,7 +417,7 @@ const getCurrentUser = mapParameters(
getUser,
(_input, user: { id: number }) => [{ id: user.id }]
)
// ^? Composable<(input: unknown, env: { id: number }) => User>
// ^? Composable<(input: unknown, ctx: { id: number }) => User>
```

## pipe
Expand Down Expand Up @@ -635,23 +635,23 @@ An `ErrorList` is a special kind of error that carries a list of errors that can
const fn = composable(() => {
throw new ErrorList([
new InputError('Custom input error', ['contact', 'id']),
new EnvironmentError('Custom env error', ['currentUser', 'role']),
new ContextError('Custom context error', ['currentUser', 'role']),
])
})
const result = await fn()
// {
// success: false,
// errors: [
// new InputError('Custom input error', ['contact', 'id']),
// new EnvironmentError('Custom env error', ['currentUser', 'role']),
// new ContextError('Custom context error', ['currentUser', 'role']),
// ],
// }
```

## EnvironmentError
An `EnvironmentError` is a special kind of error that represents an error in the environment schema.
## ContextError
An `ContextError` is a special kind of error that represents an error in the context schema.

It has an optional second parameter that is an array of strings representing the path to the error in the environment schema.
It has an optional second parameter that is an array of strings representing the path to the error in the context schema.

```ts
const fn = withSchema(
Expand All @@ -665,31 +665,31 @@ const result = await fn({ id: '1' }, { user: { id: 1 } })
/* {
success: false,
errors: [
new EnvironmentError(
new ContextError(
'Expected string, received number',
['user', 'id'],
),
],
} */
```

You can also use the `EnvironmentError` constructor to throw errors within the composable:
You can also use the `ContextError` constructor to throw errors within the composable:

```ts
const fn = composable(() => {
throw new EnvironmentError('Custom env error', ['currentUser', 'role'])
throw new ContextError('Custom context error', ['currentUser', 'role'])
})
```

## InputError
Similar to `EnvironmentError`, an `InputError` is a special kind of error that represents an error in the input schema.
Similar to `ContextError`, an `InputError` is a special kind of error that represents an error in the input schema.

## isEnvironmentError
`isEnvironmentError` is a helper function that will check if an error is an instance of `EnvironmentError`.
## isContextError
`isContextError` is a helper function that will check if an error is an instance of `ContextError`.

```ts
isEnvironmentError(new EnvironmentError('yes')) // true
isEnvironmentError(new Error('nope')) // false
isContextError(new ContextError('yes')) // true
isContextError(new Error('nope')) // false
```

## isInputError
Expand Down Expand Up @@ -777,68 +777,68 @@ type Data = UnpackData<typeof fn>
// ^? string
```
# Combinators with Environment
The environment is a concept of an argument that is passed to every functions of a sequential composition. When it comes to parallel compositions, all arguments are already forwarded to every function.
# Combinators with Context
The context is a concept of an argument that is passed to every functions of a sequential composition. When it comes to parallel compositions, all arguments are already forwarded to every function.
However in sequential compositions, we need a set of special combinators that will forward the environment - the second parameter - to every function in the composition.
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 `environment` to get this behavior.
Use the sequential combinators from the namespace `context` to get this behavior.
For a deeper explanation check the [`environment` docs](./environments.md).
For a deeper explanation check the [`context` docs](./context.md).
## environment.branch
It is the same as `branch` but it will forward the environment to the next composable.
## context.branch
It is the same as `branch` but it will forward the context to the next composable.
```ts
import { environment } from 'composable-functions'
import { context } from 'composable-functions'

const getIdOrEmail = composable((data: { id?: number, email?: string }) => {
return data.id ?? data.email
})
const findUserById = composable((id: number, env: { user: User }) => {
if (!env.user.admin) {
const findUserById = composable((id: number, ctx: { user: User }) => {
if (!ctx.user.admin) {
throw new Error('Unauthorized')
}
return db.users.find({ id })
})
const findUserByEmail = composable((email: string, env: { user: User }) => {
if (!env.user.admin) {
const findUserByEmail = composable((email: string, ctx: { user: User }) => {
if (!ctx.user.admin) {
throw new Error('Unauthorized')
}
return db.users.find
})
const findUserByIdOrEmail = environment.branch(
const findUserByIdOrEmail = context.branch(
getIdOrEmail,
(data) => (typeof data === "number" ? findUserById : findUserByEmail),
)
const result = await findUserByIdOrEmail({ id: 1 }, { user: { admin: true } })
```
## environment.pipe
Similar to `pipe` but it will forward the environment to the next composable.
## context.pipe
Similar to `pipe` but it will forward the context to the next composable.

```ts
import { environment } from 'composable-functions'
import { context } from 'composable-functions'

const a = composable((aNumber: number, env: { user: User }) => String(aNumber))
const b = composable((aString: string, env: { user: User }) => aString == '1')
const c = composable((aBoolean: boolean, env: { user: User }) => aBoolean && env.user.admin)
const a = composable((aNumber: number, ctx: { user: User }) => String(aNumber))
const b = composable((aString: string, ctx: { user: User }) => aString == '1')
const c = composable((aBoolean: boolean, ctx: { user: User }) => aBoolean && ctx.user.admin)

const d = environment.pipe(a, b, c)
const d = context.pipe(a, b, c)

const result = await d(1, { user: { admin: true } })
```

## environment.sequence
Similar to `sequence` but it will forward the environment to the next composable.
## context.sequence
Similar to `sequence` but it will forward the context to the next composable.

```ts
import { environment } from 'composable-functions'
import { context } from 'composable-functions'

const a = composable((aNumber: number, env: { user: User }) => String(aNumber))
const b = composable((aString: string, env: { user: User }) => aString === '1')
const c = composable((aBoolean: boolean, env: { user: User }) => aBoolean && env.user.admin)
const a = composable((aNumber: number, ctx: { user: User }) => String(aNumber))
const b = composable((aString: string, ctx: { user: User }) => aString === '1')
const c = composable((aBoolean: boolean, ctx: { user: User }) => aBoolean && ctx.user.admin)

const d = environment.sequence(a, b, c)
const d = context.sequence(a, b, c)

const result = await d(1, { user: { admin: true } })
```
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ A set of types and functions to make compositions easy and safe.
- 🏝️ Isolated Business Logic: Split your code into composable functions, making your code easier to test and maintain.
- 🔒 End-to-End Type Safety: Achieve end-to-end type safety from the backend to the UI with serializable results, ensuring data integrity across your entire application.
- ⚡ Parallel and Sequential Compositions: Compose functions both in parallel - with `all` and `collect` - and sequentially - with `pipe`, `branch`, and `sequence` -, to manage complex data flows optimizing your code for performance and clarity.
- 🕵️‍♂️ Runtime Validation: Use `withSchema` or `applySchema` with your favorite parser for optional runtime validation of inputs and environments, enforcing data integrity only when needed.
- 🕵️‍♂️ Runtime Validation: Use `withSchema` or `applySchema` with your favorite parser for optional runtime validation of inputs and context, enforcing data integrity only when needed.
- 🚑 Resilient Error Handling: Leverage enhanced combinators like `mapErrors` and `catchFailure` to transform and handle errors more effectively.
- 📊 Traceable Compositions: Use the `trace` function to log and monitor your composable functions’ inputs and results, simplifying debugging and monitoring.

Expand All @@ -31,7 +31,7 @@ A set of types and functions to make compositions easy and safe.
- [Guides](#guides)
- [Migrating from domain-functions](#migrating-from-domain-functions)
- [Handling external input](#handling-external-input)
- [Defining constants for multiple functions (environments)](#defining-constants-for-multiple-functions-environments)
- [Defining constants for multiple functions (context)](#defining-constants-for-multiple-functions-context)
- [Using custom parsers](#using-custom-parsers)
- [Using Deno](#using-deno)
- [Acknowledgements](#acknowledgements)
Expand Down Expand Up @@ -277,7 +277,7 @@ const getProfile = composable(async (id: string) => {

#### [Migrating from domain-functions](./migrating-df.md)
#### [Handling external input](./with-schema.md)
#### [Defining constants for multiple functions (environments)](./environments.md)
#### [Defining constants for multiple functions (context)](./context.md)
#### [Using custom parsers](./examples/arktype/README.md)

## Using Deno
Expand Down
92 changes: 92 additions & 0 deletions context.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
## Context

Sometimes you want to ensure the safety of certain values that are constant accross sequential compositions.
This parameter is called context. And to simplify the composition of this kind of Composable
we always define it with a single input. Therefore we have the type

```ts
Composable<(input: I, context: C) => O>
```

A common use case would be a sequence of functions that depend on an authorization system.
The currently authenticated user would have to be propagated every time there is a sequential composition.
To avoid such awkwardness we use context:

```tsx
import { context } from 'composable-functions'
const dangerousFunction = composable(async (input: string, { user } : { user: { name: string, admin: boolean } }) => {
// do something that only the admin can do
})

const carryUser = context.pipe(gatherInput, dangerousFunction)
```

## Composing with context

These combinators are useful for composing functions with context. Note that the standard parallel compositions will work just fine with the concept of context.

### `pipe`

The context.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'

const a = composable((str: string, ctx: { user: User }) => str === '1')
const b = composable((bool: boolean, ctx: { user: User }) => bool && ctx.user.admin)

const pipeline = context.pipe(a, b)

const result = await pipeline('1', { user: { admin: true } })
/*
result = {
success: true,
data: true,
errors: []
}
*/
```

### `sequence`
The context.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'

const a = composable((str: string, ctx: { user: User }) => str === '1')
const b = composable((bool: boolean, ctx: { user: User }) => bool && ctx.user.admin)

const sequence = context.sequence(a, b)

const result = await sequence('1', { user: { admin: true } })
/*
result = {
success: true,
data: [true, true],
errors: []
}
*/
```

### `branch`

The context.branch function adds conditional logic to your compositions, forwarding the context to each branch as needed.

```ts
import { composable, context } from 'composable-functions'

const adminIncrement = composable((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 result = await incrementUntilEven(1, { user: { admin: true } })
/*
result = {
success: true,
data: 2,
errors: []
}
*/
```
Loading

0 comments on commit 0c30eb0

Please sign in to comment.