@drmxrcy/tcg-core 0.0.0-202602060542
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/README.md +882 -0
- package/package.json +58 -0
- package/src/__tests__/alpha-clash-engine-definition.test.ts +319 -0
- package/src/__tests__/createMockAlphaClashGame.ts +462 -0
- package/src/__tests__/createMockGrandArchiveGame.ts +373 -0
- package/src/__tests__/createMockGundamGame.ts +379 -0
- package/src/__tests__/createMockLorcanaGame.ts +328 -0
- package/src/__tests__/createMockOnePieceGame.ts +429 -0
- package/src/__tests__/createMockRiftboundGame.ts +462 -0
- package/src/__tests__/grand-archive-engine-definition.test.ts +118 -0
- package/src/__tests__/gundam-engine-definition.test.ts +110 -0
- package/src/__tests__/integration-complete-game.test.ts +508 -0
- package/src/__tests__/integration-network-sync.test.ts +469 -0
- package/src/__tests__/lorcana-engine-definition.test.ts +100 -0
- package/src/__tests__/move-enumeration.test.ts +725 -0
- package/src/__tests__/multiplayer-engine.test.ts +555 -0
- package/src/__tests__/one-piece-engine-definition.test.ts +114 -0
- package/src/__tests__/riftbound-engine-definition.test.ts +124 -0
- package/src/actions/action-definition.test.ts +201 -0
- package/src/actions/action-definition.ts +122 -0
- package/src/actions/action-timing.test.ts +490 -0
- package/src/actions/action-timing.ts +257 -0
- package/src/cards/card-definition.test.ts +268 -0
- package/src/cards/card-definition.ts +27 -0
- package/src/cards/card-instance.test.ts +422 -0
- package/src/cards/card-instance.ts +49 -0
- package/src/cards/computed-properties.test.ts +530 -0
- package/src/cards/computed-properties.ts +84 -0
- package/src/cards/conditional-modifiers.test.ts +390 -0
- package/src/cards/modifiers.test.ts +286 -0
- package/src/cards/modifiers.ts +51 -0
- package/src/engine/MULTIPLAYER.md +425 -0
- package/src/engine/__tests__/rule-engine-flow.test.ts +348 -0
- package/src/engine/__tests__/rule-engine-history.test.ts +535 -0
- package/src/engine/__tests__/rule-engine-moves.test.ts +488 -0
- package/src/engine/__tests__/rule-engine.test.ts +366 -0
- package/src/engine/index.ts +14 -0
- package/src/engine/multiplayer-engine.example.ts +571 -0
- package/src/engine/multiplayer-engine.ts +409 -0
- package/src/engine/rule-engine.test.ts +286 -0
- package/src/engine/rule-engine.ts +1539 -0
- package/src/engine/tracker-system.ts +172 -0
- package/src/examples/__tests__/coin-flip-game.test.ts +641 -0
- package/src/filtering/card-filter.test.ts +230 -0
- package/src/filtering/card-filter.ts +91 -0
- package/src/filtering/card-query.test.ts +901 -0
- package/src/filtering/card-query.ts +273 -0
- package/src/filtering/filter-matching.test.ts +944 -0
- package/src/filtering/filter-matching.ts +315 -0
- package/src/flow/SERIALIZATION.md +428 -0
- package/src/flow/__tests__/flow-definition.test.ts +427 -0
- package/src/flow/__tests__/flow-manager.test.ts +756 -0
- package/src/flow/__tests__/flow-serialization.test.ts +565 -0
- package/src/flow/flow-definition.ts +453 -0
- package/src/flow/flow-manager.ts +1044 -0
- package/src/flow/index.ts +35 -0
- package/src/game-definition/__tests__/game-definition-validation.test.ts +359 -0
- package/src/game-definition/__tests__/game-definition.test.ts +291 -0
- package/src/game-definition/__tests__/move-definitions.test.ts +328 -0
- package/src/game-definition/game-definition.ts +261 -0
- package/src/game-definition/index.ts +28 -0
- package/src/game-definition/move-definitions.ts +188 -0
- package/src/game-definition/validation.ts +183 -0
- package/src/history/history-manager.test.ts +497 -0
- package/src/history/history-manager.ts +312 -0
- package/src/history/history-operations.ts +122 -0
- package/src/history/index.ts +9 -0
- package/src/history/types.ts +255 -0
- package/src/index.ts +32 -0
- package/src/logging/index.ts +27 -0
- package/src/logging/log-formatter.ts +187 -0
- package/src/logging/logger.ts +276 -0
- package/src/logging/types.ts +148 -0
- package/src/moves/create-move.test.ts +331 -0
- package/src/moves/create-move.ts +64 -0
- package/src/moves/move-enumeration.ts +228 -0
- package/src/moves/move-executor.test.ts +431 -0
- package/src/moves/move-executor.ts +195 -0
- package/src/moves/move-system.test.ts +380 -0
- package/src/moves/move-system.ts +463 -0
- package/src/moves/standard-moves.ts +231 -0
- package/src/operations/card-operations.test.ts +236 -0
- package/src/operations/card-operations.ts +116 -0
- package/src/operations/card-registry-impl.test.ts +251 -0
- package/src/operations/card-registry-impl.ts +70 -0
- package/src/operations/card-registry.test.ts +234 -0
- package/src/operations/card-registry.ts +106 -0
- package/src/operations/counter-operations.ts +152 -0
- package/src/operations/game-operations.test.ts +280 -0
- package/src/operations/game-operations.ts +140 -0
- package/src/operations/index.ts +24 -0
- package/src/operations/operations-impl.test.ts +354 -0
- package/src/operations/operations-impl.ts +468 -0
- package/src/operations/zone-operations.test.ts +295 -0
- package/src/operations/zone-operations.ts +223 -0
- package/src/rng/seeded-rng.test.ts +339 -0
- package/src/rng/seeded-rng.ts +123 -0
- package/src/targeting/index.ts +48 -0
- package/src/targeting/target-definition.test.ts +273 -0
- package/src/targeting/target-definition.ts +37 -0
- package/src/targeting/target-dsl.ts +279 -0
- package/src/targeting/target-resolver.ts +486 -0
- package/src/targeting/target-validation.test.ts +994 -0
- package/src/targeting/target-validation.ts +286 -0
- package/src/telemetry/events.ts +202 -0
- package/src/telemetry/index.ts +21 -0
- package/src/telemetry/telemetry-manager.ts +127 -0
- package/src/telemetry/types.ts +68 -0
- package/src/testing/__tests__/testing-utilities-integration.test.ts +161 -0
- package/src/testing/index.ts +88 -0
- package/src/testing/test-assertions.test.ts +341 -0
- package/src/testing/test-assertions.ts +256 -0
- package/src/testing/test-card-factory.test.ts +228 -0
- package/src/testing/test-card-factory.ts +111 -0
- package/src/testing/test-context-factory.ts +187 -0
- package/src/testing/test-end-assertions.test.ts +262 -0
- package/src/testing/test-end-assertions.ts +95 -0
- package/src/testing/test-engine-builder.test.ts +389 -0
- package/src/testing/test-engine-builder.ts +46 -0
- package/src/testing/test-flow-assertions.test.ts +284 -0
- package/src/testing/test-flow-assertions.ts +115 -0
- package/src/testing/test-player-builder.test.ts +132 -0
- package/src/testing/test-player-builder.ts +46 -0
- package/src/testing/test-replay-assertions.test.ts +356 -0
- package/src/testing/test-replay-assertions.ts +164 -0
- package/src/testing/test-rng-helpers.test.ts +260 -0
- package/src/testing/test-rng-helpers.ts +190 -0
- package/src/testing/test-state-builder.test.ts +373 -0
- package/src/testing/test-state-builder.ts +99 -0
- package/src/testing/test-zone-factory.test.ts +295 -0
- package/src/testing/test-zone-factory.ts +224 -0
- package/src/types/branded-utils.ts +54 -0
- package/src/types/branded.test.ts +175 -0
- package/src/types/branded.ts +33 -0
- package/src/types/index.ts +8 -0
- package/src/types/state.test.ts +198 -0
- package/src/types/state.ts +154 -0
- package/src/validation/card-type-guards.test.ts +242 -0
- package/src/validation/card-type-guards.ts +179 -0
- package/src/validation/index.ts +40 -0
- package/src/validation/schema-builders.test.ts +403 -0
- package/src/validation/schema-builders.ts +345 -0
- package/src/validation/type-guard-builder.test.ts +216 -0
- package/src/validation/type-guard-builder.ts +109 -0
- package/src/validation/validator-builder.test.ts +375 -0
- package/src/validation/validator-builder.ts +273 -0
- package/src/zones/index.ts +28 -0
- package/src/zones/zone-factory.test.ts +183 -0
- package/src/zones/zone-factory.ts +44 -0
- package/src/zones/zone-operations.test.ts +800 -0
- package/src/zones/zone-operations.ts +306 -0
- package/src/zones/zone-state-helpers.test.ts +337 -0
- package/src/zones/zone-state-helpers.ts +128 -0
- package/src/zones/zone-visibility.test.ts +156 -0
- package/src/zones/zone-visibility.ts +36 -0
- package/src/zones/zone.test.ts +186 -0
- package/src/zones/zone.ts +66 -0
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zod schema composition utilities for building complex validation schemas
|
|
3
|
+
*
|
|
4
|
+
* Provides helper functions for composing, extending, and merging Zod schemas
|
|
5
|
+
* in a type-safe and reusable way.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { type ZodObject, type ZodRawShape, type ZodTypeAny, z } from "zod";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Creates a card schema from a shape object
|
|
12
|
+
*
|
|
13
|
+
* This is a thin wrapper around z.object() that provides better semantics
|
|
14
|
+
* for card-related schemas and can be extended with additional features.
|
|
15
|
+
*
|
|
16
|
+
* @param shape - The shape of the schema
|
|
17
|
+
* @returns A Zod object schema
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```typescript
|
|
21
|
+
* const cardSchema = createCardSchema({
|
|
22
|
+
* id: z.string(),
|
|
23
|
+
* name: z.string(),
|
|
24
|
+
* type: z.string(),
|
|
25
|
+
* basePower: z.number().optional(),
|
|
26
|
+
* });
|
|
27
|
+
*
|
|
28
|
+
* type Card = z.infer<typeof cardSchema>;
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export function createCardSchema<T extends ZodRawShape>(
|
|
32
|
+
shape: T,
|
|
33
|
+
): ZodObject<T> {
|
|
34
|
+
return z.object(shape);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Extends a base schema with additional fields
|
|
39
|
+
*
|
|
40
|
+
* Creates a new schema that includes all fields from the base schema
|
|
41
|
+
* plus the new fields from the extension.
|
|
42
|
+
*
|
|
43
|
+
* @param baseSchema - The base schema to extend
|
|
44
|
+
* @param extension - Additional fields to add
|
|
45
|
+
* @returns A new schema with combined fields
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```typescript
|
|
49
|
+
* const baseCardSchema = createCardSchema({
|
|
50
|
+
* id: z.string(),
|
|
51
|
+
* name: z.string(),
|
|
52
|
+
* });
|
|
53
|
+
*
|
|
54
|
+
* const creatureSchema = extendSchema(baseCardSchema, {
|
|
55
|
+
* power: z.number(),
|
|
56
|
+
* toughness: z.number(),
|
|
57
|
+
* });
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export function extendSchema<T extends ZodRawShape, E extends ZodRawShape>(
|
|
61
|
+
baseSchema: ZodObject<T>,
|
|
62
|
+
extension: E,
|
|
63
|
+
): ZodObject<any> {
|
|
64
|
+
return baseSchema.extend(extension) as ZodObject<any>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Merges multiple schemas into one
|
|
69
|
+
*
|
|
70
|
+
* Combines all fields from all provided schemas into a single schema.
|
|
71
|
+
* Later schemas can override fields from earlier schemas.
|
|
72
|
+
*
|
|
73
|
+
* @param schemas - Schemas to merge (at least 2)
|
|
74
|
+
* @returns A new schema with all fields merged
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```typescript
|
|
78
|
+
* const baseSchema = createCardSchema({ id: z.string() });
|
|
79
|
+
* const typeSchema = createCardSchema({ type: z.string() });
|
|
80
|
+
* const statsSchema = createCardSchema({ power: z.number() });
|
|
81
|
+
*
|
|
82
|
+
* const fullSchema = mergeSchemas(baseSchema, typeSchema, statsSchema);
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
export function mergeSchemas<T extends ZodRawShape, U extends ZodRawShape>(
|
|
86
|
+
schema1: ZodObject<T>,
|
|
87
|
+
schema2: ZodObject<U>,
|
|
88
|
+
): ZodObject<any>;
|
|
89
|
+
|
|
90
|
+
export function mergeSchemas<
|
|
91
|
+
T extends ZodRawShape,
|
|
92
|
+
U extends ZodRawShape,
|
|
93
|
+
V extends ZodRawShape,
|
|
94
|
+
>(
|
|
95
|
+
schema1: ZodObject<T>,
|
|
96
|
+
schema2: ZodObject<U>,
|
|
97
|
+
schema3: ZodObject<V>,
|
|
98
|
+
): ZodObject<any>;
|
|
99
|
+
|
|
100
|
+
export function mergeSchemas<
|
|
101
|
+
T extends ZodRawShape,
|
|
102
|
+
U extends ZodRawShape,
|
|
103
|
+
V extends ZodRawShape,
|
|
104
|
+
W extends ZodRawShape,
|
|
105
|
+
>(
|
|
106
|
+
schema1: ZodObject<T>,
|
|
107
|
+
schema2: ZodObject<U>,
|
|
108
|
+
schema3: ZodObject<V>,
|
|
109
|
+
schema4: ZodObject<W>,
|
|
110
|
+
): ZodObject<any>;
|
|
111
|
+
|
|
112
|
+
export function mergeSchemas(
|
|
113
|
+
...schemas: ZodObject<ZodRawShape>[]
|
|
114
|
+
): ZodObject<any> {
|
|
115
|
+
if (schemas.length < 2) {
|
|
116
|
+
throw new Error("mergeSchemas requires at least 2 schemas");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return schemas.reduce((acc, schema) => acc.merge(schema)) as ZodObject<any>;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Composes multiple schemas by intersecting them
|
|
124
|
+
*
|
|
125
|
+
* Creates a schema that validates against all provided schemas.
|
|
126
|
+
* This is useful when you want to ensure data matches multiple independent schemas.
|
|
127
|
+
*
|
|
128
|
+
* @param schemas - Array of schemas to compose
|
|
129
|
+
* @returns A composed schema
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* ```typescript
|
|
133
|
+
* const hasId = z.object({ id: z.string() });
|
|
134
|
+
* const hasName = z.object({ name: z.string() });
|
|
135
|
+
* const hasType = z.object({ type: z.string() });
|
|
136
|
+
*
|
|
137
|
+
* const fullSchema = composeSchemas([hasId, hasName, hasType]);
|
|
138
|
+
* ```
|
|
139
|
+
*/
|
|
140
|
+
export function composeSchemas(schemas: ZodTypeAny[]): ZodTypeAny {
|
|
141
|
+
if (schemas.length === 0) {
|
|
142
|
+
throw new Error("composeSchemas requires at least 1 schema");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (schemas.length === 1) {
|
|
146
|
+
return schemas[0];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Use merge for object schemas
|
|
150
|
+
return schemas.reduce((acc: ZodTypeAny, schema: ZodTypeAny): ZodTypeAny => {
|
|
151
|
+
if (acc instanceof z.ZodObject && schema instanceof z.ZodObject) {
|
|
152
|
+
return acc.merge(schema) as ZodTypeAny;
|
|
153
|
+
}
|
|
154
|
+
return z.intersection(acc, schema);
|
|
155
|
+
}) as ZodTypeAny;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Makes all fields in a schema optional
|
|
160
|
+
*
|
|
161
|
+
* Creates a new schema where all fields from the original schema
|
|
162
|
+
* are optional. Useful for partial updates or patch operations.
|
|
163
|
+
*
|
|
164
|
+
* @param schema - The schema to make optional
|
|
165
|
+
* @returns A schema with all fields optional
|
|
166
|
+
*
|
|
167
|
+
* @example
|
|
168
|
+
* ```typescript
|
|
169
|
+
* const requiredSchema = createCardSchema({
|
|
170
|
+
* id: z.string(),
|
|
171
|
+
* name: z.string(),
|
|
172
|
+
* type: z.string(),
|
|
173
|
+
* });
|
|
174
|
+
*
|
|
175
|
+
* const optionalSchema = createOptionalSchema(requiredSchema);
|
|
176
|
+
* // Now id, name, and type are all optional
|
|
177
|
+
* ```
|
|
178
|
+
*/
|
|
179
|
+
export function createOptionalSchema<T extends ZodRawShape>(
|
|
180
|
+
schema: ZodObject<T>,
|
|
181
|
+
): ZodObject<{ [K in keyof T]: ZodTypeAny }> {
|
|
182
|
+
return schema.partial() as ZodObject<{ [K in keyof T]: ZodTypeAny }>;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Creates a strict schema that doesn't allow extra fields
|
|
187
|
+
*
|
|
188
|
+
* By default, Zod schemas allow extra fields. This function creates
|
|
189
|
+
* a schema that will reject objects with fields not defined in the schema.
|
|
190
|
+
*
|
|
191
|
+
* @param shape - The shape of the schema
|
|
192
|
+
* @returns A strict schema
|
|
193
|
+
*
|
|
194
|
+
* @example
|
|
195
|
+
* ```typescript
|
|
196
|
+
* const schema = createStrictSchema({
|
|
197
|
+
* id: z.string(),
|
|
198
|
+
* name: z.string(),
|
|
199
|
+
* });
|
|
200
|
+
*
|
|
201
|
+
* // This will fail because of the extra field
|
|
202
|
+
* schema.parse({ id: "1", name: "Card", extra: "not allowed" });
|
|
203
|
+
* ```
|
|
204
|
+
*/
|
|
205
|
+
export function createStrictSchema<T extends ZodRawShape>(
|
|
206
|
+
shape: T,
|
|
207
|
+
): ZodObject<T> {
|
|
208
|
+
return z.object(shape).strict();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Creates a schema for validating arrays of items
|
|
213
|
+
*
|
|
214
|
+
* @param itemSchema - Schema for individual items
|
|
215
|
+
* @param options - Optional constraints (min, max length)
|
|
216
|
+
* @returns A schema for validating arrays
|
|
217
|
+
*
|
|
218
|
+
* @example
|
|
219
|
+
* ```typescript
|
|
220
|
+
* const cardSchema = createCardSchema({
|
|
221
|
+
* id: z.string(),
|
|
222
|
+
* name: z.string(),
|
|
223
|
+
* });
|
|
224
|
+
*
|
|
225
|
+
* const deckSchema = createArraySchema(cardSchema, { min: 40, max: 60 });
|
|
226
|
+
* ```
|
|
227
|
+
*/
|
|
228
|
+
export function createArraySchema<T extends ZodTypeAny>(
|
|
229
|
+
itemSchema: T,
|
|
230
|
+
options?: { min?: number; max?: number },
|
|
231
|
+
): z.ZodArray<T> {
|
|
232
|
+
let arraySchema = z.array(itemSchema);
|
|
233
|
+
|
|
234
|
+
if (options?.min !== undefined) {
|
|
235
|
+
arraySchema = arraySchema.min(options.min);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (options?.max !== undefined) {
|
|
239
|
+
arraySchema = arraySchema.max(options.max);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return arraySchema;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Creates a discriminated union schema
|
|
247
|
+
*
|
|
248
|
+
* Useful for validating objects that have different shapes based on a discriminator field.
|
|
249
|
+
*
|
|
250
|
+
* @param discriminator - The field that determines which schema to use
|
|
251
|
+
* @param schemas - Map of discriminator values to schemas
|
|
252
|
+
* @returns A discriminated union schema
|
|
253
|
+
*
|
|
254
|
+
* @example
|
|
255
|
+
* ```typescript
|
|
256
|
+
* const cardSchema = createDiscriminatedUnion("type", [
|
|
257
|
+
* z.object({ type: z.literal("creature"), power: z.number() }),
|
|
258
|
+
* z.object({ type: z.literal("instant"), damage: z.number() }),
|
|
259
|
+
* ]);
|
|
260
|
+
* ```
|
|
261
|
+
*/
|
|
262
|
+
export function createDiscriminatedUnion<K extends string>(
|
|
263
|
+
discriminator: K,
|
|
264
|
+
schemas: [ZodObject<any>, ZodObject<any>, ...ZodObject<any>[]],
|
|
265
|
+
): z.ZodDiscriminatedUnion<K, any> {
|
|
266
|
+
return z.discriminatedUnion(discriminator, schemas as any);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Creates a record schema for dynamic key-value mappings
|
|
271
|
+
*
|
|
272
|
+
* @param keySchema - Schema for the keys
|
|
273
|
+
* @param valueSchema - Schema for the values
|
|
274
|
+
* @returns A record schema
|
|
275
|
+
*
|
|
276
|
+
* @example
|
|
277
|
+
* ```typescript
|
|
278
|
+
* // Map of player IDs to scores
|
|
279
|
+
* const scoresSchema = createRecordSchema(z.string(), z.number());
|
|
280
|
+
* ```
|
|
281
|
+
*/
|
|
282
|
+
export function createRecordSchema<
|
|
283
|
+
K extends z.ZodTypeAny,
|
|
284
|
+
V extends z.ZodTypeAny,
|
|
285
|
+
>(keySchema: K, valueSchema: V): z.ZodRecord<K, V> {
|
|
286
|
+
return z.record(keySchema, valueSchema);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Creates a schema with custom refinements and error messages
|
|
291
|
+
*
|
|
292
|
+
* @param baseSchema - The base schema to refine
|
|
293
|
+
* @param refinement - Refinement function
|
|
294
|
+
* @param message - Error message if refinement fails
|
|
295
|
+
* @returns A refined schema
|
|
296
|
+
*
|
|
297
|
+
* @example
|
|
298
|
+
* ```typescript
|
|
299
|
+
* const powerSchema = z.number();
|
|
300
|
+
* const positivePowerSchema = createRefinedSchema(
|
|
301
|
+
* powerSchema,
|
|
302
|
+
* (val) => val > 0,
|
|
303
|
+
* "Power must be positive"
|
|
304
|
+
* );
|
|
305
|
+
* ```
|
|
306
|
+
*/
|
|
307
|
+
export function createRefinedSchema<T extends ZodTypeAny>(
|
|
308
|
+
baseSchema: T,
|
|
309
|
+
refinement: (val: z.infer<T>) => boolean,
|
|
310
|
+
message: string,
|
|
311
|
+
): z.ZodEffects<T> {
|
|
312
|
+
return baseSchema.refine(refinement, { message });
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Creates a schema with multiple refinements
|
|
317
|
+
*
|
|
318
|
+
* @param baseSchema - The base schema to refine
|
|
319
|
+
* @param refinements - Array of refinements with messages
|
|
320
|
+
* @returns A refined schema
|
|
321
|
+
*
|
|
322
|
+
* @example
|
|
323
|
+
* ```typescript
|
|
324
|
+
* const passwordSchema = createMultiRefinedSchema(z.string(), [
|
|
325
|
+
* { check: (val) => val.length >= 8, message: "Must be at least 8 characters" },
|
|
326
|
+
* { check: (val) => /[A-Z]/.test(val), message: "Must contain uppercase" },
|
|
327
|
+
* { check: (val) => /[0-9]/.test(val), message: "Must contain number" },
|
|
328
|
+
* ]);
|
|
329
|
+
* ```
|
|
330
|
+
*/
|
|
331
|
+
export function createMultiRefinedSchema<T extends ZodTypeAny>(
|
|
332
|
+
baseSchema: T,
|
|
333
|
+
refinements: Array<{
|
|
334
|
+
check: (val: z.infer<T>) => boolean;
|
|
335
|
+
message: string;
|
|
336
|
+
}>,
|
|
337
|
+
): ZodTypeAny {
|
|
338
|
+
let schema: ZodTypeAny = baseSchema;
|
|
339
|
+
|
|
340
|
+
for (const { check, message } of refinements) {
|
|
341
|
+
schema = schema.refine(check, { message });
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return schema;
|
|
345
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { createTypeGuard } from "./type-guard-builder";
|
|
3
|
+
|
|
4
|
+
describe("createTypeGuard", () => {
|
|
5
|
+
describe("basic type guards", () => {
|
|
6
|
+
it("should create a type guard for a string field", () => {
|
|
7
|
+
type Card = { type: string; name: string };
|
|
8
|
+
const isCreature = createTypeGuard<Card, "type", "creature">(
|
|
9
|
+
"type",
|
|
10
|
+
"creature",
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
const creature: Card = { type: "creature", name: "Dragon" };
|
|
14
|
+
const instant: Card = { type: "instant", name: "Lightning Bolt" };
|
|
15
|
+
|
|
16
|
+
expect(isCreature(creature)).toBe(true);
|
|
17
|
+
expect(isCreature(instant)).toBe(false);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("should create a type guard for a number field", () => {
|
|
21
|
+
type Item = { id: number; category: string };
|
|
22
|
+
const isItem42 = createTypeGuard<Item, "id", 42>("id", 42);
|
|
23
|
+
|
|
24
|
+
const item42: Item = { id: 42, category: "test" };
|
|
25
|
+
const item10: Item = { id: 10, category: "test" };
|
|
26
|
+
|
|
27
|
+
expect(isItem42(item42)).toBe(true);
|
|
28
|
+
expect(isItem42(item10)).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("should create a type guard for a boolean field", () => {
|
|
32
|
+
type Config = { enabled: boolean; name: string };
|
|
33
|
+
const isEnabled = createTypeGuard<Config, "enabled", true>(
|
|
34
|
+
"enabled",
|
|
35
|
+
true,
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const enabledConfig: Config = { enabled: true, name: "test" };
|
|
39
|
+
const disabledConfig: Config = { enabled: false, name: "test" };
|
|
40
|
+
|
|
41
|
+
expect(isEnabled(enabledConfig)).toBe(true);
|
|
42
|
+
expect(isEnabled(disabledConfig)).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("complex types", () => {
|
|
47
|
+
it("should work with union types", () => {
|
|
48
|
+
type Card = { type: "creature" | "instant" | "sorcery"; name: string };
|
|
49
|
+
const isCreature = createTypeGuard<Card, "type", "creature">(
|
|
50
|
+
"type",
|
|
51
|
+
"creature",
|
|
52
|
+
);
|
|
53
|
+
const isInstant = createTypeGuard<Card, "type", "instant">(
|
|
54
|
+
"type",
|
|
55
|
+
"instant",
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const creature: Card = { type: "creature", name: "Dragon" };
|
|
59
|
+
const instant: Card = { type: "instant", name: "Lightning Bolt" };
|
|
60
|
+
|
|
61
|
+
expect(isCreature(creature)).toBe(true);
|
|
62
|
+
expect(isCreature(instant)).toBe(false);
|
|
63
|
+
expect(isInstant(instant)).toBe(true);
|
|
64
|
+
expect(isInstant(creature)).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("should work with optional fields", () => {
|
|
68
|
+
type Card = { type?: string; name: string };
|
|
69
|
+
const isCreature = createTypeGuard<Card, "type", "creature">(
|
|
70
|
+
"type",
|
|
71
|
+
"creature",
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const creature: Card = { type: "creature", name: "Dragon" };
|
|
75
|
+
const noType: Card = { name: "Unknown" };
|
|
76
|
+
|
|
77
|
+
expect(isCreature(creature)).toBe(true);
|
|
78
|
+
expect(isCreature(noType)).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("should work with nested objects", () => {
|
|
82
|
+
type Card = {
|
|
83
|
+
metadata: {
|
|
84
|
+
category: string;
|
|
85
|
+
version: number;
|
|
86
|
+
};
|
|
87
|
+
name: string;
|
|
88
|
+
};
|
|
89
|
+
const isStandard = createTypeGuard<
|
|
90
|
+
Card,
|
|
91
|
+
"metadata",
|
|
92
|
+
{ category: "standard"; version: 1 }
|
|
93
|
+
>("metadata", { category: "standard", version: 1 });
|
|
94
|
+
|
|
95
|
+
const standardCard: Card = {
|
|
96
|
+
metadata: { category: "standard", version: 1 },
|
|
97
|
+
name: "Card",
|
|
98
|
+
};
|
|
99
|
+
const modernCard: Card = {
|
|
100
|
+
metadata: { category: "modern", version: 1 },
|
|
101
|
+
name: "Card",
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
expect(isStandard(standardCard)).toBe(true);
|
|
105
|
+
expect(isStandard(modernCard)).toBe(false);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe("type narrowing", () => {
|
|
110
|
+
it("should narrow types correctly in TypeScript", () => {
|
|
111
|
+
type Card = {
|
|
112
|
+
type: "creature" | "instant";
|
|
113
|
+
name: string;
|
|
114
|
+
power?: number;
|
|
115
|
+
};
|
|
116
|
+
const isCreature = createTypeGuard<Card, "type", "creature">(
|
|
117
|
+
"type",
|
|
118
|
+
"creature",
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const card: Card = { type: "creature", name: "Dragon", power: 5 };
|
|
122
|
+
|
|
123
|
+
if (isCreature(card)) {
|
|
124
|
+
// TypeScript should know that card.type is "creature" here
|
|
125
|
+
const typeValue: "creature" = card.type;
|
|
126
|
+
expect(typeValue).toBe("creature");
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe("edge cases", () => {
|
|
132
|
+
it("should handle null and undefined gracefully", () => {
|
|
133
|
+
type Card = { type: string | null | undefined; name: string };
|
|
134
|
+
const isCreature = createTypeGuard<Card, "type", "creature">(
|
|
135
|
+
"type",
|
|
136
|
+
"creature",
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const nullCard: Card = { type: null, name: "Card" };
|
|
140
|
+
const undefinedCard: Card = { type: undefined, name: "Card" };
|
|
141
|
+
const validCard: Card = { type: "creature", name: "Card" };
|
|
142
|
+
|
|
143
|
+
expect(isCreature(nullCard)).toBe(false);
|
|
144
|
+
expect(isCreature(undefinedCard)).toBe(false);
|
|
145
|
+
expect(isCreature(validCard)).toBe(true);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("should handle empty strings", () => {
|
|
149
|
+
type Card = { type: string; name: string };
|
|
150
|
+
const isEmpty = createTypeGuard<Card, "type", "">("type", "");
|
|
151
|
+
|
|
152
|
+
const emptyCard: Card = { type: "", name: "Card" };
|
|
153
|
+
const nonEmptyCard: Card = { type: "creature", name: "Card" };
|
|
154
|
+
|
|
155
|
+
expect(isEmpty(emptyCard)).toBe(true);
|
|
156
|
+
expect(isEmpty(nonEmptyCard)).toBe(false);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("should handle objects without the specified field", () => {
|
|
160
|
+
type PartialCard = { name: string; type?: string };
|
|
161
|
+
const isCreature = createTypeGuard<PartialCard, "type", "creature">(
|
|
162
|
+
"type",
|
|
163
|
+
"creature",
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
const card: PartialCard = { name: "Card" };
|
|
167
|
+
|
|
168
|
+
expect(isCreature(card)).toBe(false);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe("array values", () => {
|
|
173
|
+
it("should create a type guard for array field values", () => {
|
|
174
|
+
type Card = { types: string[]; name: string };
|
|
175
|
+
const hasCreatureTypes = createTypeGuard<Card, "types", string[]>(
|
|
176
|
+
"types",
|
|
177
|
+
["creature", "dragon"],
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
const dragonCard: Card = {
|
|
181
|
+
types: ["creature", "dragon"],
|
|
182
|
+
name: "Dragon",
|
|
183
|
+
};
|
|
184
|
+
const goblinCard: Card = {
|
|
185
|
+
types: ["creature", "goblin"],
|
|
186
|
+
name: "Goblin",
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
expect(hasCreatureTypes(dragonCard)).toBe(true);
|
|
190
|
+
expect(hasCreatureTypes(goblinCard)).toBe(false);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe("performance", () => {
|
|
195
|
+
it("should be efficient for multiple checks", () => {
|
|
196
|
+
type Card = { type: string; name: string };
|
|
197
|
+
const isCreature = createTypeGuard<Card, "type", "creature">(
|
|
198
|
+
"type",
|
|
199
|
+
"creature",
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const creature: Card = { type: "creature", name: "Dragon" };
|
|
203
|
+
const instant: Card = { type: "instant", name: "Lightning Bolt" };
|
|
204
|
+
|
|
205
|
+
const startTime = performance.now();
|
|
206
|
+
for (let i = 0; i < 10000; i++) {
|
|
207
|
+
isCreature(creature);
|
|
208
|
+
isCreature(instant);
|
|
209
|
+
}
|
|
210
|
+
const endTime = performance.now();
|
|
211
|
+
|
|
212
|
+
// Should complete in reasonable time (< 1000ms for 20k checks, higher threshold for CI parallel execution)
|
|
213
|
+
expect(endTime - startTime).toBeLessThan(1000);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
});
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type guard builder utilities for creating type narrowing functions
|
|
3
|
+
*
|
|
4
|
+
* These utilities help create type-safe predicates that TypeScript can use
|
|
5
|
+
* for type narrowing in conditional blocks.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Creates a type guard function that checks if an object's field matches a specific value
|
|
10
|
+
*
|
|
11
|
+
* @template T - The object type to guard
|
|
12
|
+
* @template K - The key of the field to check (must be a key of T)
|
|
13
|
+
* @template V - The value type to check against
|
|
14
|
+
*
|
|
15
|
+
* @param field - The field name to check
|
|
16
|
+
* @param value - The value to compare against
|
|
17
|
+
* @returns A type guard function that narrows T based on the field value
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```typescript
|
|
21
|
+
* type Card = { type: "creature" | "instant"; name: string };
|
|
22
|
+
* const isCreature = createTypeGuard<Card, "type", "creature">("type", "creature");
|
|
23
|
+
*
|
|
24
|
+
* const card: Card = { type: "creature", name: "Dragon" };
|
|
25
|
+
* if (isCreature(card)) {
|
|
26
|
+
* // TypeScript knows card.type is "creature" here
|
|
27
|
+
* console.log(card.type); // Type: "creature"
|
|
28
|
+
* }
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export function createTypeGuard<T, K extends keyof T, V extends T[K]>(
|
|
32
|
+
field: K,
|
|
33
|
+
value: V,
|
|
34
|
+
): (obj: T) => obj is T & Record<K, V> {
|
|
35
|
+
return (obj: T): obj is T & Record<K, V> => {
|
|
36
|
+
// Handle null and undefined gracefully
|
|
37
|
+
if (obj === null || obj === undefined) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const fieldValue = obj[field];
|
|
42
|
+
|
|
43
|
+
// Handle primitive comparisons
|
|
44
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
45
|
+
// Deep equality check for objects
|
|
46
|
+
return deepEqual(fieldValue, value);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (Array.isArray(value)) {
|
|
50
|
+
// Deep equality check for arrays
|
|
51
|
+
return deepEqual(fieldValue, value);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Simple equality check for primitives
|
|
55
|
+
return fieldValue === value;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Deep equality comparison for objects and arrays
|
|
61
|
+
* Used internally by createTypeGuard for complex value comparisons
|
|
62
|
+
*
|
|
63
|
+
* @param a - First value to compare
|
|
64
|
+
* @param b - Second value to compare
|
|
65
|
+
* @returns true if values are deeply equal, false otherwise
|
|
66
|
+
*/
|
|
67
|
+
function deepEqual(a: unknown, b: unknown): boolean {
|
|
68
|
+
// Handle primitive types
|
|
69
|
+
if (a === b) {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Handle null and undefined
|
|
74
|
+
if (a === null || b === null || a === undefined || b === undefined) {
|
|
75
|
+
return a === b;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Handle different types
|
|
79
|
+
if (typeof a !== typeof b) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Handle arrays
|
|
84
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
85
|
+
if (a.length !== b.length) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
return a.every((item, index) => deepEqual(item, b[index]));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Handle objects
|
|
92
|
+
if (typeof a === "object" && typeof b === "object") {
|
|
93
|
+
const keysA = Object.keys(a as object);
|
|
94
|
+
const keysB = Object.keys(b as object);
|
|
95
|
+
|
|
96
|
+
if (keysA.length !== keysB.length) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return keysA.every((key) =>
|
|
101
|
+
deepEqual(
|
|
102
|
+
(a as Record<string, unknown>)[key],
|
|
103
|
+
(b as Record<string, unknown>)[key],
|
|
104
|
+
),
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return false;
|
|
109
|
+
}
|