mirror of
https://github.com/renovatebot/renovate.git
synced 2025-01-13 07:26:26 +00:00
feat: Support Zod values in Result
transforms (#23583)
This commit is contained in:
parent
a9af34cf8c
commit
674c6fca49
4 changed files with 150 additions and 33 deletions
|
@ -101,10 +101,9 @@ export class RubyGemsDatasource extends Datasource {
|
|||
packageName: string
|
||||
): AsyncResult<ReleaseResult, Error | ZodError> {
|
||||
const url = joinUrlParts(registryUrl, '/info', packageName);
|
||||
return Result.wrap(this.http.get(url)).transform(({ body }) => {
|
||||
const res = GemInfo.safeParse(body);
|
||||
return res.success ? Result.ok(res.data) : Result.err(res.error);
|
||||
});
|
||||
return Result.wrap(this.http.get(url)).transform(({ body }) =>
|
||||
GemInfo.safeParse(body)
|
||||
);
|
||||
}
|
||||
|
||||
private getReleasesViaDeprecatedAPI(
|
||||
|
@ -117,10 +116,7 @@ export class RubyGemsDatasource extends Datasource {
|
|||
const bufPromise = this.http.getBuffer(url);
|
||||
return Result.wrap(bufPromise).transform(({ body }) => {
|
||||
const data = Marshal.parse(body);
|
||||
const releases = MarshalledVersionInfo.safeParse(data);
|
||||
return releases.success
|
||||
? Result.ok(releases.data)
|
||||
: Result.err(releases.error);
|
||||
return MarshalledVersionInfo.safeParse(data);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -372,7 +372,7 @@ export class Http<Opts extends HttpOptions = HttpOptions> {
|
|||
): AsyncResult<ResT, SafeJsonError> {
|
||||
const args = this.resolveArgs<ResT>(arg1, arg2, arg3);
|
||||
return Result.wrap(this.requestJson<ResT>('get', args)).transform(
|
||||
(response) => response.body
|
||||
(response) => Result.ok(response.body)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { z } from 'zod';
|
||||
import { logger } from '../../test/util';
|
||||
import { AsyncResult, Result } from './result';
|
||||
|
||||
|
@ -68,6 +69,18 @@ describe('util/result', () => {
|
|||
}, 'nullable');
|
||||
expect(res).toEqual(Result.err('oops'));
|
||||
});
|
||||
|
||||
it('wraps zod parse result', () => {
|
||||
const schema = z.string().transform((x) => x.toUpperCase());
|
||||
expect(Result.wrap(schema.safeParse('foo'))).toEqual(Result.ok('FOO'));
|
||||
expect(Result.wrap(schema.safeParse(42))).toMatchObject(
|
||||
Result.err({
|
||||
issues: [
|
||||
{ code: 'invalid_type', expected: 'string', received: 'number' },
|
||||
],
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Unwrapping', () => {
|
||||
|
@ -149,6 +162,12 @@ describe('util/result', () => {
|
|||
'Result: unhandled transform error'
|
||||
);
|
||||
});
|
||||
|
||||
it('automatically converts zod values', () => {
|
||||
const schema = z.string().transform((x) => x.toUpperCase());
|
||||
const res = Result.ok('foo').transform((x) => schema.safeParse(x));
|
||||
expect(res).toEqual(Result.ok('FOO'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('Catch', () => {
|
||||
|
@ -416,6 +435,22 @@ describe('util/result', () => {
|
|||
|
||||
expect(res).toEqual(Result.ok('F-O-O'));
|
||||
});
|
||||
|
||||
it('asynchronously transforms Result to zod values', async () => {
|
||||
const schema = z.string().transform((x) => x.toUpperCase());
|
||||
const res = await Result.ok('foo').transform((x) =>
|
||||
Promise.resolve(schema.safeParse(x))
|
||||
);
|
||||
expect(res).toEqual(Result.ok('FOO'));
|
||||
});
|
||||
|
||||
it('transforms AsyncResult to zod values', async () => {
|
||||
const schema = z.string().transform((x) => x.toUpperCase());
|
||||
const res = await AsyncResult.ok('foo').transform((x) =>
|
||||
schema.safeParse(x)
|
||||
);
|
||||
expect(res).toEqual(Result.ok('FOO'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('Catch', () => {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { SafeParseReturnType, ZodError } from 'zod';
|
||||
import { logger } from '../logger';
|
||||
|
||||
interface Ok<T> {
|
||||
|
@ -20,6 +21,45 @@ interface Err<E> {
|
|||
|
||||
type Res<T, E> = Ok<T> | Err<E>;
|
||||
|
||||
function isZodResult<Input, Output>(
|
||||
input: unknown
|
||||
): input is SafeParseReturnType<Input, NonNullable<Output>> {
|
||||
if (
|
||||
typeof input !== 'object' ||
|
||||
input === null ||
|
||||
Object.keys(input).length !== 2 ||
|
||||
!('success' in input) ||
|
||||
typeof input.success !== 'boolean'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (input.success) {
|
||||
return (
|
||||
'data' in input &&
|
||||
typeof input.data !== 'undefined' &&
|
||||
input.data !== null
|
||||
);
|
||||
} else {
|
||||
return 'error' in input && input.error instanceof ZodError;
|
||||
}
|
||||
}
|
||||
|
||||
function fromZodResult<Input, Output>(
|
||||
input: SafeParseReturnType<Input, NonNullable<Output>>
|
||||
): Result<Output, ZodError<Input>> {
|
||||
return input.success ? Result.ok(input.data) : Result.err(input.error);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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`.
|
||||
*/
|
||||
type RawValue<T> = Exclude<
|
||||
NonNullable<T>,
|
||||
SafeParseReturnType<unknown, NonNullable<T>> | Promise<unknown>
|
||||
>;
|
||||
|
||||
/**
|
||||
* Class for representing a result that can fail.
|
||||
*
|
||||
|
@ -72,19 +112,25 @@ export class Result<T, E = Error> {
|
|||
*
|
||||
* ```
|
||||
*/
|
||||
static wrap<T, E = Error>(callback: () => NonNullable<T>): Result<T, E>;
|
||||
static wrap<T, Input = any>(
|
||||
zodResult: SafeParseReturnType<Input, NonNullable<T>>
|
||||
): Result<T, ZodError<Input>>;
|
||||
static wrap<T, E = Error>(callback: () => RawValue<T>): Result<T, E>;
|
||||
static wrap<T, E = Error, EE = never>(
|
||||
promise: Promise<Result<T, EE>>
|
||||
): AsyncResult<T, E | EE>;
|
||||
static wrap<T, E = Error>(
|
||||
promise: Promise<NonNullable<T>>
|
||||
): AsyncResult<T, E>;
|
||||
static wrap<T, E = Error, EE = never>(
|
||||
static wrap<T, E = Error>(promise: Promise<RawValue<T>>): AsyncResult<T, E>;
|
||||
static wrap<T, E = Error, EE = never, Input = any>(
|
||||
input:
|
||||
| (() => NonNullable<T>)
|
||||
| SafeParseReturnType<Input, NonNullable<T>>
|
||||
| (() => RawValue<T>)
|
||||
| Promise<Result<T, EE>>
|
||||
| Promise<NonNullable<T>>
|
||||
): Result<T, E | EE> | AsyncResult<T, E | EE> {
|
||||
| Promise<RawValue<T>>
|
||||
): Result<T, ZodError<Input>> | Result<T, E | EE> | AsyncResult<T, E | EE> {
|
||||
if (isZodResult<Input, T>(input)) {
|
||||
return fromZodResult(input);
|
||||
}
|
||||
|
||||
if (input instanceof Promise) {
|
||||
return AsyncResult.wrap(input as never);
|
||||
}
|
||||
|
@ -244,6 +290,8 @@ export class Result<T, E = Error> {
|
|||
* Uncaught errors are logged and wrapped to `Result._uncaught()`,
|
||||
* which leads to re-throwing them in `unwrap()`.
|
||||
*
|
||||
* Zod `.safeParse()` results are converted automatically.
|
||||
*
|
||||
* ```ts
|
||||
*
|
||||
* // SYNC
|
||||
|
@ -267,23 +315,35 @@ export class Result<T, E = Error> {
|
|||
transform<U, EE>(
|
||||
fn: (value: NonNullable<T>) => AsyncResult<U, E | EE>
|
||||
): AsyncResult<U, E | EE>;
|
||||
transform<U, Input = any>(
|
||||
fn: (value: NonNullable<T>) => SafeParseReturnType<Input, NonNullable<U>>
|
||||
): Result<U, E | ZodError<Input>>;
|
||||
transform<U, Input = any>(
|
||||
fn: (
|
||||
value: NonNullable<T>
|
||||
) => Promise<SafeParseReturnType<Input, NonNullable<U>>>
|
||||
): AsyncResult<U, E | ZodError<Input>>;
|
||||
transform<U, EE>(
|
||||
fn: (value: NonNullable<T>) => Promise<Result<U, E | EE>>
|
||||
): AsyncResult<U, E | EE>;
|
||||
transform<U>(
|
||||
fn: (value: NonNullable<T>) => Promise<NonNullable<U>>
|
||||
fn: (value: NonNullable<T>) => Promise<RawValue<U>>
|
||||
): AsyncResult<U, E>;
|
||||
transform<U>(fn: (value: NonNullable<T>) => NonNullable<U>): Result<U, E>;
|
||||
transform<U, EE>(
|
||||
transform<U>(fn: (value: NonNullable<T>) => RawValue<U>): Result<U, E>;
|
||||
transform<U, EE, Input = any>(
|
||||
fn: (
|
||||
value: NonNullable<T>
|
||||
) =>
|
||||
| Result<U, E | EE>
|
||||
| AsyncResult<U, E | EE>
|
||||
| SafeParseReturnType<Input, NonNullable<U>>
|
||||
| Promise<SafeParseReturnType<Input, NonNullable<U>>>
|
||||
| Promise<Result<U, E | EE>>
|
||||
| Promise<NonNullable<U>>
|
||||
| NonNullable<U>
|
||||
): Result<U, E | EE> | AsyncResult<U, E | EE> {
|
||||
| Promise<RawValue<U>>
|
||||
| RawValue<U>
|
||||
):
|
||||
| Result<U, E | EE | ZodError<Input>>
|
||||
| AsyncResult<U, E | EE | ZodError<Input>> {
|
||||
if (!this.res.ok) {
|
||||
return Result.err(this.res.err);
|
||||
}
|
||||
|
@ -299,6 +359,10 @@ export class Result<T, E = Error> {
|
|||
return result;
|
||||
}
|
||||
|
||||
if (isZodResult<Input, U>(result)) {
|
||||
return fromZodResult(result);
|
||||
}
|
||||
|
||||
if (result instanceof Promise) {
|
||||
return AsyncResult.wrap(result, (err) => {
|
||||
logger.warn({ err }, 'Result: unhandled async transform error');
|
||||
|
@ -383,8 +447,11 @@ export class AsyncResult<T, E> implements PromiseLike<Result<T, E>> {
|
|||
return new AsyncResult(Promise.resolve(Result.err(err)));
|
||||
}
|
||||
|
||||
static wrap<T, E = Error, EE = never>(
|
||||
promise: Promise<Result<T, EE>> | Promise<NonNullable<T>>,
|
||||
static wrap<T, E = Error, EE = never, Input = any>(
|
||||
promise:
|
||||
| Promise<SafeParseReturnType<Input, NonNullable<T>>>
|
||||
| Promise<Result<T, EE>>
|
||||
| Promise<RawValue<T>>,
|
||||
onErr?: (err: NonNullable<E>) => Result<T, E>
|
||||
): AsyncResult<T, E | EE> {
|
||||
return new AsyncResult(
|
||||
|
@ -393,6 +460,11 @@ export class AsyncResult<T, E> implements PromiseLike<Result<T, E>> {
|
|||
if (value instanceof Result) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (isZodResult<Input, T>(value)) {
|
||||
return fromZodResult(value);
|
||||
}
|
||||
|
||||
return Result.ok(value);
|
||||
})
|
||||
.catch((err) => {
|
||||
|
@ -469,6 +541,8 @@ export class AsyncResult<T, E> implements PromiseLike<Result<T, E>> {
|
|||
* Uncaught errors are logged and wrapped to `Result._uncaught()`,
|
||||
* which leads to re-throwing them in `unwrap()`.
|
||||
*
|
||||
* Zod `.safeParse()` results are converted automatically.
|
||||
*
|
||||
* ```ts
|
||||
*
|
||||
* const { val, err } = await Result.wrap(
|
||||
|
@ -485,25 +559,33 @@ export class AsyncResult<T, E> implements PromiseLike<Result<T, E>> {
|
|||
transform<U, EE>(
|
||||
fn: (value: NonNullable<T>) => AsyncResult<U, E | EE>
|
||||
): AsyncResult<U, E | EE>;
|
||||
transform<U, Input = any>(
|
||||
fn: (value: NonNullable<T>) => SafeParseReturnType<Input, NonNullable<U>>
|
||||
): AsyncResult<U, E | ZodError<Input>>;
|
||||
transform<U, Input = any>(
|
||||
fn: (
|
||||
value: NonNullable<T>
|
||||
) => Promise<SafeParseReturnType<Input, NonNullable<U>>>
|
||||
): AsyncResult<U, E | ZodError<Input>>;
|
||||
transform<U, EE>(
|
||||
fn: (value: NonNullable<T>) => Promise<Result<U, E | EE>>
|
||||
): AsyncResult<U, E | EE>;
|
||||
transform<U>(
|
||||
fn: (value: NonNullable<T>) => Promise<NonNullable<U>>
|
||||
fn: (value: NonNullable<T>) => Promise<RawValue<U>>
|
||||
): AsyncResult<U, E>;
|
||||
transform<U>(
|
||||
fn: (value: NonNullable<T>) => NonNullable<U>
|
||||
): AsyncResult<U, E>;
|
||||
transform<U, EE>(
|
||||
transform<U>(fn: (value: NonNullable<T>) => RawValue<U>): AsyncResult<U, E>;
|
||||
transform<U, EE, Input = any>(
|
||||
fn: (
|
||||
value: NonNullable<T>
|
||||
) =>
|
||||
| Result<U, E | EE>
|
||||
| AsyncResult<U, E | EE>
|
||||
| SafeParseReturnType<Input, NonNullable<U>>
|
||||
| Promise<SafeParseReturnType<Input, NonNullable<U>>>
|
||||
| Promise<Result<U, E | EE>>
|
||||
| Promise<NonNullable<U>>
|
||||
| NonNullable<U>
|
||||
): AsyncResult<U, E | EE> {
|
||||
| Promise<RawValue<U>>
|
||||
| RawValue<U>
|
||||
): AsyncResult<U, E | EE | ZodError<Input>> {
|
||||
return new AsyncResult(
|
||||
this.asyncResult
|
||||
.then((oldResult) => {
|
||||
|
@ -523,6 +605,10 @@ export class AsyncResult<T, E> implements PromiseLike<Result<T, E>> {
|
|||
return result;
|
||||
}
|
||||
|
||||
if (isZodResult<Input, U>(result)) {
|
||||
return fromZodResult(result);
|
||||
}
|
||||
|
||||
if (result instanceof Promise) {
|
||||
return AsyncResult.wrap(result, (err) => {
|
||||
logger.warn(
|
||||
|
|
Loading…
Reference in a new issue