Skip to content

Commit

Permalink
feat(result): Support wrapping Zod schemas (#23725)
Browse files Browse the repository at this point in the history
  • Loading branch information
zharinov committed Aug 6, 2023
1 parent 9dc51d0 commit c5c9969
Show file tree
Hide file tree
Showing 2 changed files with 65 additions and 11 deletions.
20 changes: 19 additions & 1 deletion lib/util/result.spec.ts
@@ -1,4 +1,4 @@
import { z } from 'zod';
import { ZodError, z } from 'zod';
import { logger } from '../../test/util';
import { AsyncResult, Result } from './result';

Expand Down Expand Up @@ -84,6 +84,13 @@ describe('util/result', () => {
})
);
});

it('wraps Zod schema', () => {
const schema = z.string().transform((x) => x.toUpperCase());
const parse = Result.wrapSchema(schema);
expect(parse('foo')).toEqual(Result.ok('FOO'));
expect(parse(42)).toMatchObject(Result.err(expect.any(ZodError)));
});
});

describe('Unwrapping', () => {
Expand Down Expand Up @@ -259,6 +266,17 @@ describe('util/result', () => {
const res = Result.wrapNullable(Promise.reject('oops'), 'nullable');
await expect(res).resolves.toEqual(Result.err('oops'));
});

it('wraps Zod async schema', async () => {
const schema = z
.string()
.transform((x) => Promise.resolve(x.toUpperCase()));
const parse = Result.wrapSchemaAsync(schema);
await expect(parse('foo')).resolves.toEqual(Result.ok('FOO'));
await expect(parse(42)).resolves.toMatchObject(
Result.err(expect.any(ZodError))
);
});
});

describe('Unwrapping', () => {
Expand Down
56 changes: 46 additions & 10 deletions lib/util/result.ts
@@ -1,4 +1,4 @@
import { SafeParseReturnType, ZodError } from 'zod';
import { SafeParseReturnType, ZodError, ZodType, ZodTypeDef } from 'zod';
import { logger } from '../logger';

type Val = NonNullable<unknown>;
Expand Down Expand Up @@ -54,6 +54,14 @@ function fromZodResult<ZodInput, ZodOutput extends Val>(
return input.success ? Result.ok(input.data) : Result.err(input.error);
}

type SchemaParseFn<T extends Val, Input = unknown> = (
input: unknown
) => Result<T, ZodError<Input>>;

type SchemaAsyncParseFn<T extends Val, Input = unknown> = (
input: unknown
) => AsyncResult<T, ZodError<Input>>;

/**
* All non-nullable values that also are not Promises nor Zod results.
* It's useful for restricting Zod results to not return `null` or `undefined`.
Expand Down Expand Up @@ -115,7 +123,7 @@ export class Result<T extends Val, E extends Val = Error> {
*
* ```
*/
static wrap<T extends Val, Input = any>(
static wrap<T extends Val, Input = unknown>(
zodResult: SafeParseReturnType<Input, T>
): Result<T, ZodError<Input>>;
static wrap<T extends Val, E extends Val = Error>(
Expand All @@ -131,7 +139,7 @@ export class Result<T extends Val, E extends Val = Error> {
T extends Val,
E extends Val = Error,
EE extends Val = never,
Input = any
Input = unknown
>(
input:
| SafeParseReturnType<Input, T>
Expand Down Expand Up @@ -271,6 +279,34 @@ export class Result<T extends Val, E extends Val = Error> {
}
}

/**
* Wraps a Zod schema and returns a parse function that returns a `Result`.
*/
static wrapSchema<
T extends Val,
Schema extends ZodType<T, ZodTypeDef, Input>,
Input = unknown
>(schema: Schema): SchemaParseFn<T, Input> {
return (input) => {
const result = schema.safeParse(input);
return fromZodResult(result);
};
}

/**
* Wraps a Zod schema and returns a parse function that returns an `AsyncResult`.
*/
static wrapSchemaAsync<
T extends Val,
Schema extends ZodType<T, ZodTypeDef, Input>,
Input = unknown
>(schema: Schema): SchemaAsyncParseFn<T, Input> {
return (input) => {
const result = schema.safeParseAsync(input);
return AsyncResult.wrap(result);
};
}

/**
* Returns a discriminated union for type-safe consumption of the result.
* When `fallback` is provided, the error is discarded and value is returned directly.
Expand Down Expand Up @@ -350,10 +386,10 @@ export class Result<T extends Val, E extends Val = Error> {
transform<U extends Val, EE extends Val>(
fn: (value: T) => AsyncResult<U, E | EE>
): AsyncResult<U, E | EE>;
transform<U extends Val, Input = any>(
transform<U extends Val, Input = unknown>(
fn: (value: T) => SafeParseReturnType<Input, NonNullable<U>>
): Result<U, E | ZodError<Input>>;
transform<U extends Val, Input = any>(
transform<U extends Val, Input = unknown>(
fn: (value: T) => Promise<SafeParseReturnType<Input, NonNullable<U>>>
): AsyncResult<U, E | ZodError<Input>>;
transform<U extends Val, EE extends Val>(
Expand All @@ -363,7 +399,7 @@ export class Result<T extends Val, E extends Val = Error> {
fn: (value: T) => Promise<RawValue<U>>
): AsyncResult<U, E>;
transform<U extends Val>(fn: (value: T) => RawValue<U>): Result<U, E>;
transform<U extends Val, EE extends Val, Input = any>(
transform<U extends Val, EE extends Val, Input = unknown>(
fn: (
value: T
) =>
Expand Down Expand Up @@ -486,7 +522,7 @@ export class AsyncResult<T extends Val, E extends Val>
T extends Val,
E extends Val = Error,
EE extends Val = never,
Input = any
Input = unknown
>(
promise:
| Promise<SafeParseReturnType<Input, T>>
Expand Down Expand Up @@ -602,10 +638,10 @@ export class AsyncResult<T extends Val, E extends Val>
transform<U extends Val, EE extends Val>(
fn: (value: T) => AsyncResult<U, E | EE>
): AsyncResult<U, E | EE>;
transform<U extends Val, Input = any>(
transform<U extends Val, Input = unknown>(
fn: (value: T) => SafeParseReturnType<Input, NonNullable<U>>
): AsyncResult<U, E | ZodError<Input>>;
transform<U extends Val, Input = any>(
transform<U extends Val, Input = unknown>(
fn: (value: T) => Promise<SafeParseReturnType<Input, NonNullable<U>>>
): AsyncResult<U, E | ZodError<Input>>;
transform<U extends Val, EE extends Val>(
Expand All @@ -615,7 +651,7 @@ export class AsyncResult<T extends Val, E extends Val>
fn: (value: T) => Promise<RawValue<U>>
): AsyncResult<U, E>;
transform<U extends Val>(fn: (value: T) => RawValue<U>): AsyncResult<U, E>;
transform<U extends Val, EE extends Val, Input = any>(
transform<U extends Val, EE extends Val, Input = unknown>(
fn: (
value: T
) =>
Expand Down

0 comments on commit c5c9969

Please sign in to comment.