Skip to content

Commit

Permalink
Merge pull request #448 from tsuburin/master
Browse files Browse the repository at this point in the history
Emulation of Rust's ? operator
  • Loading branch information
supermacro authored Oct 22, 2023
2 parents 20a12ef + 0cfab20 commit 9f547a9
Show file tree
Hide file tree
Showing 4 changed files with 186 additions and 1 deletion.
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export { Result, ok, Ok, err, Err, fromThrowable } from './result'
export { Result, ok, Ok, err, Err, fromThrowable, safeTry } from './result'
export { ResultAsync, okAsync, errAsync, fromPromise, fromSafePromise } from './result-async'
7 changes: 7 additions & 0 deletions src/result-async.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,13 @@ export class ResultAsync<T, E> implements PromiseLike<Result<T, E>> {
return this._promise.then((res) => res.unwrapOr(t))
}

/**
* Emulates Rust's `?` operator in `safeTry`'s body. See also `safeTry`.
*/
async *safeUnwrap(): AsyncGenerator<Err<never, E>, T> {
return yield* await this._promise.then((res) => res.safeUnwrap())
}

// Makes ResultAsync implement PromiseLike<Result>
then<A, B>(
successCallback?: (res: Result<T, E>) => A | PromiseLike<A>,
Expand Down
65 changes: 65 additions & 0 deletions src/result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,49 @@ export const ok = <T, E = never>(value: T): Ok<T, E> => new Ok(value)

export const err = <T = never, E = unknown>(err: E): Err<T, E> => new Err(err)

/**
* Evaluates the given generator to a Result returned or an Err yielded from it,
* whichever comes first.
*
* This function, in combination with `Result.safeUnwrap()`, is intended to emulate
* Rust's ? operator.
* See `/tests/safeTry.test.ts` for examples.
*
* @param body - What is evaluated. In body, `yield* result.safeUnwrap()` works as
* Rust's `result?` expression.
* @returns The first occurence of either an yielded Err or a returned Result.
*/
export function safeTry<T, E>(body: () => Generator<Err<never, 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 `Result.safeUnwrap()`, is intended to emulate
* Rust's ? operator.
* See `/tests/safeTry.test.ts` for examples.
*
* @param body - What is evaluated. In body, `yield* result.safeUnwrap()` and
* `yield* resultAsync.safeUnwrap()` work as Rust's `result?` expression.
* @returns The first occurence of either an yielded Err or a returned Result.
*/
// NOTE:
// Since body is potentially throwable because `await` can be used in it,
// Promise<Result<T, E>>, not ResultAsync<T, E>, is used as the return type.
export function safeTry<T, E>(
body: () => AsyncGenerator<Err<never, E>, Result<T, E>>,
): Promise<Result<T, E>>
export function safeTry<T, E>(
body:
| (() => Generator<Err<never, E>, Result<T, E>>)
| (() => AsyncGenerator<Err<never, E>, Result<T, E>>),
): Result<T, E> | Promise<Result<T, E>> {
const n = body().next()
if (n instanceof Promise) {
return n.then((r) => r.value)
}
return n.value
}

interface IResult<T, E> {
/**
* Used to check if a `Result` is an `OK`
Expand Down Expand Up @@ -170,6 +213,11 @@ interface IResult<T, E> {
*/
match<A>(ok: (t: T) => A, err: (e: E) => A): A

/**
* Emulates Rust's `?` operator in `safeTry`'s body. See also `safeTry`.
*/
safeUnwrap(): Generator<Err<never, E>, T>

/**
* **This method is unsafe, and should only be used in a test environments**
*
Expand Down Expand Up @@ -244,6 +292,14 @@ export class Ok<T, E> implements IResult<T, E> {
return ok(this.value)
}

safeUnwrap(): Generator<Err<never, E>, T> {
const value = this.value
/* eslint-disable-next-line require-yield */
return (function* () {
return value
})()
}

_unsafeUnwrap(_?: ErrorConfig): T {
return this.value
}
Expand Down Expand Up @@ -307,6 +363,15 @@ export class Err<T, E> implements IResult<T, E> {
return err(this.error)
}

safeUnwrap(): Generator<Err<never, E>, T> {
const error = this.error
return (function* () {
yield err(error)

throw new Error('Do not use this generator out of `safeTry`')
})()
}

_unsafeUnwrap(config?: ErrorConfig): T {
throw createNeverThrowError('Called `_unsafeUnwrap` on an Err', this, config)
}
Expand Down
113 changes: 113 additions & 0 deletions tests/safeTry.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import {
safeTry,
ok,
okAsync,
err,
errAsync,
Ok,
Err
} from "../src"

describe('Returns what is returned from the generator function', () => {
const val = "value"

test("With synchronous Ok", () => {
const res = safeTry(function*() {
return ok(val)
})
expect(res).toBeInstanceOf(Ok)
expect(res._unsafeUnwrap()).toBe(val)
})

test("With synchronous Err", () => {
const res = safeTry(function*() {
return err(val)
})
expect(res).toBeInstanceOf(Err)
expect(res._unsafeUnwrapErr()).toBe(val)
})

test("With async Ok", async () => {
const res = await safeTry(async function*() {
return await okAsync(val)
})
expect(res).toBeInstanceOf(Ok)
expect(res._unsafeUnwrap()).toBe(val)
})

test("With async Err", async () => {
const res = await safeTry(async function*() {
return await errAsync(val)
})
expect(res).toBeInstanceOf(Err)
expect(res._unsafeUnwrapErr()).toBe(val)
})
})

describe("Returns the first occurence of Err instance as yiled*'s operand", () => {
test("With synchronous results", () => {
const errVal = "err"
const okValues = Array<string>()

const result = safeTry(function*() {
const okFoo = yield* ok("foo").safeUnwrap()
okValues.push(okFoo)

const okBar = yield* ok("bar").safeUnwrap()
okValues.push(okBar)

yield* err(errVal).safeUnwrap()

throw new Error("This line should not be executed")
})

expect(okValues).toMatchObject(["foo", "bar"])

expect(result).toBeInstanceOf(Err)
expect(result._unsafeUnwrapErr()).toBe(errVal)
})

test("With async results", async () => {
const errVal = "err"
const okValues = Array<string>()

const result = await safeTry(async function*() {
const okFoo = yield* okAsync("foo").safeUnwrap()
okValues.push(okFoo)

const okBar = yield* okAsync("bar").safeUnwrap()
okValues.push(okBar)

yield* errAsync(errVal).safeUnwrap()

throw new Error("This line should not be executed")
})

expect(okValues).toMatchObject(["foo", "bar"])

expect(result).toBeInstanceOf(Err)
expect(result._unsafeUnwrapErr()).toBe(errVal)
})

test("Mix results of synchronous and async in AsyncGenerator", async () => {
const errVal = "err"
const okValues = Array<string>()

const result = await safeTry(async function*() {
const okFoo = yield* okAsync("foo").safeUnwrap()
okValues.push(okFoo)

const okBar = yield* ok("bar").safeUnwrap()
okValues.push(okBar)

yield* err(errVal).safeUnwrap()

throw new Error("This line should not be executed")
})

expect(okValues).toMatchObject(["foo", "bar"])

expect(result).toBeInstanceOf(Err)
expect(result._unsafeUnwrapErr()).toBe(errVal)
})
})

0 comments on commit 9f547a9

Please sign in to comment.