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

Invert parameters in apply-schema so we can validate schema type based on composable parameters #145

Closed
wants to merge 12 commits into from
Closed
5 changes: 3 additions & 2 deletions examples/arktype/src/adapters.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
composable,
Composable,
ComposableWithSchema,
EnvironmentError,
failure,
InputError,
Expand Down Expand Up @@ -44,7 +45,7 @@ function withArkSchema<I, E>(
) {
return function <Output>(
handler: (input: I, environment: E) => Output,
): Composable<(input?: unknown, environment?: unknown) => Awaited<Output>> {
): ComposableWithSchema<Awaited<Output>> {
return applyArkSchema(inputSchema, environmentSchema)(composable(handler))
}
}
Expand Down Expand Up @@ -73,7 +74,7 @@ function applyArkSchema<I, E>(
return failure([...inputErrors, ...envErrors])
}
return fn(result.data as I, envResult.data as E)
} as Composable<(input?: unknown, environment?: unknown) => UnpackData<A>>
} as ComposableWithSchema<UnpackData<A>>
}
}

Expand Down
4 changes: 2 additions & 2 deletions migrating-df.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ This document will guide you through the migration process.
- [Runtime code](#runtime-code)

## First steps
The first thing you want to know is that the old `DomainFunction<T>` is equivalent to `Composable<(input?: unknown, environment?: unknown) => 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, 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.

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.

Expand Down Expand Up @@ -285,7 +285,7 @@ if (result.errors.some(isInputError)) {
#### Type utilities
| Domain Functions | Composable Functions |
|---|---|
| `DomainFunction<string>` | `Composable<(input?: unknown, environment?: unknown) => string>` |
| `DomainFunction<string>` | `ComposableWithSchema<string>` |
| `SuccessResult<T>` | `Success<T>` |
| `ErrorResult` | `Failure` |
| `UnpackData<DomainFunction>` | `UnpackData<Composable>` |
Expand Down
53 changes: 38 additions & 15 deletions src/combinators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ function mergeObjects<T extends unknown[] = unknown[]>(
* // ^? Composable<({ aNumber }: { aNumber: number }) => { aBoolean: boolean }>
* ```
*/
function pipe<Fns extends [Composable, ...Composable[]]>(...fns: Fns) {
function pipe<Fns extends [Composable, ...Composable[]]>(
...fns: Fns
): PipeReturn<CanComposeInSequence<Fns>> {
const last = <T extends any[]>(arr: T): Last<T> => arr.at(-1)
return map(sequence(...fns), last as never) as PipeReturn<
CanComposeInSequence<Fns>
Expand All @@ -89,7 +91,13 @@ function pipe<Fns extends [Composable, ...Composable[]]>(...fns: Fns) {
* // ^? Composable<(id: number) => [string, number, boolean]>
* ```
*/
function all<Fns extends Composable[]>(...fns: Fns) {
function all<Fns extends Composable[]>(
...fns: Fns
): Composable<
(...args: Parameters<NonNullable<CanComposeInParallel<Fns>[0]>>) => {
[k in keyof Fns]: UnpackData<Fns[k]>
}
> {
return (async (...args) => {
const results = await Promise.all(fns.map((fn) => fn(...args)))

Expand Down Expand Up @@ -119,7 +127,17 @@ function all<Fns extends Composable[]>(...fns: Fns) {
* // ^? Composable<() => { a: string, b: number }>
* ```
*/
function collect<Fns extends Record<string, Composable>>(fns: Fns) {
function collect<Fns extends Record<string, Composable>>(
fns: Fns,
): Composable<
(
...args: Parameters<
Exclude<CanComposeInParallel<RecordToTuple<Fns>>[0], undefined>
>
) => {
[key in keyof Fns]: UnpackData<Fns[key]>
}
> {
const fnsWithKey = Object.entries(fns).map(([key, cf]) =>
map(cf, (result) => ({ [key]: result })),
)
Expand Down Expand Up @@ -148,7 +166,10 @@ function collect<Fns extends Record<string, Composable>>(fns: Fns) {
* // ^? Composable<(aNumber: number) => [string, boolean]>
* ```
*/
function sequence<Fns extends [Composable, ...Composable[]]>(...fns: Fns) {

function sequence<Fns extends [Composable, ...Composable[]]>(
...fns: Fns
): SequenceReturn<CanComposeInSequence<Fns>> {
return (async (...args) => {
const [head, ...tail] = fns

Expand Down Expand Up @@ -188,12 +209,12 @@ function map<Fn extends Composable, O>(
...originalInput: Parameters<Fn>
) => O | Promise<O>,
): Composable<(...args: Parameters<Fn>) => O> {
return (async (...args) => {
return async (...args) => {
const result = await fn(...args)
if (!result.success) return failure(result.errors)

return composable(mapper)(result.data, ...args)
}) as Composable<(...args: Parameters<Fn>) => O>
}
}

/**
Expand Down Expand Up @@ -279,11 +300,11 @@ function catchFailure<
* }))
* ```
*/
function mapErrors<Fn extends Composable>(
fn: Fn,
function mapErrors<P extends unknown[], Output>(
fn: Composable<(...args: P) => Output>,
mapper: (err: Error[]) => Error[] | Promise<Error[]>,
) {
return (async (...args) => {
): Composable<(...args: P) => Output> {
return async (...args) => {
const res = await fn(...args)
if (res.success) return success(res.data)
const mapped = await composable(mapper)(res.errors)
Expand All @@ -292,7 +313,7 @@ function mapErrors<Fn extends Composable>(
} else {
return failure(mapped.errors)
}
}) as Fn
}
}

/**
Expand All @@ -318,15 +339,17 @@ function trace(
result: Result<unknown>,
...originalInput: unknown[]
) => Promise<void> | void,
) {
return <Fn extends Composable>(fn: Fn) =>
(async (...args) => {
): <P extends unknown[], Output>(
fn: Composable<(...args: P) => Output>,
) => Composable<(...args: P) => Output> {
return (fn) =>
async (...args) => {
const originalResult = await fn(...args)
const traceResult = await composable(traceFn)(originalResult, ...args)
if (traceResult.success) return originalResult

return failure(traceResult.errors)
}) as Fn
}
}

/**
Expand Down
95 changes: 49 additions & 46 deletions src/constructors.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { mapErrors } from './combinators.ts'
import { EnvironmentError, ErrorList, InputError } from './errors.ts'
import type { Composable, Failure, ParserSchema, Success } from './types.ts'
import { UnpackData } from './types.ts'
import type {
Composable,
ComposableWithSchema,
Failure,
ParserSchema,
Success,
} from './types.ts'

/**
* It receives any data (T) and returns a Success<T> object.
Expand Down Expand Up @@ -61,18 +66,16 @@ function composable<T extends (...args: any[]) => any>(fn: T): Composable<T> {
* expect(data).toBe(n + 1)
* ```
*/
function fromSuccess<O, T extends Composable<(...a: any[]) => O>>(
fn: T,
function fromSuccess<O, P extends any[]>(
fn: Composable<(...a: P) => O>,
onError: (errors: Error[]) => Error[] | Promise<Error[]> = (e) => e,
) {
return (async (...args: any[]) => {
): (...args: P) => Promise<O> {
return async (...args: P) => {
const result = await mapErrors(fn, onError)(...args)
if (result.success) return result.data

throw new ErrorList(result.errors)
}) as T extends Composable<(...a: infer P) => infer O>
? (...args: P) => Promise<O>
: never
}
}

/**
Expand All @@ -97,11 +100,11 @@ function fromSuccess<O, T extends Composable<(...a: any[]) => O>>(
function withSchema<I, E>(
inputSchema?: ParserSchema<I>,
environmentSchema?: ParserSchema<E>,
) {
return <Output>(
handler: (input: I, environment: E) => Output,
): Composable<(input?: unknown, environment?: unknown) => Awaited<Output>> =>
applySchema(inputSchema, environmentSchema)(composable(handler)) as never
): <Output>(
hander: (input: I, environment: E) => Output,
) => ComposableWithSchema<Output> {
return ((handler) =>
applySchema(composable(handler), inputSchema, environmentSchema))
}

/**
Expand All @@ -112,47 +115,47 @@ function withSchema<I, E>(
* @returns a composable function that will assert the input and environment types at runtime.
* @example
* ```ts
* const fn = composable((
* { greeting }: { greeting: string },
* { user }: { user: { name: string } },
* ) => ({
* message: `${greeting} ${user.name}`
* }))
* const safeFunction = applySchema(
* fn,
* z.object({ greeting: z.string() }),
* z.object({
* user: z.object({ name: z.string() })
* }),
* )
* const fn = safeFunction(composable((
* { greeting }: { greeting: string },
* { user }: { user: { name: string } },
* ) => ({
* message: `${greeting} ${user.name}`
* })))
* ```
*/
function applySchema<I, E>(
inputSchema?: ParserSchema<I>,
environmentSchema?: ParserSchema<E>,
) {
return <A extends Composable>(fn: A) => {
return ((input: I, environment: E) => {
const envResult = (environmentSchema ?? alwaysUnknownSchema).safeParse(
environment,
)
const result = (inputSchema ?? alwaysUnknownSchema).safeParse(input)
function applySchema<Input, Environment, R>(
fn: Composable<(input?: Input, environment?: Environment) => R>,
inputSchema?: ParserSchema<Input>,
environmentSchema?: ParserSchema<Environment>,
): ComposableWithSchema<R> {
return (input?: unknown, environment?: unknown) => {
const envResult = (environmentSchema ?? alwaysUnknownSchema).safeParse(
environment,
)
const result = (inputSchema ?? alwaysUnknownSchema).safeParse(input)

if (!result.success || !envResult.success) {
const inputErrors = result.success
? []
: result.error.issues.map(
(error) => new InputError(error.message, error.path as string[]),
)
const envErrors = envResult.success
? []
: envResult.error.issues.map(
(error) =>
new EnvironmentError(error.message, error.path as string[]),
)
return Promise.resolve(failure([...inputErrors, ...envErrors]))
}
return fn(result.data as I, envResult.data as E)
}) as Composable<(input?: unknown, environment?: unknown) => UnpackData<A>>
if (!result.success || !envResult.success) {
const inputErrors = result.success
? []
: result.error.issues.map(
(error) => new InputError(error.message, error.path as string[]),
)
const envErrors = envResult.success
? []
: envResult.error.issues.map(
(error) =>
new EnvironmentError(error.message, error.path as string[]),
)
return Promise.resolve(failure([...inputErrors, ...envErrors]))
}
return fn(result.data as Input, envResult.data as Environment)
}
}

Expand Down
13 changes: 8 additions & 5 deletions src/environment/combinators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ function applyEnvironmentToList<
* ({ aString }) => ({ aBoolean: aString == '1' }),
* )
* const d = environment.pipe(a, b)
* // ^? Composable<(input?: unknown, environment?: unknown) => { aBoolean: boolean }>
* // ^? ComposableWithSchema<{ aBoolean: boolean }>
* ```
*/
function pipe<Fns extends Composable[]>(...fns: Fns) {
function pipe<Fns extends Composable[]>(...fns: Fns): PipeReturn<Fns> {
return ((input: any, environment: any) =>
A.pipe(...applyEnvironmentToList(fns, environment))(
input,
Expand All @@ -45,11 +45,11 @@ function pipe<Fns extends Composable[]>(...fns: Fns) {
* const a = withSchema(z.number())((aNumber) => String(aNumber))
* const b = withSchema(z.string())((aString) => aString === '1')
* const aComposable = environment.sequence(a, b)
* // ^? Composable<(input?: unknown, environment?: unknown) => [string, boolean]>
* // ^? ComposableWithSchema<[string, boolean]>
* ```
*/

function sequence<Fns extends Composable[]>(...fns: Fns) {
function sequence<Fns extends Composable[]>(...fns: Fns): SequenceReturn<Fns> {
return ((input: any, environment: any) =>
A.sequence(...applyEnvironmentToList(fns, environment))(
input,
Expand All @@ -64,7 +64,10 @@ function branch<
Resolver extends (
o: UnpackData<SourceComposable>,
) => Composable | null | Promise<Composable | null>,
>(cf: SourceComposable, resolver: Resolver) {
>(
cf: SourceComposable,
resolver: Resolver,
): BranchReturn<SourceComposable, Resolver> {
return (async (...args: Parameters<SourceComposable>) => {
const [input, environment] = args
const result = await cf(input, environment)
Expand Down
10 changes: 5 additions & 5 deletions src/environment/tests/branch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
success,
withSchema,
} from '../../index.ts'
import { Composable, UnpackData } from '../../types.ts'
import { Composable, ComposableWithSchema, UnpackData } from '../../types.ts'

describe('branch', () => {
it('should pipe a composable with arbitrary types', async () => {
Expand Down Expand Up @@ -40,7 +40,7 @@ describe('branch', () => {
type _R = Expect<
Equal<
typeof c,
Composable<(input?: unknown, environment?: unknown) => number>
ComposableWithSchema<number>
>
>

Expand All @@ -60,7 +60,7 @@ describe('branch', () => {
type _R = Expect<
Equal<
typeof d,
Composable<(input?: unknown, environment?: unknown) => number | string>
ComposableWithSchema<number | string>
>
>

Expand Down Expand Up @@ -120,7 +120,7 @@ describe('branch', () => {
type _R = Expect<
Equal<
typeof c,
Composable<(input?: unknown, environment?: unknown) => number>
ComposableWithSchema<number>
>
>

Expand All @@ -139,7 +139,7 @@ describe('branch', () => {
type _R = Expect<
Equal<
typeof c,
Composable<(input?: unknown, environment?: unknown) => number>
ComposableWithSchema<number>
>
>

Expand Down
Loading
Loading