diff --git a/lib/modules/datasource/index.ts b/lib/modules/datasource/index.ts index 54f2aaf265f43761f77f86553821716336d1b49c..5edb609a5a9686afc54a0b651cb8532fa25968c8 100644 --- a/lib/modules/datasource/index.ts +++ b/lib/modules/datasource/index.ts @@ -345,7 +345,7 @@ export function getRawPkgReleases( return AsyncResult.err('no-package-name'); } - return Result.wrapNullable(getRawReleases(config), 'no-result') + return Result.wrapNullable(getRawReleases(config), 'no-result' as const) .catch((e) => { if (e instanceof ExternalHostError) { e.hostType = config.datasource; diff --git a/lib/modules/datasource/rubygems/index.ts b/lib/modules/datasource/rubygems/index.ts index 246658368a6199360306de3ead088e87969dde4e..a2a69ec0c62e3d161de2732fa07c34698d87e9b8 100644 --- a/lib/modules/datasource/rubygems/index.ts +++ b/lib/modules/datasource/rubygems/index.ts @@ -13,10 +13,10 @@ import { MetadataCache } from './metadata-cache'; import { GemInfo, MarshalledVersionInfo } from './schema'; import { VersionsEndpointCache } from './versions-endpoint-cache'; -function unlessServerSide<T, E>( - err: E, - cb: () => AsyncResult<T, E> -): AsyncResult<T, E> { +function unlessServerSide< + T extends NonNullable<unknown>, + E extends NonNullable<unknown> +>(err: E, cb: () => AsyncResult<T, E>): AsyncResult<T, E> { if (err instanceof HttpError && err.response?.statusCode) { const code = err.response.statusCode; if (code >= 500 && code <= 599) { diff --git a/lib/modules/datasource/rubygems/metadata-cache.ts b/lib/modules/datasource/rubygems/metadata-cache.ts index 9bae7437cf7fad75a9b8298aebf7aeb9a03aed62..41860faf0fb3a6c83574ae4b989df693f7132c31 100644 --- a/lib/modules/datasource/rubygems/metadata-cache.ts +++ b/lib/modules/datasource/rubygems/metadata-cache.ts @@ -23,14 +23,14 @@ export class MetadataCache { const cacheKey = `metadata-cache:${registryUrl}:${packageName}`; const hash = toSha256(versions.join('')); - const loadCache = (): AsyncResult<ReleaseResult, unknown> => + const loadCache = (): AsyncResult<ReleaseResult, NonNullable<unknown>> => Result.wrapNullable( packageCache.get<CacheRecord>(cacheNs, cacheKey), - 'cache-not-found' + 'cache-not-found' as const ).transform((cache) => { return hash === cache.hash ? Result.ok(cache.data) - : Result.err('cache-outdated'); + : Result.err('cache-outdated' as const); }); const saveCache = async (data: ReleaseResult): Promise<ReleaseResult> => { diff --git a/lib/util/result.spec.ts b/lib/util/result.spec.ts index 319ff8217699a16b097fa5e4a69c30cd786dff65..fe26d945238b405268dec09f6b7bc8cca30c48f9 100644 --- a/lib/util/result.spec.ts +++ b/lib/util/result.spec.ts @@ -40,7 +40,10 @@ describe('util/result', () => { }); it('wraps nullable callback', () => { - const res = Result.wrapNullable(() => 42, 'oops'); + const res: Result<number, 'oops'> = Result.wrapNullable( + (): number | null => 42, + 'oops' + ); expect(res).toEqual(Result.ok(42)); }); @@ -225,7 +228,10 @@ describe('util/result', () => { }); it('wraps nullable promise', async () => { - const res = Result.wrapNullable(Promise.resolve(42), 'oops'); + const res: AsyncResult<number, 'oops'> = Result.wrapNullable( + Promise.resolve<number | null>(42), + 'oops' + ); await expect(res).resolves.toEqual(Result.ok(42)); }); diff --git a/lib/util/result.ts b/lib/util/result.ts index 526cf6b8898984649fa99d14883ba4690676c1e2..b41d2715d86dcafd428f9ef60bd7a1827826adf6 100644 --- a/lib/util/result.ts +++ b/lib/util/result.ts @@ -1,15 +1,18 @@ import { SafeParseReturnType, ZodError } from 'zod'; import { logger } from '../logger'; -interface Ok<T> { +type Val = NonNullable<unknown>; +type Nullable<T extends Val> = T | null | undefined; + +interface Ok<T extends Val> { readonly ok: true; - readonly val: NonNullable<T>; + readonly val: T; readonly err?: never; } -interface Err<E> { +interface Err<E extends Val> { readonly ok: false; - readonly err: NonNullable<E>; + readonly err: E; readonly val?: never; /** @@ -19,11 +22,11 @@ interface Err<E> { readonly _uncaught?: true; } -type Res<T, E> = Ok<T> | Err<E>; +type Res<T extends Val, E extends Val> = Ok<T> | Err<E>; -function isZodResult<Input, Output>( +function isZodResult<Input, Output extends Val>( input: unknown -): input is SafeParseReturnType<Input, NonNullable<Output>> { +): input is SafeParseReturnType<Input, Output> { if ( typeof input !== 'object' || input === null || @@ -45,9 +48,9 @@ function isZodResult<Input, Output>( } } -function fromZodResult<Input, Output>( - input: SafeParseReturnType<Input, NonNullable<Output>> -): Result<Output, ZodError<Input>> { +function fromZodResult<ZodInput, ZodOutput extends Val>( + input: SafeParseReturnType<ZodInput, ZodOutput> +): Result<ZodOutput, ZodError<ZodInput>> { return input.success ? Result.ok(input.data) : Result.err(input.error); } @@ -55,9 +58,9 @@ function fromZodResult<Input, Output>( * 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> +type RawValue<T extends Val> = Exclude< + T, + SafeParseReturnType<unknown, T> | Promise<unknown> >; /** @@ -68,18 +71,18 @@ type RawValue<T> = Exclude< * - `.transform()` are pipes which can be chained * - `.unwrap()` is the point of consumption */ -export class Result<T, E = Error> { +export class Result<T extends Val, E extends Val = Error> { private constructor(private readonly res: Res<T, E>) {} - static ok<T>(val: NonNullable<T>): Result<T, never> { + static ok<T extends Val>(val: T): Result<T, never> { return new Result({ ok: true, val }); } - static err<E>(err: NonNullable<E>): Result<never, E> { + static err<E extends Val>(err: E): Result<never, E> { return new Result({ ok: false, err }); } - static _uncaught<E>(err: NonNullable<E>): Result<never, E> { + static _uncaught<E extends Val>(err: E): Result<never, E> { return new Result({ ok: false, err, _uncaught: true }); } @@ -112,17 +115,26 @@ export class Result<T, E = Error> { * * ``` */ - static wrap<T, Input = any>( - zodResult: SafeParseReturnType<Input, NonNullable<T>> + static wrap<T extends Val, Input = any>( + zodResult: SafeParseReturnType<Input, 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 extends Val, E extends Val = Error>( + callback: () => RawValue<T> + ): Result<T, E>; + static wrap<T extends Val, E extends Val = Error, EE extends Val = never>( promise: Promise<Result<T, EE>> ): AsyncResult<T, E | EE>; - static wrap<T, E = Error>(promise: Promise<RawValue<T>>): AsyncResult<T, E>; - static wrap<T, E = Error, EE = never, Input = any>( + static wrap<T extends Val, E extends Val = Error>( + promise: Promise<RawValue<T>> + ): AsyncResult<T, E>; + static wrap< + T extends Val, + E extends Val = Error, + EE extends Val = never, + Input = any + >( input: - | SafeParseReturnType<Input, NonNullable<T>> + | SafeParseReturnType<Input, T> | (() => RawValue<T>) | Promise<Result<T, EE>> | Promise<RawValue<T>> @@ -151,7 +163,7 @@ export class Result<T, E = Error> { * hence never re-thrown. * * Since functions and promises returning nullable can't be wrapped with `Result.wrap()` - * because `val` is constrained by being `NonNullable<T>`, `null` and `undefined` + * because `val` is constrained by being `NonNullable`, `null` and `undefined` * must be converted to some sort of `err` value. * * This method does exactly this, i.g. it is the feature-rich shorthand for: @@ -187,47 +199,70 @@ export class Result<T, E = Error> { * * ``` */ - static wrapNullable<T, E = Error, NullableError = Error>( - callback: () => T, - nullableError: NonNullable<NullableError> - ): Result<T, E | NullableError>; - static wrapNullable<T, E = Error, NullError = Error, UndefinedError = Error>( - callback: () => T, - nullError: NonNullable<NullError>, - undefinedError: NonNullable<UndefinedError> - ): Result<T, E | NullError | UndefinedError>; - static wrapNullable<T, E = Error, NullableError = Error>( - promise: Promise<T>, - nullableError: NonNullable<NullableError> - ): AsyncResult<T, E | NullableError>; - static wrapNullable<T, E = Error, NullError = Error, UndefinedError = Error>( - promise: Promise<T>, - nullError: NonNullable<NullError>, - undefinedError: NonNullable<UndefinedError> - ): AsyncResult<T, E | NullError | UndefinedError>; - static wrapNullable<T, E = Error, NullError = Error, UndefinedError = Error>( - input: (() => T) | Promise<T>, - arg2: NonNullable<NullError>, - arg3?: NonNullable<UndefinedError> + static wrapNullable< + T extends Val, + E extends Val = Error, + ErrForNullable extends Val = Error + >( + callback: () => Nullable<T>, + errForNullable: ErrForNullable + ): Result<T, E | ErrForNullable>; + static wrapNullable< + T extends Val, + E extends Val = Error, + ErrForNull extends Val = Error, + ErrForUndefined extends Val = Error + >( + callback: () => Nullable<T>, + errForNull: ErrForNull, + errForUndefined: ErrForUndefined + ): Result<T, E | ErrForNull | ErrForUndefined>; + static wrapNullable< + T extends Val, + E extends Val = Error, + ErrForNullable extends Val = Error + >( + promise: Promise<Nullable<T>>, + errForNullable: ErrForNullable + ): AsyncResult<T, E | ErrForNullable>; + static wrapNullable< + T extends Val, + E extends Val = Error, + ErrForNull extends Val = Error, + ErrForUndefined extends Val = Error + >( + promise: Promise<Nullable<T>>, + errForNull: ErrForNull, + errForUndefined: ErrForUndefined + ): AsyncResult<T, E | ErrForNull | ErrForUndefined>; + static wrapNullable< + T extends Val, + E extends Val = Error, + ErrForNull extends Val = Error, + ErrForUndefined extends Val = Error + >( + input: (() => Nullable<T>) | Promise<Nullable<T>>, + arg2: ErrForNull, + arg3?: ErrForUndefined ): - | Result<T, E | NullError | UndefinedError> - | AsyncResult<T, E | NullError | UndefinedError> { - const nullError = arg2; - const undefinedError = arg3 ?? arg2; + | Result<T, E | ErrForNull | ErrForUndefined> + | AsyncResult<T, E | ErrForNull | ErrForUndefined> { + const errForNull = arg2; + const errForUndefined = arg3 ?? arg2; if (input instanceof Promise) { - return AsyncResult.wrapNullable(input, nullError, undefinedError); + return AsyncResult.wrapNullable(input, errForNull, errForUndefined); } try { const result = input(); if (result === null) { - return Result.err(nullError); + return Result.err(errForNull); } if (result === undefined) { - return Result.err(undefinedError); + return Result.err(errForUndefined); } return Result.ok(result); @@ -255,8 +290,8 @@ export class Result<T, E = Error> { * ``` */ unwrap(): Res<T, E>; - unwrap(fallback: NonNullable<T>): NonNullable<T>; - unwrap(fallback?: NonNullable<T>): Res<T, E> | NonNullable<T> { + unwrap(fallback: T): T; + unwrap(fallback?: T): Res<T, E> | T { if (this.res.ok) { return fallback === undefined ? this.res : this.res.val; } @@ -275,7 +310,7 @@ export class Result<T, E = Error> { /** * Returns the ok-value or throw the error. */ - unwrapOrThrow(): NonNullable<T> { + unwrapOrThrow(): T { if (this.res.ok) { return this.res.val; } @@ -309,30 +344,28 @@ export class Result<T, E = Error> { * * ``` */ - transform<U, EE>( - fn: (value: NonNullable<T>) => Result<U, E | EE> + transform<U extends Val, EE extends Val>( + fn: (value: T) => Result<U, E | EE> ): Result<U, E | EE>; - transform<U, EE>( - fn: (value: NonNullable<T>) => AsyncResult<U, E | EE> + transform<U extends Val, EE extends Val>( + fn: (value: T) => AsyncResult<U, E | EE> ): AsyncResult<U, E | EE>; - transform<U, Input = any>( - fn: (value: NonNullable<T>) => SafeParseReturnType<Input, NonNullable<U>> + transform<U extends Val, Input = any>( + fn: (value: T) => SafeParseReturnType<Input, NonNullable<U>> ): Result<U, E | ZodError<Input>>; - transform<U, Input = any>( - fn: ( - value: NonNullable<T> - ) => Promise<SafeParseReturnType<Input, NonNullable<U>>> + transform<U extends Val, Input = any>( + fn: (value: T) => Promise<SafeParseReturnType<Input, NonNullable<U>>> ): AsyncResult<U, E | ZodError<Input>>; - transform<U, EE>( - fn: (value: NonNullable<T>) => Promise<Result<U, E | EE>> + transform<U extends Val, EE extends Val>( + fn: (value: T) => Promise<Result<U, E | EE>> ): AsyncResult<U, E | EE>; - transform<U>( - fn: (value: NonNullable<T>) => Promise<RawValue<U>> + transform<U extends Val>( + fn: (value: T) => Promise<RawValue<U>> ): AsyncResult<U, E>; - transform<U>(fn: (value: NonNullable<T>) => RawValue<U>): Result<U, E>; - transform<U, EE, Input = any>( + transform<U extends Val>(fn: (value: T) => RawValue<U>): Result<U, E>; + transform<U extends Val, EE extends Val, Input = any>( fn: ( - value: NonNullable<T> + value: T ) => | Result<U, E | EE> | AsyncResult<U, E | EE> @@ -377,18 +410,18 @@ export class Result<T, E = Error> { } } - catch<U = T, EE = E>( - fn: (err: NonNullable<E>) => Result<U, E | EE> + catch<U extends Val = T, EE extends Val = E>( + fn: (err: E) => Result<U, E | EE> ): Result<T | U, E | EE>; - catch<U = T, EE = E>( - fn: (err: NonNullable<E>) => AsyncResult<U, E | EE> + catch<U extends Val = T, EE extends Val = E>( + fn: (err: E) => AsyncResult<U, E | EE> ): AsyncResult<T | U, E | EE>; - catch<U = T, EE = E>( - fn: (err: NonNullable<E>) => Promise<Result<U, E | EE>> + catch<U extends Val = T, EE extends Val = E>( + fn: (err: E) => Promise<Result<U, E | EE>> ): AsyncResult<T | U, E | EE>; - catch<U = T, EE = E>( + catch<U extends Val = T, EE extends Val = E>( fn: ( - err: NonNullable<E> + err: E ) => Result<U, E | EE> | AsyncResult<U, E | EE> | Promise<Result<U, E | EE>> ): Result<T | U, E | EE> | AsyncResult<T | U, E | EE> { if (this.res.ok) { @@ -426,7 +459,9 @@ export class Result<T, E = Error> { * * All the methods resemble `Result` methods, but work asynchronously. */ -export class AsyncResult<T, E> implements PromiseLike<Result<T, E>> { +export class AsyncResult<T extends Val, E extends Val> + implements PromiseLike<Result<T, E>> +{ private constructor(private asyncResult: Promise<Result<T, E>>) {} then<TResult1 = Result<T, E>>( @@ -438,18 +473,23 @@ export class AsyncResult<T, E> implements PromiseLike<Result<T, E>> { return this.asyncResult.then(onfulfilled); } - static ok<T>(val: NonNullable<T>): AsyncResult<T, never> { + static ok<T extends Val>(val: T): AsyncResult<T, never> { return new AsyncResult(Promise.resolve(Result.ok(val))); } - static err<E>(err: NonNullable<E>): AsyncResult<never, E> { + static err<E extends Val>(err: NonNullable<E>): AsyncResult<never, E> { // eslint-disable-next-line promise/no-promise-in-callback return new AsyncResult(Promise.resolve(Result.err(err))); } - static wrap<T, E = Error, EE = never, Input = any>( + static wrap< + T extends Val, + E extends Val = Error, + EE extends Val = never, + Input = any + >( promise: - | Promise<SafeParseReturnType<Input, NonNullable<T>>> + | Promise<SafeParseReturnType<Input, T>> | Promise<Result<T, EE>> | Promise<RawValue<T>>, onErr?: (err: NonNullable<E>) => Result<T, E> @@ -476,20 +516,25 @@ export class AsyncResult<T, E> implements PromiseLike<Result<T, E>> { ); } - static wrapNullable<T, E, NullError, UndefinedError>( - promise: Promise<T>, - nullError: NonNullable<NullError>, - undefinedError: NonNullable<UndefinedError> - ): AsyncResult<T, E | NullError | UndefinedError> { + static wrapNullable< + T extends Val, + E extends Val, + ErrForNull extends Val, + ErrForUndefined extends Val + >( + promise: Promise<Nullable<T>>, + errForNull: NonNullable<ErrForNull>, + errForUndefined: NonNullable<ErrForUndefined> + ): AsyncResult<T, E | ErrForNull | ErrForUndefined> { return new AsyncResult( promise .then((value) => { if (value === null) { - return Result.err(nullError); + return Result.err(errForNull); } if (value === undefined) { - return Result.err(undefinedError); + return Result.err(errForUndefined); } return Result.ok(value); @@ -517,19 +562,17 @@ export class AsyncResult<T, E> implements PromiseLike<Result<T, E>> { * ``` */ unwrap(): Promise<Res<T, E>>; - unwrap(fallback: NonNullable<T>): Promise<NonNullable<T>>; - unwrap( - fallback?: NonNullable<T> - ): Promise<Res<T, E>> | Promise<NonNullable<T>> { + unwrap(fallback: T): Promise<T>; + unwrap(fallback?: T): Promise<Res<T, E>> | Promise<T> { return fallback === undefined ? this.asyncResult.then<Res<T, E>>((res) => res.unwrap()) - : this.asyncResult.then<NonNullable<T>>((res) => res.unwrap(fallback)); + : this.asyncResult.then<T>((res) => res.unwrap(fallback)); } /** * Returns the ok-value or throw the error. */ - async unwrapOrThrow(): Promise<NonNullable<T>> { + async unwrapOrThrow(): Promise<T> { const result = await this.asyncResult; return result.unwrapOrThrow(); } @@ -553,30 +596,28 @@ export class AsyncResult<T, E> implements PromiseLike<Result<T, E>> { * * ``` */ - transform<U, EE>( - fn: (value: NonNullable<T>) => Result<U, E | EE> + transform<U extends Val, EE extends Val>( + fn: (value: T) => Result<U, E | EE> ): AsyncResult<U, E | EE>; - transform<U, EE>( - fn: (value: NonNullable<T>) => AsyncResult<U, E | EE> + transform<U extends Val, EE extends Val>( + fn: (value: T) => AsyncResult<U, E | EE> ): AsyncResult<U, E | EE>; - transform<U, Input = any>( - fn: (value: NonNullable<T>) => SafeParseReturnType<Input, NonNullable<U>> + transform<U extends Val, Input = any>( + fn: (value: T) => SafeParseReturnType<Input, NonNullable<U>> ): AsyncResult<U, E | ZodError<Input>>; - transform<U, Input = any>( - fn: ( - value: NonNullable<T> - ) => Promise<SafeParseReturnType<Input, NonNullable<U>>> + transform<U extends Val, Input = any>( + fn: (value: T) => Promise<SafeParseReturnType<Input, NonNullable<U>>> ): AsyncResult<U, E | ZodError<Input>>; - transform<U, EE>( - fn: (value: NonNullable<T>) => Promise<Result<U, E | EE>> + transform<U extends Val, EE extends Val>( + fn: (value: T) => Promise<Result<U, E | EE>> ): AsyncResult<U, E | EE>; - transform<U>( - fn: (value: NonNullable<T>) => Promise<RawValue<U>> + transform<U extends Val>( + fn: (value: T) => Promise<RawValue<U>> ): AsyncResult<U, E>; - transform<U>(fn: (value: NonNullable<T>) => RawValue<U>): AsyncResult<U, E>; - transform<U, EE, Input = any>( + transform<U extends Val>(fn: (value: T) => RawValue<U>): AsyncResult<U, E>; + transform<U extends Val, EE extends Val, Input = any>( fn: ( - value: NonNullable<T> + value: T ) => | Result<U, E | EE> | AsyncResult<U, E | EE> @@ -632,16 +673,16 @@ export class AsyncResult<T, E> implements PromiseLike<Result<T, E>> { ); } - catch<U = T, EE = E>( + catch<U extends Val = T, EE extends Val = E>( fn: (err: NonNullable<E>) => Result<U, E | EE> ): AsyncResult<T | U, E | EE>; - catch<U = T, EE = E>( + catch<U extends Val = T, EE extends Val = E>( fn: (err: NonNullable<E>) => AsyncResult<U, E | EE> ): AsyncResult<T | U, E | EE>; - catch<U = T, EE = E>( + catch<U extends Val = T, EE extends Val = E>( fn: (err: NonNullable<E>) => Promise<Result<U, E | EE>> ): AsyncResult<T | U, E | EE>; - catch<U = T, EE = E>( + catch<U extends Val = T, EE extends Val = E>( fn: ( err: NonNullable<E> ) => Result<U, E | EE> | AsyncResult<U, E | EE> | Promise<Result<U, E | EE>>