@voidhash/mimic 0.0.1-alpha.6 → 0.0.1-alpha.7
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.
- package/.turbo/turbo-build.log +13 -13
- package/dist/index.cjs +132 -39
- package/dist/index.d.cts +196 -98
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +196 -98
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +132 -39
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/primitives/Array.ts +45 -21
- package/src/primitives/Boolean.ts +7 -5
- package/src/primitives/Either.ts +13 -11
- package/src/primitives/Literal.ts +6 -4
- package/src/primitives/Number.ts +12 -10
- package/src/primitives/String.ts +13 -11
- package/src/primitives/Struct.ts +64 -15
- package/src/primitives/Tree.ts +101 -30
- package/src/primitives/Union.ts +43 -16
- package/src/primitives/shared.ts +106 -5
- package/tests/primitives/Struct.test.ts +250 -0
- package/tests/primitives/Tree.test.ts +122 -0
package/src/primitives/Struct.ts
CHANGED
|
@@ -6,7 +6,7 @@ import * as ProxyEnvironment from "../ProxyEnvironment";
|
|
|
6
6
|
import * as Transform from "../Transform";
|
|
7
7
|
import type { Primitive, PrimitiveInternal, MaybeUndefined, AnyPrimitive, Validator, InferState, InferProxy, InferSnapshot } from "../Primitive";
|
|
8
8
|
import { ValidationError } from "../Primitive";
|
|
9
|
-
import { runValidators } from "./shared";
|
|
9
|
+
import { runValidators, applyDefaults, StructSetInput } from "./shared";
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
/**
|
|
@@ -25,6 +25,16 @@ export type InferStructSnapshot<TFields extends Record<string, AnyPrimitive>> =
|
|
|
25
25
|
readonly [K in keyof TFields]: InferSnapshot<TFields[K]>;
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Maps a schema definition to a partial update type.
|
|
30
|
+
* For nested structs, allows recursive partial updates.
|
|
31
|
+
*/
|
|
32
|
+
export type StructUpdateValue<TFields extends Record<string, AnyPrimitive>> = {
|
|
33
|
+
readonly [K in keyof TFields]?: TFields[K] extends StructPrimitive<infer F, any>
|
|
34
|
+
? StructUpdateValue<F> | InferState<TFields[K]>
|
|
35
|
+
: InferState<TFields[K]>;
|
|
36
|
+
};
|
|
37
|
+
|
|
28
38
|
/**
|
|
29
39
|
* Maps a schema definition to its proxy type.
|
|
30
40
|
* Provides nested field access + get()/set()/toSnapshot() methods for the whole struct.
|
|
@@ -34,8 +44,10 @@ export type StructProxy<TFields extends Record<string, AnyPrimitive>, TDefined e
|
|
|
34
44
|
} & {
|
|
35
45
|
/** Gets the entire struct value */
|
|
36
46
|
get(): MaybeUndefined<InferStructState<TFields>, TDefined>;
|
|
37
|
-
/** Sets the entire struct value */
|
|
38
|
-
set(value:
|
|
47
|
+
/** Sets the entire struct value (only fields that are required without defaults must be provided) */
|
|
48
|
+
set(value: StructSetInput<TFields>): void;
|
|
49
|
+
/** Updates only the specified fields (partial update, handles nested structs recursively) */
|
|
50
|
+
update(value: StructUpdateValue<TFields>): void;
|
|
39
51
|
/** Returns a readonly snapshot of the struct for rendering */
|
|
40
52
|
toSnapshot(): MaybeUndefined<InferStructSnapshot<TFields>, TDefined>;
|
|
41
53
|
};
|
|
@@ -47,12 +59,14 @@ interface StructPrimitiveSchema<TFields extends Record<string, AnyPrimitive>> {
|
|
|
47
59
|
readonly validators: readonly Validator<InferStructState<TFields>>[];
|
|
48
60
|
}
|
|
49
61
|
|
|
50
|
-
export class StructPrimitive<TFields extends Record<string, AnyPrimitive>, TDefined extends boolean = false>
|
|
51
|
-
implements Primitive<InferStructState<TFields>, StructProxy<TFields, TDefined
|
|
62
|
+
export class StructPrimitive<TFields extends Record<string, AnyPrimitive>, TDefined extends boolean = false, THasDefault extends boolean = false>
|
|
63
|
+
implements Primitive<InferStructState<TFields>, StructProxy<TFields, TDefined>, TDefined, THasDefault>
|
|
52
64
|
{
|
|
53
65
|
readonly _tag = "StructPrimitive" as const;
|
|
54
66
|
readonly _State!: InferStructState<TFields>;
|
|
55
67
|
readonly _Proxy!: StructProxy<TFields, TDefined>;
|
|
68
|
+
readonly _TDefined!: TDefined;
|
|
69
|
+
readonly _THasDefault!: THasDefault;
|
|
56
70
|
|
|
57
71
|
private readonly _schema: StructPrimitiveSchema<TFields>;
|
|
58
72
|
|
|
@@ -70,7 +84,7 @@ export class StructPrimitive<TFields extends Record<string, AnyPrimitive>, TDefi
|
|
|
70
84
|
}
|
|
71
85
|
|
|
72
86
|
/** Mark this struct as required */
|
|
73
|
-
required(): StructPrimitive<TFields, true> {
|
|
87
|
+
required(): StructPrimitive<TFields, true, THasDefault> {
|
|
74
88
|
return new StructPrimitive({
|
|
75
89
|
...this._schema,
|
|
76
90
|
required: true,
|
|
@@ -78,10 +92,12 @@ export class StructPrimitive<TFields extends Record<string, AnyPrimitive>, TDefi
|
|
|
78
92
|
}
|
|
79
93
|
|
|
80
94
|
/** Set a default value for this struct */
|
|
81
|
-
default(defaultValue:
|
|
95
|
+
default(defaultValue: StructSetInput<TFields>): StructPrimitive<TFields, true, true> {
|
|
96
|
+
// Apply defaults to the provided value
|
|
97
|
+
const merged = applyDefaults(this as AnyPrimitive, defaultValue as Partial<InferStructState<TFields>>) as InferStructState<TFields>;
|
|
82
98
|
return new StructPrimitive({
|
|
83
99
|
...this._schema,
|
|
84
|
-
defaultValue,
|
|
100
|
+
defaultValue: merged,
|
|
85
101
|
});
|
|
86
102
|
}
|
|
87
103
|
|
|
@@ -91,7 +107,7 @@ export class StructPrimitive<TFields extends Record<string, AnyPrimitive>, TDefi
|
|
|
91
107
|
}
|
|
92
108
|
|
|
93
109
|
/** Add a custom validation rule (useful for cross-field validation) */
|
|
94
|
-
refine(fn: (value: InferStructState<TFields>) => boolean, message: string): StructPrimitive<TFields, TDefined> {
|
|
110
|
+
refine(fn: (value: InferStructState<TFields>) => boolean, message: string): StructPrimitive<TFields, TDefined, THasDefault> {
|
|
95
111
|
return new StructPrimitive({
|
|
96
112
|
...this._schema,
|
|
97
113
|
validators: [...this._schema.validators, { validate: fn, message }],
|
|
@@ -130,17 +146,47 @@ export class StructPrimitive<TFields extends Record<string, AnyPrimitive>, TDefi
|
|
|
130
146
|
return snapshot as InferStructSnapshot<TFields>;
|
|
131
147
|
};
|
|
132
148
|
|
|
133
|
-
// Create the base object with get/set/toSnapshot methods
|
|
149
|
+
// Create the base object with get/set/update/toSnapshot methods
|
|
134
150
|
const base = {
|
|
135
151
|
get: (): MaybeUndefined<InferStructState<TFields>, TDefined> => {
|
|
136
152
|
const state = env.getState(operationPath) as InferStructState<TFields> | undefined;
|
|
137
153
|
return (state ?? defaultValue) as MaybeUndefined<InferStructState<TFields>, TDefined>;
|
|
138
154
|
},
|
|
139
|
-
set: (value:
|
|
155
|
+
set: (value: StructSetInput<TFields>) => {
|
|
156
|
+
// Apply defaults for missing fields
|
|
157
|
+
const merged = applyDefaults(this as AnyPrimitive, value as Partial<InferStructState<TFields>>) as InferStructState<TFields>;
|
|
140
158
|
env.addOperation(
|
|
141
|
-
Operation.fromDefinition(operationPath, this._opDefinitions.set,
|
|
159
|
+
Operation.fromDefinition(operationPath, this._opDefinitions.set, merged)
|
|
142
160
|
);
|
|
143
161
|
},
|
|
162
|
+
update: (value: StructUpdateValue<TFields>) => {
|
|
163
|
+
for (const key in value) {
|
|
164
|
+
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
165
|
+
const fieldValue = value[key as keyof TFields];
|
|
166
|
+
if (fieldValue === undefined) continue; // Skip undefined values
|
|
167
|
+
|
|
168
|
+
const fieldPrimitive = fields[key as keyof TFields];
|
|
169
|
+
if (!fieldPrimitive) continue; // Skip unknown fields
|
|
170
|
+
|
|
171
|
+
const fieldPath = operationPath.append(key);
|
|
172
|
+
const fieldProxy = fieldPrimitive._internal.createProxy(env, fieldPath);
|
|
173
|
+
|
|
174
|
+
// Check if this is a nested struct and value is a plain object (partial update)
|
|
175
|
+
if (
|
|
176
|
+
fieldPrimitive._tag === "StructPrimitive" &&
|
|
177
|
+
typeof fieldValue === "object" &&
|
|
178
|
+
fieldValue !== null &&
|
|
179
|
+
!Array.isArray(fieldValue)
|
|
180
|
+
) {
|
|
181
|
+
// Recursively update nested struct
|
|
182
|
+
(fieldProxy as { update: (v: unknown) => void }).update(fieldValue);
|
|
183
|
+
} else {
|
|
184
|
+
// Set the field value directly
|
|
185
|
+
(fieldProxy as { set: (v: unknown) => void }).set(fieldValue);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
},
|
|
144
190
|
toSnapshot: (): MaybeUndefined<InferStructSnapshot<TFields>, TDefined> => {
|
|
145
191
|
const snapshot = buildSnapshot();
|
|
146
192
|
return snapshot as MaybeUndefined<InferStructSnapshot<TFields>, TDefined>;
|
|
@@ -150,13 +196,16 @@ export class StructPrimitive<TFields extends Record<string, AnyPrimitive>, TDefi
|
|
|
150
196
|
// Use a JavaScript Proxy to intercept field access
|
|
151
197
|
return new globalThis.Proxy(base as StructProxy<TFields, TDefined>, {
|
|
152
198
|
get: (target, prop, _receiver) => {
|
|
153
|
-
// Return base methods (get, set, toSnapshot)
|
|
199
|
+
// Return base methods (get, set, update, toSnapshot)
|
|
154
200
|
if (prop === "get") {
|
|
155
201
|
return target.get;
|
|
156
202
|
}
|
|
157
203
|
if (prop === "set") {
|
|
158
204
|
return target.set;
|
|
159
205
|
}
|
|
206
|
+
if (prop === "update") {
|
|
207
|
+
return target.update;
|
|
208
|
+
}
|
|
160
209
|
if (prop === "toSnapshot") {
|
|
161
210
|
return target.toSnapshot;
|
|
162
211
|
}
|
|
@@ -176,7 +225,7 @@ export class StructPrimitive<TFields extends Record<string, AnyPrimitive>, TDefi
|
|
|
176
225
|
return undefined;
|
|
177
226
|
},
|
|
178
227
|
has: (_target, prop) => {
|
|
179
|
-
if (prop === "get" || prop === "set" || prop === "toSnapshot") return true;
|
|
228
|
+
if (prop === "get" || prop === "set" || prop === "update" || prop === "toSnapshot") return true;
|
|
180
229
|
if (typeof prop === "string" && prop in fields) return true;
|
|
181
230
|
return false;
|
|
182
231
|
},
|
|
@@ -343,6 +392,6 @@ export class StructPrimitive<TFields extends Record<string, AnyPrimitive>, TDefi
|
|
|
343
392
|
/** Creates a new StructPrimitive with the given fields */
|
|
344
393
|
export const Struct = <TFields extends Record<string, AnyPrimitive>>(
|
|
345
394
|
fields: TFields
|
|
346
|
-
): StructPrimitive<TFields, false> =>
|
|
395
|
+
): StructPrimitive<TFields, false, false> =>
|
|
347
396
|
new StructPrimitive({ required: false, defaultValue: undefined, fields, validators: [] });
|
|
348
397
|
|
package/src/primitives/Tree.ts
CHANGED
|
@@ -5,11 +5,11 @@ import * as OperationPath from "../OperationPath";
|
|
|
5
5
|
import * as ProxyEnvironment from "../ProxyEnvironment";
|
|
6
6
|
import * as Transform from "../Transform";
|
|
7
7
|
import * as FractionalIndex from "../FractionalIndex";
|
|
8
|
-
import type { Primitive, PrimitiveInternal, Validator, InferProxy } from "./shared";
|
|
9
|
-
import { ValidationError } from "./shared";
|
|
8
|
+
import type { Primitive, PrimitiveInternal, Validator, InferProxy, AnyPrimitive, StructSetInput } from "./shared";
|
|
9
|
+
import { ValidationError, applyDefaults } from "./shared";
|
|
10
10
|
import { runValidators } from "./shared";
|
|
11
11
|
import type { AnyTreeNodePrimitive, InferTreeNodeType, InferTreeNodeDataState, InferTreeNodeChildren } from "./TreeNode";
|
|
12
|
-
import { InferStructState } from "./Struct";
|
|
12
|
+
import { InferStructState, StructUpdateValue, StructPrimitive } from "./Struct";
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
/**
|
|
@@ -105,6 +105,23 @@ export type TreeNodeSnapshot<TNode extends AnyTreeNodePrimitive> = {
|
|
|
105
105
|
export type InferTreeSnapshot<T extends TreePrimitive<any>> =
|
|
106
106
|
T extends TreePrimitive<infer TRoot> ? TreeNodeSnapshot<TRoot> : never;
|
|
107
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Helper type to infer the update value type from a TreeNode's data
|
|
110
|
+
*/
|
|
111
|
+
export type TreeNodeUpdateValue<TNode extends AnyTreeNodePrimitive> =
|
|
112
|
+
TNode["data"] extends StructPrimitive<infer TFields, any, any>
|
|
113
|
+
? StructUpdateValue<TFields>
|
|
114
|
+
: never;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Helper type to infer the input type for node data (with defaults applied).
|
|
118
|
+
* Uses StructSetInput for struct data, making fields with defaults optional.
|
|
119
|
+
*/
|
|
120
|
+
export type TreeNodeDataSetInput<TNode extends AnyTreeNodePrimitive> =
|
|
121
|
+
TNode["data"] extends StructPrimitive<infer TFields, any, any>
|
|
122
|
+
? StructSetInput<TFields>
|
|
123
|
+
: InferTreeNodeDataState<TNode>;
|
|
124
|
+
|
|
108
125
|
/**
|
|
109
126
|
* Typed proxy for a specific node type - provides type-safe data access
|
|
110
127
|
*/
|
|
@@ -117,6 +134,8 @@ export interface TypedNodeProxy<TNode extends AnyTreeNodePrimitive> {
|
|
|
117
134
|
readonly data: InferProxy<TNode["data"]>;
|
|
118
135
|
/** Get the raw node state */
|
|
119
136
|
get(): TypedTreeNodeState<TNode>;
|
|
137
|
+
/** Updates only the specified data fields (partial update, handles nested structs recursively) */
|
|
138
|
+
update(value: TreeNodeUpdateValue<TNode>): void;
|
|
120
139
|
}
|
|
121
140
|
|
|
122
141
|
/**
|
|
@@ -158,40 +177,40 @@ export interface TreeProxy<TRoot extends AnyTreeNodePrimitive> {
|
|
|
158
177
|
/** Gets a node proxy by ID with type narrowing capabilities */
|
|
159
178
|
node(id: string): TreeNodeProxyBase<TRoot> | undefined;
|
|
160
179
|
|
|
161
|
-
/** Insert a new node as the first child */
|
|
180
|
+
/** Insert a new node as the first child (applies defaults for node data) */
|
|
162
181
|
insertFirst<TNode extends AnyTreeNodePrimitive>(
|
|
163
182
|
parentId: string | null,
|
|
164
183
|
nodeType: TNode,
|
|
165
|
-
data:
|
|
184
|
+
data: TreeNodeDataSetInput<TNode>
|
|
166
185
|
): string;
|
|
167
186
|
|
|
168
|
-
/** Insert a new node as the last child */
|
|
187
|
+
/** Insert a new node as the last child (applies defaults for node data) */
|
|
169
188
|
insertLast<TNode extends AnyTreeNodePrimitive>(
|
|
170
189
|
parentId: string | null,
|
|
171
190
|
nodeType: TNode,
|
|
172
|
-
data:
|
|
191
|
+
data: TreeNodeDataSetInput<TNode>
|
|
173
192
|
): string;
|
|
174
193
|
|
|
175
|
-
/** Insert a new node at a specific index among siblings */
|
|
194
|
+
/** Insert a new node at a specific index among siblings (applies defaults for node data) */
|
|
176
195
|
insertAt<TNode extends AnyTreeNodePrimitive>(
|
|
177
196
|
parentId: string | null,
|
|
178
197
|
index: number,
|
|
179
198
|
nodeType: TNode,
|
|
180
|
-
data:
|
|
199
|
+
data: TreeNodeDataSetInput<TNode>
|
|
181
200
|
): string;
|
|
182
201
|
|
|
183
|
-
/** Insert a new node after a sibling */
|
|
202
|
+
/** Insert a new node after a sibling (applies defaults for node data) */
|
|
184
203
|
insertAfter<TNode extends AnyTreeNodePrimitive>(
|
|
185
204
|
siblingId: string,
|
|
186
205
|
nodeType: TNode,
|
|
187
|
-
data:
|
|
206
|
+
data: TreeNodeDataSetInput<TNode>
|
|
188
207
|
): string;
|
|
189
208
|
|
|
190
|
-
/** Insert a new node before a sibling */
|
|
209
|
+
/** Insert a new node before a sibling (applies defaults for node data) */
|
|
191
210
|
insertBefore<TNode extends AnyTreeNodePrimitive>(
|
|
192
211
|
siblingId: string,
|
|
193
212
|
nodeType: TNode,
|
|
194
|
-
data:
|
|
213
|
+
data: TreeNodeDataSetInput<TNode>
|
|
195
214
|
): string;
|
|
196
215
|
|
|
197
216
|
/** Remove a node and all its descendants */
|
|
@@ -218,6 +237,13 @@ export interface TreeProxy<TRoot extends AnyTreeNodePrimitive> {
|
|
|
218
237
|
nodeType: TNode
|
|
219
238
|
): InferProxy<TNode["data"]>;
|
|
220
239
|
|
|
240
|
+
/** Updates only the specified data fields of a node (partial update) */
|
|
241
|
+
updateAt<TNode extends AnyTreeNodePrimitive>(
|
|
242
|
+
id: string,
|
|
243
|
+
nodeType: TNode,
|
|
244
|
+
value: TreeNodeUpdateValue<TNode>
|
|
245
|
+
): void;
|
|
246
|
+
|
|
221
247
|
/** Convert tree to a nested snapshot for UI rendering */
|
|
222
248
|
toSnapshot(): TreeNodeSnapshot<TRoot> | undefined;
|
|
223
249
|
}
|
|
@@ -229,12 +255,14 @@ interface TreePrimitiveSchema<TRoot extends AnyTreeNodePrimitive> {
|
|
|
229
255
|
readonly validators: readonly Validator<TreeState<TRoot>>[];
|
|
230
256
|
}
|
|
231
257
|
|
|
232
|
-
export class TreePrimitive<TRoot extends AnyTreeNodePrimitive>
|
|
233
|
-
implements Primitive<TreeState<TRoot>, TreeProxy<TRoot
|
|
258
|
+
export class TreePrimitive<TRoot extends AnyTreeNodePrimitive, TDefined extends boolean = false, THasDefault extends boolean = false>
|
|
259
|
+
implements Primitive<TreeState<TRoot>, TreeProxy<TRoot>, TDefined, THasDefault>
|
|
234
260
|
{
|
|
235
261
|
readonly _tag = "TreePrimitive" as const;
|
|
236
262
|
readonly _State!: TreeState<TRoot>;
|
|
237
263
|
readonly _Proxy!: TreeProxy<TRoot>;
|
|
264
|
+
readonly _TDefined!: TDefined;
|
|
265
|
+
readonly _THasDefault!: THasDefault;
|
|
238
266
|
|
|
239
267
|
private readonly _schema: TreePrimitiveSchema<TRoot>;
|
|
240
268
|
private _nodeTypeRegistry: Map<string, AnyTreeNodePrimitive> | undefined;
|
|
@@ -271,7 +299,7 @@ export class TreePrimitive<TRoot extends AnyTreeNodePrimitive>
|
|
|
271
299
|
}
|
|
272
300
|
|
|
273
301
|
/** Mark this tree as required */
|
|
274
|
-
required(): TreePrimitive<TRoot> {
|
|
302
|
+
required(): TreePrimitive<TRoot, true, THasDefault> {
|
|
275
303
|
return new TreePrimitive({
|
|
276
304
|
...this._schema,
|
|
277
305
|
required: true,
|
|
@@ -279,7 +307,7 @@ export class TreePrimitive<TRoot extends AnyTreeNodePrimitive>
|
|
|
279
307
|
}
|
|
280
308
|
|
|
281
309
|
/** Set a default value for this tree */
|
|
282
|
-
default(defaultValue: TreeState<TRoot>): TreePrimitive<TRoot> {
|
|
310
|
+
default(defaultValue: TreeState<TRoot>): TreePrimitive<TRoot, true, true> {
|
|
283
311
|
return new TreePrimitive({
|
|
284
312
|
...this._schema,
|
|
285
313
|
defaultValue,
|
|
@@ -292,7 +320,7 @@ export class TreePrimitive<TRoot extends AnyTreeNodePrimitive>
|
|
|
292
320
|
}
|
|
293
321
|
|
|
294
322
|
/** Add a custom validation rule */
|
|
295
|
-
refine(fn: (value: TreeState<TRoot>) => boolean, message: string): TreePrimitive<TRoot> {
|
|
323
|
+
refine(fn: (value: TreeState<TRoot>) => boolean, message: string): TreePrimitive<TRoot, TDefined, THasDefault> {
|
|
296
324
|
return new TreePrimitive({
|
|
297
325
|
...this._schema,
|
|
298
326
|
validators: [...this._schema.validators, { validate: fn, message }],
|
|
@@ -404,11 +432,16 @@ export class TreePrimitive<TRoot extends AnyTreeNodePrimitive>
|
|
|
404
432
|
);
|
|
405
433
|
}
|
|
406
434
|
const nodePath = operationPath.append(nodeState.id);
|
|
435
|
+
const dataProxy = nodeType.data._internal.createProxy(env, nodePath) as InferProxy<TNode["data"]>;
|
|
407
436
|
return {
|
|
408
437
|
id: nodeState.id,
|
|
409
438
|
type: nodeType.type as InferTreeNodeType<TNode>,
|
|
410
|
-
data:
|
|
439
|
+
data: dataProxy,
|
|
411
440
|
get: () => nodeState as TypedTreeNodeState<TNode>,
|
|
441
|
+
update: (value: TreeNodeUpdateValue<TNode>) => {
|
|
442
|
+
// Delegate to the data proxy's update method
|
|
443
|
+
(dataProxy as { update: (v: unknown) => void }).update(value);
|
|
444
|
+
},
|
|
412
445
|
};
|
|
413
446
|
},
|
|
414
447
|
|
|
@@ -474,7 +507,7 @@ export class TreePrimitive<TRoot extends AnyTreeNodePrimitive>
|
|
|
474
507
|
insertFirst: <TNode extends AnyTreeNodePrimitive>(
|
|
475
508
|
parentId: string | null,
|
|
476
509
|
nodeType: TNode,
|
|
477
|
-
data:
|
|
510
|
+
data: TreeNodeDataSetInput<TNode>
|
|
478
511
|
): string => {
|
|
479
512
|
const state = getCurrentState();
|
|
480
513
|
const siblings = getOrderedChildren(state, parentId);
|
|
@@ -496,13 +529,16 @@ export class TreePrimitive<TRoot extends AnyTreeNodePrimitive>
|
|
|
496
529
|
throw new ValidationError("Tree already has a root node");
|
|
497
530
|
}
|
|
498
531
|
|
|
532
|
+
// Apply defaults to node data
|
|
533
|
+
const mergedData = applyDefaults(nodeType.data as AnyPrimitive, data as Partial<InferTreeNodeDataState<TNode>>) as InferTreeNodeDataState<TNode>;
|
|
534
|
+
|
|
499
535
|
env.addOperation(
|
|
500
536
|
Operation.fromDefinition(operationPath, this._opDefinitions.insert, {
|
|
501
537
|
id,
|
|
502
538
|
type: nodeType.type,
|
|
503
539
|
parentId,
|
|
504
540
|
pos,
|
|
505
|
-
data,
|
|
541
|
+
data: mergedData,
|
|
506
542
|
})
|
|
507
543
|
);
|
|
508
544
|
|
|
@@ -512,7 +548,7 @@ export class TreePrimitive<TRoot extends AnyTreeNodePrimitive>
|
|
|
512
548
|
insertLast: <TNode extends AnyTreeNodePrimitive>(
|
|
513
549
|
parentId: string | null,
|
|
514
550
|
nodeType: TNode,
|
|
515
|
-
data:
|
|
551
|
+
data: TreeNodeDataSetInput<TNode>
|
|
516
552
|
): string => {
|
|
517
553
|
const state = getCurrentState();
|
|
518
554
|
const siblings = getOrderedChildren(state, parentId);
|
|
@@ -534,13 +570,16 @@ export class TreePrimitive<TRoot extends AnyTreeNodePrimitive>
|
|
|
534
570
|
throw new ValidationError("Tree already has a root node");
|
|
535
571
|
}
|
|
536
572
|
|
|
573
|
+
// Apply defaults to node data
|
|
574
|
+
const mergedData = applyDefaults(nodeType.data as AnyPrimitive, data as Partial<InferTreeNodeDataState<TNode>>) as InferTreeNodeDataState<TNode>;
|
|
575
|
+
|
|
537
576
|
env.addOperation(
|
|
538
577
|
Operation.fromDefinition(operationPath, this._opDefinitions.insert, {
|
|
539
578
|
id,
|
|
540
579
|
type: nodeType.type,
|
|
541
580
|
parentId,
|
|
542
581
|
pos,
|
|
543
|
-
data,
|
|
582
|
+
data: mergedData,
|
|
544
583
|
})
|
|
545
584
|
);
|
|
546
585
|
|
|
@@ -551,7 +590,7 @@ export class TreePrimitive<TRoot extends AnyTreeNodePrimitive>
|
|
|
551
590
|
parentId: string | null,
|
|
552
591
|
index: number,
|
|
553
592
|
nodeType: TNode,
|
|
554
|
-
data:
|
|
593
|
+
data: TreeNodeDataSetInput<TNode>
|
|
555
594
|
): string => {
|
|
556
595
|
const state = getCurrentState();
|
|
557
596
|
const siblings = getOrderedChildren(state, parentId);
|
|
@@ -575,13 +614,16 @@ export class TreePrimitive<TRoot extends AnyTreeNodePrimitive>
|
|
|
575
614
|
throw new ValidationError("Tree already has a root node");
|
|
576
615
|
}
|
|
577
616
|
|
|
617
|
+
// Apply defaults to node data
|
|
618
|
+
const mergedData = applyDefaults(nodeType.data as AnyPrimitive, data as Partial<InferTreeNodeDataState<TNode>>) as InferTreeNodeDataState<TNode>;
|
|
619
|
+
|
|
578
620
|
env.addOperation(
|
|
579
621
|
Operation.fromDefinition(operationPath, this._opDefinitions.insert, {
|
|
580
622
|
id,
|
|
581
623
|
type: nodeType.type,
|
|
582
624
|
parentId,
|
|
583
625
|
pos,
|
|
584
|
-
data,
|
|
626
|
+
data: mergedData,
|
|
585
627
|
})
|
|
586
628
|
);
|
|
587
629
|
|
|
@@ -591,7 +633,7 @@ export class TreePrimitive<TRoot extends AnyTreeNodePrimitive>
|
|
|
591
633
|
insertAfter: <TNode extends AnyTreeNodePrimitive>(
|
|
592
634
|
siblingId: string,
|
|
593
635
|
nodeType: TNode,
|
|
594
|
-
data:
|
|
636
|
+
data: TreeNodeDataSetInput<TNode>
|
|
595
637
|
): string => {
|
|
596
638
|
const state = getCurrentState();
|
|
597
639
|
const sibling = state.find(n => n.id === siblingId);
|
|
@@ -610,13 +652,16 @@ export class TreePrimitive<TRoot extends AnyTreeNodePrimitive>
|
|
|
610
652
|
const parentType = getParentType(parentId);
|
|
611
653
|
this._validateChildType(parentType, nodeType.type);
|
|
612
654
|
|
|
655
|
+
// Apply defaults to node data
|
|
656
|
+
const mergedData = applyDefaults(nodeType.data as AnyPrimitive, data as Partial<InferTreeNodeDataState<TNode>>) as InferTreeNodeDataState<TNode>;
|
|
657
|
+
|
|
613
658
|
env.addOperation(
|
|
614
659
|
Operation.fromDefinition(operationPath, this._opDefinitions.insert, {
|
|
615
660
|
id,
|
|
616
661
|
type: nodeType.type,
|
|
617
662
|
parentId,
|
|
618
663
|
pos,
|
|
619
|
-
data,
|
|
664
|
+
data: mergedData,
|
|
620
665
|
})
|
|
621
666
|
);
|
|
622
667
|
|
|
@@ -626,7 +671,7 @@ export class TreePrimitive<TRoot extends AnyTreeNodePrimitive>
|
|
|
626
671
|
insertBefore: <TNode extends AnyTreeNodePrimitive>(
|
|
627
672
|
siblingId: string,
|
|
628
673
|
nodeType: TNode,
|
|
629
|
-
data:
|
|
674
|
+
data: TreeNodeDataSetInput<TNode>
|
|
630
675
|
): string => {
|
|
631
676
|
const state = getCurrentState();
|
|
632
677
|
const sibling = state.find(n => n.id === siblingId);
|
|
@@ -645,13 +690,16 @@ export class TreePrimitive<TRoot extends AnyTreeNodePrimitive>
|
|
|
645
690
|
const parentType = getParentType(parentId);
|
|
646
691
|
this._validateChildType(parentType, nodeType.type);
|
|
647
692
|
|
|
693
|
+
// Apply defaults to node data
|
|
694
|
+
const mergedData = applyDefaults(nodeType.data as AnyPrimitive, data as Partial<InferTreeNodeDataState<TNode>>) as InferTreeNodeDataState<TNode>;
|
|
695
|
+
|
|
648
696
|
env.addOperation(
|
|
649
697
|
Operation.fromDefinition(operationPath, this._opDefinitions.insert, {
|
|
650
698
|
id,
|
|
651
699
|
type: nodeType.type,
|
|
652
700
|
parentId,
|
|
653
701
|
pos,
|
|
654
|
-
data,
|
|
702
|
+
data: mergedData,
|
|
655
703
|
})
|
|
656
704
|
);
|
|
657
705
|
|
|
@@ -890,6 +938,29 @@ export class TreePrimitive<TRoot extends AnyTreeNodePrimitive>
|
|
|
890
938
|
return nodeType.data._internal.createProxy(env, nodePath) as InferProxy<TNode["data"]>;
|
|
891
939
|
},
|
|
892
940
|
|
|
941
|
+
updateAt: <TNode extends AnyTreeNodePrimitive>(
|
|
942
|
+
id: string,
|
|
943
|
+
nodeType: TNode,
|
|
944
|
+
value: TreeNodeUpdateValue<TNode>
|
|
945
|
+
): void => {
|
|
946
|
+
// Get the node to verify its type
|
|
947
|
+
const state = getCurrentState();
|
|
948
|
+
const node = state.find(n => n.id === id);
|
|
949
|
+
if (!node) {
|
|
950
|
+
throw new ValidationError(`Node not found: ${id}`);
|
|
951
|
+
}
|
|
952
|
+
if (node.type !== nodeType.type) {
|
|
953
|
+
throw new ValidationError(
|
|
954
|
+
`Node is of type "${node.type}", not "${nodeType.type}"`
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
const nodePath = operationPath.append(id);
|
|
959
|
+
const dataProxy = nodeType.data._internal.createProxy(env, nodePath);
|
|
960
|
+
// Delegate to the data proxy's update method
|
|
961
|
+
(dataProxy as { update: (v: unknown) => void }).update(value);
|
|
962
|
+
},
|
|
963
|
+
|
|
893
964
|
toSnapshot: (): TreeNodeSnapshot<TRoot> | undefined => {
|
|
894
965
|
const state = getCurrentState();
|
|
895
966
|
const rootNode = state.find(n => n.parentId === null);
|
|
@@ -1111,7 +1182,7 @@ export interface TreeOptions<TRoot extends AnyTreeNodePrimitive> {
|
|
|
1111
1182
|
/** Creates a new TreePrimitive with the given root node type */
|
|
1112
1183
|
export const Tree = <TRoot extends AnyTreeNodePrimitive>(
|
|
1113
1184
|
options: TreeOptions<TRoot>
|
|
1114
|
-
): TreePrimitive<TRoot> =>
|
|
1185
|
+
): TreePrimitive<TRoot, false, false> =>
|
|
1115
1186
|
new TreePrimitive({
|
|
1116
1187
|
required: false,
|
|
1117
1188
|
defaultValue: undefined,
|
package/src/primitives/Union.ts
CHANGED
|
@@ -7,14 +7,14 @@ import * as Transform from "../Transform";
|
|
|
7
7
|
import type { Primitive, PrimitiveInternal, MaybeUndefined, AnyPrimitive, InferState, InferProxy, InferSnapshot } from "../Primitive";
|
|
8
8
|
import { ValidationError } from "../Primitive";
|
|
9
9
|
import { LiteralPrimitive } from "./Literal";
|
|
10
|
-
import { StructPrimitive } from "./Struct";
|
|
11
|
-
import { runValidators } from "./shared";
|
|
10
|
+
import { StructPrimitive, InferStructState } from "./Struct";
|
|
11
|
+
import { runValidators, applyDefaults, StructSetInput } from "./shared";
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Type constraint for union variants - must be struct primitives
|
|
16
16
|
*/
|
|
17
|
-
export type UnionVariants = Record<string, StructPrimitive<any, any>>;
|
|
17
|
+
export type UnionVariants = Record<string, StructPrimitive<any, any, any>>;
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Infer the union state type from variants
|
|
@@ -30,6 +30,16 @@ export type InferUnionSnapshot<TVariants extends UnionVariants> = {
|
|
|
30
30
|
[K in keyof TVariants]: InferSnapshot<TVariants[K]>;
|
|
31
31
|
}[keyof TVariants];
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Compute the input type for union.set() operations.
|
|
35
|
+
* For each variant, uses StructSetInput to make fields with defaults optional.
|
|
36
|
+
*/
|
|
37
|
+
export type UnionSetInput<TVariants extends UnionVariants> = {
|
|
38
|
+
[K in keyof TVariants]: TVariants[K] extends StructPrimitive<infer TFields, any, any>
|
|
39
|
+
? StructSetInput<TFields>
|
|
40
|
+
: InferState<TVariants[K]>;
|
|
41
|
+
}[keyof TVariants];
|
|
42
|
+
|
|
33
43
|
/**
|
|
34
44
|
* Proxy for accessing union variants
|
|
35
45
|
*/
|
|
@@ -37,8 +47,8 @@ export interface UnionProxy<TVariants extends UnionVariants, _TDiscriminator ext
|
|
|
37
47
|
/** Gets the current union value */
|
|
38
48
|
get(): MaybeUndefined<InferUnionState<TVariants>, TDefined>;
|
|
39
49
|
|
|
40
|
-
/** Sets the entire union value */
|
|
41
|
-
set(value:
|
|
50
|
+
/** Sets the entire union value (applies defaults for variant fields) */
|
|
51
|
+
set(value: UnionSetInput<TVariants>): void;
|
|
42
52
|
|
|
43
53
|
/** Access a specific variant's proxy (assumes the variant is active) */
|
|
44
54
|
as<K extends keyof TVariants>(variant: K): InferProxy<TVariants[K]>;
|
|
@@ -59,12 +69,14 @@ interface UnionPrimitiveSchema<TVariants extends UnionVariants, TDiscriminator e
|
|
|
59
69
|
readonly variants: TVariants;
|
|
60
70
|
}
|
|
61
71
|
|
|
62
|
-
export class UnionPrimitive<TVariants extends UnionVariants, TDiscriminator extends string = "type", TDefined extends boolean = false>
|
|
63
|
-
implements Primitive<InferUnionState<TVariants>, UnionProxy<TVariants, TDiscriminator, TDefined
|
|
72
|
+
export class UnionPrimitive<TVariants extends UnionVariants, TDiscriminator extends string = "type", TDefined extends boolean = false, THasDefault extends boolean = false>
|
|
73
|
+
implements Primitive<InferUnionState<TVariants>, UnionProxy<TVariants, TDiscriminator, TDefined>, TDefined, THasDefault>
|
|
64
74
|
{
|
|
65
75
|
readonly _tag = "UnionPrimitive" as const;
|
|
66
76
|
readonly _State!: InferUnionState<TVariants>;
|
|
67
77
|
readonly _Proxy!: UnionProxy<TVariants, TDiscriminator, TDefined>;
|
|
78
|
+
readonly _TDefined!: TDefined;
|
|
79
|
+
readonly _THasDefault!: THasDefault;
|
|
68
80
|
|
|
69
81
|
private readonly _schema: UnionPrimitiveSchema<TVariants, TDiscriminator>;
|
|
70
82
|
|
|
@@ -82,7 +94,7 @@ export class UnionPrimitive<TVariants extends UnionVariants, TDiscriminator exte
|
|
|
82
94
|
}
|
|
83
95
|
|
|
84
96
|
/** Mark this union as required */
|
|
85
|
-
required(): UnionPrimitive<TVariants, TDiscriminator, true> {
|
|
97
|
+
required(): UnionPrimitive<TVariants, TDiscriminator, true, THasDefault> {
|
|
86
98
|
return new UnionPrimitive({
|
|
87
99
|
...this._schema,
|
|
88
100
|
required: true,
|
|
@@ -90,10 +102,12 @@ export class UnionPrimitive<TVariants extends UnionVariants, TDiscriminator exte
|
|
|
90
102
|
}
|
|
91
103
|
|
|
92
104
|
/** Set a default value for this union */
|
|
93
|
-
default(defaultValue:
|
|
105
|
+
default(defaultValue: UnionSetInput<TVariants>): UnionPrimitive<TVariants, TDiscriminator, true, true> {
|
|
106
|
+
// Apply defaults to the variant
|
|
107
|
+
const merged = this._applyVariantDefaults(defaultValue as Partial<InferUnionState<TVariants>>);
|
|
94
108
|
return new UnionPrimitive({
|
|
95
109
|
...this._schema,
|
|
96
|
-
defaultValue,
|
|
110
|
+
defaultValue: merged,
|
|
97
111
|
});
|
|
98
112
|
}
|
|
99
113
|
|
|
@@ -119,7 +133,7 @@ export class UnionPrimitive<TVariants extends UnionVariants, TDiscriminator exte
|
|
|
119
133
|
const variant = this._schema.variants[key]!;
|
|
120
134
|
const discriminatorField = variant.fields[this._schema.discriminator];
|
|
121
135
|
if (discriminatorField && discriminatorField._tag === "LiteralPrimitive") {
|
|
122
|
-
const literalPrimitive = discriminatorField as LiteralPrimitive<any, any>;
|
|
136
|
+
const literalPrimitive = discriminatorField as LiteralPrimitive<any, any, any>;
|
|
123
137
|
if (literalPrimitive.literal === discriminatorValue) {
|
|
124
138
|
return key;
|
|
125
139
|
}
|
|
@@ -128,6 +142,17 @@ export class UnionPrimitive<TVariants extends UnionVariants, TDiscriminator exte
|
|
|
128
142
|
return undefined;
|
|
129
143
|
}
|
|
130
144
|
|
|
145
|
+
/** Apply defaults to a variant value based on the discriminator */
|
|
146
|
+
private _applyVariantDefaults(value: Partial<InferUnionState<TVariants>>): InferUnionState<TVariants> {
|
|
147
|
+
const variantKey = this._findVariantKey(value as InferUnionState<TVariants>);
|
|
148
|
+
if (!variantKey) {
|
|
149
|
+
return value as InferUnionState<TVariants>;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const variantPrimitive = this._schema.variants[variantKey]!;
|
|
153
|
+
return applyDefaults(variantPrimitive as AnyPrimitive, value) as InferUnionState<TVariants>;
|
|
154
|
+
}
|
|
155
|
+
|
|
131
156
|
readonly _internal: PrimitiveInternal<InferUnionState<TVariants>, UnionProxy<TVariants, TDiscriminator, TDefined>> = {
|
|
132
157
|
createProxy: (
|
|
133
158
|
env: ProxyEnvironment.ProxyEnvironment,
|
|
@@ -141,9 +166,11 @@ export class UnionPrimitive<TVariants extends UnionVariants, TDiscriminator exte
|
|
|
141
166
|
const state = env.getState(operationPath) as InferUnionState<TVariants> | undefined;
|
|
142
167
|
return (state ?? defaultValue) as MaybeUndefined<InferUnionState<TVariants>, TDefined>;
|
|
143
168
|
},
|
|
144
|
-
set: (value:
|
|
169
|
+
set: (value: UnionSetInput<TVariants>) => {
|
|
170
|
+
// Apply defaults for the variant
|
|
171
|
+
const merged = this._applyVariantDefaults(value as Partial<InferUnionState<TVariants>>);
|
|
145
172
|
env.addOperation(
|
|
146
|
-
Operation.fromDefinition(operationPath, this._opDefinitions.set,
|
|
173
|
+
Operation.fromDefinition(operationPath, this._opDefinitions.set, merged)
|
|
147
174
|
);
|
|
148
175
|
},
|
|
149
176
|
as: <K extends keyof TVariants>(variant: K): InferProxy<TVariants[K]> => {
|
|
@@ -311,13 +338,13 @@ export interface UnionOptions<TVariants extends UnionVariants, TDiscriminator ex
|
|
|
311
338
|
/** Creates a new UnionPrimitive with the given variants */
|
|
312
339
|
export function Union<TVariants extends UnionVariants>(
|
|
313
340
|
options: UnionOptions<TVariants, "type">
|
|
314
|
-
): UnionPrimitive<TVariants, "type", false>;
|
|
341
|
+
): UnionPrimitive<TVariants, "type", false, false>;
|
|
315
342
|
export function Union<TVariants extends UnionVariants, TDiscriminator extends string>(
|
|
316
343
|
options: UnionOptions<TVariants, TDiscriminator>
|
|
317
|
-
): UnionPrimitive<TVariants, TDiscriminator, false>;
|
|
344
|
+
): UnionPrimitive<TVariants, TDiscriminator, false, false>;
|
|
318
345
|
export function Union<TVariants extends UnionVariants, TDiscriminator extends string = "type">(
|
|
319
346
|
options: UnionOptions<TVariants, TDiscriminator>
|
|
320
|
-
): UnionPrimitive<TVariants, TDiscriminator, false> {
|
|
347
|
+
): UnionPrimitive<TVariants, TDiscriminator, false, false> {
|
|
321
348
|
const discriminator = (options.discriminator ?? "type") as TDiscriminator;
|
|
322
349
|
return new UnionPrimitive({
|
|
323
350
|
required: false,
|