| 1 | import { assertFalse, assertNone, assertSome, assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" |
| 2 | import { Equivalence, Number as Num, Option, Record, Result } from "effect" |
| 3 | import { pipe } from "effect/Function" |
| 4 | import { describe, it } from "vitest" |
| 5 | |
| 6 | describe("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 | |
| 25 | const symA = Symbol.for("a") |
| 26 | const symB = Symbol.for("b") |
| 27 | const symC = Symbol.for("c") |
| 28 | |
| 29 | const stringRecord: Record<string, number> = { a: 1, [symA]: null } |
| 30 | const symbolRecord: Record<symbol, number> = { [symA]: 1, [symB]: 2 } |
| 31 | |
| 32 | describe("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 | }) |