@voidhash/mimic 0.0.1-alpha.2 → 0.0.1-alpha.3
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 +184 -0
- package/dist/index.d.cts +98 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +98 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +184 -0
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/Primitive.ts +3 -0
- package/src/primitives/Either.ts +364 -0
- package/tests/primitives/Either.test.ts +707 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voidhash/mimic",
|
|
3
|
-
"version": "0.0.1-alpha.
|
|
3
|
+
"version": "0.0.1-alpha.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"typescript": "5.8.3",
|
|
20
20
|
"vite-tsconfig-paths": "^5.1.4",
|
|
21
21
|
"vitest": "^3.2.4",
|
|
22
|
-
"@voidhash/tsconfig": "0.0.1-alpha.
|
|
22
|
+
"@voidhash/tsconfig": "0.0.1-alpha.3"
|
|
23
23
|
},
|
|
24
24
|
"peerDependencies": {
|
|
25
25
|
"effect": "^3.19.12"
|
package/src/Primitive.ts
CHANGED
|
@@ -0,0 +1,364 @@
|
|
|
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 } 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
|
+
/**
|
|
19
|
+
* Scalar primitives that can be used as variants in Either
|
|
20
|
+
*/
|
|
21
|
+
export type ScalarPrimitive =
|
|
22
|
+
| StringPrimitive<any>
|
|
23
|
+
| NumberPrimitive<any>
|
|
24
|
+
| BooleanPrimitive<any>
|
|
25
|
+
| LiteralPrimitive<any, any>;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Infer the union state type from a tuple of scalar primitives
|
|
29
|
+
*/
|
|
30
|
+
export type InferEitherState<TVariants extends readonly ScalarPrimitive[]> =
|
|
31
|
+
InferState<TVariants[number]>;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Infer the union snapshot type from a tuple of scalar primitives
|
|
35
|
+
*/
|
|
36
|
+
export type InferEitherSnapshot<TVariants extends readonly ScalarPrimitive[]> =
|
|
37
|
+
InferState<TVariants[number]>;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Match handlers for Either - optional handlers for each scalar type
|
|
41
|
+
*/
|
|
42
|
+
export interface EitherMatchHandlers<R> {
|
|
43
|
+
string?: (value: string) => R;
|
|
44
|
+
number?: (value: number) => R;
|
|
45
|
+
boolean?: (value: boolean) => R;
|
|
46
|
+
literal?: (value: LiteralValue) => R;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Proxy for accessing Either values
|
|
51
|
+
*/
|
|
52
|
+
export interface EitherProxy<TVariants extends readonly ScalarPrimitive[], TDefined extends boolean = false> {
|
|
53
|
+
/** Gets the current value */
|
|
54
|
+
get(): MaybeUndefined<InferEitherState<TVariants>, TDefined>;
|
|
55
|
+
|
|
56
|
+
/** Sets the value to any of the allowed variant types */
|
|
57
|
+
set(value: InferEitherState<TVariants>): void;
|
|
58
|
+
|
|
59
|
+
/** Pattern match on the value type */
|
|
60
|
+
match<R>(handlers: EitherMatchHandlers<R>): R | undefined;
|
|
61
|
+
|
|
62
|
+
/** Returns a readonly snapshot of the value for rendering */
|
|
63
|
+
toSnapshot(): MaybeUndefined<InferEitherSnapshot<TVariants>, TDefined>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface EitherPrimitiveSchema<TVariants extends readonly ScalarPrimitive[]> {
|
|
67
|
+
readonly required: boolean;
|
|
68
|
+
readonly defaultValue: InferEitherState<TVariants> | undefined;
|
|
69
|
+
readonly variants: TVariants;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export class EitherPrimitive<TVariants extends readonly ScalarPrimitive[], TDefined extends boolean = false>
|
|
73
|
+
implements Primitive<InferEitherState<TVariants>, EitherProxy<TVariants, TDefined>>
|
|
74
|
+
{
|
|
75
|
+
readonly _tag = "EitherPrimitive" as const;
|
|
76
|
+
readonly _State!: InferEitherState<TVariants>;
|
|
77
|
+
readonly _Proxy!: EitherProxy<TVariants, TDefined>;
|
|
78
|
+
|
|
79
|
+
private readonly _schema: EitherPrimitiveSchema<TVariants>;
|
|
80
|
+
|
|
81
|
+
private readonly _opDefinitions = {
|
|
82
|
+
set: OperationDefinition.make({
|
|
83
|
+
kind: "either.set" as const,
|
|
84
|
+
payload: Schema.Unknown,
|
|
85
|
+
target: Schema.Unknown,
|
|
86
|
+
apply: (payload) => payload,
|
|
87
|
+
}),
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
constructor(schema: EitherPrimitiveSchema<TVariants>) {
|
|
91
|
+
this._schema = schema;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Mark this either as required */
|
|
95
|
+
required(): EitherPrimitive<TVariants, true> {
|
|
96
|
+
return new EitherPrimitive({
|
|
97
|
+
...this._schema,
|
|
98
|
+
required: true,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Set a default value for this either */
|
|
103
|
+
default(defaultValue: InferEitherState<TVariants>): EitherPrimitive<TVariants, true> {
|
|
104
|
+
return new EitherPrimitive({
|
|
105
|
+
...this._schema,
|
|
106
|
+
defaultValue,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Get the variants */
|
|
111
|
+
get variants(): TVariants {
|
|
112
|
+
return this._schema.variants;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Determine the type category of a value based on the variants
|
|
117
|
+
*/
|
|
118
|
+
private _getValueType(value: unknown): "string" | "number" | "boolean" | "literal" | undefined {
|
|
119
|
+
const valueType = typeof value;
|
|
120
|
+
|
|
121
|
+
// Check for literal matches first (they take priority)
|
|
122
|
+
for (const variant of this._schema.variants) {
|
|
123
|
+
if (variant._tag === "LiteralPrimitive") {
|
|
124
|
+
const literalVariant = variant as LiteralPrimitive<any, any>;
|
|
125
|
+
if (value === literalVariant.literal) {
|
|
126
|
+
return "literal";
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Check for type matches
|
|
132
|
+
if (valueType === "string") {
|
|
133
|
+
for (const variant of this._schema.variants) {
|
|
134
|
+
if (variant._tag === "StringPrimitive") {
|
|
135
|
+
return "string";
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (valueType === "number") {
|
|
141
|
+
for (const variant of this._schema.variants) {
|
|
142
|
+
if (variant._tag === "NumberPrimitive") {
|
|
143
|
+
return "number";
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (valueType === "boolean") {
|
|
149
|
+
for (const variant of this._schema.variants) {
|
|
150
|
+
if (variant._tag === "BooleanPrimitive") {
|
|
151
|
+
return "boolean";
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Find the matching variant for a value.
|
|
161
|
+
* For literals, matches exact value. For other types, matches by typeof.
|
|
162
|
+
*/
|
|
163
|
+
private _findMatchingVariant(value: unknown): ScalarPrimitive | undefined {
|
|
164
|
+
const valueType = typeof value;
|
|
165
|
+
|
|
166
|
+
// Check for literal matches first (they take priority)
|
|
167
|
+
for (const variant of this._schema.variants) {
|
|
168
|
+
if (variant._tag === "LiteralPrimitive") {
|
|
169
|
+
const literalVariant = variant as LiteralPrimitive<any, any>;
|
|
170
|
+
if (value === literalVariant.literal) {
|
|
171
|
+
return variant;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Check for type matches
|
|
177
|
+
if (valueType === "string") {
|
|
178
|
+
for (const variant of this._schema.variants) {
|
|
179
|
+
if (variant._tag === "StringPrimitive") {
|
|
180
|
+
return variant;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (valueType === "number") {
|
|
186
|
+
for (const variant of this._schema.variants) {
|
|
187
|
+
if (variant._tag === "NumberPrimitive") {
|
|
188
|
+
return variant;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (valueType === "boolean") {
|
|
194
|
+
for (const variant of this._schema.variants) {
|
|
195
|
+
if (variant._tag === "BooleanPrimitive") {
|
|
196
|
+
return variant;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Get the operation kind for a variant
|
|
206
|
+
*/
|
|
207
|
+
private _getVariantOperationKind(variant: ScalarPrimitive): string {
|
|
208
|
+
switch (variant._tag) {
|
|
209
|
+
case "StringPrimitive":
|
|
210
|
+
return "string.set";
|
|
211
|
+
case "NumberPrimitive":
|
|
212
|
+
return "number.set";
|
|
213
|
+
case "BooleanPrimitive":
|
|
214
|
+
return "boolean.set";
|
|
215
|
+
case "LiteralPrimitive":
|
|
216
|
+
return "literal.set";
|
|
217
|
+
default:
|
|
218
|
+
return "unknown.set";
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Validate a value against the matching variant, including running its validators.
|
|
224
|
+
* Throws ValidationError if the value doesn't match any variant or fails validation.
|
|
225
|
+
*/
|
|
226
|
+
private _validateAndApplyToVariant(value: unknown, path: OperationPath.OperationPath): void {
|
|
227
|
+
const matchingVariant = this._findMatchingVariant(value);
|
|
228
|
+
|
|
229
|
+
if (!matchingVariant) {
|
|
230
|
+
const allowedTypes = this._schema.variants.map((v) => v._tag).join(", ");
|
|
231
|
+
throw new ValidationError(
|
|
232
|
+
`EitherPrimitive.set requires a value matching one of: ${allowedTypes}, got: ${typeof value}`
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Create a synthetic operation for the variant's applyOperation
|
|
237
|
+
const variantOpKind = this._getVariantOperationKind(matchingVariant);
|
|
238
|
+
const syntheticOp: Operation.Operation<any, any, any> = {
|
|
239
|
+
kind: variantOpKind,
|
|
240
|
+
path: path,
|
|
241
|
+
payload: value,
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
// Delegate to the variant's applyOperation which runs its validators
|
|
245
|
+
// This will throw ValidationError if validation fails
|
|
246
|
+
matchingVariant._internal.applyOperation(undefined, syntheticOp);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
readonly _internal: PrimitiveInternal<InferEitherState<TVariants>, EitherProxy<TVariants, TDefined>> = {
|
|
250
|
+
createProxy: (
|
|
251
|
+
env: ProxyEnvironment.ProxyEnvironment,
|
|
252
|
+
operationPath: OperationPath.OperationPath
|
|
253
|
+
): EitherProxy<TVariants, TDefined> => {
|
|
254
|
+
const defaultValue = this._schema.defaultValue;
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
get: (): MaybeUndefined<InferEitherState<TVariants>, TDefined> => {
|
|
258
|
+
const state = env.getState(operationPath) as InferEitherState<TVariants> | undefined;
|
|
259
|
+
return (state ?? defaultValue) as MaybeUndefined<InferEitherState<TVariants>, TDefined>;
|
|
260
|
+
},
|
|
261
|
+
set: (value: InferEitherState<TVariants>) => {
|
|
262
|
+
env.addOperation(
|
|
263
|
+
Operation.fromDefinition(operationPath, this._opDefinitions.set, value)
|
|
264
|
+
);
|
|
265
|
+
},
|
|
266
|
+
match: <R,>(handlers: EitherMatchHandlers<R>): R | undefined => {
|
|
267
|
+
const currentState = env.getState(operationPath) as InferEitherState<TVariants> | undefined;
|
|
268
|
+
const effectiveState = currentState ?? defaultValue;
|
|
269
|
+
if (effectiveState === undefined) return undefined;
|
|
270
|
+
|
|
271
|
+
const valueType = this._getValueType(effectiveState);
|
|
272
|
+
if (!valueType) return undefined;
|
|
273
|
+
|
|
274
|
+
switch (valueType) {
|
|
275
|
+
case "string":
|
|
276
|
+
return handlers.string?.(effectiveState as string);
|
|
277
|
+
case "number":
|
|
278
|
+
return handlers.number?.(effectiveState as number);
|
|
279
|
+
case "boolean":
|
|
280
|
+
return handlers.boolean?.(effectiveState as boolean);
|
|
281
|
+
case "literal":
|
|
282
|
+
return handlers.literal?.(effectiveState as LiteralValue);
|
|
283
|
+
default:
|
|
284
|
+
return undefined;
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
toSnapshot: (): MaybeUndefined<InferEitherSnapshot<TVariants>, TDefined> => {
|
|
288
|
+
const state = env.getState(operationPath) as InferEitherState<TVariants> | undefined;
|
|
289
|
+
return (state ?? defaultValue) as MaybeUndefined<InferEitherSnapshot<TVariants>, TDefined>;
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
},
|
|
293
|
+
|
|
294
|
+
applyOperation: (
|
|
295
|
+
_state: InferEitherState<TVariants> | undefined,
|
|
296
|
+
operation: Operation.Operation<any, any, any>
|
|
297
|
+
): InferEitherState<TVariants> => {
|
|
298
|
+
if (operation.kind !== "either.set") {
|
|
299
|
+
throw new ValidationError(`EitherPrimitive cannot apply operation of kind: ${operation.kind}`);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const payload = operation.payload;
|
|
303
|
+
|
|
304
|
+
// Validate that the payload matches one of the variant types and passes its validators
|
|
305
|
+
this._validateAndApplyToVariant(payload, operation.path);
|
|
306
|
+
|
|
307
|
+
return payload as InferEitherState<TVariants>;
|
|
308
|
+
},
|
|
309
|
+
|
|
310
|
+
getInitialState: (): InferEitherState<TVariants> | undefined => {
|
|
311
|
+
return this._schema.defaultValue;
|
|
312
|
+
},
|
|
313
|
+
|
|
314
|
+
transformOperation: (
|
|
315
|
+
clientOp: Operation.Operation<any, any, any>,
|
|
316
|
+
serverOp: Operation.Operation<any, any, any>
|
|
317
|
+
): Transform.TransformResult => {
|
|
318
|
+
// If paths don't overlap, no transformation needed
|
|
319
|
+
if (!OperationPath.pathsOverlap(clientOp.path, serverOp.path)) {
|
|
320
|
+
return { type: "transformed", operation: clientOp };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// For same path, client wins (last-write-wins)
|
|
324
|
+
return { type: "transformed", operation: clientOp };
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Creates a new EitherPrimitive with the given scalar variant types.
|
|
331
|
+
* Validators defined on the variants are applied when validating values.
|
|
332
|
+
*
|
|
333
|
+
* @example
|
|
334
|
+
* ```typescript
|
|
335
|
+
* // String or number
|
|
336
|
+
* const value = Either(String(), Number());
|
|
337
|
+
*
|
|
338
|
+
* // String, number, or boolean
|
|
339
|
+
* const status = Either(String(), Number(), Boolean()).default("pending");
|
|
340
|
+
*
|
|
341
|
+
* // With literal types
|
|
342
|
+
* const mode = Either(Literal("auto"), Literal("manual"), Number());
|
|
343
|
+
*
|
|
344
|
+
* // With validators - validates string length and number range
|
|
345
|
+
* const constrained = Either(
|
|
346
|
+
* String().min(2).max(50),
|
|
347
|
+
* Number().max(255)
|
|
348
|
+
* );
|
|
349
|
+
* ```
|
|
350
|
+
*/
|
|
351
|
+
export function Either<TVariants extends readonly ScalarPrimitive[]>(
|
|
352
|
+
...variants: TVariants
|
|
353
|
+
): EitherPrimitive<TVariants, false> {
|
|
354
|
+
if (variants.length === 0) {
|
|
355
|
+
throw new ValidationError("Either requires at least one variant");
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return new EitherPrimitive({
|
|
359
|
+
required: false,
|
|
360
|
+
defaultValue: undefined,
|
|
361
|
+
variants,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|