518 lines
18 KiB
TypeScript
518 lines
18 KiB
TypeScript
import { Chunk, Equal, Equivalence, flow, identity, Option, pipe, Result, String as Str } from "effect"
|
|
import { inspect } from "node:util"
|
|
import { describe, it } from "vitest"
|
|
import {
|
|
assertFailure,
|
|
assertFalse,
|
|
assertNone,
|
|
assertSome,
|
|
assertSuccess,
|
|
assertTrue,
|
|
deepStrictEqual,
|
|
strictEqual,
|
|
throws
|
|
} from "./utils/assert.ts"
|
|
|
|
describe("Result", () => {
|
|
describe("Constructors", () => {
|
|
it("succeed creates a Success value", () => {
|
|
const result = Result.succeed(42)
|
|
assertTrue(Result.isSuccess(result))
|
|
strictEqual(result.success, 42)
|
|
})
|
|
|
|
it("fail creates a Failure value", () => {
|
|
const result = Result.fail("err")
|
|
assertTrue(Result.isFailure(result))
|
|
strictEqual(result.failure, "err")
|
|
})
|
|
|
|
it("void", () => {
|
|
deepStrictEqual(Result.void, Result.succeed(undefined))
|
|
})
|
|
|
|
it("try", () => {
|
|
deepStrictEqual(Result.try(() => 1), Result.succeed(1))
|
|
deepStrictEqual(
|
|
Result.try(() => {
|
|
throw "b"
|
|
}),
|
|
Result.fail("b")
|
|
)
|
|
deepStrictEqual(Result.try({ try: () => 1, catch: (e) => new Error(String(e)) }), Result.succeed(1))
|
|
deepStrictEqual(
|
|
Result.try({
|
|
try: () => {
|
|
throw "b"
|
|
},
|
|
catch: (e) => new Error(String(e))
|
|
}),
|
|
Result.fail(new Error("b"))
|
|
)
|
|
})
|
|
|
|
it("fromNullishOr", () => {
|
|
deepStrictEqual(Result.fromNullishOr(null, () => "fallback"), Result.fail("fallback"))
|
|
deepStrictEqual(Result.fromNullishOr(undefined, () => "fallback"), Result.fail("fallback"))
|
|
deepStrictEqual(Result.fromNullishOr(1, () => "fallback"), Result.succeed(1))
|
|
// data-last
|
|
deepStrictEqual(pipe(null as string | null, Result.fromNullishOr(() => "fallback")), Result.fail("fallback"))
|
|
deepStrictEqual(pipe(1 as number | null, Result.fromNullishOr(() => "fallback")), Result.succeed(1))
|
|
})
|
|
|
|
it("fromOption", () => {
|
|
deepStrictEqual(Result.fromOption(Option.none(), () => "none"), Result.fail("none"))
|
|
deepStrictEqual(Result.fromOption(Option.some(1), () => "none"), Result.succeed(1))
|
|
// data-last
|
|
deepStrictEqual(pipe(Option.none(), Result.fromOption(() => "none")), Result.fail("none"))
|
|
deepStrictEqual(pipe(Option.some(1), Result.fromOption(() => "none")), Result.succeed(1))
|
|
})
|
|
|
|
it("succeedNone", () => {
|
|
deepStrictEqual(Result.succeedNone, Result.succeed(Option.none()))
|
|
})
|
|
|
|
it("succeedSome", () => {
|
|
deepStrictEqual(Result.succeedSome(1), Result.succeed(Option.some(1)))
|
|
})
|
|
})
|
|
|
|
describe("Methods", () => {
|
|
it("toString", () => {
|
|
strictEqual(
|
|
String(Result.succeed(1)),
|
|
`success(1)`
|
|
)
|
|
strictEqual(
|
|
String(Result.fail("e")),
|
|
`failure("e")`
|
|
)
|
|
strictEqual(
|
|
String(Result.succeed(Chunk.make(1, 2, 3))),
|
|
`success(Chunk([1,2,3]))`
|
|
)
|
|
strictEqual(
|
|
String(Result.fail(Chunk.make(1, 2, 3))),
|
|
`failure(Chunk([1,2,3]))`
|
|
)
|
|
strictEqual(
|
|
String(Result.succeed(Option.some(1))),
|
|
`success(some(1))`
|
|
)
|
|
strictEqual(
|
|
String(Result.fail(Option.none())),
|
|
`failure(none())`
|
|
)
|
|
})
|
|
|
|
it("toJSON", () => {
|
|
deepStrictEqual(Result.succeed(1).toJSON(), { _id: "Result", _tag: "Success", value: 1 })
|
|
deepStrictEqual(Result.fail("e").toJSON(), { _id: "Result", _tag: "Failure", failure: "e" })
|
|
})
|
|
|
|
it("inspect", () => {
|
|
deepStrictEqual(inspect(Result.succeed(1)), inspect({ _id: "Result", _tag: "Success", value: 1 }))
|
|
deepStrictEqual(inspect(Result.fail("e")), inspect({ _id: "Result", _tag: "Failure", failure: "e" }))
|
|
})
|
|
|
|
it("Equal trait compares matching tags and values", () => {
|
|
assertTrue(Equal.equals(Result.succeed(1), Result.succeed(1)))
|
|
assertTrue(Equal.equals(Result.fail("e"), Result.fail("e")))
|
|
assertFalse(Equal.equals(Result.succeed(1), Result.fail("e")))
|
|
assertFalse(Equal.equals(Result.fail("e"), Result.succeed(1)))
|
|
})
|
|
|
|
it("pipe composes Result operations", () => {
|
|
assertSuccess(Result.succeed(1).pipe(Result.map((n) => n + 1)), 2)
|
|
})
|
|
})
|
|
|
|
describe("Type Guards", () => {
|
|
it("isResult", () => {
|
|
assertTrue(pipe(Result.succeed(1), Result.isResult))
|
|
assertTrue(pipe(Result.fail("e"), Result.isResult))
|
|
assertFalse(pipe(Option.some(1), Result.isResult))
|
|
})
|
|
|
|
it("isFailure", () => {
|
|
assertFalse(Result.isFailure(Result.succeed(1)))
|
|
assertTrue(Result.isFailure(Result.fail(1)))
|
|
})
|
|
|
|
it("isSuccess", () => {
|
|
assertTrue(Result.isSuccess(Result.succeed(1)))
|
|
assertFalse(Result.isSuccess(Result.fail(1)))
|
|
})
|
|
})
|
|
|
|
describe("Getters", () => {
|
|
it("getSuccess", () => {
|
|
assertSome(pipe(Result.succeed(1), Result.getSuccess), 1)
|
|
assertNone(pipe(Result.fail("a"), Result.getSuccess))
|
|
})
|
|
|
|
it("getFailure", () => {
|
|
assertNone(pipe(Result.succeed(1), Result.getFailure))
|
|
assertSome(pipe(Result.fail("e"), Result.getFailure), "e")
|
|
})
|
|
|
|
it("getOrElse", () => {
|
|
strictEqual(Result.getOrElse(Result.succeed(1), (error) => error + "!"), 1)
|
|
strictEqual(Result.getOrElse(Result.fail("not a number"), (error) => error + "!"), "not a number!")
|
|
// data-last
|
|
strictEqual(pipe(Result.succeed(1), Result.getOrElse((error) => error + "!")), 1)
|
|
strictEqual(pipe(Result.fail("not a number"), Result.getOrElse((error) => error + "!")), "not a number!")
|
|
})
|
|
|
|
it("getOrNull", () => {
|
|
strictEqual(Result.getOrNull(Result.succeed(1)), 1)
|
|
strictEqual(Result.getOrNull(Result.fail("a")), null)
|
|
})
|
|
|
|
it("getOrUndefined", () => {
|
|
strictEqual(Result.getOrUndefined(Result.succeed(1)), 1)
|
|
strictEqual(Result.getOrUndefined(Result.fail("a")), undefined)
|
|
})
|
|
|
|
it("getOrThrowWith", () => {
|
|
strictEqual(pipe(Result.succeed(1), Result.getOrThrowWith((e) => new Error(`Unexpected Err: ${e}`))), 1)
|
|
throws(() => pipe(Result.fail("e"), Result.getOrThrowWith((e) => new Error(`Unexpected Err: ${e}`))))
|
|
// data-first
|
|
strictEqual(Result.getOrThrowWith(Result.succeed(1), (e) => new Error(`Unexpected Err: ${e}`)), 1)
|
|
throws(() => Result.getOrThrowWith(Result.fail("e"), (e) => new Error(`Unexpected Err: ${e}`)))
|
|
})
|
|
|
|
it("getOrThrow", () => {
|
|
strictEqual(pipe(Result.succeed(1), Result.getOrThrow), 1)
|
|
throws(() => pipe(Result.fail(new Error("e")), Result.getOrThrow), new Error("e"))
|
|
})
|
|
|
|
it("merge", () => {
|
|
deepStrictEqual(Result.merge(Result.succeed(1)), 1)
|
|
deepStrictEqual(Result.merge(Result.fail("a")), "a")
|
|
})
|
|
})
|
|
|
|
describe("Mapping", () => {
|
|
it("map", () => {
|
|
const f = Result.map(Str.length)
|
|
assertSuccess(pipe(Result.succeed("abc"), f), 3)
|
|
assertFailure(pipe(Result.fail("s"), f), "s")
|
|
// data-first
|
|
assertSuccess(Result.map(Result.succeed("abc"), Str.length), 3)
|
|
assertFailure(Result.map(Result.fail("s"), Str.length), "s")
|
|
})
|
|
|
|
it("mapBoth", () => {
|
|
const f = Result.mapBoth({
|
|
onFailure: Str.length,
|
|
onSuccess: (n: number) => n > 2
|
|
})
|
|
assertSuccess(pipe(Result.succeed(1), f), false)
|
|
assertFailure(pipe(Result.fail("a"), f), 1)
|
|
// data-first
|
|
assertSuccess(
|
|
Result.mapBoth(Result.succeed(1), {
|
|
onFailure: Str.length,
|
|
onSuccess: (n: number) => n > 2
|
|
}),
|
|
false
|
|
)
|
|
assertFailure(
|
|
Result.mapBoth(Result.fail("a"), {
|
|
onFailure: Str.length,
|
|
onSuccess: (n: number) => n > 2
|
|
}),
|
|
1
|
|
)
|
|
})
|
|
|
|
it("mapError", () => {
|
|
const f = Result.mapError((n: number) => n * 2)
|
|
assertSuccess(pipe(Result.succeed("a"), f), "a")
|
|
assertFailure(pipe(Result.fail(1), f), 2)
|
|
// data-first
|
|
assertSuccess(Result.mapError(Result.succeed("a"), (n: number) => n * 2), "a")
|
|
assertFailure(Result.mapError(Result.fail(1), (n) => n * 2), 2)
|
|
})
|
|
|
|
it("tap", () => {
|
|
let sideEffect = 0
|
|
const success = pipe(
|
|
Result.succeed(42),
|
|
Result.tap((n) => {
|
|
sideEffect = n
|
|
})
|
|
)
|
|
assertSuccess(success, 42)
|
|
strictEqual(sideEffect, 42)
|
|
|
|
// failure path: side effect should not run
|
|
let failureSideEffect = 0
|
|
const failure = pipe(
|
|
Result.fail("err") as Result.Result<number, string>,
|
|
Result.tap((n) => {
|
|
failureSideEffect = n
|
|
})
|
|
)
|
|
assertFailure(failure, "err")
|
|
strictEqual(failureSideEffect, 0)
|
|
|
|
// data-first
|
|
let sideEffect2 = 0
|
|
const success2 = Result.tap(Result.succeed(10), (n) => {
|
|
sideEffect2 = n
|
|
})
|
|
assertSuccess(success2, 10)
|
|
strictEqual(sideEffect2, 10)
|
|
})
|
|
})
|
|
|
|
describe("Pattern Matching", () => {
|
|
it("match handles failure and success cases", () => {
|
|
const onFailure = (s: string) => `failure${s.length}`
|
|
const onSuccess = (s: string) => `success${s.length}`
|
|
const match = Result.match({ onFailure, onSuccess })
|
|
strictEqual(match(Result.fail("abc")), "failure3")
|
|
strictEqual(match(Result.succeed("abc")), "success3")
|
|
// data-first
|
|
strictEqual(Result.match(Result.fail("abc"), { onFailure, onSuccess }), "failure3")
|
|
strictEqual(Result.match(Result.succeed("abc"), { onFailure, onSuccess }), "success3")
|
|
})
|
|
})
|
|
|
|
describe("Utils", () => {
|
|
it("flip", () => {
|
|
assertFailure(Result.flip(Result.succeed("a")), "a")
|
|
assertSuccess(Result.flip(Result.fail("b")), "b")
|
|
})
|
|
|
|
it("liftPredicate", () => {
|
|
const isPositivePredicate = (n: number) => n > 0
|
|
const onPositivePredicateError = (n: number) => `${n} is not positive`
|
|
const isNumberRefinement = (n: string | number): n is number => typeof n === "number"
|
|
const onNumberRefinementError = (n: string | number) => `${n} is not a number`
|
|
|
|
assertSuccess(
|
|
pipe(1, Result.liftPredicate(isPositivePredicate, onPositivePredicateError)),
|
|
1
|
|
)
|
|
assertFailure(
|
|
pipe(-1, Result.liftPredicate(isPositivePredicate, onPositivePredicateError)),
|
|
"-1 is not positive"
|
|
)
|
|
assertSuccess(
|
|
pipe(1, Result.liftPredicate(isNumberRefinement, onNumberRefinementError)),
|
|
1
|
|
)
|
|
assertFailure(
|
|
pipe("string", Result.liftPredicate(isNumberRefinement, onNumberRefinementError)),
|
|
"string is not a number"
|
|
)
|
|
|
|
assertSuccess(
|
|
Result.liftPredicate(1, isPositivePredicate, onPositivePredicateError),
|
|
1
|
|
)
|
|
assertFailure(
|
|
Result.liftPredicate(-1, isPositivePredicate, onPositivePredicateError),
|
|
"-1 is not positive"
|
|
)
|
|
assertSuccess(
|
|
Result.liftPredicate(1, isNumberRefinement, onNumberRefinementError),
|
|
1
|
|
)
|
|
assertFailure(
|
|
Result.liftPredicate("string", isNumberRefinement, onNumberRefinementError),
|
|
"string is not a number"
|
|
)
|
|
})
|
|
})
|
|
|
|
describe("Filtering", () => {
|
|
it("filterOrFail", () => {
|
|
deepStrictEqual(Result.filterOrFail(Result.succeed(1), (n) => n > 0, () => "a"), Result.succeed(1))
|
|
deepStrictEqual(Result.filterOrFail(Result.succeed(1), (n) => n > 1, () => "a"), Result.fail("a"))
|
|
deepStrictEqual(Result.filterOrFail(Result.fail(1), (n) => n > 0, () => "a"), Result.fail(1))
|
|
|
|
deepStrictEqual(Result.succeed(1).pipe(Result.filterOrFail((n) => n > 0, () => "a")), Result.succeed(1))
|
|
deepStrictEqual(Result.succeed(1).pipe(Result.filterOrFail((n) => n > 1, () => "a")), Result.fail("a"))
|
|
deepStrictEqual(Result.fail(1).pipe(Result.filterOrFail((n) => n > 0, () => "a")), Result.fail(1))
|
|
})
|
|
})
|
|
|
|
describe("Equivalence", () => {
|
|
it("makeEquivalence compares successes and failures by channel", () => {
|
|
const isEquivalent = Result.makeEquivalence(
|
|
Equivalence.strictEqual<number>(),
|
|
Equivalence.strictEqual<string>()
|
|
)
|
|
deepStrictEqual(isEquivalent(Result.succeed(1), Result.succeed(1)), true)
|
|
deepStrictEqual(isEquivalent(Result.succeed(1), Result.succeed(2)), false)
|
|
deepStrictEqual(isEquivalent(Result.succeed(1), Result.fail("foo")), false)
|
|
deepStrictEqual(isEquivalent(Result.fail("foo"), Result.fail("foo")), true)
|
|
deepStrictEqual(isEquivalent(Result.fail("foo"), Result.fail("bar")), false)
|
|
deepStrictEqual(isEquivalent(Result.fail("foo"), Result.succeed(1)), false)
|
|
})
|
|
})
|
|
|
|
describe("Sequencing", () => {
|
|
it("flatMap", () => {
|
|
const f = Result.flatMap(flow(Str.length, Result.succeed))
|
|
assertSuccess(pipe(Result.succeed("abc"), f), 3)
|
|
assertFailure(pipe(Result.fail("maError"), f), "maError")
|
|
// data-first
|
|
assertSuccess(Result.flatMap(Result.succeed("abc"), flow(Str.length, Result.succeed)), 3)
|
|
assertFailure(Result.flatMap(Result.fail("maError"), flow(Str.length, Result.succeed)), "maError")
|
|
})
|
|
|
|
it("andThen replaces successes with Result or plain values", () => {
|
|
assertSuccess(pipe(Result.succeed(1), Result.andThen(() => Result.succeed(2))), 2)
|
|
assertSuccess(pipe(Result.succeed(1), Result.andThen(Result.succeed(2))), 2)
|
|
assertSuccess(pipe(Result.succeed(1), Result.andThen(2)), 2)
|
|
assertSuccess(pipe(Result.succeed(1), Result.andThen(() => 2)), 2)
|
|
assertSuccess(pipe(Result.succeed(1), Result.andThen((a) => a)), 1)
|
|
assertSuccess(Result.andThen(Result.succeed(1), () => Result.succeed(2)), 2)
|
|
assertSuccess(Result.andThen(Result.succeed(1), Result.succeed(2)), 2)
|
|
assertSuccess(Result.andThen(Result.succeed(1), () => 2), 2)
|
|
assertSuccess(Result.andThen(Result.succeed(1), 2), 2)
|
|
assertSuccess(Result.andThen(Result.succeed(1), (a) => a), 1)
|
|
})
|
|
|
|
it("all", () => {
|
|
// tuples and arrays
|
|
assertSuccess(Result.all([]), [])
|
|
assertSuccess(Result.all([Result.succeed(1)]), [1])
|
|
assertSuccess(Result.all([Result.succeed(1), Result.succeed(true)]), [1, true])
|
|
assertFailure(Result.all([Result.succeed(1), Result.fail("e")]), "e")
|
|
// structs and records
|
|
assertSuccess(Result.all({}), {})
|
|
assertSuccess(Result.all({ a: Result.succeed(1) }), { a: 1 })
|
|
assertSuccess(Result.all({ a: Result.succeed(1), b: Result.succeed(true) }), { a: 1, b: true })
|
|
assertFailure(Result.all({ a: Result.succeed(1), b: Result.fail("e") }), "e")
|
|
// iterables
|
|
const iterable = function*() {
|
|
yield Result.succeed(1)
|
|
yield Result.succeed(2)
|
|
yield Result.succeed(3)
|
|
}
|
|
assertSuccess(Result.all(iterable()), [1, 2, 3])
|
|
|
|
const iterableWithFailure = function*() {
|
|
yield Result.succeed(1)
|
|
yield Result.fail("e")
|
|
yield Result.succeed(3)
|
|
}
|
|
assertFailure(Result.all(iterableWithFailure()), "e")
|
|
})
|
|
})
|
|
|
|
describe("Error Handling", () => {
|
|
it("orElse recovers failures and preserves successes", () => {
|
|
assertSuccess(pipe(Result.succeed(1), Result.orElse(() => Result.succeed(2))), 1)
|
|
assertSuccess(pipe(Result.succeed(1), Result.orElse(() => Result.fail("b"))), 1)
|
|
assertSuccess(pipe(Result.fail("a"), Result.orElse(() => Result.succeed(2))), 2)
|
|
assertFailure(pipe(Result.fail("a"), Result.orElse(() => Result.fail("b"))), "b")
|
|
// data-first
|
|
assertSuccess(Result.orElse(Result.succeed(1), () => Result.succeed(2)), 1)
|
|
assertSuccess(Result.orElse(Result.fail("a"), () => Result.succeed(2)), 2)
|
|
assertFailure(Result.orElse(Result.fail("a"), () => Result.fail("b")), "b")
|
|
})
|
|
})
|
|
|
|
describe("Do Notation", () => {
|
|
it("Do", () => {
|
|
assertSuccess(Result.Do, {})
|
|
})
|
|
|
|
it("bindTo", () => {
|
|
assertSuccess(pipe(Result.succeed(1), Result.bindTo("a")), { a: 1 })
|
|
assertFailure(pipe(Result.fail("left"), Result.bindTo("a")), "left")
|
|
|
|
assertSuccess(pipe(Result.succeed(1), Result.bindTo("__proto__")), { ["__proto__"]: 1 })
|
|
})
|
|
|
|
it("bind", () => {
|
|
assertSuccess(pipe(Result.succeed(1), Result.bindTo("a"), Result.bind("b", ({ a }) => Result.succeed(a + 1))), {
|
|
a: 1,
|
|
b: 2
|
|
})
|
|
assertFailure(
|
|
pipe(Result.succeed(1), Result.bindTo("a"), Result.bind("b", () => Result.fail("left"))),
|
|
"left"
|
|
)
|
|
assertFailure(
|
|
pipe(Result.fail("left"), Result.bindTo("a"), Result.bind("b", () => Result.succeed(2))),
|
|
"left"
|
|
)
|
|
assertSuccess(pipe(Result.Do, Result.bind("__proto__", () => Result.succeed(1))), { ["__proto__"]: 1 })
|
|
})
|
|
|
|
it("let", () => {
|
|
assertSuccess(pipe(Result.succeed(1), Result.bindTo("a"), Result.let("b", ({ a }) => a + 1)), { a: 1, b: 2 })
|
|
assertFailure(
|
|
pipe(Result.fail("left"), Result.bindTo("a"), Result.let("b", () => 2)),
|
|
"left"
|
|
)
|
|
assertSuccess(pipe(Result.Do, Result.let("__proto__", () => 1)), { ["__proto__"]: 1 })
|
|
})
|
|
})
|
|
|
|
describe("Generators", () => {
|
|
it("gen", () => {
|
|
const a = Result.gen(function*() {
|
|
const x = yield* Result.succeed(1)
|
|
const y = yield* Result.succeed(2)
|
|
return x + y
|
|
})
|
|
const b = Result.gen(function*() {
|
|
return 10
|
|
})
|
|
const c = Result.gen(function*() {
|
|
yield* Result.succeed(1)
|
|
yield* Result.succeed(2)
|
|
})
|
|
const d = Result.gen(function*() {
|
|
yield* Result.succeed(1)
|
|
return yield* Result.succeed(2)
|
|
})
|
|
const e = Result.gen(function*() {
|
|
yield* Result.succeed(1)
|
|
yield* Result.fail("err")
|
|
return yield* Result.succeed(2)
|
|
})
|
|
const f = Result.gen(function*() {
|
|
yield* Result.fail("err")
|
|
})
|
|
const g = Result.gen({ context: "testContext" as const }, function*() {
|
|
return yield* Result.succeed(this.context)
|
|
})
|
|
|
|
assertSuccess(a, 3)
|
|
assertSuccess(b, 10)
|
|
assertSuccess(c, undefined)
|
|
assertSuccess(d, 2)
|
|
assertFailure(e, "err")
|
|
assertFailure(f, "err")
|
|
assertSuccess(g, "testContext")
|
|
})
|
|
})
|
|
|
|
describe("Transposing", () => {
|
|
it("transposeOption", () => {
|
|
assertSuccess(Result.transposeOption(Option.some(Result.succeed(1))), Option.some(1))
|
|
assertSuccess(Result.transposeOption(Option.none()), Option.none())
|
|
assertFailure(Result.transposeOption(Option.some(Result.fail("e"))), "e")
|
|
})
|
|
|
|
it("transposeMapOption", () => {
|
|
assertSuccess(Result.transposeMapOption(Option.some(Result.succeed(1)), identity), Option.some(1))
|
|
assertSuccess(Result.transposeMapOption(Option.none(), identity), Option.none())
|
|
assertFailure(Result.transposeMapOption(Option.some(Result.fail("e")), identity), "e")
|
|
// data-last
|
|
assertSuccess(pipe(Option.some(1), Result.transposeMapOption(Result.succeed)), Option.some(1))
|
|
assertSuccess(pipe(Option.none<number>(), Result.transposeMapOption(Result.succeed)), Option.none())
|
|
})
|
|
})
|
|
})
|