Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Emulation of Rust's ? operator #448

Merged
merged 6 commits into from
Oct 22, 2023
Merged
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
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*() {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So basically I can do something like

const caller = ():Result<string,MyError> => {
  // foobar: Result<string,MyError>
  const res = yield* foobar().safeUnwrap()
  return res + "hello"
)
}

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)
})
})