1import * as Data from "effect/Data"
2import * as Effect from "effect/Effect"
3import * as Equal from "effect/Equal"
4import assert from "node:assert/strict"
5import { describe, it } from "vitest"
6
7describe("Data", () => {
8 describe("Class", () => {
9 class Person extends Data.Class<{ readonly name: string; readonly age: number }> {}
10
11 it("assigns fields from constructor args", () => {
12 const p = new Person({ name: "Alice", age: 30 })
13 assert.strictEqual(p.name, "Alice")
14 assert.strictEqual(p.age, 30)
15 })
16
17 it("structural equality via Equal.equals", () => {
18 const a = new Person({ name: "Bob", age: 25 })
19 const b = new Person({ name: "Bob", age: 25 })
20 assert.strictEqual(Equal.equals(a, b), true)
21 })
22
23 it("not equal when fields differ", () => {
24 const a = new Person({ name: "Bob", age: 25 })
25 const b = new Person({ name: "Bob", age: 26 })
26 assert.strictEqual(Equal.equals(a, b), false)
27 })
28
29 it("supports .pipe()", () => {
30 const p = new Person({ name: "C", age: 1 })
31 const result = p.pipe((x) => x.name)
32 assert.strictEqual(result, "C")
33 })
34
35 it("allows classes without fields to be constructed without args", () => {
36 class Empty extends Data.Class {}
37 const e = new Empty()
38 assert.ok(e instanceof Empty)
39 })
40
41 it("fields are enumerable", () => {
42 const p = new Person({ name: "D", age: 2 })
43 assert.deepStrictEqual(Object.keys(p).sort(), ["age", "name"])
44 })
45 })
46
47 describe("TaggedClass", () => {
48 class Foo extends Data.TaggedClass("Foo")<{ readonly value: number }> {}
49
50 it("sets _tag to the provided tag", () => {
51 const f = new Foo({ value: 1 })
52 assert.strictEqual(f._tag, "Foo")
53 })
54
55 it("assigns fields", () => {
56 const f = new Foo({ value: 42 })
57 assert.strictEqual(f.value, 42)
58 })
59
60 it("structural equality includes _tag", () => {
61 const a = new Foo({ value: 1 })
62 const b = new Foo({ value: 1 })
63 assert.strictEqual(Equal.equals(a, b), true)
64 })
65
66 it("different tags are not equal", () => {
67 class Bar extends Data.TaggedClass("Bar")<{ readonly value: number }> {}
68 const a = new Foo({ value: 1 })
69 const b = new Bar({ value: 1 })
70 assert.strictEqual(Equal.equals(a, b), false)
71 })
72
73 it("allows tagged classes without fields to be constructed without args", () => {
74 class Empty extends Data.TaggedClass("Empty") {}
75 const e = new Empty()
76 assert.strictEqual(e._tag, "Empty")
77 })
78
79 it("supports .pipe()", () => {
80 const f = new Foo({ value: 10 })
81 const result = f.pipe((x) => x._tag)
82 assert.strictEqual(result, "Foo")
83 })
84 })
85
86 describe("taggedEnum", () => {
87 type Shape = Data.TaggedEnum<{
88 Circle: { readonly radius: number }
89 Rect: { readonly width: number; readonly height: number }
90 }>
91 const Shape = Data.taggedEnum<Shape>()
92
93 describe("constructors", () => {
94 it("creates a variant with _tag", () => {
95 const c = Shape.Circle({ radius: 5 })
96 assert.strictEqual(c._tag, "Circle")
97 assert.strictEqual(c.radius, 5)
98 })
99
100 it("creates different variants", () => {
101 const r = Shape.Rect({ width: 3, height: 4 })
102 assert.strictEqual(r._tag, "Rect")
103 assert.strictEqual(r.width, 3)
104 assert.strictEqual(r.height, 4)
105 })
106 })
107
108 describe("$is", () => {
109 it("returns true for matching variant", () => {
110 const c = Shape.Circle({ radius: 1 })
111 assert.strictEqual(Shape.$is("Circle")(c), true)
112 })
113
114 it("returns false for non-matching variant", () => {
115 const r = Shape.Rect({ width: 1, height: 1 })
116 assert.strictEqual(Shape.$is("Circle")(r), false)
117 })
118
119 it("returns false for non-tagged values", () => {
120 assert.strictEqual(Shape.$is("Circle")(null), false)
121 assert.strictEqual(Shape.$is("Circle")(undefined), false)
122 assert.strictEqual(Shape.$is("Circle")("string"), false)
123 assert.strictEqual(Shape.$is("Circle")({}), false)
124 })
125 })
126
127 describe("$match", () => {
128 const area = Shape.$match({
129 Circle: ({ radius }) => Math.PI * radius ** 2,
130 Rect: ({ width, height }) => width * height
131 })
132
133 it("data-last: matches Circle", () => {
134 const c = Shape.Circle({ radius: 5 })
135 assert.strictEqual(area(c), Math.PI * 25)
136 })
137
138 it("data-last: matches Rect", () => {
139 const r = Shape.Rect({ width: 3, height: 4 })
140 assert.strictEqual(area(r), 12)
141 })
142
143 it("data-first: matches directly", () => {
144 const c = Shape.Circle({ radius: 1 })
145 const result = Shape.$match(c, {
146 Circle: ({ radius }) => radius * 2,
147 Rect: () => 0
148 })
149 assert.strictEqual(result, 2)
150 })
151 })
152
153 describe("no-field variant", () => {
154 type Option = Data.TaggedEnum<{
155 None: {}
156 Some: { readonly value: number }
157 }>
158 const Option = Data.taggedEnum<Option>()
159
160 it("accepts void for empty variants", () => {
161 const n = Option.None()
162 assert.strictEqual(n._tag, "None")
163 })
164
165 it("works with $match for empty variants", () => {
166 const show = Option.$match({
167 None: () => "none",
168 Some: ({ value }) => `some(${value})`
169 })
170 assert.strictEqual(show(Option.None()), "none")
171 assert.strictEqual(show(Option.Some({ value: 42 })), "some(42)")
172 })
173 })
174 })
175
176 describe("Error", () => {
177 class MyError extends Data.Error<{ readonly code: number; readonly message: string }> {}
178
179 it("is an instance of globalThis.Error", () => {
180 const e = new MyError({ code: 500, message: "fail" })
181 assert.ok(e instanceof globalThis.Error)
182 })
183
184 it("assigns fields", () => {
185 const e = new MyError({ code: 404, message: "not found" })
186 assert.strictEqual(e.code, 404)
187 assert.strictEqual(e.message, "not found")
188 })
189
190 it("has a stack trace", () => {
191 const e = new MyError({ code: 1, message: "x" })
192 assert.ok(typeof e.stack === "string")
193 assert.ok(e.stack!.length > 0)
194 })
195
196 it("allows errors without fields to be constructed without args", () => {
197 class Empty extends Data.Error {}
198 const e = new Empty()
199 assert.ok(e instanceof globalThis.Error)
200 })
201
202 it("can be yielded in Effect.gen to fail", () =>
203 Effect.gen(function*() {
204 const exit = yield* Effect.exit(
205 Effect.gen(function*() {
206 return yield* new MyError({ code: 1, message: "boom" })
207 })
208 )
209 assert.ok(exit._tag === "Failure")
210 }).pipe(Effect.runPromise))
211
212 it("toJSON includes fields", () => {
213 const e = new MyError({ code: 1, message: "test" })
214 const json = (e as any).toJSON()
215 assert.strictEqual(json.code, 1)
216 assert.strictEqual(json.message, "test")
217 })
218 })
219
220 describe("TaggedError", () => {
221 class NotFound extends Data.TaggedError("NotFound")<{
222 readonly resource: string
223 }> {}
224
225 it("sets _tag", () => {
226 const e = new NotFound({ resource: "/x" })
227 assert.strictEqual(e._tag, "NotFound")
228 })
229
230 it("assigns fields", () => {
231 const e = new NotFound({ resource: "/users" })
232 assert.strictEqual(e.resource, "/users")
233 })
234
235 it("is an instance of globalThis.Error", () => {
236 const e = new NotFound({ resource: "r" })
237 assert.ok(e instanceof globalThis.Error)
238 })
239
240 it("can be caught with Effect.catchTag", () =>
241 Effect.gen(function*() {
242 const result = yield* Effect.gen(function*() {
243 return yield* new NotFound({ resource: "/test" })
244 }).pipe(
245 Effect.catchTag("NotFound", (e) => Effect.succeed(`caught: ${e.resource}`))
246 )
247 assert.strictEqual(result, "caught: /test")
248 }).pipe(Effect.runPromise))
249
250 it("allows tagged errors without fields to be constructed without args", () => {
251 class Boom extends Data.TaggedError("Boom") {}
252 const e = new Boom()
253 assert.strictEqual(e._tag, "Boom")
254 assert.ok(e instanceof globalThis.Error)
255 })
256
257 it("sets .name to the tag", () => {
258 const e = new NotFound({ resource: "x" })
259 assert.strictEqual(e.name, "NotFound")
260 })
261
262 it("different tags produce non-equal errors", () => {
263 class Forbidden extends Data.TaggedError("Forbidden")<{ readonly resource: string }> {}
264 const a = new NotFound({ resource: "x" })
265 const b = new Forbidden({ resource: "x" })
266 assert.strictEqual(Equal.equals(a, b), false)
267 })
268 })
269})