@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voidhash/mimic",
3
- "version": "0.0.1-alpha.2",
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.2"
22
+ "@voidhash/tsconfig": "0.0.1-alpha.3"
23
23
  },
24
24
  "peerDependencies": {
25
25
  "effect": "^3.19.12"
package/src/Primitive.ts CHANGED
@@ -26,6 +26,9 @@ export * from "./primitives/Lazy";
26
26
  // Union Primitive
27
27
  export * from "./primitives/Union";
28
28
 
29
+ // Either Primitive
30
+ export * from "./primitives/Either";
31
+
29
32
  // TreeNode Primitive
30
33
  export * from "./primitives/TreeNode";
31
34
  // Tree Primitive
@@ -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
+