Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 143 additions & 0 deletions packages/result/src/lib/Result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ResultError } from './ResultError';

const ValueProperty = Symbol.for('@sapphire/result:Result.value');
const SuccessProperty = Symbol.for('@sapphire/result:Result.success');
const UnwrapSafeProperty = Symbol.for('@sapphire/result:Result.safeUnwrap');

/**
* A type used to express computations that can fail, it can be used for returning and propagating errors. This is a
Expand Down Expand Up @@ -522,6 +523,7 @@ export class Result<T, E, const Success extends boolean = boolean> {
* @seealso {@link unwrapOrElse}
* @seealso {@link unwrapErr}
* @seealso {@link unwrapRaw}
* @seealso {@link unwrapSafe}
*
* @example
* ```typescript
Expand Down Expand Up @@ -553,6 +555,7 @@ export class Result<T, E, const Success extends boolean = boolean> {
* @seealso {@link unwrapOr}
* @seealso {@link unwrapOrElse}
* @seealso {@link unwrapRaw}
* @seealso {@link unwrapSafe}
*
* @example
* ```typescript
Expand Down Expand Up @@ -585,6 +588,7 @@ export class Result<T, E, const Success extends boolean = boolean> {
* @seealso {@link unwrapOrElse}
* @seealso {@link unwrapErr}
* @seealso {@link unwrapRaw}
* @seealso {@link unwrapSafe}
*
* @param defaultValue The default value.
*
Expand All @@ -611,6 +615,7 @@ export class Result<T, E, const Success extends boolean = boolean> {
* @seealso {@link unwrapOr}
* @seealso {@link unwrapErr}
* @seealso {@link unwrapRaw}
* @seealso {@link unwrapSafe}
*
* @param op The predicate.
*
Expand All @@ -636,6 +641,7 @@ export class Result<T, E, const Success extends boolean = boolean> {
* @seealso {@link unwrapOr}
* @seealso {@link unwrapOrElse}
* @seealso {@link unwrapErr}
* @seealso {@link unwrapSafe}
*
* @example
* ```typescript
Expand All @@ -659,6 +665,28 @@ export class Result<T, E, const Success extends boolean = boolean> {
return this[ValueProperty] as T;
}

/**
* Returns the contained `Ok` value or yelds the contained `Err` value.
* Emulates Rust's `?` operator in `safeTry`'s body. See also {@link Result.safeTry}.
*
* If used outside of a `safeTry`'s' body, throws the contained error.
* @seealso {@link unwrap}
* @seealso {@link unwrapOr}
* @seealso {@link unwrapErr}
* @seealso {@link unwrapRaw}
* @seealso {@link unwrapSafe}
*
* @see {@link https://doc.rust-lang.org/std/result/enum.Result.html#method.unwrap_safe}
*/
public *unwrapSafe(): Generator<Err<E, T>, T> {
if (this.isOk()) {
return this[ValueProperty];
}

yield this as Err<E, T>;
throw new ResultError('Should not be used outside of a safe try generator', this[ValueProperty]);
}

/**
* Returns `result` if the result is `Ok`, otherwise returns the `Err` value of itself.
* @param result The result to check.
Expand Down Expand Up @@ -1002,6 +1030,17 @@ export class Result<T, E, const Success extends boolean = boolean> {
return this.match({ ok: () => 'Ok', err: () => 'Err' });
}

/**
* This function, in combination with `[$]`, is intended to emulate
* Rust's ? operator.
*
* @see {@link Result.safeTry}
* @see {@link https://doc.rust-lang.org/std/result/enum.Result.html#method.safeTry}
*/
public get [UnwrapSafeProperty](): Generator<Err<E, T>, T> {
return this.unwrapSafe();
}

// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
public static ok<T = undefined, E = any>(this: void, value?: T): Ok<T, E>;
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
Expand Down Expand Up @@ -1149,6 +1188,100 @@ export class Result<T, E, const Success extends boolean = boolean> {

return err(errors as UnwrapErrArray<Entries>);
}

/**
* Evaluates the given generator to a Result returned or an Err yielded from it,
* whichever comes first.
*
* This function, in combination with `[$]`, is intended to emulate
* Rust's ? operator.
*
* @example
* ```typescript
* const result = Result.safeTry(function* ({ $ }) {
* const first = yield* ok(1)[$];
* const second = yield* ok(1)[$];
*
* return ok(first + second);
* });
*
* result.match({
* ok: (value) => value, // 2
* err: (error) => {}
* });
*```
*
* @example
* ```typescript
* const resultAsync = Result.safeTry(async function* ({ $async }) {
* const first = yield* $async(Result.fromAsync(() => Promise.resolve(1)));
* const second = yield* ok(1)[$];
*
* return ok(first + second);
* });
*
* resultAsync.match({
* ok: (value) => value, // 2
* err: (error) => {}
* });
* ```
* @param body - What is evaluated. In body, `yield* result[$]` works as
* Rust's `result?` expression.
* @returns The first occurence of either an yielded Err or a returned Result.
*/
public static safeTry<T, E>(body: (options: SafeTryOptions) => Generator<Err<E>, Result<T, E>>): Result<T, E>;

/**
* Evaluates the given generator to a Result returned or an Err yielded from it,
* whichever comes first.
*
* This function, in combination with `[$]`, is intended to emulate
* Rust's ? operator.
*
* @example
* ```typescript
* const result = Result.safeTry(function* ({ $ }) {
* const first = yield* ok(1)[$];
* const second = yield* ok(1)[$];
*
* return ok(first + second);
* });
*
* result.match({
* ok: (value) => value, // 2
* err: (error) => {}
* });
*```
*
* @example
* ```typescript
* const resultAsync = Result.safeTry(async function* ({ $async }) {
* const first = yield* $async(Result.fromAsync(() => Promise.resolve(1)));
* const second = yield* ok(1)[$];
*
* return ok(first + second);
* });
*
* resultAsync.match({
* ok: (value) => value, // 2
* err: (error) => {}
* });
* ```
* @param body - What is evaluated. In body, `yield* result[$]` works as
* Rust's `result?` expression.
* @returns The first occurence of either an yielded Err or a returned Result.
*/
public static safeTry<T, E>(body: (options: SafeTryOptions) => AsyncGenerator<Err<E>, Result<T, E>>): Promise<Result<T, E>>;
public static safeTry<T, E>(
body: ((options: SafeTryOptions) => Generator<Err<E>, Result<T, E>>) | ((options: SafeTryOptions) => AsyncGenerator<Err<E>, Result<T, E>>)
): Result<T, E> | Promise<Result<T, E>> {
const n = body({ $: UnwrapSafeProperty, $async: unwrapSafeAsync }).next();
if (n instanceof Promise) {
return n.then((r) => r.value);
}

return n.value;
}
}

export namespace Result {
Expand All @@ -1173,6 +1306,11 @@ function resolve<T, E>(value: Result.Resolvable<T, E>): Result<T, E> {
return Result.isResult(value) ? value : ok(value);
}

async function* unwrapSafeAsync<T, E>(result: Promise<Result<T, E>>): AsyncGenerator<Err<E>, T> {
const _result = await result;
return yield* _result.unwrapSafe();
}

export type ResultResolvable<T, E = any, Success extends boolean = boolean> = Result.Resolvable<T, E, Success>;

export type Ok<T, E = any> = Result.Ok<T, E>;
Expand All @@ -1184,3 +1322,8 @@ export type UnwrapErr<T extends AnyResult> = Result.UnwrapErr<T>;

export type UnwrapOkArray<T extends readonly AnyResult[] | []> = Result.UnwrapOkArray<T>;
export type UnwrapErrArray<T extends readonly AnyResult[] | []> = Result.UnwrapErrArray<T>;

export interface SafeTryOptions {
$: typeof UnwrapSafeProperty;
$async: <T, E>(result: Promise<Result<T, E>>) => AsyncGenerator<Err<E>, T>;
}
96 changes: 96 additions & 0 deletions packages/result/tests/Result.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,102 @@ describe('Result', () => {
});
});

describe('safeTry', () => {
/* eslint-disable func-names, require-yield */
test('GIVEN a successful result THEN return Ok', () => {
const result = Result.safeTry(function* () {
return ok(42);
});

expect(result).toEqual(ok(42));
});

test('GIVEN a async successful result THEN return Ok', async () => {
const result = await Result.safeTry(async function* () {
return Result.fromAsync(() => Promise.resolve(42));
});

expect(result).toEqual(ok(42));
});

test('GIVEN a unsuccessful result THEN return Err', () => {
const result = Result.safeTry(function* () {
return err('Error!');
});

expect(result).toEqual(err('Error!'));
});

test('GIVEN a async unsuccessful result THEN return Err', async () => {
const result = await Result.safeTry(async function* () {
return Result.fromAsync(() => Promise.reject(new Error('Error!')));
});

expect(result).toEqual(err(new Error('Error!')));
});

test('GIVEN a mixed successful results THEN should return the last OK', () => {
const result = Result.safeTry(function* ({ $ }) {
const first = yield* ok(1)[$];
const second = yield* ok(2)[$];
return ok(first + second);
});

expect(result).toEqual(ok(3));
});

test('GIVEN a mixed async successful results THEN should return the last OK', async () => {
const result = await Result.safeTry(async function* ({ $, $async }) {
const first = yield* ok(1)[$];
const second = yield* $async(Result.fromAsync(() => Promise.resolve(2)));
return ok(first + second);
});

expect(result).toEqual(ok(3));
});

test('GIVEN a mixed results THEN should stop and return first Err', () => {
const values: number[] = [];

const result = Result.safeTry(function* ({ $ }) {
const first = yield* ok(1)[$];
values.push(first);

const second = yield* ok(2)[$];
values.push(second);

yield* err('Error!')[$];
const third = yield* ok(3)[$];

return ok(first + second + third);
});

expect(result).toEqual(err('Error!'));
expect(values).toEqual([1, 2]);
});

test('GIVEN a mixed async results THEN should stop and return first Err', async () => {
const values: number[] = [];

const result = await Result.safeTry(async function* ({ $, $async }) {
const first = yield* ok(1)[$];
values.push(first);

const second = yield* $async(Result.fromAsync(() => Promise.resolve(2)));
values.push(second);

yield* err('Error!')[$];
const third = yield* ok(3)[$];

return ok(first + second + third);
});

expect(result).toEqual(err('Error!'));
expect(values).toEqual([1, 2]);
});
/* eslint-enable func-names, require-yield */
});

describe('all', () => {
test('GIVEN empty array THEN returns Result<[], never>', () => {
expect<Result<[], never>>(Result.all([])).toEqual(ok([]));
Expand Down