1import { assert, describe, it } from "@effect/vitest"
2import { Context, Effect, Effectable, Exit } from "effect"
3
4describe("Effectable", () => {
5 describe("Mixin", () => {
6 class Box {
7 constructor(readonly value: number) {}
8 double() {
9 return this.value * 2
10 }
11 }
12
13 class EffectBox extends Effectable.Mixin(Box) {
14 override asEffect() {
15 return Effect.succeed(this.value)
16 }
17 }
18
19 it.effect("evaluates asEffect", () =>
20 Effect.gen(function*() {
21 const effectValue = yield* new EffectBox(1)
22 assert.strictEqual(effectValue, 1)
23 }))
24
25 it.effect("propagates failures", () =>
26 Effect.gen(function*() {
27 class FailingBox extends Effectable.Mixin(Box) {
28 override asEffect() {
29 return Effect.fail(this.value)
30 }
31 }
32
33 assert.deepStrictEqual(yield* Effect.exit(new FailingBox(1)), Exit.fail(1))
34 }))
35
36 it.effect("uses provided services", () =>
37 Effect.gen(function*() {
38 class Multiplier extends Context.Service<Multiplier, number>()("Multiplier") {}
39 class ServiceBox extends Effectable.Mixin(Box) {
40 override asEffect() {
41 return Effect.map(Multiplier, (multiplier) => this.value * multiplier)
42 }
43 }
44
45 const value = yield* new ServiceBox(2).pipe(Effect.provideService(Multiplier, 3))
46 assert.strictEqual(value, 6)
47 }))
48
49 it("shadows base prototype methods without modifying them", () => {
50 class PrintableBox extends Box {
51 override toString() {
52 return "Box"
53 }
54 toJSON(): unknown {
55 return { _id: "Box" }
56 }
57 }
58 class PrintableEffectBox extends Effectable.Mixin(PrintableBox) {
59 override asEffect() {
60 return Effect.succeed(this.value)
61 }
62 }
63
64 const box = new PrintableEffectBox(1)
65 assert.deepStrictEqual(box.toJSON(), { _id: "Effect", op: "Effectable" })
66 assert.deepStrictEqual(JSON.parse(box.toString()), box.toJSON())
67 assert.strictEqual(new PrintableBox(1).toString(), "Box")
68 assert.deepStrictEqual(new PrintableBox(1).toJSON(), { _id: "Box" })
69 })
70
71 it("inserts the Effect prototype between the subclass and original class", () => {
72 const box = new EffectBox(1)
73 const protos: Array<object> = []
74 let current: object | null = Object.getPrototypeOf(box)
75 while (current !== null && current !== Box.prototype) {
76 protos.push(current)
77 current = Object.getPrototypeOf(current)
78 }
79 assert.strictEqual(current, Box.prototype)
80 assert.isTrue(protos.some((proto) => Effect.TypeId in proto))
81 })
82
83 it("preserves the original constructor and instance members", () => {
84 const box = new EffectBox(2)
85 assert.isTrue(box instanceof EffectBox)
86 assert.isTrue(box instanceof Box)
87 assert.strictEqual(box.constructor, EffectBox)
88 assert.strictEqual(box.value, 2)
89 assert.strictEqual(box.double(), 4)
90 })
91
92 it("makes instances behave as Effects", () => {
93 const box = new EffectBox(3)
94 assert.isTrue(Effect.isEffect(box))
95 assert.strictEqual(box.pipe((b) => b.value), 3)
96 })
97
98 it("does not modify the original class prototype", () => {
99 assert.isFalse(Effect.TypeId in Box.prototype)
100 assert.isFalse(Effect.isEffect(new Box(1)))
101 })
102 })
103})