1import { assertFalse, assertNone, assertSome, assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils"
2import { Equivalence, Number as Num, Option, Record, Result } from "effect"
3import { pipe } from "effect/Function"
4import { describe, it } from "vitest"
5
6describe("Record", () => {
7 it("singleton", () => {
8 deepStrictEqual(Record.singleton("a", 1), { a: 1 })
9 deepStrictEqual(Record.singleton("__proto__", 1), { ["__proto__"]: 1 })
10 })
11
12 it("makeReducerUnion", () => {
13 const R = Record.makeReducerUnion(Num.ReducerSum)
14
15 deepStrictEqual(R.combine({ a: 1, b: 2 }, { a: 3, b: 4, c: 5 }), { a: 4, b: 6, c: 5 })
16 })
17
18 it("makeReducerIntersection", () => {
19 const R = Record.makeReducerIntersection(Num.ReducerSum)
20
21 deepStrictEqual(R.combine({ a: 1, b: 2 }, { a: 3, b: 4, c: 5 }), { a: 4, b: 6 })
22 })
23})
24
25const symA = Symbol.for("a")
26const symB = Symbol.for("b")
27const symC = Symbol.for("c")
28
29const stringRecord: Record<string, number> = { a: 1, [symA]: null }
30const symbolRecord: Record<symbol, number> = { [symA]: 1, [symB]: 2 }
31
32describe("Record", () => {
33 describe("string | symbol APIs", () => {
34 it("empty", () => {
35 deepStrictEqual(Record.empty(), {})
36 })
37
38 it("fromIterableWith", () => {
39 deepStrictEqual(Record.fromIterableWith([1, 2, 3, 4], (a) => [a === 3 ? "a" : String(a), a * 2]), {
40 "1": 2,
41 "2": 4,
42 a: 6,
43 "4": 8
44 })
45 deepStrictEqual(Record.fromIterableWith([1, 2, 3, 4], (a) => [a === 3 ? symA : String(a), a * 2]), {
46 "1": 2,
47 "2": 4,
48 [symA]: 6,
49 "4": 8
50 })
51 })
52
53 it("fromIterableBy", () => {
54 const users = [
55 { id: "2", name: "name2" },
56 { id: "1", name: "name1" }
57 ]
58 deepStrictEqual(Record.fromIterableBy(users, (user) => user.id), {
59 "2": { id: "2", name: "name2" },
60 "1": { id: "1", name: "name1" }
61 })
62
63 deepStrictEqual(Record.fromIterableBy(["a", symA], (s) => s), { a: "a", [symA]: symA })
64
65 deepStrictEqual(pipe(users, Record.fromIterableBy((user) => user.id)), {
66 "2": { id: "2", name: "name2" },
67 "1": { id: "1", name: "name1" }
68 })
69 })
70
71 it("fromEntries", () => {
72 deepStrictEqual(Record.fromEntries([["1", 2], ["2", 4], ["3", 6], ["4", 8]]), {
73 "1": 2,
74 "2": 4,
75 "3": 6,
76 "4": 8
77 })
78 deepStrictEqual(Record.fromEntries([["1", 2], ["2", 4], ["3", 6], ["4", 8], [symA, 10], [symB, 12]]), {
79 "1": 2,
80 "2": 4,
81 "3": 6,
82 "4": 8,
83 [symA]: 10,
84 [symB]: 12
85 })
86 })
87
88 it("has", () => {
89 assertTrue(Record.has(stringRecord, "a"))
90 assertFalse(Record.has(stringRecord, "c"))
91
92 assertTrue(Record.has(symbolRecord, symA))
93 assertFalse(Record.has(symbolRecord, symC))
94 })
95
96 it("get", () => {
97 assertNone(pipe(Record.empty<string>(), Record.get("a")))
98 assertSome(pipe(stringRecord, Record.get("a")), 1)
99
100 assertNone(pipe(Record.empty<symbol>(), Record.get(symA)))
101 assertSome(pipe(symbolRecord, Record.get(symA)), 1)
102 })
103
104 it("modify", () => {
105 assertNone(pipe(Record.empty<string>(), Record.modify("a", (n) => n + 1)))
106 assertSome(pipe(stringRecord, Record.modify("a", (n: number) => n + 1)), { a: 2, [symA]: null })
107 assertSome(pipe(stringRecord, Record.modify("a", (n: number) => String(n))), { a: "1", [symA]: null })
108
109 assertNone(pipe(Record.empty<symbol>(), Record.modify(symA, (n) => n + 1)))
110 assertSome(pipe(symbolRecord, Record.modify(symA, (n: number) => n + 1)), { [symA]: 2, [symB]: 2 })
111 assertSome(
112 pipe(symbolRecord, Record.modify(symA, (n: number) => String(n))),
113 { [symA]: "1", [symB]: 2 }
114 )
115 })
116
117 it("replace", () => {
118 assertNone(pipe(Record.empty<string>(), Record.replace("a", 2)))
119 assertSome(pipe(stringRecord, Record.replace("a", 2)), { a: 2, [symA]: null })
120 assertSome(pipe(stringRecord, Record.replace("a", true)), { a: true, [symA]: null })
121
122 assertNone(pipe(Record.empty<symbol>(), Record.replace(symA, 2)))
123 assertSome(pipe(symbolRecord, Record.replace(symA, 2)), { [symA]: 2, [symB]: 2 })
124 assertSome(pipe(symbolRecord, Record.replace(symA, true)), { [symA]: true, [symB]: 2 })
125 })
126
127 it("remove", () => {
128 deepStrictEqual(Record.remove(stringRecord, "a"), { [symA]: null })
129 deepStrictEqual(Record.remove(stringRecord, "c"), stringRecord)
130
131 deepStrictEqual(Record.remove(symbolRecord, symA), { [symB]: 2 })
132 deepStrictEqual(Record.remove(symbolRecord, symC), symbolRecord)
133 })
134
135 describe("pop", () => {
136 it("should return the value associated with the given key, if the key is present in the record", () => {
137 const result1 = Record.pop(stringRecord, "a")
138 assertSome(result1, [1, { [symA]: null }])
139
140 const result2 = Record.pop(symbolRecord, symA)
141 assertSome(result2, [1, { [symB]: 2 }])
142 })
143
144 it("should return none if the key is not present in the record", () => {
145 const result1 = Record.pop(stringRecord, "c")
146 assertNone(result1)
147
148 const result2 = Record.pop(symbolRecord, symC)
149 assertNone(result2)
150 })
151 })
152
153 describe("set", () => {
154 it("should replace an existing value", () => {
155 deepStrictEqual(Record.set(stringRecord, "a", 2), { a: 2, [symA]: null })
156
157 deepStrictEqual(Record.set(symbolRecord, symA, 2), { [symA]: 2, [symB]: 2 })
158 })
159
160 it("should add the key / value pair", () => {
161 deepStrictEqual(Record.set(stringRecord, "c", 3), { a: 1, [symA]: null, c: 3 })
162
163 deepStrictEqual(Record.set(symbolRecord, symC, 3), { [symA]: 1, [symB]: 2, [symC]: 3 })
164 })
165 })
166
167 describe("assignProperty", () => {
168 it("preserves __proto__ as an own property", () => {
169 const record: Record<string, unknown> = {}
170 const prototype = Object.getPrototypeOf(record)
171 const value = { polluted: true }
172
173 Record.assignProperty(record, "__proto__", value)
174
175 strictEqual(Object.getPrototypeOf(record), prototype)
176 assertTrue(Object.hasOwn(record, "__proto__"))
177 strictEqual(record["__proto__"], value)
178 })
179 })
180
181 it("singleton", () => {
182 deepStrictEqual(Record.singleton("a", 1), { a: 1 })
183
184 deepStrictEqual(Record.singleton(symA, 1), { [symA]: 1 })
185 })
186 })
187
188 describe("string only APIs", () => {
189 it("map", () => {
190 deepStrictEqual(pipe(stringRecord, Record.map((n) => n * 2)), { a: 2, [symA]: null })
191 deepStrictEqual(pipe(stringRecord, Record.map((n, k) => `${k}-${n}`)), { a: "a-1", [symA]: null })
192 })
193
194 it("collect", () => {
195 const x = { a: 1, b: 2, c: 3, [symA]: null }
196 deepStrictEqual(Record.collect(x, (key, n) => [key, n]), [["a", 1], ["b", 2], ["c", 3]])
197 })
198
199 it("toEntries", () => {
200 const x = { a: 1, b: 2, c: 3, [symA]: null }
201 deepStrictEqual(Record.toEntries(x), [["a", 1], ["b", 2], ["c", 3]])
202 })
203
204 it("filterMap", () => {
205 const x: Record<string, number> = { a: 1, b: 2, c: 3, [symA]: null }
206 const filtered = Record.filterMap(x, (value, key) => (value > 2 ? Result.succeed(key) : Result.failVoid))
207 deepStrictEqual(filtered, { c: "c" })
208 })
209
210 it("getSomes", () => {
211 const x = { a: Option.some(1), b: Option.none(), c: Option.some(2), [symA]: null }
212 deepStrictEqual(Record.getSomes(x), { a: 1, c: 2 })
213 })
214
215 it("filter", () => {
216 const x: Record<string, number> = { a: 1, b: 2, c: 3, d: 4, [symA]: null }
217 deepStrictEqual(Record.filter(x, (value) => value > 2), { c: 3, d: 4 })
218 })
219
220 it("partition", () => {
221 const f = (n: number) => (n > 2 ? Result.succeed(n + 1) : Result.fail(n - 1))
222 deepStrictEqual(Record.partition({}, f), [{}, {}])
223 deepStrictEqual(Record.partition({ a: 1, b: 3, [symA]: null }, f), [{ b: 4 }, { a: 0 }])
224 })
225
226 it("separate", () => {
227 deepStrictEqual(
228 Record.separate({ a: Result.fail("e"), b: Result.succeed(1), [symA]: null }),
229 [{ b: 1 }, { a: "e" }]
230 )
231 // should ignore non own properties
232 const o: Record.ReadonlyRecord<"a", Result.Result<number, string>> = Object.create({ a: 1 })
233 deepStrictEqual(pipe(o, Record.separate), [{}, {}])
234 })
235
236 it("isEmptyRecord", () => {
237 deepStrictEqual(Record.isEmptyRecord({}), true)
238 deepStrictEqual(Record.isEmptyRecord({ [symA]: null }), true)
239 deepStrictEqual(Record.isEmptyRecord({ a: 3 }), false)
240 })
241
242 it("isEmptyReadonlyRecord", () => {
243 deepStrictEqual(Record.isEmptyReadonlyRecord({}), true)
244 deepStrictEqual(Record.isEmptyReadonlyRecord({ [symA]: null }), true)
245 deepStrictEqual(Record.isEmptyReadonlyRecord({ a: 3 }), false)
246 })
247
248 it("size", () => {
249 deepStrictEqual(Record.size({ a: "a", b: 1, c: true, [symA]: null }), 3)
250 })
251
252 it("keys", () => {
253 deepStrictEqual(Record.keys({ a: 1, b: 2, [symA]: null }), ["a", "b"])
254 })
255
256 it("values", () => {
257 deepStrictEqual(Record.values({ a: 1, b: 2, [symA]: null }), [1, 2])
258 })
259
260 it("isSubrecord", () => {
261 assertTrue(Record.isSubrecord(Record.empty(), {}))
262 assertTrue(Record.isSubrecord(Record.empty<string>(), { a: 1 }))
263 assertTrue(Record.isSubrecord({ a: 1 }, { a: 1 }))
264 assertTrue(Record.isSubrecord(stringRecord, { a: 1 }))
265 assertTrue(Record.isSubrecord({ a: 1 }, stringRecord))
266 assertTrue(Record.isSubrecord({ a: 1 } as Record<string, number>, { a: 1, b: 2 }))
267 assertTrue(Record.isSubrecord({ b: 2, a: 1 }, { a: 1, b: 2 }))
268 assertFalse(Record.isSubrecord({ a: 1 }, { a: 2 }))
269 assertFalse(Record.isSubrecord({ b: 2 } as Record<string, number>, { a: 1 }))
270 })
271
272 it("reduce", () => {
273 // data-first
274 deepStrictEqual(
275 Record.reduce({ k1: "a", k2: "b", [symA]: null }, "-", (accumulator, value, key) => accumulator + key + value),
276 "-k1ak2b"
277 )
278 // data-last
279 deepStrictEqual(
280 pipe(
281 { k1: "a", k2: "b", [symA]: null },
282 Record.reduce("-", (accumulator, value, key) => accumulator + key + value)
283 ),
284 "-k1ak2b"
285 )
286 })
287
288 it("every", () => {
289 assertTrue(Record.every((n: number) => n <= 2)({ a: 1, b: 2, [symA]: null }))
290 assertFalse(Record.every((n: number) => n <= 1)({ a: 1, b: 2, [symA]: null }))
291 })
292
293 it("some", () => {
294 assertTrue(Record.some((n: number) => n <= 1)({ a: 1, b: 2, [symA]: null }))
295 assertFalse(Record.some((n: number) => n <= 0)({ a: 1, b: 2, [symA]: null }))
296 })
297
298 it("union", () => {
299 const combine = (s1: string, s2: string) => s1 + s2
300 const x: Record.ReadonlyRecord<string, string> = {
301 a: "a1",
302 b: "b1",
303 c: "c1",
304 [symA]: null
305 }
306 const y: Record.ReadonlyRecord<string, string> = {
307 b: "b2",
308 c: "c2",
309 d: "d2",
310 [symA]: null
311 }
312 deepStrictEqual(Record.union(x, {}, combine), x)
313 deepStrictEqual(Record.union({}, x, combine), x)
314 deepStrictEqual(Record.union(x, {}, combine), x)
315 deepStrictEqual(Record.union({}, x, combine), x)
316 deepStrictEqual(Record.union(x, y, combine), {
317 a: "a1",
318 b: "b1b2",
319 c: "c1c2",
320 d: "d2"
321 })
322 })
323
324 it("intersection", () => {
325 const combine = (s1: string, s2: string) => s1 + s2
326 const x: Record.ReadonlyRecord<string, string> = {
327 a: "a1",
328 b: "b1",
329 c: "c1",
330 [symA]: null
331 }
332 const y: Record.ReadonlyRecord<string, string> = {
333 b: "b2",
334 c: "c2",
335 d: "d2",
336 [symA]: null
337 }
338 deepStrictEqual(Record.intersection(x, {}, combine), {})
339 deepStrictEqual(Record.intersection({}, y, combine), {})
340 deepStrictEqual(Record.intersection(x, y, combine), {
341 b: "b1b2",
342 c: "c1c2"
343 })
344 })
345
346 it("difference", () => {
347 const x: Record.ReadonlyRecord<string, string> = {
348 a: "a1",
349 b: "b1",
350 c: "c1",
351 [symA]: null
352 }
353 const y: Record.ReadonlyRecord<string, string> = {
354 b: "b2",
355 c: "c2",
356 d: "d2",
357 [symA]: null
358 }
359 deepStrictEqual(Record.difference({}, x), x)
360 deepStrictEqual(Record.difference(x, {}), x)
361 deepStrictEqual(Record.difference({}, x), x)
362 deepStrictEqual(Record.difference(x, {}), x)
363 deepStrictEqual(Record.difference(x, y), {
364 a: "a1",
365 d: "d2"
366 })
367 })
368
369 it("makeEquivalence", () => {
370 deepStrictEqual(Record.makeEquivalence(Equivalence.strictEqual<number>())({ a: 1 }, { a: 1 }), true)
371 deepStrictEqual(Record.makeEquivalence(Equivalence.strictEqual<number>())({ a: 1 }, stringRecord), true)
372 deepStrictEqual(Record.makeEquivalence(Equivalence.strictEqual<number>())({ a: 1 }, { a: 2 }), false)
373 deepStrictEqual(Record.makeEquivalence(Equivalence.strictEqual<number>())({ a: 1 }, { b: 1 }), false)
374 const noPrototypeObject = Object.create(null)
375 deepStrictEqual(Record.makeEquivalence(Equivalence.strictEqual<number>())(noPrototypeObject, { b: 1 }), false)
376 })
377
378 it("mapKeys", () => {
379 deepStrictEqual(pipe({ a: 1, b: 2, [symA]: null }, Record.mapKeys((key) => key.toUpperCase())), {
380 A: 1,
381 B: 2
382 })
383 })
384
385 it("mapEntries", () => {
386 deepStrictEqual(pipe(stringRecord, Record.mapEntries((a, key) => [key.toUpperCase(), a + 1])), { A: 2 })
387 })
388
389 describe("findFirst", () => {
390 it("refinement/predicate", () => {
391 const record = {
392 a: 1,
393 b: 2,
394 c: 1
395 }
396 deepStrictEqual(
397 pipe(record, Record.findFirst((v) => v < 2)),
398 Option.some(["a", 1])
399 )
400 deepStrictEqual(
401 pipe(record, Record.findFirst((v, k) => v < 2 && k !== "a")),
402 Option.some(["c", 1])
403 )
404 assertNone(
405 pipe(record, Record.findFirst((v) => v > 2))
406 )
407 deepStrictEqual(
408 Record.findFirst(record, (v) => v < 2),
409 Option.some(["a", 1])
410 )
411 })
412 })
413 })
414})