| 1 | import { Optic, Option, Result, Schema, SchemaIssue } from "effect" |
| 2 | import { describe, it } from "vitest" |
| 3 | import { assertSuccess, assertTrue, deepStrictEqual, strictEqual, throws } from "./utils/assert.ts" |
| 4 | |
| 5 | const addOne = (n: number) => n + 1 |
| 6 | |
| 7 | function getFailure<A>(result: Result.Result<A, SchemaIssue.Issue>): SchemaIssue.Issue { |
| 8 | assertTrue(Result.isFailure(result)) |
| 9 | return result.failure |
| 10 | } |
| 11 | |
| 12 | function assertFailureMessage<A>(result: Result.Result<A, SchemaIssue.Issue>, message: string) { |
| 13 | strictEqual(SchemaIssue.defaultFormatter(getFailure(result)), message) |
| 14 | } |
| 15 | |
| 16 | function assertFailureIssue<A>(result: Result.Result<A, SchemaIssue.Issue>, issue: SchemaIssue.Issue) { |
| 17 | strictEqual(getFailure(result), issue) |
| 18 | } |
| 19 | |
| 20 | function assertPointerFailure<A>( |
| 21 | result: Result.Result<A, SchemaIssue.Issue>, |
| 22 | path: ReadonlyArray<PropertyKey> |
| 23 | ): SchemaIssue.Issue { |
| 24 | const failure = getFailure(result) |
| 25 | assertTrue(failure._tag === "Pointer") |
| 26 | deepStrictEqual(failure.path, path) |
| 27 | return failure.issue |
| 28 | } |
| 29 | |
| 30 | function assertMissingKeyFailure<A>(result: Result.Result<A, SchemaIssue.Issue>, key: PropertyKey) { |
| 31 | assertTrue(assertPointerFailure(result, [key])._tag === "MissingKey") |
| 32 | } |
| 33 | |
| 34 | describe("Optic", () => { |
| 35 | it("replace should throw an error if the object has a non-Object constructor or null prototype", () => { |
| 36 | { |
| 37 | class A { |
| 38 | readonly x: number |
| 39 | constructor(x: number) { |
| 40 | this.x = x |
| 41 | } |
| 42 | } |
| 43 | const optic = Optic.id<A>().key("x") |
| 44 | throws(() => optic.replace(2, new A(1)), "Cannot clone object with non-Object constructor or null prototype") |
| 45 | } |
| 46 | |
| 47 | // null prototype is allowed |
| 48 | { |
| 49 | type S = { readonly a: number } |
| 50 | const obj = Object.create(null) |
| 51 | obj.a = 1 |
| 52 | const optic = Optic.id<S>().key("a") |
| 53 | deepStrictEqual(optic.replace(2, obj), { a: 2 }) |
| 54 | } |
| 55 | }) |
| 56 | |
| 57 | it("preserves __proto__ as an own property", () => { |
| 58 | const value = { polluted: true } |
| 59 | const optics: ReadonlyArray<readonly [Optic.Optional<any, any>, object]> = [ |
| 60 | [Optic.id<Record<string, unknown>>().key("__proto__"), {}], |
| 61 | [Optic.id<Record<string, unknown>>().optionalKey("__proto__"), {}], |
| 62 | [Optic.id<Record<string, unknown>>().at("__proto__"), { ["__proto__"]: 0 }] |
| 63 | ] |
| 64 | |
| 65 | for (const [optic, source] of optics) { |
| 66 | const out = optic.replace(value, source) |
| 67 | strictEqual(Object.getPrototypeOf(out), Object.prototype) |
| 68 | assertTrue(Object.hasOwn(out, "__proto__")) |
| 69 | strictEqual(out["__proto__"], value) |
| 70 | } |
| 71 | }) |
| 72 | |
| 73 | it("id", () => { |
| 74 | const iso = Optic.id<number>() |
| 75 | |
| 76 | strictEqual(iso.get(1), 1) |
| 77 | strictEqual(iso.set(1), 1) |
| 78 | strictEqual(iso.modify(addOne)(1), 2) |
| 79 | }) |
| 80 | |
| 81 | describe("standalone functions", () => { |
| 82 | const lens = Optic.id<{ readonly value: number }>().key("value") |
| 83 | const optional = Optic.id<Record<string, number>>().at("value") |
| 84 | const prism = Optic.some<number>() |
| 85 | const traversal = Optic.id<ReadonlyArray<number>>().forEach((item) => item) |
| 86 | |
| 87 | it("supports data-first usage", () => { |
| 88 | strictEqual(Optic.get({ value: 1 }, lens), 1) |
| 89 | assertSuccess(Optic.getResult({ value: 1 }, optional), 1) |
| 90 | deepStrictEqual(Optic.set(1, prism), Option.some(1)) |
| 91 | deepStrictEqual(Optic.replace({ value: 1 }, lens, 2), { value: 2 }) |
| 92 | assertSuccess(Optic.replaceResult({ value: 1 }, optional, 2), { value: 2 }) |
| 93 | deepStrictEqual(Optic.modify({ value: 1 }, lens, addOne), { value: 2 }) |
| 94 | deepStrictEqual(Optic.getAll([1, 2, 3], traversal), [1, 2, 3]) |
| 95 | deepStrictEqual(Optic.modifyAll([1, 2, 3], traversal, addOne), [2, 3, 4]) |
| 96 | }) |
| 97 | |
| 98 | it("supports data-last usage", () => { |
| 99 | strictEqual(Optic.get(lens)({ value: 1 }), 1) |
| 100 | assertSuccess(Optic.getResult(optional)({ value: 1 }), 1) |
| 101 | deepStrictEqual(Optic.set(prism)(1), Option.some(1)) |
| 102 | deepStrictEqual(Optic.replace(lens, 2)({ value: 1 }), { value: 2 }) |
| 103 | assertSuccess(Optic.replaceResult(optional, 2)({ value: 1 }), { value: 2 }) |
| 104 | deepStrictEqual(Optic.modify(lens, addOne)({ value: 1 }), { value: 2 }) |
| 105 | deepStrictEqual(Optic.getAll(traversal)([1, 2, 3]), [1, 2, 3]) |
| 106 | deepStrictEqual(Optic.modifyAll(traversal, addOne)([1, 2, 3]), [2, 3, 4]) |
| 107 | }) |
| 108 | }) |
| 109 | |
| 110 | describe("compose", () => { |
| 111 | it("sets through composed isos without reading a source", () => { |
| 112 | const value = Optic.makeIso<{ readonly value: number }, number>( |
| 113 | (s) => s.value, |
| 114 | (value) => ({ value }) |
| 115 | ) |
| 116 | const text = Optic.makeIso<number, string>( |
| 117 | String, |
| 118 | Number |
| 119 | ) |
| 120 | const optic = value.compose(text) |
| 121 | |
| 122 | deepStrictEqual(optic.set("2"), { value: 2 }) |
| 123 | deepStrictEqual(optic.replace("2", { value: 1 }), { value: 2 }) |
| 124 | }) |
| 125 | |
| 126 | it("sets through composed prisms without reading a source", () => { |
| 127 | const value = Optic.makePrism<{ readonly value: number }, number>( |
| 128 | (s) => Result.succeed(s.value), |
| 129 | (value) => ({ value }) |
| 130 | ) |
| 131 | const text = Optic.makePrism<number, string>( |
| 132 | (n) => Result.succeed(String(n)), |
| 133 | Number |
| 134 | ) |
| 135 | const optic = value.compose(text) |
| 136 | |
| 137 | deepStrictEqual(optic.set("2"), { value: 2 }) |
| 138 | }) |
| 139 | |
| 140 | it("preserves capabilities and behavior across every optic kind", () => { |
| 141 | const optics = { |
| 142 | Iso: Optic.makeIso<number, number>((n) => n, (n) => n), |
| 143 | Lens: Optic.makeLens<number, number>((n) => n, (n) => n), |
| 144 | Prism: Optic.makePrism<number, number>(Result.succeed, (n) => n), |
| 145 | Optional: Optic.makeOptional<number, number>(Result.succeed, (n) => Result.succeed(n)) |
| 146 | } as const |
| 147 | const expected = { |
| 148 | Iso: { Iso: "Iso", Lens: "Lens", Prism: "Prism", Optional: "Optional" }, |
| 149 | Lens: { Iso: "Lens", Lens: "Lens", Prism: "Optional", Optional: "Optional" }, |
| 150 | Prism: { Iso: "Prism", Lens: "Optional", Prism: "Prism", Optional: "Optional" }, |
| 151 | Optional: { Iso: "Optional", Lens: "Optional", Prism: "Optional", Optional: "Optional" } |
| 152 | } as const |
| 153 | |
| 154 | for (const left of Object.keys(optics) as Array<keyof typeof optics>) { |
| 155 | for (const right of Object.keys(optics) as Array<keyof typeof optics>) { |
| 156 | const optic: any = optics[left].compose(optics[right] as any) |
| 157 | const kind = expected[left][right] |
| 158 | |
| 159 | strictEqual("get" in optic, kind === "Iso" || kind === "Lens") |
| 160 | strictEqual("set" in optic, kind === "Iso" || kind === "Prism") |
| 161 | assertSuccess(optic.getResult(1), 1) |
| 162 | assertSuccess(optic.replaceResult(2, 1), 2) |
| 163 | } |
| 164 | } |
| 165 | }) |
| 166 | |
| 167 | it("preserves optional setter failures", () => { |
| 168 | const issue = new SchemaIssue.InvalidValue({ message: "cannot replace" }) |
| 169 | const optic = Optic.makeOptional<number, number>( |
| 170 | Result.succeed, |
| 171 | () => Result.fail(issue) |
| 172 | ).compose(Optic.makeIso<number, number>((n) => n, (n) => n)) |
| 173 | |
| 174 | assertFailureIssue(optic.replaceResult(2, 1), issue) |
| 175 | strictEqual(optic.replace(2, 1), 1) |
| 176 | strictEqual(optic.modify(addOne)(1), 1) |
| 177 | }) |
| 178 | |
| 179 | it("composes identity in both directions", () => { |
| 180 | const value = Optic.makeIso<number, string>(String, Number) |
| 181 | const left = Optic.id<number>().compose(value) |
| 182 | const right = value.compose(Optic.id<string>()) |
| 183 | const identity = Optic.id<number>().compose(Optic.id<number>()) |
| 184 | |
| 185 | strictEqual(left.get(1), "1") |
| 186 | strictEqual(right.get(1), "1") |
| 187 | strictEqual(left.set("2"), 2) |
| 188 | strictEqual(right.set("2"), 2) |
| 189 | strictEqual(identity.get(1), 1) |
| 190 | strictEqual(identity.set(2), 2) |
| 191 | }) |
| 192 | |
| 193 | it("is independent of composition grouping", () => { |
| 194 | const wrapped = Optic.makeIso<{ readonly value: number }, number>( |
| 195 | (s) => s.value, |
| 196 | (value) => ({ value }) |
| 197 | ) |
| 198 | const text = Optic.makeIso<number, string>(String, Number) |
| 199 | const chars = Optic.makeIso<string, ReadonlyArray<string>>( |
| 200 | (s) => [...s], |
| 201 | (chars) => chars.join("") |
| 202 | ) |
| 203 | const left = wrapped.compose(text).compose(chars) |
| 204 | const right = wrapped.compose(text.compose(chars)) |
| 205 | |
| 206 | deepStrictEqual(left.get({ value: 12 }), ["1", "2"]) |
| 207 | deepStrictEqual(right.get({ value: 12 }), ["1", "2"]) |
| 208 | deepStrictEqual(left.set(["3", "4"]), { value: 34 }) |
| 209 | deepStrictEqual(right.set(["3", "4"]), { value: 34 }) |
| 210 | }) |
| 211 | }) |
| 212 | |
| 213 | describe("key", () => { |
| 214 | describe("Struct", () => { |
| 215 | it("required key", () => { |
| 216 | type S = { readonly a: number } |
| 217 | const optic = Optic.id<S>().key("a") |
| 218 | |
| 219 | strictEqual(optic.get({ a: 1 }), 1) |
| 220 | deepStrictEqual(optic.replace(2, { a: 1 }), { a: 2 }) |
| 221 | deepStrictEqual(optic.modify(addOne)({ a: 1 }), { a: 2 }) |
| 222 | }) |
| 223 | |
| 224 | it("nested required keys", () => { |
| 225 | type S = { |
| 226 | readonly a: { |
| 227 | readonly b: number |
| 228 | } |
| 229 | } |
| 230 | const optic = Optic.id<S>().key("a").key("b") |
| 231 | |
| 232 | strictEqual(optic.get({ a: { b: 1 } }), 1) |
| 233 | deepStrictEqual(optic.replace(2, { a: { b: 1 } }), { a: { b: 2 } }) |
| 234 | deepStrictEqual(optic.modify(addOne)({ a: { b: 1 } }), { a: { b: 2 } }) |
| 235 | }) |
| 236 | |
| 237 | it("optional key (with undefined)", () => { |
| 238 | type S = { readonly a?: number | undefined } |
| 239 | const optic = Optic.id<S>().key("a") |
| 240 | const f = (n: number | undefined) => n !== undefined ? n + 1 : undefined |
| 241 | |
| 242 | strictEqual(optic.get({ a: 1 }), 1) |
| 243 | strictEqual(optic.get({}), undefined) |
| 244 | strictEqual(optic.get({ a: undefined }), undefined) |
| 245 | deepStrictEqual(optic.replace(2, { a: 1 }), { a: 2 }) |
| 246 | deepStrictEqual(optic.replace(2, {}), { a: 2 }) |
| 247 | deepStrictEqual(optic.replace(2, { a: undefined }), { a: 2 }) |
| 248 | deepStrictEqual(optic.replace(undefined, { a: 1 }), { a: undefined }) |
| 249 | deepStrictEqual(optic.replace(undefined, {}), { a: undefined }) |
| 250 | deepStrictEqual(optic.modify(f)({ a: 1 }), { a: 2 }) |
| 251 | deepStrictEqual(optic.modify(f)({}), { a: undefined }) |
| 252 | deepStrictEqual(optic.modify(f)({ a: undefined }), { a: undefined }) |
| 253 | }) |
| 254 | }) |
| 255 | |
| 256 | describe("Tuple", () => { |
| 257 | it("required element", () => { |
| 258 | type S = readonly [number] |
| 259 | const optic = Optic.id<S>().key(0) |
| 260 | |
| 261 | strictEqual(optic.get([1]), 1) |
| 262 | deepStrictEqual(optic.replace(2, [1]), [2]) |
| 263 | deepStrictEqual(optic.modify(addOne)([1]), [2]) |
| 264 | }) |
| 265 | |
| 266 | it("nested required element", () => { |
| 267 | type S = readonly [string, readonly [number]] |
| 268 | const optic = Optic.id<S>().key(1).key(0) |
| 269 | |
| 270 | strictEqual(optic.get(["a", [1]]), 1) |
| 271 | deepStrictEqual(optic.replace(2, ["a", [1]]), ["a", [2]]) |
| 272 | deepStrictEqual(optic.modify(addOne)(["a", [1]]), ["a", [2]]) |
| 273 | }) |
| 274 | |
| 275 | it("optional element (with undefined)", () => { |
| 276 | type S = readonly [number, (number | undefined)?] |
| 277 | const optic = Optic.id<S>().key(1) |
| 278 | const f = (n: number | undefined) => n !== undefined ? n + 1 : undefined |
| 279 | |
| 280 | strictEqual(optic.get([1, 2]), 2) |
| 281 | strictEqual(optic.get([1]), undefined) |
| 282 | strictEqual(optic.get([1, undefined]), undefined) |
| 283 | deepStrictEqual(optic.replace(2, [1, 2]), [1, 2]) |
| 284 | deepStrictEqual(optic.replace(2, [1]), [1, 2]) |
| 285 | deepStrictEqual(optic.replace(2, [1, undefined]), [1, 2]) |
| 286 | deepStrictEqual(optic.replace(undefined, [1, 2]), [1, undefined]) |
| 287 | deepStrictEqual(optic.replace(undefined, [1]), [1, undefined]) |
| 288 | deepStrictEqual(optic.modify(f)([1, 2]), [1, 3]) |
| 289 | deepStrictEqual(optic.modify(f)([1]), [1, undefined]) |
| 290 | deepStrictEqual(optic.modify(f)([1, undefined]), [1, undefined]) |
| 291 | }) |
| 292 | }) |
| 293 | |
| 294 | it("Mixed structs and tuples", () => { |
| 295 | type S = { |
| 296 | readonly a: readonly [number] |
| 297 | } |
| 298 | const optic = Optic.id<S>().key("a").key(0) |
| 299 | |
| 300 | strictEqual(optic.get({ a: [1] }), 1) |
| 301 | deepStrictEqual(optic.replace(2, { a: [1] }), { a: [2] }) |
| 302 | deepStrictEqual(optic.modify(addOne)({ a: [1] }), { a: [2] }) |
| 303 | }) |
| 304 | }) |
| 305 | |
| 306 | describe("optionalKey", () => { |
| 307 | describe("Struct", () => { |
| 308 | it("exact optional key (without undefined)", () => { |
| 309 | type S = { readonly a?: number } |
| 310 | const optic = Optic.id<S>().optionalKey("a") |
| 311 | const f = (n: number | undefined) => n !== undefined ? n + 1 : undefined |
| 312 | |
| 313 | strictEqual(optic.get({ a: 1 }), 1) |
| 314 | strictEqual(optic.get({}), undefined) |
| 315 | deepStrictEqual(optic.replace(2, { a: 1 }), { a: 2 }) |
| 316 | deepStrictEqual(optic.replace(2, {}), { a: 2 }) |
| 317 | deepStrictEqual(optic.replace(undefined, { a: 1 }), {}) |
| 318 | deepStrictEqual(optic.replace(undefined, {}), {}) |
| 319 | deepStrictEqual(optic.modify(f)({ a: 1 }), { a: 2 }) |
| 320 | deepStrictEqual(optic.modify(f)({}), {}) |
| 321 | }) |
| 322 | }) |
| 323 | |
| 324 | it("Record", () => { |
| 325 | type S = { [x: string]: number } |
| 326 | const optic = Optic.id<S>().optionalKey("a") |
| 327 | const f = (n: number | undefined) => n !== undefined ? n + 1 : undefined |
| 328 | |
| 329 | strictEqual(optic.get({ a: 1, b: 2 }), 1) |
| 330 | strictEqual(optic.get({ b: 2 }), undefined) |
| 331 | deepStrictEqual(optic.replace(2, { a: 1, b: 2 }), { a: 2, b: 2 }) |
| 332 | deepStrictEqual(optic.replace(2, { b: 2 }), { a: 2, b: 2 }) |
| 333 | deepStrictEqual(optic.replace(undefined, { a: 1 }), {}) |
| 334 | deepStrictEqual(optic.replace(undefined, { b: 2 }), { b: 2 }) |
| 335 | deepStrictEqual(optic.modify(f)({ a: 1 }), { a: 2 }) |
| 336 | deepStrictEqual(optic.modify(f)({ b: 2 }), { b: 2 }) |
| 337 | }) |
| 338 | |
| 339 | describe("Tuple", () => { |
| 340 | it("exact optional element (without undefined)", () => { |
| 341 | type S = readonly [number, number?] |
| 342 | const optic = Optic.id<S>().optionalKey(1) |
| 343 | const f = (n: number | undefined) => n !== undefined ? n + 1 : undefined |
| 344 | |
| 345 | strictEqual(optic.get([1, 2]), 2) |
| 346 | strictEqual(optic.get([1]), undefined) |
| 347 | deepStrictEqual(optic.replace(3, [1, 2]), [1, 3]) |
| 348 | deepStrictEqual(optic.replace(3, [1]), [1, 3]) |
| 349 | deepStrictEqual(optic.replace(undefined, [1, 2]), [1]) |
| 350 | deepStrictEqual(optic.replace(undefined, [1]), [1]) |
| 351 | deepStrictEqual(optic.modify(f)([1, 2]), [1, 3]) |
| 352 | deepStrictEqual(optic.modify(f)([1]), [1]) |
| 353 | }) |
| 354 | |
| 355 | it("string index", () => { |
| 356 | type S = readonly [number, number?, number?] |
| 357 | const optic = Optic.id<S>().optionalKey("1") |
| 358 | |
| 359 | deepStrictEqual(optic.replace(undefined, [1, 2, 3]), [1, 3]) |
| 360 | }) |
| 361 | }) |
| 362 | |
| 363 | it("Array", () => { |
| 364 | type S = ReadonlyArray<number> |
| 365 | const optic = Optic.id<S>().optionalKey(1) |
| 366 | const f = (n: number | undefined) => n !== undefined ? n + 1 : undefined |
| 367 | |
| 368 | strictEqual(optic.get([1, 2, 3]), 2) |
| 369 | strictEqual(optic.get([1]), undefined) |
| 370 | deepStrictEqual(optic.replace(4, [1, 2, 3]), [1, 4, 3]) |
| 371 | deepStrictEqual(optic.replace(4, [1]), [1, 4]) |
| 372 | deepStrictEqual(optic.replace(undefined, [1, 2]), [1]) |
| 373 | deepStrictEqual(optic.replace(undefined, [1, 2, 3]), [1, 3]) |
| 374 | deepStrictEqual(optic.replace(undefined, [1]), [1]) |
| 375 | deepStrictEqual(optic.modify(f)([1, 2]), [1, 3]) |
| 376 | deepStrictEqual(optic.modify(f)([1, 2, 4]), [1, 3, 4]) |
| 377 | deepStrictEqual(optic.modify(f)([1]), [1]) |
| 378 | }) |
| 379 | }) |
| 380 | |
| 381 | describe("check", () => { |
| 382 | it("single check", () => { |
| 383 | type S = number |
| 384 | const optic = Optic.id<S>().check(Schema.isGreaterThan(0)) |
| 385 | assertSuccess(optic.getResult(1), 1) |
| 386 | assertFailureMessage(optic.getResult(0), `Expected a value greater than 0`) |
| 387 | strictEqual(optic.set(1), 1) |
| 388 | strictEqual(optic.set(0), 0) |
| 389 | deepStrictEqual(optic.modify(addOne)(1), 2) |
| 390 | deepStrictEqual(optic.modify(addOne)(0), 0) |
| 391 | }) |
| 392 | |
| 393 | it("multiple checks", () => { |
| 394 | type S = number |
| 395 | const optic = Optic.id<S>().check(Schema.isInt(), Schema.isGreaterThan(0)) |
| 396 | assertSuccess(optic.getResult(1), 1) |
| 397 | assertFailureMessage(optic.getResult(0), `Expected a value greater than 0`) |
| 398 | assertFailureMessage(optic.getResult(1.1), `Expected an integer`) |
| 399 | assertFailureMessage( |
| 400 | optic.getResult(-1.1), |
| 401 | `Expected an integer |
| 402 | Expected a value greater than 0` |
| 403 | ) |
| 404 | deepStrictEqual(optic.modify(addOne)(1), 2) |
| 405 | deepStrictEqual(optic.modify(addOne)(0), 0) |
| 406 | deepStrictEqual(optic.modify(addOne)(1.1), 1.1) |
| 407 | deepStrictEqual(optic.modify(addOne)(-1.1), -1.1) |
| 408 | }) |
| 409 | |
| 410 | it("combines checks across successive calls", () => { |
| 411 | const optic = Optic.id<number>() |
| 412 | .check(Schema.isInt()) |
| 413 | .check(Schema.isGreaterThan(0)) |
| 414 | |
| 415 | assertFailureMessage( |
| 416 | optic.getResult(-1.1), |
| 417 | `Expected an integer |
| 418 | Expected a value greater than 0` |
| 419 | ) |
| 420 | }) |
| 421 | }) |
| 422 | |
| 423 | it("refine", () => { |
| 424 | type B = { readonly _tag: "b"; readonly b: number } |
| 425 | type S = { readonly _tag: "a"; readonly a: string } | B |
| 426 | const optic = Optic.id<S>().refine( |
| 427 | (s: S): s is B => s._tag === "b", |
| 428 | { expected: `"b" tag` } |
| 429 | ).key("b") |
| 430 | |
| 431 | assertSuccess(optic.getResult({ _tag: "b", b: 1 }), 1) |
| 432 | assertFailureMessage(optic.getResult({ _tag: "a", a: "value" }), `Expected "b" tag`) |
| 433 | deepStrictEqual(optic.modify(addOne)({ _tag: "a", a: "value" }), { _tag: "a", a: "value" }) |
| 434 | deepStrictEqual(optic.modify(addOne)({ _tag: "b", b: 1 }), { _tag: "b", b: 2 }) |
| 435 | }) |
| 436 | |
| 437 | it("tag", () => { |
| 438 | type S = { readonly _tag: "a"; readonly a: string } | { readonly _tag: "b"; readonly b: number } |
| 439 | const optic = Optic.id<S>().tag("b").key("b") |
| 440 | |
| 441 | assertSuccess(optic.getResult({ _tag: "b", b: 1 }), 1) |
| 442 | assertFailureMessage(optic.getResult({ _tag: "a", a: "value" }), `Expected "b" tag`) |
| 443 | deepStrictEqual(optic.modify(addOne)({ _tag: "a", a: "value" }), { _tag: "a", a: "value" }) |
| 444 | deepStrictEqual(optic.modify(addOne)({ _tag: "b", b: 1 }), { _tag: "b", b: 2 }) |
| 445 | }) |
| 446 | |
| 447 | describe("at", () => { |
| 448 | it("Record", () => { |
| 449 | type S = { [x: string]: number } |
| 450 | const optic = Optic.id<S>().at("a") |
| 451 | |
| 452 | assertSuccess(optic.getResult({ a: 1, b: 2 }), 1) |
| 453 | assertMissingKeyFailure(optic.getResult({ b: 2 }), "a") |
| 454 | assertSuccess(optic.replaceResult(2, { a: 1, b: 2 }), { a: 2, b: 2 }) |
| 455 | assertMissingKeyFailure(optic.replaceResult(2, { b: 2 }), "a") |
| 456 | deepStrictEqual(optic.replace(2, { a: 1, b: 2 }), { a: 2, b: 2 }) |
| 457 | deepStrictEqual(optic.replace(2, { b: 2 }), { b: 2 }) |
| 458 | }) |
| 459 | |
| 460 | it("Array", () => { |
| 461 | type S = ReadonlyArray<number> |
| 462 | const optic = Optic.id<S>().at(0) |
| 463 | |
| 464 | assertSuccess(optic.getResult([1, 2]), 1) |
| 465 | assertMissingKeyFailure(optic.getResult([]), 0) |
| 466 | assertSuccess(optic.replaceResult(3, [1, 2]), [3, 2]) |
| 467 | assertMissingKeyFailure(optic.replaceResult(2, []), 0) |
| 468 | deepStrictEqual(optic.replace(3, [1, 2]), [3, 2]) |
| 469 | deepStrictEqual(optic.replace(2, []), []) |
| 470 | }) |
| 471 | }) |
| 472 | |
| 473 | it("key & check", () => { |
| 474 | type S = { readonly a: number } |
| 475 | const optic = Optic.id<S>().key("a").check(Schema.isGreaterThan(0)) |
| 476 | assertSuccess(optic.getResult({ a: 1 }), 1) |
| 477 | assertFailureMessage(optic.getResult({ a: 0 }), `Expected a value greater than 0`) |
| 478 | assertSuccess(optic.replaceResult(2, { a: 1 }), { a: 2 }) |
| 479 | assertFailureMessage(optic.replaceResult(2, { a: 0 }), `Expected a value greater than 0`) |
| 480 | deepStrictEqual(optic.replace(2, { a: 1 }), { a: 2 }) |
| 481 | deepStrictEqual(optic.replace(2, { a: 0 }), { a: 0 }) |
| 482 | }) |
| 483 | |
| 484 | describe("pick", () => { |
| 485 | it("Struct", () => { |
| 486 | type S = { readonly a: string; readonly b: number; readonly c: boolean } |
| 487 | const optic = Optic.id<S>().pick(["a", "c"]) |
| 488 | |
| 489 | deepStrictEqual(optic.replace({ a: "a2", c: false }, { a: "a", b: 1, c: true }), { a: "a2", b: 1, c: false }) |
| 490 | }) |
| 491 | |
| 492 | it("deletes optional fields omitted from the replacement", () => { |
| 493 | type S = { readonly a?: string; readonly b: number } |
| 494 | const optic = Optic.id<S>().pick(["a"]) |
| 495 | |
| 496 | deepStrictEqual(optic.replace({}, { a: "a", b: 1 }), { b: 1 }) |
| 497 | }) |
| 498 | }) |
| 499 | |
| 500 | describe("omit", () => { |
| 501 | it("Struct", () => { |
| 502 | type S = { readonly a: string; readonly b: number; readonly c: boolean } |
| 503 | const optic = Optic.id<S>().omit(["b"]) |
| 504 | |
| 505 | deepStrictEqual(optic.replace({ a: "a2", c: false }, { a: "a", b: 1, c: true }), { a: "a2", b: 1, c: false }) |
| 506 | }) |
| 507 | |
| 508 | it("deletes optional fields omitted from the replacement", () => { |
| 509 | type S = { readonly a?: string; readonly b: number } |
| 510 | const optic = Optic.id<S>().omit(["b"]) |
| 511 | |
| 512 | deepStrictEqual(optic.replace({}, { a: "a", b: 1 }), { b: 1 }) |
| 513 | }) |
| 514 | }) |
| 515 | |
| 516 | describe("forEach", () => { |
| 517 | it("Array", () => { |
| 518 | type Post = { title: string; likes: number } |
| 519 | type S = { user: { posts: ReadonlyArray<Post> } } |
| 520 | |
| 521 | const _like = Optic.id<S>().key("user").key("posts").forEach((post) => |
| 522 | post.key("likes").check(Schema.isGreaterThan(0)) |
| 523 | ) |
| 524 | |
| 525 | const addLike = _like.modify((likes) => likes.map((l) => l + 1)) |
| 526 | |
| 527 | deepStrictEqual( |
| 528 | addLike({ user: { posts: [{ title: "a", likes: 0 }, { title: "b", likes: 1 }, { title: "c", likes: 0 }] } }), |
| 529 | { |
| 530 | user: { posts: [{ title: "a", likes: 0 }, { title: "b", likes: 2 }, { title: "c", likes: 0 }] } |
| 531 | } |
| 532 | ) |
| 533 | }) |
| 534 | |
| 535 | it("Record", () => { |
| 536 | const optic = Optic.entries<number>().forEach((entry) => entry.key(1).check(Schema.isGreaterThan(0))) |
| 537 | |
| 538 | deepStrictEqual( |
| 539 | optic.modify((entries) => entries.map((value) => value + 1))({ a: 0, b: 1, c: 0 }), |
| 540 | { a: 0, b: 2, c: 0 } |
| 541 | ) |
| 542 | }) |
| 543 | |
| 544 | it("fails when the replacement count does not match", () => { |
| 545 | const optic = Optic.id<ReadonlyArray<number>>().forEach((element) => element) |
| 546 | |
| 547 | assertFailureMessage( |
| 548 | optic.replaceResult([2], [1, 2]), |
| 549 | "each: replacement length mismatch: 1 !== 2" |
| 550 | ) |
| 551 | }) |
| 552 | |
| 553 | it("fails when an inner setter fails", () => { |
| 554 | const issue = new SchemaIssue.InvalidValue({ message: "cannot replace" }) |
| 555 | const optic = Optic.id<ReadonlyArray<number>>().forEach(() => |
| 556 | Optic.makeOptional<number, number>( |
| 557 | Result.succeed, |
| 558 | () => Result.fail(issue) |
| 559 | ) |
| 560 | ) |
| 561 | |
| 562 | strictEqual(assertPointerFailure(optic.replaceResult([2], [1]), [0]), issue) |
| 563 | }) |
| 564 | }) |
| 565 | |
| 566 | describe("modifyAll", () => { |
| 567 | it("Array", () => { |
| 568 | type Post = { title: string; likes: number } |
| 569 | type S = { user: { posts: ReadonlyArray<Post> } } |
| 570 | |
| 571 | const _like = Optic.id<S>().key("user").key("posts").forEach((post) => |
| 572 | post.key("likes").check(Schema.isGreaterThan(0)) |
| 573 | ) |
| 574 | |
| 575 | const addLike = _like.modifyAll((like) => like + 1) |
| 576 | |
| 577 | deepStrictEqual( |
| 578 | addLike({ user: { posts: [{ title: "a", likes: 0 }, { title: "b", likes: 1 }, { title: "c", likes: 0 }] } }), |
| 579 | { |
| 580 | user: { posts: [{ title: "a", likes: 0 }, { title: "b", likes: 2 }, { title: "c", likes: 0 }] } |
| 581 | } |
| 582 | ) |
| 583 | }) |
| 584 | |
| 585 | it("Record", () => { |
| 586 | const optic = Optic.entries<number>().forEach((entry) => entry.key(1).check(Schema.isGreaterThan(0))) |
| 587 | |
| 588 | deepStrictEqual( |
| 589 | optic.modify((entries) => entries.map((value) => value + 1))({ a: 0, b: 1, c: 0 }), |
| 590 | { a: 0, b: 2, c: 0 } |
| 591 | ) |
| 592 | }) |
| 593 | |
| 594 | it("returns the original source when the traversal fails", () => { |
| 595 | type S = { readonly values?: ReadonlyArray<number> } |
| 596 | const issue = new SchemaIssue.InvalidValue({ message: "missing values" }) |
| 597 | const optic: Optic.Traversal<S, number> = Optic.makeOptional( |
| 598 | (s) => s.values === undefined ? Result.fail(issue) : Result.succeed(s.values), |
| 599 | (values, s) => s.values === undefined ? Result.fail(issue) : Result.succeed({ ...s, values }) |
| 600 | ) |
| 601 | const source: S = {} |
| 602 | |
| 603 | strictEqual(optic.modifyAll(addOne)(source), source) |
| 604 | }) |
| 605 | }) |
| 606 | |
| 607 | it("notUndefined", () => { |
| 608 | const optic = Optic.id<number | undefined>().notUndefined() |
| 609 | assertSuccess(optic.getResult(1), 1) |
| 610 | assertFailureMessage(optic.getResult(undefined), "Expected a value other than `undefined`") |
| 611 | |
| 612 | deepStrictEqual(optic.replace(2, undefined), 2) |
| 613 | deepStrictEqual(optic.replace(2, 1), 2) |
| 614 | }) |
| 615 | |
| 616 | it("getAll", () => { |
| 617 | type S = { |
| 618 | readonly a: ReadonlyArray<number> |
| 619 | } |
| 620 | const optic = Optic.id<S>().key("a").forEach((a) => a.check(Schema.isGreaterThan(0))) |
| 621 | const getAll = Optic.getAll(optic) |
| 622 | deepStrictEqual(getAll({ a: [1, 2, 3] }), [1, 2, 3]) |
| 623 | deepStrictEqual(getAll({ a: [1, -2, 3] }), [1, 3]) |
| 624 | }) |
| 625 | |
| 626 | it("getAll returns an empty array when the traversal fails", () => { |
| 627 | const focusIssue = new SchemaIssue.InvalidValue({ message: "cannot focus" }) |
| 628 | const replaceIssue = new SchemaIssue.InvalidValue({ message: "cannot replace" }) |
| 629 | const optic: Optic.Traversal<number, number> = Optic.makeOptional<number, ReadonlyArray<number>>( |
| 630 | () => Result.fail(focusIssue), |
| 631 | () => Result.fail(replaceIssue) |
| 632 | ) |
| 633 | |
| 634 | deepStrictEqual(Optic.getAll(optic)(1), []) |
| 635 | }) |
| 636 | |
| 637 | it("replace copies only objects and arrays along the focused path", () => { |
| 638 | type Task = { id: number; done: boolean; title: string } |
| 639 | type Project = { id: number; name: string; tasks: Array<Task> } |
| 640 | type State = { user: { id: string; name: string }; settings: { theme: "light" | "dark" }; projects: Array<Project> } |
| 641 | |
| 642 | const s1: State = { |
| 643 | user: { id: "u1", name: "Ada" }, |
| 644 | settings: { theme: "light" }, |
| 645 | projects: [ |
| 646 | { id: 1, name: "Alpha", tasks: [{ id: 1, done: false, title: "T1" }] }, |
| 647 | { id: 2, name: "Beta", tasks: [{ id: 2, done: false, title: "T2" }] } |
| 648 | ] |
| 649 | } |
| 650 | |
| 651 | // build an optic to projects[1].tasks[0].done |
| 652 | const _done = Optic.id<State>() |
| 653 | .key("projects").key(1) |
| 654 | .key("tasks").key(0) |
| 655 | .key("done") |
| 656 | |
| 657 | // Real update -> copy only the path |
| 658 | const s2 = _done.replace(true, s1) |
| 659 | assertTrue(s2 !== s1, "root should change on real update") |
| 660 | assertTrue(s2.user === s1.user, "unrelated branch reused") |
| 661 | assertTrue(s2.settings === s1.settings, "unrelated branch reused") |
| 662 | assertTrue(s2.projects !== s1.projects, "parent array on the path should be new") |
| 663 | assertTrue(s2.projects[0] === s1.projects[0], "sibling project reused") |
| 664 | assertTrue(s2.projects[1] !== s1.projects[1], "changed project replaced") |
| 665 | assertTrue(s2.projects[1].tasks !== s1.projects[1].tasks, "parent array of changed element should be new") |
| 666 | assertTrue(s2.projects[1].tasks[0] !== s1.projects[1].tasks[0], "changed task replaced") |
| 667 | assertTrue(s2.projects[1].tasks[0].done === true, "value updated") |
| 668 | }) |
| 669 | |
| 670 | it("fromChecks", () => { |
| 671 | const optic = Optic.id<number>().compose(Optic.fromChecks(Schema.isGreaterThan(0), Schema.isInt())) |
| 672 | assertSuccess(optic.getResult(1), 1) |
| 673 | assertFailureMessage(optic.getResult(0), `Expected a value greater than 0`) |
| 674 | assertFailureMessage(optic.getResult(1.1), `Expected an integer`) |
| 675 | }) |
| 676 | |
| 677 | describe("Option", () => { |
| 678 | it("some", () => { |
| 679 | const optic = Optic.id<Option.Option<number>>().compose(Optic.some()) |
| 680 | assertSuccess(optic.getResult(Option.some(1)), 1) |
| 681 | assertFailureMessage(optic.getResult(Option.none()), `Expected a Some value`) |
| 682 | deepStrictEqual(optic.set(2), Option.some(2)) |
| 683 | }) |
| 684 | |
| 685 | it("none", () => { |
| 686 | const optic = Optic.id<Option.Option<number>>().compose(Optic.none()) |
| 687 | assertSuccess(optic.getResult(Option.none()), undefined) |
| 688 | assertFailureMessage(optic.getResult(Option.some(1)), `Expected a None value`) |
| 689 | deepStrictEqual(optic.set(undefined), Option.none()) |
| 690 | }) |
| 691 | }) |
| 692 | |
| 693 | describe("Result", () => { |
| 694 | it("success", () => { |
| 695 | const optic = Optic.id<Result.Result<number, string>>().compose(Optic.success()) |
| 696 | assertSuccess(optic.getResult(Result.succeed(1)), 1) |
| 697 | assertFailureMessage(optic.getResult(Result.fail("error")), `Expected a Result.Success value`) |
| 698 | deepStrictEqual(optic.set(2), Result.succeed(2)) |
| 699 | }) |
| 700 | |
| 701 | it("failure", () => { |
| 702 | const optic = Optic.id<Result.Result<number, string>>().compose(Optic.failure()) |
| 703 | assertSuccess(optic.getResult(Result.fail("error")), "error") |
| 704 | assertFailureMessage(optic.getResult(Result.succeed(1)), `Expected a Result.Failure value`) |
| 705 | deepStrictEqual(optic.set("new error"), Result.fail("new error")) |
| 706 | }) |
| 707 | }) |
| 708 | }) |