1import { Optic, Option, Result, Schema, SchemaIssue } from "effect"
2import { describe, it } from "vitest"
3import { assertSuccess, assertTrue, deepStrictEqual, strictEqual, throws } from "./utils/assert.ts"
4
5const addOne = (n: number) => n + 1
6
7function getFailure<A>(result: Result.Result<A, SchemaIssue.Issue>): SchemaIssue.Issue {
8 assertTrue(Result.isFailure(result))
9 return result.failure
10}
11
12function assertFailureMessage<A>(result: Result.Result<A, SchemaIssue.Issue>, message: string) {
13 strictEqual(SchemaIssue.defaultFormatter(getFailure(result)), message)
14}
15
16function assertFailureIssue<A>(result: Result.Result<A, SchemaIssue.Issue>, issue: SchemaIssue.Issue) {
17 strictEqual(getFailure(result), issue)
18}
19
20function 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
30function assertMissingKeyFailure<A>(result: Result.Result<A, SchemaIssue.Issue>, key: PropertyKey) {
31 assertTrue(assertPointerFailure(result, [key])._tag === "MissingKey")
32}
33
34describe("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
402Expected 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
418Expected 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})