@voidhash/mimic 0.0.1-alpha.1 → 0.0.1-alpha.111
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 +51 -0
- package/LICENSE.md +663 -0
- package/dist/Document-ChuFrTk1.cjs +571 -0
- package/dist/Document-CwiAFTIq.mjs +438 -0
- package/dist/Document-CwiAFTIq.mjs.map +1 -0
- package/dist/Presence-DKKP4v5X.d.cts +91 -0
- package/dist/Presence-DKKP4v5X.d.cts.map +1 -0
- package/dist/Presence-DdMVKcOv.mjs +110 -0
- package/dist/Presence-DdMVKcOv.mjs.map +1 -0
- package/dist/Presence-N8u7Eppr.d.mts +91 -0
- package/dist/Presence-N8u7Eppr.d.mts.map +1 -0
- package/dist/Presence-gWrmGBeu.cjs +126 -0
- package/dist/Primitive-CvFVxR8_.d.cts +1175 -0
- package/dist/Primitive-CvFVxR8_.d.cts.map +1 -0
- package/dist/Primitive-lEhQyGVL.d.mts +1175 -0
- package/dist/Primitive-lEhQyGVL.d.mts.map +1 -0
- package/dist/chunk-CLMFDpHK.mjs +18 -0
- package/dist/client/index.cjs +1456 -0
- package/dist/client/index.d.cts +692 -0
- package/dist/client/index.d.cts.map +1 -0
- package/dist/client/index.d.mts +692 -0
- package/dist/client/index.d.mts.map +1 -0
- package/dist/client/index.mjs +1413 -0
- package/dist/client/index.mjs.map +1 -0
- package/dist/index.cjs +2577 -0
- package/dist/index.d.cts +143 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +143 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +2526 -0
- package/dist/index.mjs.map +1 -0
- package/dist/server/index.cjs +191 -0
- package/dist/server/index.d.cts +148 -0
- package/dist/server/index.d.cts.map +1 -0
- package/dist/server/index.d.mts +148 -0
- package/dist/server/index.d.mts.map +1 -0
- package/dist/server/index.mjs +182 -0
- package/dist/server/index.mjs.map +1 -0
- package/package.json +25 -13
- package/src/EffectSchema.ts +374 -0
- package/src/Primitive.ts +3 -0
- package/src/client/ClientDocument.ts +1 -1
- package/src/client/errors.ts +10 -10
- package/src/index.ts +1 -0
- package/src/primitives/Array.ts +57 -22
- package/src/primitives/Boolean.ts +33 -19
- package/src/primitives/Either.ts +379 -0
- package/src/primitives/Lazy.ts +16 -2
- package/src/primitives/Literal.ts +33 -20
- package/src/primitives/Number.ts +39 -26
- package/src/primitives/String.ts +40 -25
- package/src/primitives/Struct.ts +126 -29
- package/src/primitives/Tree.ts +119 -32
- package/src/primitives/TreeNode.ts +77 -30
- package/src/primitives/Union.ts +56 -29
- package/src/primitives/shared.ts +111 -9
- package/src/server/errors.ts +6 -6
- package/tests/EffectSchema.test.ts +546 -0
- package/tests/primitives/Array.test.ts +108 -0
- package/tests/primitives/Either.test.ts +707 -0
- package/tests/primitives/Struct.test.ts +250 -0
- package/tests/primitives/Tree.test.ts +250 -0
- package/tsdown.config.ts +1 -1
package/src/primitives/Array.ts
CHANGED
|
@@ -5,9 +5,10 @@ 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, MaybeUndefined, AnyPrimitive, Validator, InferState, InferProxy, InferSnapshot } from "../Primitive";
|
|
8
|
+
import type { Primitive, PrimitiveInternal, MaybeUndefined, AnyPrimitive, Validator, InferState, InferProxy, InferSnapshot, InferSetInput } from "../Primitive";
|
|
9
9
|
import { ValidationError } from "../Primitive";
|
|
10
|
-
import { runValidators } from "./shared";
|
|
10
|
+
import { runValidators, applyDefaults } from "./shared";
|
|
11
|
+
import { StructPrimitive, StructSetInput } from "./Struct";
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -46,15 +47,28 @@ export interface ArrayEntrySnapshot<TElement extends AnyPrimitive> {
|
|
|
46
47
|
*/
|
|
47
48
|
export type ArraySnapshot<TElement extends AnyPrimitive> = readonly ArrayEntrySnapshot<TElement>[];
|
|
48
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Compute the input type for array element values.
|
|
52
|
+
* Uses StructSetInput directly for struct elements so that:
|
|
53
|
+
* - Fields that are required AND have no default must be provided
|
|
54
|
+
* - Fields that are optional OR have defaults can be omitted
|
|
55
|
+
*
|
|
56
|
+
* For non-struct elements, falls back to InferSetInput.
|
|
57
|
+
*/
|
|
58
|
+
export type ArrayElementSetInput<TElement extends AnyPrimitive> =
|
|
59
|
+
TElement extends StructPrimitive<infer TFields, any, any>
|
|
60
|
+
? StructSetInput<TFields>
|
|
61
|
+
: InferSetInput<TElement>;
|
|
62
|
+
|
|
49
63
|
export interface ArrayProxy<TElement extends AnyPrimitive> {
|
|
50
64
|
/** Gets the current array entries (sorted by position) */
|
|
51
65
|
get(): ArrayState<TElement>;
|
|
52
|
-
/** Replaces the entire array with new values (generates new IDs and positions) */
|
|
53
|
-
set(values: readonly
|
|
54
|
-
/** Appends a value to the end of the array */
|
|
55
|
-
push(value:
|
|
56
|
-
/** Inserts a value at the specified visual index */
|
|
57
|
-
insertAt(index: number, value:
|
|
66
|
+
/** Replaces the entire array with new values (generates new IDs and positions, applies defaults) */
|
|
67
|
+
set(values: readonly ArrayElementSetInput<TElement>[]): void;
|
|
68
|
+
/** Appends a value to the end of the array (applies defaults for struct elements) */
|
|
69
|
+
push(value: ArrayElementSetInput<TElement>): void;
|
|
70
|
+
/** Inserts a value at the specified visual index (applies defaults for struct elements) */
|
|
71
|
+
insertAt(index: number, value: ArrayElementSetInput<TElement>): void;
|
|
58
72
|
/** Removes the element with the specified ID */
|
|
59
73
|
remove(id: string): void;
|
|
60
74
|
/** Moves an element to a new visual index */
|
|
@@ -77,12 +91,22 @@ interface ArrayPrimitiveSchema<TElement extends AnyPrimitive> {
|
|
|
77
91
|
readonly validators: readonly Validator<ArrayState<TElement>>[];
|
|
78
92
|
}
|
|
79
93
|
|
|
80
|
-
|
|
81
|
-
|
|
94
|
+
/** Input type for array set() - an array of element set inputs */
|
|
95
|
+
export type ArraySetInput<TElement extends AnyPrimitive> = readonly ArrayElementSetInput<TElement>[];
|
|
96
|
+
|
|
97
|
+
/** Input type for array update() - same as set() for arrays */
|
|
98
|
+
export type ArrayUpdateInput<TElement extends AnyPrimitive> = readonly ArrayElementSetInput<TElement>[];
|
|
99
|
+
|
|
100
|
+
export class ArrayPrimitive<TElement extends AnyPrimitive, TRequired extends boolean = false, THasDefault extends boolean = false>
|
|
101
|
+
implements Primitive<ArrayState<TElement>, ArrayProxy<TElement>, TRequired, THasDefault, ArraySetInput<TElement>, ArrayUpdateInput<TElement>>
|
|
82
102
|
{
|
|
83
103
|
readonly _tag = "ArrayPrimitive" as const;
|
|
84
104
|
readonly _State!: ArrayState<TElement>;
|
|
85
105
|
readonly _Proxy!: ArrayProxy<TElement>;
|
|
106
|
+
readonly _TRequired!: TRequired;
|
|
107
|
+
readonly _THasDefault!: THasDefault;
|
|
108
|
+
readonly TSetInput!: ArraySetInput<TElement>;
|
|
109
|
+
readonly TUpdateInput!: ArrayUpdateInput<TElement>;
|
|
86
110
|
|
|
87
111
|
private readonly _schema: ArrayPrimitiveSchema<TElement>;
|
|
88
112
|
|
|
@@ -118,7 +142,7 @@ export class ArrayPrimitive<TElement extends AnyPrimitive>
|
|
|
118
142
|
}
|
|
119
143
|
|
|
120
144
|
/** Mark this array as required */
|
|
121
|
-
required(): ArrayPrimitive<TElement> {
|
|
145
|
+
required(): ArrayPrimitive<TElement, true, THasDefault> {
|
|
122
146
|
return new ArrayPrimitive({
|
|
123
147
|
...this._schema,
|
|
124
148
|
required: true,
|
|
@@ -126,7 +150,7 @@ export class ArrayPrimitive<TElement extends AnyPrimitive>
|
|
|
126
150
|
}
|
|
127
151
|
|
|
128
152
|
/** Set a default value for this array */
|
|
129
|
-
default(defaultValue: ArrayState<TElement>): ArrayPrimitive<TElement> {
|
|
153
|
+
default(defaultValue: ArrayState<TElement>): ArrayPrimitive<TElement, TRequired, true> {
|
|
130
154
|
return new ArrayPrimitive({
|
|
131
155
|
...this._schema,
|
|
132
156
|
defaultValue,
|
|
@@ -139,7 +163,7 @@ export class ArrayPrimitive<TElement extends AnyPrimitive>
|
|
|
139
163
|
}
|
|
140
164
|
|
|
141
165
|
/** Add a custom validation rule */
|
|
142
|
-
refine(fn: (value: ArrayState<TElement>) => boolean, message: string): ArrayPrimitive<TElement> {
|
|
166
|
+
refine(fn: (value: ArrayState<TElement>) => boolean, message: string): ArrayPrimitive<TElement, TRequired, THasDefault> {
|
|
143
167
|
return new ArrayPrimitive({
|
|
144
168
|
...this._schema,
|
|
145
169
|
validators: [...this._schema.validators, { validate: fn, message }],
|
|
@@ -147,7 +171,7 @@ export class ArrayPrimitive<TElement extends AnyPrimitive>
|
|
|
147
171
|
}
|
|
148
172
|
|
|
149
173
|
/** Minimum array length */
|
|
150
|
-
minLength(length: number): ArrayPrimitive<TElement> {
|
|
174
|
+
minLength(length: number): ArrayPrimitive<TElement, TRequired, THasDefault> {
|
|
151
175
|
return this.refine(
|
|
152
176
|
(v) => v.length >= length,
|
|
153
177
|
`Array must have at least ${length} elements`
|
|
@@ -155,7 +179,7 @@ export class ArrayPrimitive<TElement extends AnyPrimitive>
|
|
|
155
179
|
}
|
|
156
180
|
|
|
157
181
|
/** Maximum array length */
|
|
158
|
-
maxLength(length: number): ArrayPrimitive<TElement> {
|
|
182
|
+
maxLength(length: number): ArrayPrimitive<TElement, TRequired, THasDefault> {
|
|
159
183
|
return this.refine(
|
|
160
184
|
(v) => v.length <= length,
|
|
161
185
|
`Array must have at most ${length} elements`
|
|
@@ -173,12 +197,17 @@ export class ArrayPrimitive<TElement extends AnyPrimitive>
|
|
|
173
197
|
return sortByPos(state);
|
|
174
198
|
};
|
|
175
199
|
|
|
200
|
+
// Helper to apply defaults for element values
|
|
201
|
+
const applyElementDefaults = (value: ArrayElementSetInput<TElement>): InferState<TElement> => {
|
|
202
|
+
return applyDefaults(elementPrimitive, value as Partial<InferState<TElement>>) as InferState<TElement>;
|
|
203
|
+
};
|
|
204
|
+
|
|
176
205
|
return {
|
|
177
206
|
get: (): ArrayState<TElement> => {
|
|
178
207
|
return getCurrentState();
|
|
179
208
|
},
|
|
180
209
|
|
|
181
|
-
set: (values: readonly
|
|
210
|
+
set: (values: readonly ArrayElementSetInput<TElement>[]) => {
|
|
182
211
|
// Generate entries with new IDs and sequential positions
|
|
183
212
|
const entries: ArrayEntry<InferState<TElement>>[] = [];
|
|
184
213
|
let prevPos: string | null = null;
|
|
@@ -186,7 +215,9 @@ export class ArrayPrimitive<TElement extends AnyPrimitive>
|
|
|
186
215
|
for (const value of values) {
|
|
187
216
|
const id = env.generateId();
|
|
188
217
|
const pos = generatePosBetween(prevPos, null);
|
|
189
|
-
|
|
218
|
+
// Apply defaults to element value
|
|
219
|
+
const mergedValue = applyElementDefaults(value);
|
|
220
|
+
entries.push({ id, pos, value: mergedValue });
|
|
190
221
|
prevPos = pos;
|
|
191
222
|
}
|
|
192
223
|
|
|
@@ -195,27 +226,31 @@ export class ArrayPrimitive<TElement extends AnyPrimitive>
|
|
|
195
226
|
);
|
|
196
227
|
},
|
|
197
228
|
|
|
198
|
-
push: (value:
|
|
229
|
+
push: (value: ArrayElementSetInput<TElement>) => {
|
|
199
230
|
const sorted = getCurrentState();
|
|
200
231
|
const lastPos = sorted.length > 0 ? sorted[sorted.length - 1]!.pos : null;
|
|
201
232
|
const id = env.generateId();
|
|
202
233
|
const pos = generatePosBetween(lastPos, null);
|
|
234
|
+
// Apply defaults to element value
|
|
235
|
+
const mergedValue = applyElementDefaults(value);
|
|
203
236
|
|
|
204
237
|
env.addOperation(
|
|
205
|
-
Operation.fromDefinition(operationPath, this._opDefinitions.insert, { id, pos, value })
|
|
238
|
+
Operation.fromDefinition(operationPath, this._opDefinitions.insert, { id, pos, value: mergedValue })
|
|
206
239
|
);
|
|
207
240
|
},
|
|
208
241
|
|
|
209
|
-
insertAt: (index: number, value:
|
|
242
|
+
insertAt: (index: number, value: ArrayElementSetInput<TElement>) => {
|
|
210
243
|
const sorted = getCurrentState();
|
|
211
244
|
const leftPos = index > 0 && sorted[index - 1] ? sorted[index - 1]!.pos : null;
|
|
212
245
|
const rightPos = index < sorted.length && sorted[index] ? sorted[index]!.pos : null;
|
|
213
246
|
|
|
214
247
|
const id = env.generateId();
|
|
215
248
|
const pos = generatePosBetween(leftPos, rightPos);
|
|
249
|
+
// Apply defaults to element value
|
|
250
|
+
const mergedValue = applyElementDefaults(value);
|
|
216
251
|
|
|
217
252
|
env.addOperation(
|
|
218
|
-
Operation.fromDefinition(operationPath, this._opDefinitions.insert, { id, pos, value })
|
|
253
|
+
Operation.fromDefinition(operationPath, this._opDefinitions.insert, { id, pos, value: mergedValue })
|
|
219
254
|
);
|
|
220
255
|
},
|
|
221
256
|
|
|
@@ -452,6 +487,6 @@ export class ArrayPrimitive<TElement extends AnyPrimitive>
|
|
|
452
487
|
}
|
|
453
488
|
|
|
454
489
|
/** Creates a new ArrayPrimitive with the given element type */
|
|
455
|
-
export const Array = <TElement extends AnyPrimitive>(element: TElement): ArrayPrimitive<TElement> =>
|
|
490
|
+
export const Array = <TElement extends AnyPrimitive>(element: TElement): ArrayPrimitive<TElement, false, false> =>
|
|
456
491
|
new ArrayPrimitive({ required: false, defaultValue: undefined, element, validators: [] });
|
|
457
492
|
|
|
@@ -4,17 +4,22 @@ import * as Operation from "../Operation";
|
|
|
4
4
|
import * as OperationPath from "../OperationPath";
|
|
5
5
|
import * as ProxyEnvironment from "../ProxyEnvironment";
|
|
6
6
|
import * as Transform from "../Transform";
|
|
7
|
-
import type { Primitive, PrimitiveInternal, MaybeUndefined, Validator } from "./shared";
|
|
7
|
+
import type { Primitive, PrimitiveInternal, MaybeUndefined, Validator, NeedsValue } from "./shared";
|
|
8
8
|
import { runValidators, isCompatibleOperation, ValidationError } from "./shared";
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
type InferSetInput<TRequired extends boolean = false, THasDefault extends boolean = false> = NeedsValue<boolean, TRequired, THasDefault>
|
|
12
|
+
type InferUpdateInput<TRequired extends boolean = false, THasDefault extends boolean = false> = NeedsValue<boolean, TRequired, THasDefault>
|
|
13
|
+
|
|
14
|
+
export interface BooleanProxy<TRequired extends boolean = false, THasDefault extends boolean = false> {
|
|
12
15
|
/** Gets the current boolean value */
|
|
13
|
-
get(): MaybeUndefined<boolean,
|
|
16
|
+
get(): MaybeUndefined<boolean, TRequired, THasDefault>;
|
|
14
17
|
/** Sets the boolean value, generating a boolean.set operation */
|
|
15
|
-
set(value:
|
|
18
|
+
set(value: InferSetInput<TRequired, THasDefault>): void;
|
|
19
|
+
/** This is the same as set. Updates the boolean value, generating a boolean.set operation */
|
|
20
|
+
update(value: InferUpdateInput<TRequired, THasDefault>): void;
|
|
16
21
|
/** Returns a readonly snapshot of the boolean value for rendering */
|
|
17
|
-
toSnapshot(): MaybeUndefined<boolean,
|
|
22
|
+
toSnapshot(): MaybeUndefined<boolean, TRequired, THasDefault>;
|
|
18
23
|
}
|
|
19
24
|
|
|
20
25
|
interface BooleanPrimitiveSchema {
|
|
@@ -23,10 +28,14 @@ interface BooleanPrimitiveSchema {
|
|
|
23
28
|
readonly validators: readonly Validator<boolean>[];
|
|
24
29
|
}
|
|
25
30
|
|
|
26
|
-
export class BooleanPrimitive<
|
|
31
|
+
export class BooleanPrimitive<TRequired extends boolean = false, THasDefault extends boolean = false> implements Primitive<boolean, BooleanProxy<TRequired, THasDefault>, TRequired, THasDefault, InferSetInput<TRequired, THasDefault>, InferUpdateInput<TRequired, THasDefault>> {
|
|
27
32
|
readonly _tag = "BooleanPrimitive" as const;
|
|
28
33
|
readonly _State!: boolean;
|
|
29
|
-
readonly _Proxy!: BooleanProxy<
|
|
34
|
+
readonly _Proxy!: BooleanProxy<TRequired, THasDefault>;
|
|
35
|
+
readonly _TRequired!: TRequired;
|
|
36
|
+
readonly _THasDefault!: THasDefault;
|
|
37
|
+
readonly TUpdateInput!: InferUpdateInput<TRequired, THasDefault>;
|
|
38
|
+
readonly TSetInput!: InferSetInput<TRequired, THasDefault>;
|
|
30
39
|
|
|
31
40
|
private readonly _schema: BooleanPrimitiveSchema;
|
|
32
41
|
|
|
@@ -44,7 +53,7 @@ export class BooleanPrimitive<TDefined extends boolean = false> implements Primi
|
|
|
44
53
|
}
|
|
45
54
|
|
|
46
55
|
/** Mark this boolean as required */
|
|
47
|
-
required(): BooleanPrimitive<true> {
|
|
56
|
+
required(): BooleanPrimitive<true, THasDefault> {
|
|
48
57
|
return new BooleanPrimitive({
|
|
49
58
|
...this._schema,
|
|
50
59
|
required: true,
|
|
@@ -52,7 +61,7 @@ export class BooleanPrimitive<TDefined extends boolean = false> implements Primi
|
|
|
52
61
|
}
|
|
53
62
|
|
|
54
63
|
/** Set a default value for this boolean */
|
|
55
|
-
default(defaultValue: boolean): BooleanPrimitive<true> {
|
|
64
|
+
default(defaultValue: boolean): BooleanPrimitive<TRequired, true> {
|
|
56
65
|
return new BooleanPrimitive({
|
|
57
66
|
...this._schema,
|
|
58
67
|
defaultValue,
|
|
@@ -60,34 +69,39 @@ export class BooleanPrimitive<TDefined extends boolean = false> implements Primi
|
|
|
60
69
|
}
|
|
61
70
|
|
|
62
71
|
/** Add a custom validation rule */
|
|
63
|
-
refine(fn: (value: boolean) => boolean, message: string): BooleanPrimitive<
|
|
72
|
+
refine(fn: (value: boolean) => boolean, message: string): BooleanPrimitive<TRequired, THasDefault> {
|
|
64
73
|
return new BooleanPrimitive({
|
|
65
74
|
...this._schema,
|
|
66
75
|
validators: [...this._schema.validators, { validate: fn, message }],
|
|
67
76
|
});
|
|
68
77
|
}
|
|
69
78
|
|
|
70
|
-
readonly _internal: PrimitiveInternal<boolean, BooleanProxy<
|
|
71
|
-
createProxy: (env: ProxyEnvironment.ProxyEnvironment, operationPath: OperationPath.OperationPath): BooleanProxy<
|
|
79
|
+
readonly _internal: PrimitiveInternal<boolean, BooleanProxy<TRequired, THasDefault>> = {
|
|
80
|
+
createProxy: (env: ProxyEnvironment.ProxyEnvironment, operationPath: OperationPath.OperationPath): BooleanProxy<TRequired, THasDefault> => {
|
|
72
81
|
const defaultValue = this._schema.defaultValue;
|
|
73
82
|
return {
|
|
74
|
-
get: (): MaybeUndefined<boolean,
|
|
83
|
+
get: (): MaybeUndefined<boolean, TRequired, THasDefault> => {
|
|
75
84
|
const state = env.getState(operationPath) as boolean | undefined;
|
|
76
|
-
return (state ?? defaultValue) as MaybeUndefined<boolean,
|
|
85
|
+
return (state ?? defaultValue) as MaybeUndefined<boolean, TRequired, THasDefault>;
|
|
86
|
+
},
|
|
87
|
+
set: (value: InferSetInput<TRequired, THasDefault>) => {
|
|
88
|
+
env.addOperation(
|
|
89
|
+
Operation.fromDefinition(operationPath, this._opDefinitions.set, value)
|
|
90
|
+
);
|
|
77
91
|
},
|
|
78
|
-
|
|
92
|
+
update: (value: InferUpdateInput<TRequired, THasDefault>) => {
|
|
79
93
|
env.addOperation(
|
|
80
94
|
Operation.fromDefinition(operationPath, this._opDefinitions.set, value)
|
|
81
95
|
);
|
|
82
96
|
},
|
|
83
|
-
toSnapshot: (): MaybeUndefined<boolean,
|
|
97
|
+
toSnapshot: (): MaybeUndefined<boolean, TRequired, THasDefault> => {
|
|
84
98
|
const state = env.getState(operationPath) as boolean | undefined;
|
|
85
|
-
return (state ?? defaultValue) as MaybeUndefined<boolean,
|
|
99
|
+
return (state ?? defaultValue) as MaybeUndefined<boolean, TRequired, THasDefault>;
|
|
86
100
|
},
|
|
87
101
|
};
|
|
88
102
|
},
|
|
89
103
|
|
|
90
|
-
applyOperation: (
|
|
104
|
+
applyOperation: (_state: boolean | undefined, operation: Operation.Operation<any, any, any>): boolean => {
|
|
91
105
|
if (operation.kind !== "boolean.set") {
|
|
92
106
|
throw new ValidationError(`BooleanPrimitive cannot apply operation of kind: ${operation.kind}`);
|
|
93
107
|
}
|
|
@@ -123,6 +137,6 @@ export class BooleanPrimitive<TDefined extends boolean = false> implements Primi
|
|
|
123
137
|
}
|
|
124
138
|
|
|
125
139
|
/** Creates a new BooleanPrimitive */
|
|
126
|
-
export const Boolean = (): BooleanPrimitive<false> =>
|
|
140
|
+
export const Boolean = (): BooleanPrimitive<false, false> =>
|
|
127
141
|
new BooleanPrimitive({ required: false, defaultValue: undefined, validators: [] });
|
|
128
142
|
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import * as OperationDefinition from "../OperationDefinition";
|
|
3
|
+
import * as Operation from "../Operation";
|
|
4
|
+
import * as OperationPath from "../OperationPath";
|
|
5
|
+
import * as ProxyEnvironment from "../ProxyEnvironment";
|
|
6
|
+
import * as Transform from "../Transform";
|
|
7
|
+
import type { Primitive, PrimitiveInternal, MaybeUndefined, InferState, NeedsValue } from "./shared";
|
|
8
|
+
import { ValidationError } from "./shared";
|
|
9
|
+
import { StringPrimitive } from "./String";
|
|
10
|
+
import { NumberPrimitive } from "./Number";
|
|
11
|
+
import { BooleanPrimitive } from "./Boolean";
|
|
12
|
+
import { LiteralPrimitive, LiteralValue } from "./Literal";
|
|
13
|
+
|
|
14
|
+
// =============================================================================
|
|
15
|
+
// Either Primitive - Simple Type Union
|
|
16
|
+
// =============================================================================
|
|
17
|
+
|
|
18
|
+
type InferSetInput<TVariants extends readonly ScalarPrimitive[], TRequired extends boolean = false, THasDefault extends boolean = false> = NeedsValue<InferEitherState<TVariants>, TRequired, THasDefault>
|
|
19
|
+
type InferUpdateInput<TVariants extends readonly ScalarPrimitive[], TRequired extends boolean = false, THasDefault extends boolean = false> = NeedsValue<InferEitherState<TVariants>, TRequired, THasDefault>
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Scalar primitives that can be used as variants in Either
|
|
23
|
+
*/
|
|
24
|
+
export type ScalarPrimitive =
|
|
25
|
+
| StringPrimitive<any, any>
|
|
26
|
+
| NumberPrimitive<any, any>
|
|
27
|
+
| BooleanPrimitive<any, any>
|
|
28
|
+
| LiteralPrimitive<any, any, any>;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Infer the union state type from a tuple of scalar primitives
|
|
32
|
+
*/
|
|
33
|
+
export type InferEitherState<TVariants extends readonly ScalarPrimitive[]> =
|
|
34
|
+
InferState<TVariants[number]>;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Infer the union snapshot type from a tuple of scalar primitives
|
|
38
|
+
*/
|
|
39
|
+
export type InferEitherSnapshot<TVariants extends readonly ScalarPrimitive[]> =
|
|
40
|
+
InferState<TVariants[number]>;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Match handlers for Either - optional handlers for each scalar type
|
|
44
|
+
*/
|
|
45
|
+
export interface EitherMatchHandlers<R> {
|
|
46
|
+
string?: (value: string) => R;
|
|
47
|
+
number?: (value: number) => R;
|
|
48
|
+
boolean?: (value: boolean) => R;
|
|
49
|
+
literal?: (value: LiteralValue) => R;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Proxy for accessing Either values
|
|
54
|
+
*/
|
|
55
|
+
export interface EitherProxy<TVariants extends readonly ScalarPrimitive[], TRequired extends boolean = false, THasDefault extends boolean = false> {
|
|
56
|
+
/** Gets the current value */
|
|
57
|
+
get(): MaybeUndefined<InferEitherState<TVariants>, TRequired, THasDefault>;
|
|
58
|
+
|
|
59
|
+
/** Sets the value to any of the allowed variant types */
|
|
60
|
+
set(value: InferSetInput<TVariants, TRequired, THasDefault>): void;
|
|
61
|
+
|
|
62
|
+
/** This is the same as set. Updates the value, generating an either.set operation */
|
|
63
|
+
update(value: InferUpdateInput<TVariants, TRequired, THasDefault>): void;
|
|
64
|
+
|
|
65
|
+
/** Pattern match on the value type */
|
|
66
|
+
match<R>(handlers: EitherMatchHandlers<R>): R | undefined;
|
|
67
|
+
|
|
68
|
+
/** Returns a readonly snapshot of the value for rendering */
|
|
69
|
+
toSnapshot(): MaybeUndefined<InferEitherSnapshot<TVariants>, TRequired, THasDefault>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface EitherPrimitiveSchema<TVariants extends readonly ScalarPrimitive[]> {
|
|
73
|
+
readonly required: boolean;
|
|
74
|
+
readonly defaultValue: InferEitherState<TVariants> | undefined;
|
|
75
|
+
readonly variants: TVariants;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export class EitherPrimitive<TVariants extends readonly ScalarPrimitive[], TRequired extends boolean = false, THasDefault extends boolean = false>
|
|
79
|
+
implements Primitive<InferEitherState<TVariants>, EitherProxy<TVariants, TRequired, THasDefault>, TRequired, THasDefault, InferSetInput<TVariants, TRequired, THasDefault>, InferUpdateInput<TVariants, TRequired, THasDefault>>
|
|
80
|
+
{
|
|
81
|
+
readonly _tag = "EitherPrimitive" as const;
|
|
82
|
+
readonly _State!: InferEitherState<TVariants>;
|
|
83
|
+
readonly _Proxy!: EitherProxy<TVariants, TRequired, THasDefault>;
|
|
84
|
+
readonly _TRequired!: TRequired;
|
|
85
|
+
readonly _THasDefault!: THasDefault;
|
|
86
|
+
readonly TUpdateInput!: InferUpdateInput<TVariants, TRequired, THasDefault>;
|
|
87
|
+
readonly TSetInput!: InferSetInput<TVariants, TRequired, THasDefault>;
|
|
88
|
+
|
|
89
|
+
private readonly _schema: EitherPrimitiveSchema<TVariants>;
|
|
90
|
+
|
|
91
|
+
private readonly _opDefinitions = {
|
|
92
|
+
set: OperationDefinition.make({
|
|
93
|
+
kind: "either.set" as const,
|
|
94
|
+
payload: Schema.Unknown,
|
|
95
|
+
target: Schema.Unknown,
|
|
96
|
+
apply: (payload) => payload,
|
|
97
|
+
}),
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
constructor(schema: EitherPrimitiveSchema<TVariants>) {
|
|
101
|
+
this._schema = schema;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Mark this either as required */
|
|
105
|
+
required(): EitherPrimitive<TVariants, true, THasDefault> {
|
|
106
|
+
return new EitherPrimitive({
|
|
107
|
+
...this._schema,
|
|
108
|
+
required: true,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Set a default value for this either */
|
|
113
|
+
default(defaultValue: InferEitherState<TVariants>): EitherPrimitive<TVariants, TRequired, true> {
|
|
114
|
+
return new EitherPrimitive({
|
|
115
|
+
...this._schema,
|
|
116
|
+
defaultValue,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Get the variants */
|
|
121
|
+
get variants(): TVariants {
|
|
122
|
+
return this._schema.variants;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Determine the type category of a value based on the variants
|
|
127
|
+
*/
|
|
128
|
+
private _getValueType(value: unknown): "string" | "number" | "boolean" | "literal" | undefined {
|
|
129
|
+
const valueType = typeof value;
|
|
130
|
+
|
|
131
|
+
// Check for literal matches first (they take priority)
|
|
132
|
+
for (const variant of this._schema.variants) {
|
|
133
|
+
if (variant._tag === "LiteralPrimitive") {
|
|
134
|
+
const literalVariant = variant as LiteralPrimitive<any, any, any>;
|
|
135
|
+
if (value === literalVariant.literal) {
|
|
136
|
+
return "literal";
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Check for type matches
|
|
142
|
+
if (valueType === "string") {
|
|
143
|
+
for (const variant of this._schema.variants) {
|
|
144
|
+
if (variant._tag === "StringPrimitive") {
|
|
145
|
+
return "string";
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (valueType === "number") {
|
|
151
|
+
for (const variant of this._schema.variants) {
|
|
152
|
+
if (variant._tag === "NumberPrimitive") {
|
|
153
|
+
return "number";
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (valueType === "boolean") {
|
|
159
|
+
for (const variant of this._schema.variants) {
|
|
160
|
+
if (variant._tag === "BooleanPrimitive") {
|
|
161
|
+
return "boolean";
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Find the matching variant for a value.
|
|
171
|
+
* For literals, matches exact value. For other types, matches by typeof.
|
|
172
|
+
*/
|
|
173
|
+
private _findMatchingVariant(value: unknown): ScalarPrimitive | undefined {
|
|
174
|
+
const valueType = typeof value;
|
|
175
|
+
|
|
176
|
+
// Check for literal matches first (they take priority)
|
|
177
|
+
for (const variant of this._schema.variants) {
|
|
178
|
+
if (variant._tag === "LiteralPrimitive") {
|
|
179
|
+
const literalVariant = variant as LiteralPrimitive<any, any, any>;
|
|
180
|
+
if (value === literalVariant.literal) {
|
|
181
|
+
return variant;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Check for type matches
|
|
187
|
+
if (valueType === "string") {
|
|
188
|
+
for (const variant of this._schema.variants) {
|
|
189
|
+
if (variant._tag === "StringPrimitive") {
|
|
190
|
+
return variant;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (valueType === "number") {
|
|
196
|
+
for (const variant of this._schema.variants) {
|
|
197
|
+
if (variant._tag === "NumberPrimitive") {
|
|
198
|
+
return variant;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (valueType === "boolean") {
|
|
204
|
+
for (const variant of this._schema.variants) {
|
|
205
|
+
if (variant._tag === "BooleanPrimitive") {
|
|
206
|
+
return variant;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Get the operation kind for a variant
|
|
216
|
+
*/
|
|
217
|
+
private _getVariantOperationKind(variant: ScalarPrimitive): string {
|
|
218
|
+
switch (variant._tag) {
|
|
219
|
+
case "StringPrimitive":
|
|
220
|
+
return "string.set";
|
|
221
|
+
case "NumberPrimitive":
|
|
222
|
+
return "number.set";
|
|
223
|
+
case "BooleanPrimitive":
|
|
224
|
+
return "boolean.set";
|
|
225
|
+
case "LiteralPrimitive":
|
|
226
|
+
return "literal.set";
|
|
227
|
+
default:
|
|
228
|
+
return "unknown.set";
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Validate a value against the matching variant, including running its validators.
|
|
234
|
+
* Throws ValidationError if the value doesn't match any variant or fails validation.
|
|
235
|
+
*/
|
|
236
|
+
private _validateAndApplyToVariant(value: unknown, path: OperationPath.OperationPath): void {
|
|
237
|
+
const matchingVariant = this._findMatchingVariant(value);
|
|
238
|
+
|
|
239
|
+
if (!matchingVariant) {
|
|
240
|
+
const allowedTypes = this._schema.variants.map((v) => v._tag).join(", ");
|
|
241
|
+
throw new ValidationError(
|
|
242
|
+
`EitherPrimitive.set requires a value matching one of: ${allowedTypes}, got: ${typeof value}`
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Create a synthetic operation for the variant's applyOperation
|
|
247
|
+
const variantOpKind = this._getVariantOperationKind(matchingVariant);
|
|
248
|
+
const syntheticOp: Operation.Operation<any, any, any> = {
|
|
249
|
+
kind: variantOpKind,
|
|
250
|
+
path: path,
|
|
251
|
+
payload: value,
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
// Delegate to the variant's applyOperation which runs its validators
|
|
255
|
+
// This will throw ValidationError if validation fails
|
|
256
|
+
matchingVariant._internal.applyOperation(undefined, syntheticOp);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
readonly _internal: PrimitiveInternal<InferEitherState<TVariants>, EitherProxy<TVariants, TRequired, THasDefault>> = {
|
|
260
|
+
createProxy: (
|
|
261
|
+
env: ProxyEnvironment.ProxyEnvironment,
|
|
262
|
+
operationPath: OperationPath.OperationPath
|
|
263
|
+
): EitherProxy<TVariants, TRequired, THasDefault> => {
|
|
264
|
+
const defaultValue = this._schema.defaultValue;
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
get: (): MaybeUndefined<InferEitherState<TVariants>, TRequired, THasDefault> => {
|
|
268
|
+
const state = env.getState(operationPath) as InferEitherState<TVariants> | undefined;
|
|
269
|
+
return (state ?? defaultValue) as MaybeUndefined<InferEitherState<TVariants>, TRequired, THasDefault>;
|
|
270
|
+
},
|
|
271
|
+
set: (value: InferSetInput<TVariants, TRequired, THasDefault>) => {
|
|
272
|
+
env.addOperation(
|
|
273
|
+
Operation.fromDefinition(operationPath, this._opDefinitions.set, value)
|
|
274
|
+
);
|
|
275
|
+
},
|
|
276
|
+
update: (value: InferUpdateInput<TVariants, TRequired, THasDefault>) => {
|
|
277
|
+
env.addOperation(
|
|
278
|
+
Operation.fromDefinition(operationPath, this._opDefinitions.set, value)
|
|
279
|
+
);
|
|
280
|
+
},
|
|
281
|
+
match: <R,>(handlers: EitherMatchHandlers<R>): R | undefined => {
|
|
282
|
+
const currentState = env.getState(operationPath) as InferEitherState<TVariants> | undefined;
|
|
283
|
+
const effectiveState = currentState ?? defaultValue;
|
|
284
|
+
if (effectiveState === undefined) return undefined;
|
|
285
|
+
|
|
286
|
+
const valueType = this._getValueType(effectiveState);
|
|
287
|
+
if (!valueType) return undefined;
|
|
288
|
+
|
|
289
|
+
switch (valueType) {
|
|
290
|
+
case "string":
|
|
291
|
+
return handlers.string?.(effectiveState as string);
|
|
292
|
+
case "number":
|
|
293
|
+
return handlers.number?.(effectiveState as number);
|
|
294
|
+
case "boolean":
|
|
295
|
+
return handlers.boolean?.(effectiveState as boolean);
|
|
296
|
+
case "literal":
|
|
297
|
+
return handlers.literal?.(effectiveState as LiteralValue);
|
|
298
|
+
default:
|
|
299
|
+
return undefined;
|
|
300
|
+
}
|
|
301
|
+
},
|
|
302
|
+
toSnapshot: (): MaybeUndefined<InferEitherSnapshot<TVariants>, TRequired, THasDefault> => {
|
|
303
|
+
const state = env.getState(operationPath) as InferEitherState<TVariants> | undefined;
|
|
304
|
+
return (state ?? defaultValue) as MaybeUndefined<InferEitherSnapshot<TVariants>, TRequired, THasDefault>;
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
},
|
|
308
|
+
|
|
309
|
+
applyOperation: (
|
|
310
|
+
_state: InferEitherState<TVariants> | undefined,
|
|
311
|
+
operation: Operation.Operation<any, any, any>
|
|
312
|
+
): InferEitherState<TVariants> => {
|
|
313
|
+
if (operation.kind !== "either.set") {
|
|
314
|
+
throw new ValidationError(`EitherPrimitive cannot apply operation of kind: ${operation.kind}`);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const payload = operation.payload;
|
|
318
|
+
|
|
319
|
+
// Validate that the payload matches one of the variant types and passes its validators
|
|
320
|
+
this._validateAndApplyToVariant(payload, operation.path);
|
|
321
|
+
|
|
322
|
+
return payload as InferEitherState<TVariants>;
|
|
323
|
+
},
|
|
324
|
+
|
|
325
|
+
getInitialState: (): InferEitherState<TVariants> | undefined => {
|
|
326
|
+
return this._schema.defaultValue;
|
|
327
|
+
},
|
|
328
|
+
|
|
329
|
+
transformOperation: (
|
|
330
|
+
clientOp: Operation.Operation<any, any, any>,
|
|
331
|
+
serverOp: Operation.Operation<any, any, any>
|
|
332
|
+
): Transform.TransformResult => {
|
|
333
|
+
// If paths don't overlap, no transformation needed
|
|
334
|
+
if (!OperationPath.pathsOverlap(clientOp.path, serverOp.path)) {
|
|
335
|
+
return { type: "transformed", operation: clientOp };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// For same path, client wins (last-write-wins)
|
|
339
|
+
return { type: "transformed", operation: clientOp };
|
|
340
|
+
},
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Creates a new EitherPrimitive with the given scalar variant types.
|
|
346
|
+
* Validators defined on the variants are applied when validating values.
|
|
347
|
+
*
|
|
348
|
+
* @example
|
|
349
|
+
* ```typescript
|
|
350
|
+
* // String or number
|
|
351
|
+
* const value = Either(String(), Number());
|
|
352
|
+
*
|
|
353
|
+
* // String, number, or boolean
|
|
354
|
+
* const status = Either(String(), Number(), Boolean()).default("pending");
|
|
355
|
+
*
|
|
356
|
+
* // With literal types
|
|
357
|
+
* const mode = Either(Literal("auto"), Literal("manual"), Number());
|
|
358
|
+
*
|
|
359
|
+
* // With validators - validates string length and number range
|
|
360
|
+
* const constrained = Either(
|
|
361
|
+
* String().min(2).max(50),
|
|
362
|
+
* Number().max(255)
|
|
363
|
+
* );
|
|
364
|
+
* ```
|
|
365
|
+
*/
|
|
366
|
+
export function Either<TVariants extends readonly ScalarPrimitive[]>(
|
|
367
|
+
...variants: TVariants
|
|
368
|
+
): EitherPrimitive<TVariants, false, false> {
|
|
369
|
+
if (variants.length === 0) {
|
|
370
|
+
throw new ValidationError("Either requires at least one variant");
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return new EitherPrimitive({
|
|
374
|
+
required: false,
|
|
375
|
+
defaultValue: undefined,
|
|
376
|
+
variants,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|