mirror of
https://github.com/renovatebot/renovate.git
synced 2025-01-13 15:36:25 +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
|
packageName: string
|
||||||
): AsyncResult<ReleaseResult, Error | ZodError> {
|
): AsyncResult<ReleaseResult, Error | ZodError> {
|
||||||
const url = joinUrlParts(registryUrl, '/info', packageName);
|
const url = joinUrlParts(registryUrl, '/info', packageName);
|
||||||
return Result.wrap(this.http.get(url)).transform(({ body }) => {
|
return Result.wrap(this.http.get(url)).transform(({ body }) =>
|
||||||
const res = GemInfo.safeParse(body);
|
GemInfo.safeParse(body)
|
||||||
return res.success ? Result.ok(res.data) : Result.err(res.error);
|
);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getReleasesViaDeprecatedAPI(
|
private getReleasesViaDeprecatedAPI(
|
||||||
|
@ -117,10 +116,7 @@ export class RubyGemsDatasource extends Datasource {
|
||||||
const bufPromise = this.http.getBuffer(url);
|
const bufPromise = this.http.getBuffer(url);
|
||||||
return Result.wrap(bufPromise).transform(({ body }) => {
|
return Result.wrap(bufPromise).transform(({ body }) => {
|
||||||
const data = Marshal.parse(body);
|
const data = Marshal.parse(body);
|
||||||
const releases = MarshalledVersionInfo.safeParse(data);
|
return MarshalledVersionInfo.safeParse(data);
|
||||||
return releases.success
|
|
||||||
? Result.ok(releases.data)
|
|
||||||
: Result.err(releases.error);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -372,7 +372,7 @@ export class Http<Opts extends HttpOptions = HttpOptions> {
|
||||||
): AsyncResult<ResT, SafeJsonError> {
|
): AsyncResult<ResT, SafeJsonError> {
|
||||||
const args = this.resolveArgs<ResT>(arg1, arg2, arg3);
|
const args = this.resolveArgs<ResT>(arg1, arg2, arg3);
|
||||||
return Result.wrap(this.requestJson<ResT>('get', args)).transform(
|
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 { logger } from '../../test/util';
|
||||||
import { AsyncResult, Result } from './result';
|
import { AsyncResult, Result } from './result';
|
||||||
|
|
||||||
|
@ -68,6 +69,18 @@ describe('util/result', () => {
|
||||||
}, 'nullable');
|
}, 'nullable');
|
||||||
expect(res).toEqual(Result.err('oops'));
|
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', () => {
|
describe('Unwrapping', () => {
|
||||||
|
@ -149,6 +162,12 @@ describe('util/result', () => {
|
||||||
'Result: unhandled transform error'
|
'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', () => {
|
describe('Catch', () => {
|
||||||
|
@ -416,6 +435,22 @@ describe('util/result', () => {
|
||||||
|
|
||||||
expect(res).toEqual(Result.ok('F-O-O'));
|
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', () => {
|
describe('Catch', () => {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { SafeParseReturnType, ZodError } from 'zod';
|
||||||
import { logger } from '../logger';
|
import { logger } from '../logger';
|
||||||
|
|
||||||
interface Ok<T> {
|
interface Ok<T> {
|
||||||
|
@ -20,6 +21,45 @@ interface Err<E> {
|
||||||
|
|
||||||
type Res<T, E> = Ok<T> | 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.
|
* 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>(
|
static wrap<T, E = Error, EE = never>(
|
||||||
promise: Promise<Result<T, EE>>
|
promise: Promise<Result<T, EE>>
|
||||||
): AsyncResult<T, E | EE>;
|
): AsyncResult<T, E | EE>;
|
||||||
static wrap<T, E = Error>(
|
static wrap<T, E = Error>(promise: Promise<RawValue<T>>): AsyncResult<T, E>;
|
||||||
promise: Promise<NonNullable<T>>
|
static wrap<T, E = Error, EE = never, Input = any>(
|
||||||
): AsyncResult<T, E>;
|
|
||||||
static wrap<T, E = Error, EE = never>(
|
|
||||||
input:
|
input:
|
||||||
| (() => NonNullable<T>)
|
| SafeParseReturnType<Input, NonNullable<T>>
|
||||||
|
| (() => RawValue<T>)
|
||||||
| Promise<Result<T, EE>>
|
| Promise<Result<T, EE>>
|
||||||
| Promise<NonNullable<T>>
|
| Promise<RawValue<T>>
|
||||||
): Result<T, E | EE> | AsyncResult<T, E | EE> {
|
): Result<T, ZodError<Input>> | Result<T, E | EE> | AsyncResult<T, E | EE> {
|
||||||
|
if (isZodResult<Input, T>(input)) {
|
||||||
|
return fromZodResult(input);
|
||||||
|
}
|
||||||
|
|
||||||
if (input instanceof Promise) {
|
if (input instanceof Promise) {
|
||||||
return AsyncResult.wrap(input as never);
|
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()`,
|
* Uncaught errors are logged and wrapped to `Result._uncaught()`,
|
||||||
* which leads to re-throwing them in `unwrap()`.
|
* which leads to re-throwing them in `unwrap()`.
|
||||||
*
|
*
|
||||||
|
* Zod `.safeParse()` results are converted automatically.
|
||||||
|
*
|
||||||
* ```ts
|
* ```ts
|
||||||
*
|
*
|
||||||
* // SYNC
|
* // SYNC
|
||||||
|
@ -267,23 +315,35 @@ export class Result<T, E = Error> {
|
||||||
transform<U, EE>(
|
transform<U, EE>(
|
||||||
fn: (value: NonNullable<T>) => AsyncResult<U, E | EE>
|
fn: (value: NonNullable<T>) => AsyncResult<U, E | EE>
|
||||||
): 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>(
|
transform<U, EE>(
|
||||||
fn: (value: NonNullable<T>) => Promise<Result<U, E | EE>>
|
fn: (value: NonNullable<T>) => Promise<Result<U, E | EE>>
|
||||||
): AsyncResult<U, E | EE>;
|
): AsyncResult<U, E | EE>;
|
||||||
transform<U>(
|
transform<U>(
|
||||||
fn: (value: NonNullable<T>) => Promise<NonNullable<U>>
|
fn: (value: NonNullable<T>) => Promise<RawValue<U>>
|
||||||
): AsyncResult<U, E>;
|
): AsyncResult<U, E>;
|
||||||
transform<U>(fn: (value: NonNullable<T>) => NonNullable<U>): Result<U, E>;
|
transform<U>(fn: (value: NonNullable<T>) => RawValue<U>): Result<U, E>;
|
||||||
transform<U, EE>(
|
transform<U, EE, Input = any>(
|
||||||
fn: (
|
fn: (
|
||||||
value: NonNullable<T>
|
value: NonNullable<T>
|
||||||
) =>
|
) =>
|
||||||
| Result<U, E | EE>
|
| Result<U, E | EE>
|
||||||
| AsyncResult<U, E | EE>
|
| AsyncResult<U, E | EE>
|
||||||
|
| SafeParseReturnType<Input, NonNullable<U>>
|
||||||
|
| Promise<SafeParseReturnType<Input, NonNullable<U>>>
|
||||||
| Promise<Result<U, E | EE>>
|
| Promise<Result<U, E | EE>>
|
||||||
| Promise<NonNullable<U>>
|
| Promise<RawValue<U>>
|
||||||
| NonNullable<U>
|
| RawValue<U>
|
||||||
): Result<U, E | EE> | AsyncResult<U, E | EE> {
|
):
|
||||||
|
| Result<U, E | EE | ZodError<Input>>
|
||||||
|
| AsyncResult<U, E | EE | ZodError<Input>> {
|
||||||
if (!this.res.ok) {
|
if (!this.res.ok) {
|
||||||
return Result.err(this.res.err);
|
return Result.err(this.res.err);
|
||||||
}
|
}
|
||||||
|
@ -299,6 +359,10 @@ export class Result<T, E = Error> {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isZodResult<Input, U>(result)) {
|
||||||
|
return fromZodResult(result);
|
||||||
|
}
|
||||||
|
|
||||||
if (result instanceof Promise) {
|
if (result instanceof Promise) {
|
||||||
return AsyncResult.wrap(result, (err) => {
|
return AsyncResult.wrap(result, (err) => {
|
||||||
logger.warn({ err }, 'Result: unhandled async transform error');
|
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)));
|
return new AsyncResult(Promise.resolve(Result.err(err)));
|
||||||
}
|
}
|
||||||
|
|
||||||
static wrap<T, E = Error, EE = never>(
|
static wrap<T, E = Error, EE = never, Input = any>(
|
||||||
promise: Promise<Result<T, EE>> | Promise<NonNullable<T>>,
|
promise:
|
||||||
|
| Promise<SafeParseReturnType<Input, NonNullable<T>>>
|
||||||
|
| Promise<Result<T, EE>>
|
||||||
|
| Promise<RawValue<T>>,
|
||||||
onErr?: (err: NonNullable<E>) => Result<T, E>
|
onErr?: (err: NonNullable<E>) => Result<T, E>
|
||||||
): AsyncResult<T, E | EE> {
|
): AsyncResult<T, E | EE> {
|
||||||
return new AsyncResult(
|
return new AsyncResult(
|
||||||
|
@ -393,6 +460,11 @@ export class AsyncResult<T, E> implements PromiseLike<Result<T, E>> {
|
||||||
if (value instanceof Result) {
|
if (value instanceof Result) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isZodResult<Input, T>(value)) {
|
||||||
|
return fromZodResult(value);
|
||||||
|
}
|
||||||
|
|
||||||
return Result.ok(value);
|
return Result.ok(value);
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.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()`,
|
* Uncaught errors are logged and wrapped to `Result._uncaught()`,
|
||||||
* which leads to re-throwing them in `unwrap()`.
|
* which leads to re-throwing them in `unwrap()`.
|
||||||
*
|
*
|
||||||
|
* Zod `.safeParse()` results are converted automatically.
|
||||||
|
*
|
||||||
* ```ts
|
* ```ts
|
||||||
*
|
*
|
||||||
* const { val, err } = await Result.wrap(
|
* const { val, err } = await Result.wrap(
|
||||||
|
@ -485,25 +559,33 @@ export class AsyncResult<T, E> implements PromiseLike<Result<T, E>> {
|
||||||
transform<U, EE>(
|
transform<U, EE>(
|
||||||
fn: (value: NonNullable<T>) => AsyncResult<U, E | EE>
|
fn: (value: NonNullable<T>) => AsyncResult<U, E | EE>
|
||||||
): 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>(
|
transform<U, EE>(
|
||||||
fn: (value: NonNullable<T>) => Promise<Result<U, E | EE>>
|
fn: (value: NonNullable<T>) => Promise<Result<U, E | EE>>
|
||||||
): AsyncResult<U, E | EE>;
|
): AsyncResult<U, E | EE>;
|
||||||
transform<U>(
|
transform<U>(
|
||||||
fn: (value: NonNullable<T>) => Promise<NonNullable<U>>
|
fn: (value: NonNullable<T>) => Promise<RawValue<U>>
|
||||||
): AsyncResult<U, E>;
|
): AsyncResult<U, E>;
|
||||||
transform<U>(
|
transform<U>(fn: (value: NonNullable<T>) => RawValue<U>): AsyncResult<U, E>;
|
||||||
fn: (value: NonNullable<T>) => NonNullable<U>
|
transform<U, EE, Input = any>(
|
||||||
): AsyncResult<U, E>;
|
|
||||||
transform<U, EE>(
|
|
||||||
fn: (
|
fn: (
|
||||||
value: NonNullable<T>
|
value: NonNullable<T>
|
||||||
) =>
|
) =>
|
||||||
| Result<U, E | EE>
|
| Result<U, E | EE>
|
||||||
| AsyncResult<U, E | EE>
|
| AsyncResult<U, E | EE>
|
||||||
|
| SafeParseReturnType<Input, NonNullable<U>>
|
||||||
|
| Promise<SafeParseReturnType<Input, NonNullable<U>>>
|
||||||
| Promise<Result<U, E | EE>>
|
| Promise<Result<U, E | EE>>
|
||||||
| Promise<NonNullable<U>>
|
| Promise<RawValue<U>>
|
||||||
| NonNullable<U>
|
| RawValue<U>
|
||||||
): AsyncResult<U, E | EE> {
|
): AsyncResult<U, E | EE | ZodError<Input>> {
|
||||||
return new AsyncResult(
|
return new AsyncResult(
|
||||||
this.asyncResult
|
this.asyncResult
|
||||||
.then((oldResult) => {
|
.then((oldResult) => {
|
||||||
|
@ -523,6 +605,10 @@ export class AsyncResult<T, E> implements PromiseLike<Result<T, E>> {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isZodResult<Input, U>(result)) {
|
||||||
|
return fromZodResult(result);
|
||||||
|
}
|
||||||
|
|
||||||
if (result instanceof Promise) {
|
if (result instanceof Promise) {
|
||||||
return AsyncResult.wrap(result, (err) => {
|
return AsyncResult.wrap(result, (err) => {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
|
|
Loading…
Reference in a new issue