@@ -7,180 +7,176 @@ import { describe, expect, it } from "vitest";
77import { path } from "../index.js" ;
88
99interface Shop {
10- products : Array < {
11- id : string ;
12- photos : Array < { id : string ; url : string } > ;
13- } > ;
10+ products : Array < {
11+ id : string ;
12+ photos : Array < { id : string ; url : string } > ;
13+ } > ;
1414}
1515
1616describe ( "Data access" , ( ) => {
17- describe ( ".get()" , ( ) => {
18- it ( "retrieves nested properties" , ( ) => {
19- const p = path ( ( x : { a : { b : { c : string } } } ) => x . a . b . c ) ;
20- const data = { a : { b : { c : "hello" } } } ;
21- expect ( p . get ( data ) ) . toBe ( "hello" ) ;
22- } ) ;
23-
24- it ( "handles arrays" , ( ) => {
25- const p = path ( ( x : { items : string [ ] } ) => x . items [ 1 ] ) ;
26- const data = { items : [ "a" , "b" , "c" ] } ;
27- expect ( p . get ( data ) ) . toBe ( "b" ) ;
28- } ) ;
29-
30- it ( "returns undefined when intermediate objects are missing" , ( ) => {
31- const p = path < { a ?: { b ?: { c : string } } } > (
32- ( x ) => ( x as { a : { b : { c : string } } } ) . a . b . c ,
33- ) ;
34- const data = { a : undefined } ;
35- expect ( p . get ( data ) ) . toBeUndefined ( ) ;
36- } ) ;
37-
38- it ( "immutability: does not mutate original path" , ( ) => {
39- const p = path ( ( x : { a : { b : { c : string } } } ) => x . a . b . c ) ;
40- const data = { a : { b : { c : "hello" } } } ;
41- p . get ( data ) ;
42- expect ( p . segments ) . toEqual ( [ "a" , "b" , "c" ] ) ;
43- } ) ;
44- } ) ;
45-
46- describe ( ".set()" , ( ) => {
47- it ( "immutably updates nested property" , ( ) => {
48- const p = path ( ( x : { a : { b : number } } ) => x . a . b ) ;
49- const data = { a : { b : 1 } } ;
50- const updated = p . set ( data , 42 ) ;
51- expect ( updated . a . b ) . toBe ( 42 ) ;
52- expect ( data . a . b ) . toBe ( 1 ) ;
53- } ) ;
54-
55- it ( "immutability: does not mutate original path" , ( ) => {
56- const p = path ( ( x : { a : { b : number } } ) => x . a . b ) ;
57- const data = { a : { b : 1 } } ;
58- p . set ( data , 42 ) ;
59- expect ( p . segments ) . toEqual ( [ "a" , "b" ] ) ;
60- } ) ;
61-
62- describe ( "deep immutability and structural sharing" , ( ) => {
63- const getFixture = ( ) : Shop => ( {
64- products : [
65- {
66- id : "p1" ,
67- photos : [
68- { id : "ph1_1" , url : "url1_1" } ,
69- { id : "ph1_2" , url : "url1_2" } ,
70- ] ,
71- } ,
72- {
73- id : "p2" ,
74- photos : [ { id : "ph2_1" , url : "url2_1" } ] ,
75- } ,
76- ] ,
77- } ) ;
78-
79- it ( "creates new references only along the updated path" , ( ) => {
80- const data = getFixture ( ) ;
81- const p = path ( ( x : Shop ) => x . products [ 0 ] . photos [ 1 ] . url ) ;
82-
83- const updated = p . set ( data , "new_url" ) ;
84-
85- // Value is updated
86- expect ( updated . products [ 0 ] . photos [ 1 ] . url ) . toBe ( "new_url" ) ;
87- // Original is unchanged
88- expect ( data . products [ 0 ] . photos [ 1 ] . url ) . toBe ( "url1_2" ) ;
89-
90- // Root reference changed
91- expect ( updated ) . not . toBe ( data ) ;
92- // Array reference changed
93- expect ( updated . products ) . not . toBe ( data . products ) ;
94- // Object reference changed
95- expect ( updated . products [ 0 ] ) . not . toBe ( data . products [ 0 ] ) ;
96- // Inner array reference changed
97- expect ( updated . products [ 0 ] . photos ) . not . toBe (
98- data . products [ 0 ] . photos ,
99- ) ;
100- // Inner object reference changed
101- expect ( updated . products [ 0 ] . photos [ 1 ] ) . not . toBe (
102- data . products [ 0 ] . photos [ 1 ] ,
103- ) ;
104- } ) ;
105-
106- it ( "preserves original references for unchanged branches (structural sharing)" , ( ) => {
107- const data = getFixture ( ) ;
108- const p = path ( ( x : Shop ) => x . products [ 0 ] . photos [ 1 ] . url ) ;
109-
110- const updated = p . set ( data , "new_url" ) ;
111-
112- // Unchanged sibling in the products array is reused
113- expect ( updated . products [ 1 ] ) . toBe ( data . products [ 1 ] ) ;
114- // Unchanged primitive/reference in the modified product object is reused
115- expect ( updated . products [ 0 ] . id ) . toBe ( data . products [ 0 ] . id ) ;
116- // Unchanged sibling in the modified photos array is reused
117- expect ( updated . products [ 0 ] . photos [ 0 ] ) . toBe (
118- data . products [ 0 ] . photos [ 0 ] ,
119- ) ;
120- } ) ;
121- } ) ;
122- } ) ;
123-
124- describe ( "unexpected cases" , ( ) => {
125- it ( ".get(null) and .get(undefined) gracefully returns undefined" , ( ) => {
126- const p = path ( ( x : { a : number } ) => x . a ) ;
127- expect ( p . get ( null as any ) ) . toBeUndefined ( ) ;
128- expect ( p . get ( undefined as any ) ) . toBeUndefined ( ) ;
129- } ) ;
130-
131- it ( ".get() and .set() handle unexpected structures safely" , ( ) => {
132- const p = path ( ( x : { a : { b : number } } ) => x . a . b ) ;
133- // Trying to traverse into a string
134- expect ( p . get ( { a : "string" } as any ) ) . toBeUndefined ( ) ;
135-
136- // Trying to set into a string replaces the string with an object/array at that level?
137- // Or at least it doesn't crash the host program unexpectedly.
138- expect ( ( ) => p . set ( { a : "string" } as any , 42 ) ) . not . toThrow ( ) ;
139- } ) ;
140-
141- it ( "records 'then' as a normal path segment" , ( ) => {
142- const p = path ( ( x : { then : { value : string } } ) => x . then . value ) ;
143- // biome-ignore lint/suspicious/noThenProperty: testing that 'then' is recorded as a normal path segment
144- const data = { then : { value : "resolved" } } ;
145- expect ( p . get ( data ) ) . toBe ( "resolved" ) ;
146- expect ( p . segments ) . toEqual ( [ "then" , "value" ] ) ;
147- } ) ;
148-
149- it ( "preserves numeric-looking object keys as strings (e.g. '01', '1e3')" , ( ) => {
150- // "01" and "1" are distinct keys in JS; "1e3" and "1000" are distinct
151- const p01 = path ( ( x : { "01" : string } ) => x [ "01" ] ) ;
152- const p1e3 = path ( ( x : { "1e3" : number } ) => x [ "1e3" ] ) ;
153- const data = {
154- "01" : "value-01" ,
155- 1 : "value-1" ,
156- "1e3" : 1000 ,
157- 1000 : 999 ,
158- } ;
159- expect ( p01 . get ( data ) ) . toBe ( "value-01" ) ;
160- expect ( p1e3 . get ( data ) ) . toBe ( 1000 ) ;
161- expect ( p01 . segments ) . toEqual ( [ "01" ] ) ;
162- expect ( p1e3 . segments ) . toEqual ( [ "1e3" ] ) ;
163- } ) ;
164-
165- it ( "Array .set() operations with out-of-bounds indices or negative indices" , ( ) => {
166- const p = path ( ( x : { items : string [ ] } ) => x . items [ 5 ] ) ;
167- const data = { items : [ "a" ] } ;
168- const result = p . set ( data , "f" ) ;
169- expect ( result . items [ 5 ] ) . toBe ( "f" ) ;
170- expect ( result . items . length ) . toBeGreaterThan ( 1 ) ;
171-
172- // Negative index behavior checks
173- const pNeg = path ( ( x : { items : string [ ] } ) => x . items [ - 1 ] ) ;
174- expect ( ( ) => pNeg . set ( data , "z" ) ) . not . toThrow ( ) ;
175- } ) ;
176- } ) ;
177-
178- describe ( "typing incorrect cases" , ( ) => {
179- it ( "rejects .set() with a wrong type" , ( ) => {
180- const p = path ( ( x : { a : number } ) => x . a ) ;
181- const data = { a : 1 } ;
182- // @ts -expect-error
183- p . set ( data , "wrong-type" ) ;
184- } ) ;
185- } ) ;
17+ describe ( ".get()" , ( ) => {
18+ it ( "retrieves nested properties" , ( ) => {
19+ const p = path ( ( x : { a : { b : { c : string } } } ) => x . a . b . c ) ;
20+ const data = { a : { b : { c : "hello" } } } ;
21+ expect ( p . get ( data ) ) . toBe ( "hello" ) ;
22+ } ) ;
23+
24+ it ( "handles arrays" , ( ) => {
25+ const p = path ( ( x : { items : string [ ] } ) => x . items [ 1 ] ) ;
26+ const data = { items : [ "a" , "b" , "c" ] } ;
27+ expect ( p . get ( data ) ) . toBe ( "b" ) ;
28+ } ) ;
29+
30+ it ( "returns undefined when intermediate objects are missing" , ( ) => {
31+ const p = path < { a ?: { b ?: { c : string } } } > (
32+ ( x ) => ( x as { a : { b : { c : string } } } ) . a . b . c ,
33+ ) ;
34+ const data = { a : undefined } ;
35+ expect ( p . get ( data ) ) . toBeUndefined ( ) ;
36+ } ) ;
37+
38+ it ( "immutability: does not mutate original path" , ( ) => {
39+ const p = path ( ( x : { a : { b : { c : string } } } ) => x . a . b . c ) ;
40+ const data = { a : { b : { c : "hello" } } } ;
41+ p . get ( data ) ;
42+ expect ( p . segments ) . toEqual ( [ "a" , "b" , "c" ] ) ;
43+ } ) ;
44+ } ) ;
45+
46+ describe ( ".set()" , ( ) => {
47+ it ( "immutably updates nested property" , ( ) => {
48+ const p = path ( ( x : { a : { b : number } } ) => x . a . b ) ;
49+ const data = { a : { b : 1 } } ;
50+ const updated = p . set ( data , 42 ) ;
51+ expect ( updated . a . b ) . toBe ( 42 ) ;
52+ expect ( data . a . b ) . toBe ( 1 ) ;
53+ } ) ;
54+
55+ it ( "immutability: does not mutate original path" , ( ) => {
56+ const p = path ( ( x : { a : { b : number } } ) => x . a . b ) ;
57+ const data = { a : { b : 1 } } ;
58+ p . set ( data , 42 ) ;
59+ expect ( p . segments ) . toEqual ( [ "a" , "b" ] ) ;
60+ } ) ;
61+
62+ describe ( "deep immutability and structural sharing" , ( ) => {
63+ const getFixture = ( ) : Shop => ( {
64+ products : [
65+ {
66+ id : "p1" ,
67+ photos : [
68+ { id : "ph1_1" , url : "url1_1" } ,
69+ { id : "ph1_2" , url : "url1_2" } ,
70+ ] ,
71+ } ,
72+ {
73+ id : "p2" ,
74+ photos : [ { id : "ph2_1" , url : "url2_1" } ] ,
75+ } ,
76+ ] ,
77+ } ) ;
78+
79+ it ( "creates new references only along the updated path" , ( ) => {
80+ const data = getFixture ( ) ;
81+ const p = path ( ( x : Shop ) => x . products [ 0 ] . photos [ 1 ] . url ) ;
82+
83+ const updated = p . set ( data , "new_url" ) ;
84+
85+ // Value is updated
86+ expect ( updated . products [ 0 ] . photos [ 1 ] . url ) . toBe ( "new_url" ) ;
87+ // Original is unchanged
88+ expect ( data . products [ 0 ] . photos [ 1 ] . url ) . toBe ( "url1_2" ) ;
89+
90+ // Root reference changed
91+ expect ( updated ) . not . toBe ( data ) ;
92+ // Array reference changed
93+ expect ( updated . products ) . not . toBe ( data . products ) ;
94+ // Object reference changed
95+ expect ( updated . products [ 0 ] ) . not . toBe ( data . products [ 0 ] ) ;
96+ // Inner array reference changed
97+ expect ( updated . products [ 0 ] . photos ) . not . toBe ( data . products [ 0 ] . photos ) ;
98+ // Inner object reference changed
99+ expect ( updated . products [ 0 ] . photos [ 1 ] ) . not . toBe (
100+ data . products [ 0 ] . photos [ 1 ] ,
101+ ) ;
102+ } ) ;
103+
104+ it ( "preserves original references for unchanged branches (structural sharing)" , ( ) => {
105+ const data = getFixture ( ) ;
106+ const p = path ( ( x : Shop ) => x . products [ 0 ] . photos [ 1 ] . url ) ;
107+
108+ const updated = p . set ( data , "new_url" ) ;
109+
110+ // Unchanged sibling in the products array is reused
111+ expect ( updated . products [ 1 ] ) . toBe ( data . products [ 1 ] ) ;
112+ // Unchanged primitive/reference in the modified product object is reused
113+ expect ( updated . products [ 0 ] . id ) . toBe ( data . products [ 0 ] . id ) ;
114+ // Unchanged sibling in the modified photos array is reused
115+ expect ( updated . products [ 0 ] . photos [ 0 ] ) . toBe ( data . products [ 0 ] . photos [ 0 ] ) ;
116+ } ) ;
117+ } ) ;
118+ } ) ;
119+
120+ describe ( "unexpected cases" , ( ) => {
121+ it ( ".get(null) and .get(undefined) gracefully returns undefined" , ( ) => {
122+ const p = path ( ( x : { a : number } ) => x . a ) ;
123+ expect ( p . get ( null as any ) ) . toBeUndefined ( ) ;
124+ expect ( p . get ( undefined as any ) ) . toBeUndefined ( ) ;
125+ } ) ;
126+
127+ it ( ".get() and .set() handle unexpected structures safely" , ( ) => {
128+ const p = path ( ( x : { a : { b : number } } ) => x . a . b ) ;
129+ // Trying to traverse into a string
130+ expect ( p . get ( { a : "string" } as any ) ) . toBeUndefined ( ) ;
131+
132+ // Trying to set into a string replaces the string with an object/array at that level?
133+ // Or at least it doesn't crash the host program unexpectedly.
134+ expect ( ( ) => p . set ( { a : "string" } as any , 42 ) ) . not . toThrow ( ) ;
135+ } ) ;
136+
137+ it ( "records 'then' as a normal path segment" , ( ) => {
138+ const p = path ( ( x : { then : { value : string } } ) => x . then . value ) ;
139+ // biome-ignore lint/suspicious/noThenProperty: testing that 'then' is recorded as a normal path segment
140+ const data = { then : { value : "resolved" } } ;
141+ expect ( p . get ( data ) ) . toBe ( "resolved" ) ;
142+ expect ( p . segments ) . toEqual ( [ "then" , "value" ] ) ;
143+ } ) ;
144+
145+ it ( "preserves numeric-looking object keys as strings (e.g. '01', '1e3')" , ( ) => {
146+ // "01" and "1" are distinct keys in JS; "1e3" and "1000" are distinct
147+ const p01 = path ( ( x : { "01" : string } ) => x [ "01" ] ) ;
148+ const p1e3 = path ( ( x : { "1e3" : number } ) => x [ "1e3" ] ) ;
149+ const data = {
150+ "01" : "value-01" ,
151+ 1 : "value-1" ,
152+ "1e3" : 1000 ,
153+ 1000 : 999 ,
154+ } ;
155+ expect ( p01 . get ( data ) ) . toBe ( "value-01" ) ;
156+ expect ( p1e3 . get ( data ) ) . toBe ( 1000 ) ;
157+ expect ( p01 . segments ) . toEqual ( [ "01" ] ) ;
158+ expect ( p1e3 . segments ) . toEqual ( [ "1e3" ] ) ;
159+ } ) ;
160+
161+ it ( "Array .set() operations with out-of-bounds indices or negative indices" , ( ) => {
162+ const p = path ( ( x : { items : string [ ] } ) => x . items [ 5 ] ) ;
163+ const data = { items : [ "a" ] } ;
164+ const result = p . set ( data , "f" ) ;
165+ expect ( result . items [ 5 ] ) . toBe ( "f" ) ;
166+ expect ( result . items . length ) . toBeGreaterThan ( 1 ) ;
167+
168+ // Negative index behavior checks
169+ const pNeg = path ( ( x : { items : string [ ] } ) => x . items [ - 1 ] ) ;
170+ expect ( ( ) => pNeg . set ( data , "z" ) ) . not . toThrow ( ) ;
171+ } ) ;
172+ } ) ;
173+
174+ describe ( "typing incorrect cases" , ( ) => {
175+ it ( "rejects .set() with a wrong type" , ( ) => {
176+ const p = path ( ( x : { a : number } ) => x . a ) ;
177+ const data = { a : 1 } ;
178+ // @ts -expect-error
179+ p . set ( data , "wrong-type" ) ;
180+ } ) ;
181+ } ) ;
186182} ) ;
0 commit comments