composable-functions
Advanced tools
Comparing version 4.0.1 to 4.1.0
104
API.md
@@ -27,5 +27,5 @@ # API Reference | ||
- [ErrorList](#errorlist) | ||
- [EnvironmentError](#environmenterror) | ||
- [ContextError](#contexterror) | ||
- [InputError](#inputerror) | ||
- [isEnvironmentError](#isenvironmenterror) | ||
- [isContextError](#iscontexterror) | ||
- [isInputError](#isinputerror) | ||
@@ -40,6 +40,6 @@ - [Type-safe runtime utilities](#type-safe-runtime-utilities) | ||
- [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) | ||
@@ -53,3 +53,3 @@ - [serialize](#serialize) | ||
## 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. | ||
@@ -74,3 +74,3 @@ 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. | ||
type Test = typeof safeFunction | ||
// ^? Composable<(input?: unknown, env?: unknown) => { message: string }> | ||
// ^? Composable<(input?: unknown, ctx?: unknown) => { message: string }> | ||
``` | ||
@@ -158,3 +158,3 @@ | ||
## 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. | ||
@@ -172,3 +172,3 @@ See `applySchema` above for more information. | ||
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 | ||
@@ -180,3 +180,3 @@ const result = await runtimeSafeAdd('1', null) | ||
// new InputError('Expected number, received string'), | ||
// new EnvironmentError('Expected number, received null') | ||
// new ContextError('Expected number, received null') | ||
// ], | ||
@@ -427,3 +427,3 @@ // } | ||
) | ||
// ^? Composable<(input: unknown, env: { id: number }) => User> | ||
// ^? Composable<(input: unknown, ctx: { id: number }) => User> | ||
``` | ||
@@ -646,3 +646,3 @@ | ||
new InputError('Custom input error', ['contact', 'id']), | ||
new EnvironmentError('Custom env error', ['currentUser', 'role']), | ||
new ContextError('Custom context error', ['currentUser', 'role']), | ||
]) | ||
@@ -655,3 +655,3 @@ }) | ||
// new InputError('Custom input error', ['contact', 'id']), | ||
// new EnvironmentError('Custom env error', ['currentUser', 'role']), | ||
// new ContextError('Custom context error', ['currentUser', 'role']), | ||
// ], | ||
@@ -661,6 +661,6 @@ // } | ||
## 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. | ||
@@ -679,3 +679,3 @@ ```ts | ||
errors: [ | ||
new EnvironmentError( | ||
new ContextError( | ||
'Expected string, received number', | ||
@@ -688,7 +688,7 @@ ['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']) | ||
}) | ||
@@ -698,10 +698,10 @@ ``` | ||
## 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 | ||
``` | ||
@@ -794,16 +794,16 @@ | ||
# 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' | ||
@@ -813,4 +813,4 @@ const getIdOrEmail = composable((data: { id?: number, email?: string }) => { | ||
}) | ||
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') | ||
@@ -820,4 +820,4 @@ } | ||
}) | ||
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') | ||
@@ -827,3 +827,3 @@ } | ||
}) | ||
const findUserByIdOrEmail = environment.branch( | ||
const findUserByIdOrEmail = context.branch( | ||
getIdOrEmail, | ||
@@ -834,13 +834,13 @@ (data) => (typeof data === "number" ? findUserById : findUserByEmail), | ||
``` | ||
## 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) | ||
@@ -850,13 +850,13 @@ 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) | ||
@@ -863,0 +863,0 @@ const result = await d(1, { user: { admin: true } }) |
import { mapErrors } from './combinators.js'; | ||
import { EnvironmentError, ErrorList, InputError } from './errors.js'; | ||
import { ContextError, ErrorList, InputError } from './errors.js'; | ||
/** | ||
@@ -69,9 +69,9 @@ * It receives any data (T) and returns a Success<T> object. | ||
/** | ||
* Creates a composable with unknown input and environment that uses schemas to parse them into known types. | ||
* 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 environment schemas, you can pass a handler function that takes type safe input and environment. That function is gonna catch any errors and always return a Result. | ||
* 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 environmentSchema the schema for the environment | ||
* @returns a handler function that takes type safe input and environment | ||
* @param contextSchema the schema for the context | ||
* @returns a handler function that takes type safe input and context | ||
* @example | ||
@@ -88,11 +88,11 @@ * const safeFunction = withSchema( | ||
*/ | ||
function withSchema(inputSchema, environmentSchema) { | ||
return (handler) => applySchema(inputSchema, environmentSchema)(composable(handler)); | ||
function withSchema(inputSchema, contextSchema) { | ||
return (handler) => applySchema(inputSchema, contextSchema)(composable(handler)); | ||
} | ||
/** | ||
* Takes a composable and creates a composable withSchema that will assert the input and environment types according to the given schemas. | ||
* 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 | ||
* @param inputSchema the schema for the input | ||
* @param environmentSchema the schema for the environment | ||
* @returns a composable function that will assert the input and environment types at runtime. | ||
* @param contextSchema the schema for the context | ||
* @returns a composable function that will assert the input and context types at runtime. | ||
* @example | ||
@@ -114,15 +114,15 @@ * ```ts | ||
*/ | ||
function applySchema(inputSchema, environmentSchema) { | ||
return ((fn) => { | ||
return ((input, environment) => { | ||
const envResult = (environmentSchema ?? alwaysUnknownSchema).safeParse(environment); | ||
function applySchema(inputSchema, contextSchema) { | ||
return (fn) => { | ||
return ((input, context) => { | ||
const ctxResult = (contextSchema ?? alwaysUnknownSchema).safeParse(context); | ||
const result = (inputSchema ?? alwaysUnknownSchema).safeParse(input); | ||
if (!result.success || !envResult.success) { | ||
if (!result.success || !ctxResult.success) { | ||
const inputErrors = result.success ? [] : result.error.issues.map((error) => new InputError(error.message, error.path)); | ||
const envErrors = envResult.success ? [] : envResult.error.issues.map((error) => new EnvironmentError(error.message, error.path)); | ||
return Promise.resolve(failure([...inputErrors, ...envErrors])); | ||
const ctxErrors = ctxResult.success ? [] : ctxResult.error.issues.map((error) => new ContextError(error.message, error.path)); | ||
return Promise.resolve(failure([...inputErrors, ...ctxErrors])); | ||
} | ||
return fn(result.data, envResult.data); | ||
return fn(result.data, ctxResult.data); | ||
}); | ||
}); | ||
}; | ||
} | ||
@@ -129,0 +129,0 @@ const alwaysUnknownSchema = { |
@@ -26,3 +26,4 @@ /** | ||
/** | ||
* A custom error class for environment errors. | ||
* @deprecated Use `ContextError` instead | ||
* A custom error class for context errors. | ||
* | ||
@@ -38,3 +39,3 @@ * @example | ||
/** | ||
* Path of environment attribute that originated the error. | ||
* Path of context attribute that originated the error. | ||
*/ | ||
@@ -52,2 +53,26 @@ Object.defineProperty(this, "path", { | ||
/** | ||
* A custom error class for context errors. | ||
* | ||
* @example | ||
* const aComposable = withSchema()(() => { | ||
* throw new ContextError('Invalid context', 'user.name') | ||
* }) | ||
*/ | ||
class ContextError extends Error { | ||
constructor(message, path = []) { | ||
super(message); | ||
/** | ||
* Path of context attribute that originated the error. | ||
*/ | ||
Object.defineProperty(this, "path", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: void 0 | ||
}); | ||
this.name = 'ContextError'; | ||
this.path = path; | ||
} | ||
} | ||
/** | ||
* A list of errors | ||
@@ -80,7 +105,12 @@ * | ||
/** | ||
* A function to check if an `Error` or a `SerializableError` is an EnvironmentError | ||
* @deprecated Use `isContextError` instead | ||
* A function to check if an `Error` or a `SerializableError` is a ContextError | ||
*/ | ||
function isEnvironmentError(e) { | ||
return e.name === 'EnvironmentError'; | ||
const isEnvironmentError = isContextError; | ||
/** | ||
* A function to check if an `Error` or a `SerializableError` is a ContextError | ||
*/ | ||
function isContextError(e) { | ||
return e.name === 'EnvironmentError' || e.name === 'ContextError'; | ||
} | ||
export { EnvironmentError, ErrorList, InputError, isEnvironmentError, isInputError, }; | ||
export { ContextError, EnvironmentError, ErrorList, InputError, isContextError, isEnvironmentError, isInputError, }; |
@@ -5,4 +5,8 @@ export { applySchema, composable, failure, fromSuccess, success, withSchema, } from './constructors.js'; | ||
export { serialize, serializeError } from './serializer.js'; | ||
export { EnvironmentError, ErrorList, InputError, isEnvironmentError, isInputError, } from './errors.js'; | ||
// FUNCTIONS WITH ENVIRONMENT | ||
export * as environment from './environment/index.js'; | ||
export { ContextError, EnvironmentError, ErrorList, InputError, isContextError, isEnvironmentError, isInputError, } from './errors.js'; | ||
// FUNCTIONS WITH CONTEXT | ||
/** | ||
* @deprecated use `import { context }` instead | ||
*/ | ||
export * as environment from './context/index.js'; | ||
export * as context from './context/index.js'; |
@@ -1,2 +0,2 @@ | ||
import { EnvironmentError } from './errors.js'; | ||
import { ContextError } from './errors.js'; | ||
import { InputError } from './errors.js'; | ||
@@ -7,3 +7,3 @@ /** | ||
function serializeError(error) { | ||
if (error instanceof InputError || error instanceof EnvironmentError) { | ||
if (error instanceof InputError || error instanceof ContextError) { | ||
return { | ||
@@ -10,0 +10,0 @@ exception: error, |
@@ -10,4 +10,4 @@ # Migrating from domain-functions | ||
- 🤌 Simplified Function Creation: **No need to define schemas**. Create composable functions easily and efficiently without the overhead of schema definitions. | ||
- 🕵🏽 Runtime Validation: Use the [`withSchema`](./API.md#withschema) function for optional runtime validation of inputs and environments. 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 [`environment.pipe`](./API.md#environmentpipe), [`environment.sequence`](./API.md#environmentsequence), and [`environment.branch`](./API.md#environmentbranch), offer powerful ways to manage **typed environments** and contextual information across your compositions. | ||
- 🕵🏽 Runtime Validation: Use the [`withSchema`](./API.md#withschema) 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. | ||
- 🛠️ Incremental Migration: Seamlessly migrate your existing codebase incrementally. **Both `domain-functions` and `composable-functions` can coexist**, allowing you to transition module by module. | ||
@@ -21,3 +21,3 @@ - 🛟 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**. | ||
- [Combinators which shouldn't be affected](#combinators-which-shouldnt-be-affected) | ||
- [Sequential combinators and the concept of environment](#sequential-combinators-and-the-concept-of-environment) | ||
- [Sequential combinators and the concept of context](#sequential-combinators-and-the-concept-of-context) | ||
- [Modified combinators](#modified-combinators) | ||
@@ -40,5 +40,5 @@ - [map](#map) | ||
## First steps | ||
The first thing you want to know is that the old `DomainFunction<T>` is equivalent to `Composable<(input?: unknown, environment?: unknown) => T>` (AKA `ComposableWithSchema<T>`). We brought the arguments to the type signature so we could type check the compositions. A [commonly requested feature](https://github.com/seasonedcc/domain-functions/issues/80) in domain-functions. | ||
The first thing you want to know is that the old `DomainFunction<T>` is equivalent to `Composable<(input?: unknown, context?: unknown) => T>` (AKA `ComposableWithSchema<T>`). We brought the arguments to the type signature so we could type check the compositions. A [commonly requested feature](https://github.com/seasonedcc/domain-functions/issues/80) in domain-functions. | ||
A composable does not need a schema, but you can still use one for runtime assertion. What we used to call a Domain Function is now a Composable with [environment](./environments.md) and a schema. | ||
A composable does not need a schema, but you can still use one for runtime assertion. What we used to call a Domain Function is now a Composable with [context](./context.md) and a schema. | ||
@@ -50,3 +50,3 @@ The new constructor `withSchema` will work almost exactly as `makeDomainFunction`, except for the `Result` type of the resulting function. | ||
This allows us to preserve stack traces and use the familiar exception interface. To differentiate inputErrors and environmentErrors you can use the `instanceof` operator. It also opens up the possibility create any custom error your system needs. | ||
This allows us to preserve stack traces and use the familiar exception interface. To differentiate inputErrors and environmentErrors - which are now called context errors - you can use the `instanceof` operator. It also opens up the possibility create any custom error your system needs. | ||
@@ -68,3 +68,3 @@ ```ts | ||
new InputError('Required', ['name']), | ||
new EnvironmentError('Unauthorized', ['user']), | ||
new ContextError('Unauthorized', ['user']), | ||
], | ||
@@ -92,17 +92,17 @@ } | ||
## Sequential combinators and the concept of environment | ||
The environment we used to have in domain-functions is already built-in the composable's parallel combinators since all arguments are forwarded to every function. For a deeper explanation check the [`environment` docs](./environments.md). | ||
## Sequential combinators and the concept of context | ||
The `environment` we used to have in domain-functions is now called `context` and it is already built-in the composable's parallel combinators since all arguments are forwarded to every function. For a deeper explanation check the [`context` docs](./context.md). | ||
When it comes to sequential compositions, however, we need special combinators to preserve the environment so they work as the domain-functions' combinators. | ||
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 `environment` to keep this familiar behavior. | ||
Use the sequential combinators from the namespace `context` to keep this familiar behavior. | ||
```ts | ||
import { environment } from 'composable-functions' | ||
import { context } from 'composable-functions' | ||
const result = environment.pipe(fn1, fn2)(input, env) | ||
// same for `environment.sequence` and `environment.branch` | ||
const result = context.pipe(fn1, fn2)(input, ctx) | ||
// same for `context.sequence` and `context.branch` | ||
``` | ||
**Note**: The `pipe`, `sequence`, and `branch` outside of the `environment` namespace will not keep the environment through the composition. | ||
**Note**: The `pipe`, `sequence`, and `branch` outside of the `context` namespace will not keep the context through the composition. | ||
@@ -141,9 +141,9 @@ ## Modified combinators | ||
// New Composable code: | ||
import { mapErrors, isInputError, isEnvironmentError } from 'composable-functions' | ||
import { mapErrors, isInputError, isContextError } from 'composable-functions' | ||
const summarizeErrors = (errors: Error[]) => | ||
[ | ||
new Error('Number of errors: ' + errors.filter((e) => !isInputError(e) && !isEnvironmentError(e)).length, | ||
new Error('Number of errors: ' + errors.filter((e) => !isInputError(e) && !isContextError(e)).length, | ||
new InputError('Number of input errors: ' + errors.filter(isInputError).length), | ||
new EnvironmentError('Number of environment errors: ' + errors.filter(isEnvironmentError).length), | ||
new ContextError('Number of context errors: ' + errors.filter(isContextError).length), | ||
] | ||
@@ -217,3 +217,3 @@ | ||
import type { Result as DFResult } from 'domain-functions' | ||
import { isInputError, isEnvironmentError } from 'composable-functions' | ||
import { isInputError, isContextError } from 'composable-functions' | ||
import type { Result, SerializableResult } from 'composable-functions' | ||
@@ -232,3 +232,3 @@ | ||
} | ||
return result.errors.some(isEnvironmentError) | ||
return result.errors.some(isContextError) | ||
} | ||
@@ -240,3 +240,3 @@ ``` | ||
### Dealing with Failures | ||
- In the tests, change the `result.inputErrors` and `result.environmentErrors` for `result.errors`. You can also test for the name: `InputError` or `EnvironmentError` | ||
- In the tests, change the `result.inputErrors` and `result.environmentErrors` for `result.errors`. You can also test for the name: `InputError` or `ContextError` | ||
```ts | ||
@@ -248,3 +248,3 @@ // replace this | ||
``` | ||
- Elsewhere, collect the inputErrors and environmentErrors with the [`isInputError`](./API.md#isinputerror) and [`isEnvironmentError`](./API.md#isenvironmenterror) functions. | ||
- Elsewhere, collect the inputErrors and environmentErrors with the [`isInputError`](./API.md#isinputerror) and [`isContextError`](./API.md#iscontexterror) functions. | ||
```ts | ||
@@ -266,8 +266,8 @@ // replace this | ||
|---|---| | ||
| `makeDomainFunction(z.string(), z.number())((input, env) => {})` | `withSchema(z.string, z.number())((input, env) => {})` | | ||
| -- | `applySchema(z.string(), z.number())(composable((input, env) => {}))` | | ||
| `makeDomainFunction(z.string(), z.number())((input, env) => {})` | `withSchema(z.string, z.number())((input, ctx) => {})` | | ||
| -- | `applySchema(z.string(), z.number())(composable((input, ctx) => {}))` | | ||
| `makeSuccessResult(1)` | `success(1)` | | ||
| `makeErrorResult({ errors: [{ message: 'Something went wrong' }] })` | `failure([new Error('Something went wrong')])` | | ||
| `new InputError('required', 'user.name')` | `new InputError('required', ['user', 'name'])` | | ||
| `new EnvironmentError('oops', 'user.name')` | `new EnvironmentError('oops', ['user', 'name'])` | | ||
| `new EnvironmentError('oops', 'user.name')` | `new ContextError('oops', ['user', 'name'])` | | ||
| `new InputErrors([{ message: 'oops', path: 'user.name' }])` | `new ErrorList([new InputError('oops', ['user', 'name'])])` | | ||
@@ -282,9 +282,9 @@ | `new ResultError({ inputErrors: [{ message: 'oops', path: 'user.name' }] })` | `new ErrorList([new InputError('oops', ['user', 'name'])])` | | ||
| `merge(df1, df2)` | `map(all(fn1, fn2), mergeObjects)` | | ||
| `branch(df1, (res) => res ? null : df2)` | `environment.branch(fn1, (res) => res ? null : fn2)` | | ||
| -- | `branch(fn1, (res) => res ? null : fn2)` without environment | | ||
| `pipe(df1, df2)` | `environment.pipe(fn1, fn2)` | | ||
| -- | `pipe(fn1, fn2)` without environment | | ||
| `sequence(df1, df2)` | `environment.sequence(fn1, fn2)` | | ||
| -- | `sequence(fn1, fn2)` without environment | | ||
| `collectSequence({ name: nameDf, age: ageDf })` | `map(environment.sequence(nameDf, ageDf), ([name, age]) => ({ name, age }))` | | ||
| `branch(df1, (res) => res ? null : df2)` | `context.branch(fn1, (res) => res ? null : fn2)` | | ||
| -- | `branch(fn1, (res) => res ? null : fn2)` without context | | ||
| `pipe(df1, df2)` | `context.pipe(fn1, fn2)` | | ||
| -- | `pipe(fn1, fn2)` without context | | ||
| `sequence(df1, df2)` | `context.sequence(fn1, fn2)` | | ||
| -- | `sequence(fn1, fn2)` without context | | ||
| `collectSequence({ name: nameDf, age: ageDf })` | `map(context.sequence(nameDf, ageDf), ([name, age]) => ({ name, age }))` | | ||
| `map(df, (o) => ({ result: o }))` | `map(fn, (o) => ({ result: o }))` | | ||
@@ -310,6 +310,6 @@ | -- | `map(fn, (o, ...args) => ({ result: o, args }))` | | ||
| `{ success: true, data: { name: 'John' }, errors: [], inputErrors: [], environmentErrors: [] }` | `{ success: true, data: { name: 'John' }, errors: [] }` | | ||
| `{ success: false, errors: [{ message: 'Something went wrong' }], inputErrors: [{ message: 'Required', path: ['name'] }], environemntErrors: [{ message: 'Unauthorized', path: ['user'] }] }` | `{ success: false, errors: [new Error('Something went wrong'), new InputError('Required', ['name']), new EnvironmentError('Unauthorized', ['user'])] }` | | ||
| -- | with `serialize`: `{ success: false, errors: [{ message: 'Something went wrong', name: 'Error', path: [] }, { message: 'Required', name: 'InputError', path: ['name'] }, { message: 'Unauthorized', name: 'EnvironmentError', path: ['user'] }] }` | | ||
| `{ success: false, errors: [{ message: 'Something went wrong' }], inputErrors: [{ message: 'Required', path: ['name'] }], environemntErrors: [{ message: 'Unauthorized', path: ['user'] }] }` | `{ success: false, errors: [new Error('Something went wrong'), new InputError('Required', ['name']), new ContextError('Unauthorized', ['user'])] }` | | ||
| -- | with `serialize`: `{ success: false, errors: [{ message: 'Something went wrong', name: 'Error', path: [] }, { message: 'Required', name: 'InputError', path: ['name'] }, { message: 'Unauthorized', name: 'ContextError', path: ['user'] }] }` | | ||
| `result.inputErrors[0]?.message` | `result.errors.find(isInputError)?.message` | | ||
| `result.environmentErrors[0]?.message` | `result.errors.find(isEnvironmentError)?.message` | | ||
| `result.environmentErrors[0]?.message` | `result.errors.find(isContextError)?.message` | | ||
| `result.errors[0]?.exception instanceof CustomError` | `result.errors[0] instanceof CustomError` | |
{ | ||
"name": "composable-functions", | ||
"version": "4.0.1", | ||
"version": "4.1.0", | ||
"description": "Types and functions to make composition easy and safe", | ||
@@ -5,0 +5,0 @@ "author": "Seasoned", |
@@ -12,3 +12,3 @@ <p align="center"> | ||
- ⚡ 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. | ||
@@ -22,2 +22,3 @@ - 📊 Traceable Compositions: Use the `trace` function to log and monitor your composable functions’ inputs and results, simplifying debugging and monitoring. | ||
- [Composing type-safe functions](#composing-type-safe-functions) | ||
- [Adding runtime validation to the Composable](#adding-runtime-validation-to-the-composable) | ||
- [Creating primitive composables](#creating-primitive-composables) | ||
@@ -35,3 +36,3 @@ - [Sequential composition](#sequential-composition) | ||
- [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) | ||
@@ -97,2 +98,22 @@ - [Using Deno](#using-deno) | ||
### Adding runtime validation to the Composable | ||
To ensure type safety at runtime, use the `applySchema` or `withSchema` functions to validate external inputs against defined schemas. These schemas can be specified with libraries such as [Zod](https://github.com/colinhacks/zod/) or [ArkType](https://github.com/arktypeio/arktype). | ||
Note that the resulting `Composable` will have unknown types for the parameters now that we rely on runtime validation. | ||
```ts | ||
import { applySchema } from 'composable-functions' | ||
import { z } from 'zod' | ||
const addAndReturnWithRuntimeValidation = applySchema( | ||
z.number(), | ||
z.number(), | ||
)(addAndReturnString) | ||
// Or you could have defined schemas and implementation in one shot: | ||
const add = withSchema(z.number(), z.number())((a, b) => a + b) | ||
``` | ||
For more information and examples, check the [Handling external input](./with-schema.md) guide. | ||
## Creating primitive composables | ||
@@ -263,3 +284,3 @@ | ||
#### [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) | ||
@@ -266,0 +287,0 @@ |
@@ -76,9 +76,9 @@ "use strict"; | ||
/** | ||
* Creates a composable with unknown input and environment that uses schemas to parse them into known types. | ||
* 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 environment schemas, you can pass a handler function that takes type safe input and environment. That function is gonna catch any errors and always return a Result. | ||
* 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 environmentSchema the schema for the environment | ||
* @returns a handler function that takes type safe input and environment | ||
* @param contextSchema the schema for the context | ||
* @returns a handler function that takes type safe input and context | ||
* @example | ||
@@ -95,12 +95,12 @@ * const safeFunction = withSchema( | ||
*/ | ||
function withSchema(inputSchema, environmentSchema) { | ||
return (handler) => applySchema(inputSchema, environmentSchema)(composable(handler)); | ||
function withSchema(inputSchema, contextSchema) { | ||
return (handler) => applySchema(inputSchema, contextSchema)(composable(handler)); | ||
} | ||
exports.withSchema = withSchema; | ||
/** | ||
* Takes a composable and creates a composable withSchema that will assert the input and environment types according to the given schemas. | ||
* 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 | ||
* @param inputSchema the schema for the input | ||
* @param environmentSchema the schema for the environment | ||
* @returns a composable function that will assert the input and environment types at runtime. | ||
* @param contextSchema the schema for the context | ||
* @returns a composable function that will assert the input and context types at runtime. | ||
* @example | ||
@@ -122,15 +122,15 @@ * ```ts | ||
*/ | ||
function applySchema(inputSchema, environmentSchema) { | ||
return ((fn) => { | ||
return ((input, environment) => { | ||
const envResult = (environmentSchema ?? alwaysUnknownSchema).safeParse(environment); | ||
function applySchema(inputSchema, contextSchema) { | ||
return (fn) => { | ||
return ((input, context) => { | ||
const ctxResult = (contextSchema ?? alwaysUnknownSchema).safeParse(context); | ||
const result = (inputSchema ?? alwaysUnknownSchema).safeParse(input); | ||
if (!result.success || !envResult.success) { | ||
if (!result.success || !ctxResult.success) { | ||
const inputErrors = result.success ? [] : result.error.issues.map((error) => new errors_js_1.InputError(error.message, error.path)); | ||
const envErrors = envResult.success ? [] : envResult.error.issues.map((error) => new errors_js_1.EnvironmentError(error.message, error.path)); | ||
return Promise.resolve(failure([...inputErrors, ...envErrors])); | ||
const ctxErrors = ctxResult.success ? [] : ctxResult.error.issues.map((error) => new errors_js_1.ContextError(error.message, error.path)); | ||
return Promise.resolve(failure([...inputErrors, ...ctxErrors])); | ||
} | ||
return fn(result.data, envResult.data); | ||
return fn(result.data, ctxResult.data); | ||
}); | ||
}); | ||
}; | ||
} | ||
@@ -137,0 +137,0 @@ exports.applySchema = applySchema; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.isInputError = exports.isEnvironmentError = exports.InputError = exports.ErrorList = exports.EnvironmentError = void 0; | ||
exports.isInputError = exports.isEnvironmentError = exports.isContextError = exports.InputError = exports.ErrorList = exports.EnvironmentError = exports.ContextError = void 0; | ||
/** | ||
@@ -30,3 +30,4 @@ * A custom error class for input errors. | ||
/** | ||
* A custom error class for environment errors. | ||
* @deprecated Use `ContextError` instead | ||
* A custom error class for context errors. | ||
* | ||
@@ -42,3 +43,3 @@ * @example | ||
/** | ||
* Path of environment attribute that originated the error. | ||
* Path of context attribute that originated the error. | ||
*/ | ||
@@ -57,2 +58,27 @@ Object.defineProperty(this, "path", { | ||
/** | ||
* A custom error class for context errors. | ||
* | ||
* @example | ||
* const aComposable = withSchema()(() => { | ||
* throw new ContextError('Invalid context', 'user.name') | ||
* }) | ||
*/ | ||
class ContextError extends Error { | ||
constructor(message, path = []) { | ||
super(message); | ||
/** | ||
* Path of context attribute that originated the error. | ||
*/ | ||
Object.defineProperty(this, "path", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: void 0 | ||
}); | ||
this.name = 'ContextError'; | ||
this.path = path; | ||
} | ||
} | ||
exports.ContextError = ContextError; | ||
/** | ||
* A list of errors | ||
@@ -87,7 +113,13 @@ * | ||
/** | ||
* A function to check if an `Error` or a `SerializableError` is an EnvironmentError | ||
* @deprecated Use `isContextError` instead | ||
* A function to check if an `Error` or a `SerializableError` is a ContextError | ||
*/ | ||
function isEnvironmentError(e) { | ||
return e.name === 'EnvironmentError'; | ||
const isEnvironmentError = isContextError; | ||
exports.isEnvironmentError = isEnvironmentError; | ||
/** | ||
* A function to check if an `Error` or a `SerializableError` is a ContextError | ||
*/ | ||
function isContextError(e) { | ||
return e.name === 'EnvironmentError' || e.name === 'ContextError'; | ||
} | ||
exports.isEnvironmentError = isEnvironmentError; | ||
exports.isContextError = isContextError; |
@@ -26,3 +26,3 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.environment = exports.isInputError = exports.isEnvironmentError = exports.InputError = exports.ErrorList = exports.EnvironmentError = exports.serializeError = exports.serialize = exports.inputFromUrl = exports.inputFromSearch = exports.inputFromFormData = exports.inputFromForm = exports.trace = exports.sequence = exports.pipe = exports.mergeObjects = exports.mapParameters = exports.mapErrors = exports.map = exports.collect = exports.catchFailure = exports.branch = exports.all = exports.withSchema = exports.success = exports.fromSuccess = exports.failure = exports.composable = exports.applySchema = void 0; | ||
exports.context = exports.environment = exports.isInputError = exports.isEnvironmentError = exports.isContextError = exports.InputError = exports.ErrorList = exports.EnvironmentError = exports.ContextError = exports.serializeError = exports.serialize = exports.inputFromUrl = exports.inputFromSearch = exports.inputFromFormData = exports.inputFromForm = exports.trace = exports.sequence = exports.pipe = exports.mergeObjects = exports.mapParameters = exports.mapErrors = exports.map = exports.collect = exports.catchFailure = exports.branch = exports.all = exports.withSchema = exports.success = exports.fromSuccess = exports.failure = exports.composable = exports.applySchema = void 0; | ||
var constructors_js_1 = require("./constructors.js"); | ||
@@ -56,8 +56,14 @@ Object.defineProperty(exports, "applySchema", { enumerable: true, get: function () { return constructors_js_1.applySchema; } }); | ||
var errors_js_1 = require("./errors.js"); | ||
Object.defineProperty(exports, "ContextError", { enumerable: true, get: function () { return errors_js_1.ContextError; } }); | ||
Object.defineProperty(exports, "EnvironmentError", { enumerable: true, get: function () { return errors_js_1.EnvironmentError; } }); | ||
Object.defineProperty(exports, "ErrorList", { enumerable: true, get: function () { return errors_js_1.ErrorList; } }); | ||
Object.defineProperty(exports, "InputError", { enumerable: true, get: function () { return errors_js_1.InputError; } }); | ||
Object.defineProperty(exports, "isContextError", { enumerable: true, get: function () { return errors_js_1.isContextError; } }); | ||
Object.defineProperty(exports, "isEnvironmentError", { enumerable: true, get: function () { return errors_js_1.isEnvironmentError; } }); | ||
Object.defineProperty(exports, "isInputError", { enumerable: true, get: function () { return errors_js_1.isInputError; } }); | ||
// FUNCTIONS WITH ENVIRONMENT | ||
exports.environment = __importStar(require("./environment/index.js")); | ||
// FUNCTIONS WITH CONTEXT | ||
/** | ||
* @deprecated use `import { context }` instead | ||
*/ | ||
exports.environment = __importStar(require("./context/index.js")); | ||
exports.context = __importStar(require("./context/index.js")); |
@@ -10,3 +10,3 @@ "use strict"; | ||
function serializeError(error) { | ||
if (error instanceof errors_js_2.InputError || error instanceof errors_js_1.EnvironmentError) { | ||
if (error instanceof errors_js_2.InputError || error instanceof errors_js_1.ContextError) { | ||
return { | ||
@@ -13,0 +13,0 @@ exception: error, |
@@ -33,9 +33,9 @@ import type { ApplySchemaReturn, Composable, ComposableWithSchema, Failure, ParserSchema, Success } from './types.js'; | ||
/** | ||
* Creates a composable with unknown input and environment that uses schemas to parse them into known types. | ||
* 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 environment schemas, you can pass a handler function that takes type safe input and environment. That function is gonna catch any errors and always return a Result. | ||
* 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 environmentSchema the schema for the environment | ||
* @returns a handler function that takes type safe input and environment | ||
* @param contextSchema the schema for the context | ||
* @returns a handler function that takes type safe input and context | ||
* @example | ||
@@ -52,9 +52,9 @@ * const safeFunction = withSchema( | ||
*/ | ||
declare function withSchema<I, E>(inputSchema?: ParserSchema<I>, environmentSchema?: ParserSchema<E>): <Output>(hander: (input: I, environment: E) => Output) => ComposableWithSchema<Output>; | ||
declare function withSchema<I, C>(inputSchema?: ParserSchema<I>, contextSchema?: ParserSchema<C>): <Output>(hander: (input: I, context: C) => Output) => ComposableWithSchema<Output>; | ||
/** | ||
* Takes a composable and creates a composable withSchema that will assert the input and environment types according to the given schemas. | ||
* 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 | ||
* @param inputSchema the schema for the input | ||
* @param environmentSchema the schema for the environment | ||
* @returns a composable function that will assert the input and environment types at runtime. | ||
* @param contextSchema the schema for the context | ||
* @returns a composable function that will assert the input and context types at runtime. | ||
* @example | ||
@@ -76,3 +76,3 @@ * ```ts | ||
*/ | ||
declare function applySchema<ParsedInput, ParsedEnvironment>(inputSchema?: ParserSchema<ParsedInput>, environmentSchema?: ParserSchema<ParsedEnvironment>): <R, Input, Environment>(fn: Composable<(input?: Input | undefined, environment?: Environment | undefined) => R>) => ApplySchemaReturn<ParsedInput, ParsedEnvironment, Composable<(input?: Input | undefined, environment?: Environment | undefined) => R>>; | ||
declare function applySchema<ParsedInput, ParsedContext>(inputSchema?: ParserSchema<ParsedInput>, contextSchema?: ParserSchema<ParsedContext>): <R, Input, Context>(fn: Composable<(input?: Input | undefined, context?: Context | undefined) => R>) => ApplySchemaReturn<ParsedInput, ParsedContext, Composable<(input?: Input | undefined, context?: Context | undefined) => R>>; | ||
export { applySchema, composable, failure, fromSuccess, success, withSchema }; |
@@ -17,3 +17,4 @@ /** | ||
/** | ||
* A custom error class for environment errors. | ||
* @deprecated Use `ContextError` instead | ||
* A custom error class for context errors. | ||
* | ||
@@ -27,3 +28,3 @@ * @example | ||
/** | ||
* Path of environment attribute that originated the error. | ||
* Path of context attribute that originated the error. | ||
*/ | ||
@@ -34,2 +35,17 @@ path: string[]; | ||
/** | ||
* A custom error class for context errors. | ||
* | ||
* @example | ||
* const aComposable = withSchema()(() => { | ||
* throw new ContextError('Invalid context', 'user.name') | ||
* }) | ||
*/ | ||
declare class ContextError extends Error { | ||
/** | ||
* Path of context attribute that originated the error. | ||
*/ | ||
path: string[]; | ||
constructor(message: string, path?: string[]); | ||
} | ||
/** | ||
* A list of errors | ||
@@ -54,8 +70,13 @@ * | ||
/** | ||
* A function to check if an `Error` or a `SerializableError` is an EnvironmentError | ||
* @deprecated Use `isContextError` instead | ||
* A function to check if an `Error` or a `SerializableError` is a ContextError | ||
*/ | ||
declare function isEnvironmentError(e: { | ||
declare const isEnvironmentError: typeof isContextError; | ||
/** | ||
* A function to check if an `Error` or a `SerializableError` is a ContextError | ||
*/ | ||
declare function isContextError(e: { | ||
name: string; | ||
message: string; | ||
}): boolean; | ||
export { EnvironmentError, ErrorList, InputError, isEnvironmentError, isInputError, }; | ||
export { ContextError, EnvironmentError, ErrorList, InputError, isContextError, isEnvironmentError, isInputError, }; |
@@ -6,4 +6,8 @@ export { applySchema, composable, failure, fromSuccess, success, withSchema, } from './constructors.js'; | ||
export { serialize, serializeError } from './serializer.js'; | ||
export { EnvironmentError, ErrorList, InputError, isEnvironmentError, isInputError, } from './errors.js'; | ||
export { ContextError, EnvironmentError, ErrorList, InputError, isContextError, isEnvironmentError, isInputError, } from './errors.js'; | ||
export type { ApplySchemaReturn, BranchReturn, CanComposeInParallel, CanComposeInSequence, Composable, ComposableWithSchema, FailToCompose, Failure, IncompatibleArguments, MapParametersReturn, MergeObjects, ParserSchema, PipeReturn, Result, SequenceReturn, SerializableError, SerializableResult, Success, UnpackAll, UnpackData, } from './types.js'; | ||
export * as environment from './environment/index.js'; | ||
/** | ||
* @deprecated use `import { context }` instead | ||
*/ | ||
export * as environment from './context/index.js'; | ||
export * as context from './context/index.js'; |
@@ -45,3 +45,3 @@ import type { Internal } from './internal/types.js'; | ||
*/ | ||
type ComposableWithSchema<O> = Composable<(input?: unknown, environment?: unknown) => O>; | ||
type ComposableWithSchema<O> = Composable<(input?: unknown, context?: unknown) => O>; | ||
/** | ||
@@ -115,3 +115,3 @@ * Extract the type of the returned data when a Composable is successful. | ||
/** | ||
* The object used to validate either input or environment when creating composables with a schema. | ||
* The object used to validate either input or context when creating composables with a schema. | ||
*/ | ||
@@ -125,6 +125,6 @@ type ParserSchema<T extends unknown = unknown> = { | ||
error: { | ||
issues: { | ||
path: Array<string | number>; | ||
issues: ReadonlyArray<{ | ||
path: PropertyKey[]; | ||
message: string; | ||
}[]; | ||
}>; | ||
}; | ||
@@ -151,5 +151,5 @@ }; | ||
/** | ||
* Ensure that schemas are compatible with composable input and environment otherwise return a FailToCompose. | ||
* Ensure that schemas are compatible with composable input and context otherwise return a FailToCompose. | ||
*/ | ||
type ApplySchemaReturn<ParsedInput, ParsedEnvironment, Fn extends Composable> = ParsedInput extends Parameters<Fn>[0] ? ParsedEnvironment extends Parameters<Fn>[1] ? ComposableWithSchema<UnpackData<Fn>> : FailToCompose<ParsedEnvironment, Parameters<Fn>[1]> : FailToCompose<ParsedInput, Parameters<Fn>[0]>; | ||
type ApplySchemaReturn<ParsedInput, ParsedContext, Fn extends Composable> = ParsedInput extends Parameters<Fn>[0] ? ParsedContext extends Parameters<Fn>[1] ? ComposableWithSchema<UnpackData<Fn>> : FailToCompose<ParsedContext, Parameters<Fn>[1]> : FailToCompose<ParsedInput, Parameters<Fn>[0]>; | ||
/** | ||
@@ -156,0 +156,0 @@ * The return type of the mapParameters function |
@@ -13,3 +13,3 @@ # Handling External Input | ||
The `applySchema` function takes a schemas for the input and environment, and a composable, applying these schemas to ensure data integrity. | ||
The `applySchema` function takes a schemas for the input and context, and a composable, applying these schemas to ensure data integrity. | ||
@@ -31,3 +31,3 @@ ```typescript | ||
type Test = typeof fnWithSchema | ||
// ^? Composable<(input?: unknown, env?: unknown) => { message: string }> | ||
// ^? Composable<(input?: unknown, ctx?: unknown) => { message: string }> | ||
``` | ||
@@ -44,3 +44,3 @@ | ||
const runtimeSafeAdd = withSchema(z.number(), z.number())((a, b) => a + b) | ||
// ^? Composable<(input?: unknown, env?: unknown) => number> | ||
// ^? Composable<(input?: unknown, ctx?: unknown) => number> | ||
const result = await runtimeSafeAdd(1, 2) | ||
@@ -47,0 +47,0 @@ /* |
189439
2454
299