Skip to content

Commit

Permalink
refactor: Don't restrict error type for Result<T, E> (#23265)
Browse files Browse the repository at this point in the history
  • Loading branch information
zharinov committed Jul 10, 2023
1 parent 3fced20 commit 8f39f50
Show file tree
Hide file tree
Showing 4 changed files with 48 additions and 88 deletions.
7 changes: 0 additions & 7 deletions lib/modules/datasource/index.ts
Expand Up @@ -8,7 +8,6 @@ import * as packageCache from '../../util/cache/package';
import { clone } from '../../util/clone';
import { filterMap } from '../../util/filter-map';
import { regEx } from '../../util/regex';
import { Result } from '../../util/result';
import { trimTrailingSlash } from '../../util/url';
import { defaultVersioning } from '../versioning';
import * as allVersioning from '../versioning';
Expand Down Expand Up @@ -506,12 +505,6 @@ export async function getPkgReleases(
return res;
}

export function getPkgReleasesSafe(
config: GetPkgReleasesConfig
): Promise<Result<ReleaseResult | null>> {
return Result.wrap(getPkgReleases(config));
}

export function supportsDigests(datasource: string | undefined): boolean {
const ds = !!datasource && getDatasourceFor(datasource);
return !!ds && 'getDigest' in ds;
Expand Down
55 changes: 19 additions & 36 deletions lib/util/result.spec.ts
Expand Up @@ -3,50 +3,45 @@ import { Result } from './result';
describe('util/result', () => {
describe('ok', () => {
it('constructs successful result from value', () => {
expect(Result.ok(42).value()).toBe(42);
expect(Result.ok(42).value).toBe(42);
});
});

describe('err', () => {
it('constructs error result', () => {
it('constructs `true` error by default', () => {
const res = Result.err();
expect(res.error()).toEqual(new Error());
});

it('constructs error result from string', () => {
const res = Result.err('oops');
expect(res.error()?.message).toBe('oops');
expect(res.error).toBeTrue();
});

it('constructs error result from Error instance', () => {
const err = new Error('oops');
const res = Result.err(err);
expect(res.error()).toBe(err);
expect(res.error).toBe(err);
});
});

describe('wrap', () => {
it('wraps function returning successful result', () => {
const res = Result.wrap(() => 42);
expect(res.value()).toBe(42);
expect(res.value).toBe(42);
});

it('wraps function that throws an error', () => {
const res = Result.wrap(() => {
throw new Error('oops');
});
expect(res.error()?.message).toBe('oops');
expect(res.error?.message).toBe('oops');
});

it('wraps promise resolving to value', async () => {
const res = await Result.wrap(Promise.resolve(42));
expect(res.value()).toBe(42);
expect(res.value).toBe(42);
});

it('wraps promise rejecting with error', async () => {
const err = new Error('oops');
const res = await Result.wrap(Promise.reject(err));
expect(res.error()?.message).toBe('oops');
expect(res.error?.message).toBe('oops');
});
});

Expand All @@ -61,59 +56,47 @@ describe('util/result', () => {
it('no-op for error result', () => {
const err = new Error('bar');
const res = Result.err(err).transform(fn);
expect(res.value()).toBeUndefined();
expect(res.error()).toBe(err);
expect(res.value).toBeUndefined();
expect(res.error).toBe(err);
});
});

describe('unwrap', () => {
it('unwraps successful result', () => {
describe('catch', () => {
it('returns original value for successful result', () => {
const res = Result.ok(42);
expect(res.unwrap()).toEqual({ ok: true, value: 42 });
});

it('unwraps error result with fallback value', () => {
const err = new Error('oops');
const res = Result.err(err);
expect(res.unwrap(42)).toEqual({ ok: true, value: 42 });
expect(res.catch(0)).toBe(42);
});

it('unwraps error result', () => {
it('returns fallback value for error result', () => {
const err = new Error('oops');
const res = Result.err(err);
expect(res.unwrap()).toEqual({ ok: false, error: err });
expect(res.catch(42)).toBe(42);
});
});

describe('value', () => {
it('returns successful value', () => {
const res = Result.ok(42);
expect(res.value()).toBe(42);
});

it('returns fallback value for error result', () => {
const err = new Error('oops');
const res = Result.err(err);
expect(res.value(42)).toBe(42);
expect(res.value).toBe(42);
});

it('returns undefined value for error result', () => {
const err = new Error('oops');
const res = Result.err(err);
expect(res.value()).toBeUndefined();
expect(res.value).toBeUndefined();
});
});

describe('error', () => {
it('returns undefined error for successful result', () => {
const res = Result.ok(42);
expect(res.error()).toBeUndefined();
expect(res.error).toBeUndefined();
});

it('returns error for non-successful result', () => {
const err = new Error('oops');
const res = Result.err(err);
expect(res.error()).toEqual(err);
expect(res.error).toEqual(err);
});
});
});
67 changes: 25 additions & 42 deletions lib/util/result.ts
@@ -1,33 +1,29 @@
interface Ok<T> {
ok: true;
value: T;
readonly success: true;
readonly value: T;
}

interface Err {
ok: false;
error: Error;
interface Err<E> {
readonly success: false;
readonly error: E;
}

type Res<T> = Ok<T> | Err;
type Res<T, E> = Ok<T> | Err<E>;

export class Result<T> {
static ok<T>(value: T): Result<T> {
return new Result({ ok: true, value });
export class Result<T, E = Error> {
static ok<T>(value: T): Result<T, never> {
return new Result({ success: true, value });
}

static err(): Result<never>;
static err(error: Error): Result<never>;
static err(message: string): Result<never>;
static err(error?: Error | string): Result<never> {
if (typeof error === 'undefined') {
return new Result({ ok: false, error: new Error() });
static err(): Result<never, true>;
static err<E>(e: E): Result<never, E>;
static err<E>(e?: E): Result<never, E> | Result<never, true> {
if (typeof e === 'undefined' && arguments.length === 0) {
return new Result({ success: false, error: true });
}

if (typeof error === 'string') {
return new Result({ ok: false, error: new Error(error) });
}

return new Result({ ok: false, error });
const error = e as E;
return new Result({ success: false, error });
}

private static wrapCallback<T>(callback: () => T): Result<T> {
Expand Down Expand Up @@ -55,36 +51,23 @@ export class Result<T> {
: Result.wrapCallback(input);
}

private constructor(private res: Res<T>) {}
private constructor(public readonly res: Res<T, E>) {}

transform<U>(fn: (value: T) => U): Result<U> {
return this.res.ok
transform<U>(fn: (value: T) => U): Result<U, E> {
return this.res.success
? Result.ok(fn(this.res.value))
: Result.err(this.res.error);
}

unwrap(): Res<T>;
unwrap<U>(fallback: U): Res<T | U>;
unwrap<U>(fallback?: U): Res<T | U> {
if (this.res.ok) {
return this.res;
}

if (arguments.length) {
return { ok: true, value: fallback as U };
}

return this.res;
catch<U>(fallback: U): T | U {
return this.res.success ? this.res.value : fallback;
}

value(): T | undefined;
value<U>(fallback: U): T | U;
value<U>(fallback?: U): T | U | undefined {
const res = arguments.length ? this.unwrap(fallback as U) : this.unwrap();
return res.ok ? res.value : undefined;
get value(): T | undefined {
return this.res.success ? this.res.value : undefined;
}

error(): Error | undefined {
return this.res.ok ? undefined : this.res.error;
get error(): E | undefined {
return this.res.success ? undefined : this.res.error;
}
}
7 changes: 4 additions & 3 deletions lib/workers/repository/process/lookup/index.ts
Expand Up @@ -9,7 +9,7 @@ import {
getDatasourceList,
getDefaultVersioning,
getDigest,
getPkgReleasesSafe,
getPkgReleases,
isGetPkgReleasesConfig,
supportsDigests,
} from '../../../../modules/datasource';
Expand All @@ -19,6 +19,7 @@ import { ExternalHostError } from '../../../../types/errors/external-host-error'
import { clone } from '../../../../util/clone';
import { applyPackageRules } from '../../../../util/package-rules';
import { regEx } from '../../../../util/regex';
import { Result } from '../../../../util/result';
import { getBucket } from './bucket';
import { getCurrentVersion } from './current';
import { filterVersions } from './filter';
Expand Down Expand Up @@ -82,8 +83,8 @@ export async function lookupUpdates(
res.skipReason = 'is-pinned';
return res;
}
const lookupResult = (await getPkgReleasesSafe(config)).unwrap();
if (!lookupResult.ok) {
const { res: lookupResult } = await Result.wrap(getPkgReleases(config));
if (!lookupResult.success) {
throw lookupResult.error;
}
dependency = clone(lookupResult.value);
Expand Down

0 comments on commit 8f39f50

Please sign in to comment.