@goodie-forms/core 1.2.5-alpha → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +61 -0
  2. package/dist/field/FieldPath.d.ts +18 -9
  3. package/dist/field/FieldPath.d.ts.map +1 -1
  4. package/dist/field/FieldPathBuilder.d.ts +2 -2
  5. package/dist/field/FieldPathBuilder.d.ts.map +1 -1
  6. package/dist/field/Reconcile.d.ts +5 -2
  7. package/dist/field/Reconcile.d.ts.map +1 -1
  8. package/dist/form/FormController.d.ts +51 -20
  9. package/dist/form/FormController.d.ts.map +1 -1
  10. package/dist/form/FormField.d.ts +15 -10
  11. package/dist/form/FormField.d.ts.map +1 -1
  12. package/dist/index.d.ts +2 -2
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +550 -505
  15. package/dist/index.js.map +1 -1
  16. package/dist/types/DeepHelpers.d.ts +11 -0
  17. package/dist/types/DeepHelpers.d.ts.map +1 -0
  18. package/dist/types/FormHelpers.d.ts +5 -0
  19. package/dist/types/FormHelpers.d.ts.map +1 -0
  20. package/dist/types/Suppliable.d.ts +3 -0
  21. package/dist/types/Suppliable.d.ts.map +1 -0
  22. package/dist/validation/CustomValidation.d.ts +1 -1
  23. package/package.json +23 -3
  24. package/src/field/FieldPath.spec.ts +204 -0
  25. package/src/field/FieldPath.ts +63 -59
  26. package/src/field/FieldPathBuilder.spec.ts +47 -0
  27. package/src/field/FieldPathBuilder.ts +15 -81
  28. package/src/field/Reconcile.ts +12 -7
  29. package/src/form/FormController.spec.ts +55 -0
  30. package/src/form/FormController.ts +152 -121
  31. package/src/form/FormField.ts +66 -30
  32. package/src/index.ts +2 -2
  33. package/src/types/DeepHelpers.ts +15 -0
  34. package/src/types/FormHelpers.ts +13 -0
  35. package/src/types/Suppliable.ts +7 -0
  36. package/src/validation/CustomValidation.ts +1 -1
  37. package/dist/form/NonullFormField.d.ts +0 -9
  38. package/dist/form/NonullFormField.d.ts.map +0 -1
  39. package/dist/types/DeepPartial.d.ts +0 -6
  40. package/dist/types/DeepPartial.d.ts.map +0 -1
  41. package/src/form/NonullFormField.ts +0 -15
  42. package/src/types/DeepPartial.ts +0 -7
  43. package/tsconfig.json +0 -8
  44. package/vite.config.ts +0 -18
@@ -0,0 +1,11 @@
1
+ export type DeepPartial<T> = {
2
+ [K in keyof T as T[K] extends (...args: any[]) => any ? K : never]: T[K];
3
+ } & {
4
+ [K in keyof T as T[K] extends (...args: any[]) => any ? never : K]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
5
+ };
6
+ export type DeepReadonly<T> = {
7
+ [K in keyof T as T[K] extends (...args: any[]) => any ? K : never]: T[K];
8
+ } & {
9
+ readonly [K in keyof T as T[K] extends (...args: any[]) => any ? never : K]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
10
+ };
11
+ //# sourceMappingURL=DeepHelpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DeepHelpers.d.ts","sourceRoot":"","sources":["../../src/types/DeepHelpers.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,CAAC,CAAC,IAAI;KAC1B,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,GAAG,CAAC,GAAG,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC;CACzE,GAAG;KACD,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,GACjD,KAAK,GACL,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,MAAM,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;CACxD,CAAC;AAEF,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI;KAC3B,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,GAAG,CAAC,GAAG,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC;CACzE,GAAG;IACF,QAAQ,EAAE,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,GAC1D,KAAK,GACL,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,MAAM,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;CACxD,CAAC"}
@@ -0,0 +1,5 @@
1
+ import { FormField } from 'packages/core/src/form/FormField';
2
+ export type FieldValue<T extends FormField<any, any>> = NonNullable<T> extends {
3
+ value: infer V;
4
+ } ? NonNullable<V> : never;
5
+ //# sourceMappingURL=FormHelpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FormHelpers.d.ts","sourceRoot":"","sources":["../../src/types/FormHelpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAG7D,MAAM,MAAM,UAAU,CAAC,CAAC,SAAS,SAAS,CAAC,GAAG,EAAE,GAAG,CAAC,IAClD,WAAW,CAAC,CAAC,CAAC,SAAS;IAAE,KAAK,EAAE,MAAM,CAAC,CAAA;CAAE,GAAG,WAAW,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC"}
@@ -0,0 +1,3 @@
1
+ export type Suppliable<T> = T | (() => T);
2
+ export declare function supply<T>(suppliable: T | (() => T)): T;
3
+ //# sourceMappingURL=Suppliable.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Suppliable.d.ts","sourceRoot":"","sources":["../../src/types/Suppliable.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;AAE1C,wBAAgB,MAAM,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAItD"}
@@ -1,5 +1,5 @@
1
1
  import { StandardSchemaV1 } from '@standard-schema/spec';
2
- import { DeepPartial } from '../types/DeepPartial';
2
+ import { DeepPartial } from '../types/DeepHelpers';
3
3
  import { FieldPath } from '../field/FieldPath';
4
4
  export type CustomValidationIssue<TOutput extends object> = {
5
5
  path: FieldPath.StringPaths<TOutput>;
package/package.json CHANGED
@@ -1,6 +1,16 @@
1
1
  {
2
2
  "name": "@goodie-forms/core",
3
- "version": "1.2.5-alpha",
3
+ "version": "1.3.0",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "https://github.com/iGoodie/goodie-forms"
7
+ },
8
+ "author": {
9
+ "name": "Taha Anılcan Metinyurt",
10
+ "email": "igoodie@programmer.net",
11
+ "url": "https://github.com/iGoodie"
12
+ },
13
+ "license": "CC BY-SA 4.0",
4
14
  "type": "module",
5
15
  "main": "dist/index.js",
6
16
  "types": "dist/index.d.ts",
@@ -10,13 +20,20 @@
10
20
  "default": "./dist/index.js"
11
21
  }
12
22
  },
23
+ "files": [
24
+ "src",
25
+ "dist"
26
+ ],
13
27
  "publishConfig": {
14
28
  "access": "public"
15
29
  },
16
30
  "devDependencies": {
31
+ "@vitest/coverage-v8": "^4.0.18",
32
+ "type-testing": "^0.2.0",
17
33
  "typescript": "^5.9.3",
18
34
  "vite": "^7.3.1",
19
- "vite-plugin-dts": "^4.5.4"
35
+ "vite-plugin-dts": "^4.5.4",
36
+ "vitest": "^4.0.18"
20
37
  },
21
38
  "dependencies": {
22
39
  "@standard-schema/spec": "^1.1.0",
@@ -24,6 +41,9 @@
24
41
  "nanoevents": "^9.1.0"
25
42
  },
26
43
  "scripts": {
27
- "build": "vite build"
44
+ "build": "vite build",
45
+ "coverage": "vitest run --coverage",
46
+ "test": "tsc --noEmit && vitest run",
47
+ "test:watch": "vitest"
28
48
  }
29
49
  }
@@ -0,0 +1,204 @@
1
+ import { Equal, Expect } from "type-testing";
2
+ import { expect, test } from "vitest";
3
+ import { FieldPath } from "../field/FieldPath";
4
+
5
+ interface Person {
6
+ name: string;
7
+ surname: string;
8
+ address?: {
9
+ city: string;
10
+ street: string;
11
+ };
12
+ friends?: string[];
13
+ relations?: {
14
+ name: string;
15
+ points: number;
16
+ metAddress?: {
17
+ city: string;
18
+ street: string;
19
+ };
20
+ }[];
21
+ }
22
+
23
+ const person: Person = {
24
+ name: "John",
25
+ surname: "Doe",
26
+ address: {
27
+ city: "Arkham City",
28
+ street: "Sesame Street",
29
+ },
30
+ friends: ["Alice", "Bob"],
31
+ relations: [
32
+ {
33
+ name: "Alice",
34
+ points: 99,
35
+ metAddress: {
36
+ city: "Arkham City",
37
+ street: "Sesame Street",
38
+ },
39
+ },
40
+ {
41
+ name: "Bob",
42
+ points: 0,
43
+ },
44
+ ],
45
+ };
46
+
47
+ /* ------------------------------ */
48
+
49
+ test("resolves field types by path", () => {
50
+ type Shape = {
51
+ user?: {
52
+ profile?: { name?: string };
53
+ tags?: boolean[];
54
+ };
55
+ };
56
+
57
+ type typeTests = [
58
+ Expect<
59
+ Equal<
60
+ FieldPath.Resolve<Shape, ["user"]>,
61
+ { profile?: { name?: string }; tags?: boolean[] } | undefined
62
+ >
63
+ >,
64
+
65
+ Expect<
66
+ Equal<
67
+ FieldPath.Resolve<Shape, ["user", "profile"]>,
68
+ { name?: string } | undefined
69
+ >
70
+ >,
71
+
72
+ Expect<
73
+ Equal<
74
+ FieldPath.Resolve<Shape, ["user", "profile", "name"]>,
75
+ string | undefined
76
+ >
77
+ >,
78
+
79
+ Expect<
80
+ Equal<
81
+ FieldPath.Resolve<Shape, ["user", "tags", number]>,
82
+ //
83
+ boolean
84
+ >
85
+ >,
86
+
87
+ Expect<
88
+ Equal<
89
+ FieldPath.Resolve<Shape, ["user", "tags", 0]>,
90
+ //
91
+ boolean
92
+ >
93
+ >,
94
+
95
+ Expect<
96
+ Equal<
97
+ FieldPath.Resolve<Shape, ["user", "missing"]>,
98
+ //
99
+ never
100
+ >
101
+ >,
102
+ ];
103
+ });
104
+
105
+ test("parses string paths", () => {
106
+ type PersonPaths = FieldPath.StringPaths<Person>;
107
+
108
+ const path1 = FieldPath.fromStringPath("a.b");
109
+ expect(path1).toStrictEqual(["a", "b"]);
110
+
111
+ const path2 = FieldPath.fromStringPath("a.b.c");
112
+ expect(path2).toStrictEqual(["a", "b", "c"]);
113
+
114
+ const path3 = FieldPath.fromStringPath("a[99].b.c[3]");
115
+ expect(path3).toStrictEqual(["a", 99, "b", "c", 3]);
116
+
117
+ type typeTests = [
118
+ Expect<
119
+ Equal<
120
+ PersonPaths,
121
+ | "name"
122
+ | "surname"
123
+ | "address"
124
+ | "friends"
125
+ | "relations"
126
+ | "address.city"
127
+ | "address.street"
128
+ | "friends[0]"
129
+ | FieldPath._Unfoldable<`friends[${number}]`>
130
+ | "relations[0]"
131
+ | FieldPath._Unfoldable<`relations[${number}]`>
132
+ | "relations[0].metAddress"
133
+ | FieldPath._Unfoldable<`relations[${number}].metAddress`>
134
+ | "relations[0].name"
135
+ | FieldPath._Unfoldable<`relations[${number}].name`>
136
+ | "relations[0].points"
137
+ | FieldPath._Unfoldable<`relations[${number}].points`>
138
+ | "relations[0].metAddress.city"
139
+ | FieldPath._Unfoldable<`relations[${number}].metAddress.city`>
140
+ | "relations[0].metAddress.street"
141
+ | FieldPath._Unfoldable<`relations[${number}].metAddress.street`>
142
+ >
143
+ >,
144
+ ];
145
+ });
146
+
147
+ test("gets fields properly", () => {
148
+ const getFieldValue = (stringPath: FieldPath.StringPaths<Person>) =>
149
+ FieldPath.getValue(person, FieldPath.fromStringPath(stringPath));
150
+
151
+ expect(getFieldValue("name")).toBe(person.name);
152
+ expect(getFieldValue("surname")).toBe(person.surname);
153
+ expect(getFieldValue("address")).toBe(person.address);
154
+ expect(getFieldValue("address.city")).toBe(person.address?.city);
155
+ expect(getFieldValue("address.street")).toBe(person.address?.street);
156
+ expect(getFieldValue("friends")).toBe(person.friends);
157
+ expect(getFieldValue("friends[0]")).toBe(person.friends?.[0]);
158
+ expect(getFieldValue("friends[1]")).toBe(person.friends?.[1]);
159
+ expect(getFieldValue("relations")).toBe(person.relations);
160
+ expect(getFieldValue("relations[0]")).toBe(person.relations?.[0]);
161
+ expect(getFieldValue("relations[1]")).toBe(person.relations?.[1]);
162
+ expect(getFieldValue("relations[1].metAddress.city")).toBe(
163
+ person.relations?.[1].metAddress?.city,
164
+ );
165
+ });
166
+
167
+ test("sets fields properly", () => {
168
+ const obj = {
169
+ a: 1,
170
+ b: 2,
171
+ c: {
172
+ d: 3,
173
+ e: [4, 5],
174
+ },
175
+ };
176
+
177
+ type ObjPaths = FieldPath.StringPaths<typeof obj>;
178
+
179
+ const getPath = <TPath extends ObjPaths>(stringPath: TPath) => {
180
+ return FieldPath.fromStringPath(stringPath);
181
+ };
182
+
183
+ const setFieldValue = <TPath extends FieldPath.Segments>(
184
+ path: TPath,
185
+ value: FieldPath.Resolve<typeof obj, TPath>,
186
+ ) => {
187
+ FieldPath.setValue(obj, path, value);
188
+ };
189
+
190
+ setFieldValue(getPath("a"), 99);
191
+ setFieldValue(getPath("b"), 99);
192
+ setFieldValue(getPath("c.d"), 99);
193
+ setFieldValue(getPath("c.e"), [99, 99]);
194
+ setFieldValue(getPath("c.e[2]"), 99);
195
+
196
+ expect(obj).toMatchObject({
197
+ a: 99,
198
+ b: 99,
199
+ c: {
200
+ d: 99,
201
+ e: [99, 99, 99],
202
+ },
203
+ });
204
+ });
@@ -1,12 +1,7 @@
1
1
  export namespace FieldPath {
2
2
  export type Segments = readonly PropertyKey[];
3
- export type CanonicalPath = string;
4
3
  export type StringPath = string;
5
4
 
6
- export function toCanonicalPath(path: Segments) {
7
- return path.join(".") as CanonicalPath;
8
- }
9
-
10
5
  export function toStringPath(path: Segments) {
11
6
  const normalizedPath = normalize(path);
12
7
  let result = "";
@@ -100,6 +95,7 @@ export namespace FieldPath {
100
95
  }
101
96
  return true;
102
97
  }
98
+
103
99
  export function isDescendant(parentPath: Segments, childPath: Segments) {
104
100
  if (parentPath.length >= childPath.length) return false;
105
101
 
@@ -112,15 +108,13 @@ export namespace FieldPath {
112
108
  return true;
113
109
  }
114
110
 
115
- type Unfoldable<T> = T & { _____foldMark?: never } & {};
111
+ /** @internal used to mark and prevent array types, so IntellSense can suggest and accept array indices */
112
+ export type _Unfoldable<T> = T & { _____foldMark?: never } & {};
116
113
 
117
- export type Resolve<
118
- TObject,
119
- TPath extends readonly PropertyKey[],
120
- > = TPath extends []
114
+ export type Resolve<TObject, TPath extends Segments> = TPath extends []
121
115
  ? TObject
122
116
  : TPath extends readonly [infer Prop, ...infer Rest]
123
- ? Rest extends readonly PropertyKey[]
117
+ ? Rest extends Segments
124
118
  ? Resolve<ResolveStep<TObject, Prop>, Rest>
125
119
  : never
126
120
  : never;
@@ -150,7 +144,7 @@ export namespace FieldPath {
150
144
  TCanonicalStringPaths extends `${string}[*]${string}`
151
145
  ?
152
146
  | ReplaceAll<TCanonicalStringPaths, "[*]", "[0]">
153
- | Unfoldable<ReplaceAll<TCanonicalStringPaths, "[*]", `[${number}]`>>
147
+ | _Unfoldable<ReplaceAll<TCanonicalStringPaths, "[*]", `[${number}]`>>
154
148
  : TCanonicalStringPaths;
155
149
 
156
150
  type CanonicalStringPaths<TObject extends object> = {
@@ -210,9 +204,47 @@ export namespace FieldPath {
210
204
  ? K
211
205
  : never;
212
206
 
207
+ export function walkPath<
208
+ TObject extends object,
209
+ const TPath extends FieldPath.Segments,
210
+ >(object: TObject, path: TPath): { target: any; key: PropertyKey };
211
+ export function walkPath<
212
+ TObject extends object,
213
+ const TPath extends FieldPath.Segments,
214
+ >(
215
+ object: TObject,
216
+ path: TPath,
217
+ opts: { returnOnEmptyBranch: true },
218
+ ): { target: any; key: PropertyKey | null };
219
+ export function walkPath<
220
+ TObject extends object,
221
+ const TPath extends FieldPath.Segments,
222
+ >(object: TObject, path: TPath, opts?: { returnOnEmptyBranch?: boolean }) {
223
+ let current: any = object;
224
+
225
+ for (let i = 0; i < path.length - 1; i++) {
226
+ const pathFragment = path[i];
227
+ const nextFragment = path[i + 1];
228
+
229
+ if (current[pathFragment] == null) {
230
+ if (opts?.returnOnEmptyBranch) return { target: null, key: null };
231
+ current[pathFragment] = typeof nextFragment === "number" ? [] : {};
232
+ }
233
+
234
+ current = current[pathFragment];
235
+ }
236
+
237
+ const lastFragment = path[path.length - 1];
238
+
239
+ return {
240
+ target: current,
241
+ key: lastFragment,
242
+ };
243
+ }
244
+
213
245
  export function getValue<
214
246
  TObject extends object,
215
- const TPath extends readonly PropertyKey[],
247
+ const TPath extends Segments,
216
248
  >(
217
249
  object: TObject,
218
250
  path: TPath,
@@ -229,64 +261,36 @@ export namespace FieldPath {
229
261
 
230
262
  export function setValue<
231
263
  TObject extends object,
232
- const TPath extends readonly PropertyKey[],
233
- >(object: TObject, key: TPath, value: FieldPath.Resolve<TObject, TPath>) {
234
- return FieldPath.modifyValue(object, key, () => value);
264
+ const TPath extends Segments,
265
+ >(object: TObject, path: TPath, value: FieldPath.Resolve<TObject, TPath>) {
266
+ const { target, key } = walkPath(object, path);
267
+ target[key] = value;
235
268
  }
236
269
 
237
270
  export function modifyValue<
238
271
  TObject extends object,
239
- const TPath extends readonly PropertyKey[],
272
+ const TPath extends Segments,
240
273
  >(
241
274
  object: TObject,
242
275
  path: TPath,
243
276
  modifier: (
244
- currentValue: FieldPath.Resolve<TObject, TPath>,
245
- ) => FieldPath.Resolve<TObject, TPath> | void,
277
+ currentValue: FieldPath.Resolve<TObject, TPath> | undefined,
278
+ ) => void,
246
279
  ) {
247
- let current: any = object;
248
-
249
- for (let i = 0; i < path.length - 1; i++) {
250
- const pathFragment = path[i];
251
- const nextFragment = path[i + 1];
252
-
253
- if (current[pathFragment] == null) {
254
- current[pathFragment] = typeof nextFragment === "number" ? [] : {};
255
- }
256
-
257
- current = current[pathFragment];
258
- }
259
-
260
- const lastFragment = path[path.length - 1];
261
-
262
- const oldValue = current[lastFragment];
263
- const newValue = modifier(oldValue);
264
-
265
- if (newValue !== undefined) {
266
- current[lastFragment] = newValue;
267
- }
280
+ const { target, key } = walkPath(object, path);
281
+ modifier(target[key]);
268
282
  }
269
283
 
270
- export function deleteValue<
271
- TObject extends object,
272
- TPath extends readonly PropertyKey[],
273
- >(object: TObject, path: TPath) {
274
- let current: any = object;
275
-
276
- for (let i = 0; i < path.length - 1; i++) {
277
- const pathFragment = path[i];
278
-
279
- if (current[pathFragment] == null) return;
280
-
281
- current = current[pathFragment];
282
- }
284
+ export function deleteValue<TObject extends object, TPath extends Segments>(
285
+ object: TObject,
286
+ path: TPath,
287
+ ) {
288
+ const { target, key } = walkPath(object, path, {
289
+ returnOnEmptyBranch: true,
290
+ });
283
291
 
284
- const lastFragment = path[path.length - 1];
292
+ if (key == null) return;
285
293
 
286
- delete current[lastFragment];
294
+ delete target[key];
287
295
  }
288
296
  }
289
-
290
- // const x = {} as { foo: { bar: string[] } };
291
- // FieldPath.setValue(x, FieldPath.fromStringPath("foo.bar[9]"), "C");
292
- // console.log(x); // <-- { foo: { bar: [<9xempty>, "C"] } }
@@ -0,0 +1,47 @@
1
+ import { expect, test } from "vitest";
2
+ import { FieldPath } from "../field/FieldPath";
3
+ import { FieldPathBuilder } from "../field/FieldPathBuilder";
4
+
5
+ interface User {
6
+ name: string;
7
+ address: {
8
+ city: string;
9
+ street: string;
10
+ };
11
+ friends: {
12
+ name: string;
13
+ tags: string[];
14
+ }[];
15
+ coords: [100, 200];
16
+ }
17
+
18
+ const builder = new FieldPathBuilder<User>();
19
+
20
+ const data: User = {
21
+ name: "",
22
+ address: { city: "", street: "" },
23
+ friends: [{ name: "", tags: ["A", "B"] }],
24
+ coords: [100, 200] as const,
25
+ };
26
+
27
+ test("builds path segments from proxy and string paths", () => {
28
+ const path1 = builder.of((data) => data.friends[0].tags[1]);
29
+ const value1 = FieldPath.getValue(data, path1);
30
+ expect(path1).toStrictEqual(["friends", 0, "tags", 1]);
31
+ expect(value1).toStrictEqual(data.friends[0].tags[1]);
32
+
33
+ const path2 = builder.of("friends[0].tags[0]");
34
+ const value2 = FieldPath.getValue(data, path2);
35
+ expect(path2).toStrictEqual(["friends", 0, "tags", 0]);
36
+ expect(value2).toStrictEqual(data.friends[0].tags[0]);
37
+
38
+ const path3 = builder.of("friends[0].tags");
39
+ const value3 = FieldPath.getValue(data, path3);
40
+ expect(path3).toStrictEqual(["friends", 0, "tags"]);
41
+ expect(value3).toStrictEqual(data.friends[0].tags);
42
+
43
+ const path4 = builder.of("coords[1]");
44
+ const value4 = FieldPath.getValue(data, path4);
45
+ expect(path4).toStrictEqual(["coords", 1]);
46
+ expect(value4).toStrictEqual(data.coords[1]);
47
+ });
@@ -44,89 +44,23 @@ export class FieldPathBuilder<TOutput extends object> {
44
44
  }) as any;
45
45
  }
46
46
 
47
- public fromStringPath<TStrPath extends FieldPath.StringPaths<TOutput>>(
47
+ public of<TStrPath extends FieldPath.StringPaths<TOutput>>(
48
48
  stringPath: TStrPath,
49
- ) {
50
- return FieldPath.fromStringPath(
51
- stringPath,
52
- ) as unknown as FieldPath.ParseStringPath<TStrPath>;
53
- }
49
+ ): FieldPath.ParseStringPath<TStrPath>;
54
50
 
55
- public fromProxy<TProxy extends FieldPathBuilder.Proxy<any, any>>(
51
+ public of<TProxy extends FieldPathBuilder.Proxy<any, any>>(
56
52
  consumer: (data: FieldPathBuilder.Proxy<TOutput, []>) => TProxy,
57
- ): TProxy[typeof resolverCall] {
58
- return consumer(FieldPathBuilder.wrap<TOutput, []>([]))[resolverCall];
59
- }
60
- }
61
-
62
- /* ---- TESTS ---------------- */
63
-
64
- // interface User {
65
- // name: string;
66
- // address: {
67
- // city: string;
68
- // street: string;
69
- // };
70
- // friends: {
71
- // name: string;
72
- // tags: string[];
73
- // }[];
74
- // coords: [100, 200];
75
- // }
76
-
77
- // const builder = new FieldPathBuilder<User>();
78
-
79
- // const data: User = {
80
- // name: "",
81
- // address: { city: "", street: "" },
82
- // friends: [{ name: "", tags: ["A", "B"] }],
83
- // coords: [100, 200] as const,
84
- // };
85
-
86
- // const path = builder.fromProxy((data) => data.friends[0].tags[1]);
87
- // // ^?
88
- // const value = FieldPath.getValue(data, path);
89
- // // ^?
90
- // console.log(path, "=", value);
91
-
92
- // const path2 = builder.fromStringPath("friends[0].tags[0]");
93
- // // ^?
94
- // const value2 = FieldPath.getValue(data, path2);
95
- // // ^?
96
- // console.log(path2, "=", value2);
53
+ ): TProxy[typeof resolverCall];
97
54
 
98
- // const path3 = builder.fromStringPath("coords[0]");
99
- // // ^?
100
- // const value3 = FieldPath.getValue(data, path3);
101
- // console.log(path3, "=", value3);
102
-
103
- // const path4 = builder.fromStringPath("coords[1]");
104
- // // ^?
105
- // const value4 = FieldPath.getValue(data, path4);
106
- // // ^?
107
- // console.log(path4, "=", value4);
108
-
109
- // type Shape = {
110
- // user?: {
111
- // profile?: { name?: string };
112
- // tags?: boolean[];
113
- // };
114
- // };
115
-
116
- // type A = FieldPath.Resolve<Shape, ["user"]>;
117
- // // ^?
118
-
119
- // type A2 = FieldPath.Resolve<Shape, ["user", "profile"]>;
120
- // // ^?
121
-
122
- // type B = FieldPath.Resolve<Shape, ["user", "profile", "name"]>;
123
- // // ^?
124
-
125
- // type C = FieldPath.Resolve<Shape, ["user", "tags", number]>;
126
- // // ^?
127
-
128
- // type C2 = FieldPath.Resolve<Shape, ["user", "tags", 0]>;
129
- // // ^?
55
+ public of(
56
+ arg:
57
+ | FieldPath.StringPaths<TOutput>
58
+ | ((data: FieldPathBuilder.Proxy<TOutput, []>) => any),
59
+ ) {
60
+ if (typeof arg === "function") {
61
+ return arg(FieldPathBuilder.wrap<TOutput, []>([]))[resolverCall];
62
+ }
130
63
 
131
- // type D = FieldPath.Resolve<Shape, ["user", "missing"]>;
132
- // // ^?
64
+ return FieldPath.fromStringPath(arg as any);
65
+ }
66
+ }
@@ -1,4 +1,4 @@
1
- export namespace Reconsile {
1
+ export namespace Reconcile {
2
2
  export function deepEqual(
3
3
  a: any,
4
4
  b: any,
@@ -70,27 +70,32 @@ export namespace Reconsile {
70
70
  return true;
71
71
  }
72
72
 
73
- export function diff<T>(
73
+ export function arrayDiff<T>(
74
74
  prev: readonly T[],
75
75
  next: readonly T[],
76
- equals: (a: T, b: T) => boolean,
77
- filter?: (a: T) => boolean,
76
+ opts?: {
77
+ equals?: (a: T, b: T) => boolean;
78
+ include?: (a: T) => boolean;
79
+ },
78
80
  ) {
79
81
  const added: T[] = [];
80
82
  const removed: T[] = [];
81
83
  const unchanged: T[] = [];
82
84
 
85
+ const equals = (a: T, b: T) => (opts?.equals ? opts.equals(a, b) : a === b);
86
+ const include = (a: T) => (opts?.include ? opts.include(a) : true);
87
+
83
88
  for (const n of next) {
84
89
  if (prev.some((p) => equals(p, n))) {
85
- if (filter?.(n) ?? true) unchanged.push(n);
90
+ if (include(n)) unchanged.push(n);
86
91
  } else {
87
- if (filter?.(n) ?? true) added.push(n);
92
+ if (include(n)) added.push(n);
88
93
  }
89
94
  }
90
95
 
91
96
  for (const p of prev) {
92
97
  if (!next.some((n) => equals(p, n))) {
93
- if (filter?.(p) ?? true) removed.push(p);
98
+ if (include(p)) removed.push(p);
94
99
  }
95
100
  }
96
101