@schmock/schema 1.0.0
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/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +165 -0
- package/dist/test-utils.d.ts +59 -0
- package/dist/test-utils.d.ts.map +1 -0
- package/dist/test-utils.js +269 -0
- package/package.json +39 -0
- package/src/advanced-features.test.ts +911 -0
- package/src/data-quality.test.ts +415 -0
- package/src/error-handling.test.ts +507 -0
- package/src/index.test.ts +1208 -0
- package/src/index.ts +859 -0
- package/src/integration.test.ts +632 -0
- package/src/performance.test.ts +477 -0
- package/src/plugin-integration.test.ts +574 -0
- package/src/real-world.test.ts +636 -0
- package/src/test-utils.ts +357 -0
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
import type { JSONSchema7 } from "json-schema";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { generateFromSchema, schemaPlugin } from "./index";
|
|
4
|
+
import { generate, performance as perf, schemas } from "./test-utils";
|
|
5
|
+
|
|
6
|
+
describe("Performance and Memory", () => {
|
|
7
|
+
describe("Generation Speed", () => {
|
|
8
|
+
it("generates simple objects quickly", async () => {
|
|
9
|
+
const schema = schemas.simple.object({
|
|
10
|
+
id: schemas.simple.number(),
|
|
11
|
+
name: schemas.simple.string(),
|
|
12
|
+
active: { type: "boolean" },
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const times: number[] = [];
|
|
16
|
+
for (let i = 0; i < 10; i++) {
|
|
17
|
+
const { duration } = await perf.measure(() =>
|
|
18
|
+
generateFromSchema({ schema }),
|
|
19
|
+
);
|
|
20
|
+
times.push(duration);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const avgTime = times.reduce((a, b) => a + b, 0) / times.length;
|
|
24
|
+
expect(avgTime).toBeLessThan(50); // Should average under 50ms
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("handles nested objects efficiently", async () => {
|
|
28
|
+
const schema = schemas.nested.deep(
|
|
29
|
+
3,
|
|
30
|
+
schemas.simple.object({
|
|
31
|
+
id: schemas.simple.number(),
|
|
32
|
+
value: schemas.simple.string(),
|
|
33
|
+
}),
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const { duration } = await perf.measure(() =>
|
|
37
|
+
generateFromSchema({ schema }),
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
expect(duration).toBeLessThan(100); // Reasonable for nested structure
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("generates arrays efficiently", async () => {
|
|
44
|
+
const schema = schemas.simple.array(
|
|
45
|
+
schemas.simple.object({
|
|
46
|
+
id: schemas.simple.number(),
|
|
47
|
+
name: schemas.simple.string(),
|
|
48
|
+
}),
|
|
49
|
+
{ minItems: 50, maxItems: 50 },
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const { duration } = await perf.measure(() =>
|
|
53
|
+
generateFromSchema({ schema }),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
expect(duration).toBeLessThan(200); // Should handle 50 items quickly
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("handles complex schemas with multiple constraints", async () => {
|
|
60
|
+
const schema: JSONSchema7 = {
|
|
61
|
+
type: "object",
|
|
62
|
+
properties: {
|
|
63
|
+
users: {
|
|
64
|
+
type: "array",
|
|
65
|
+
items: {
|
|
66
|
+
type: "object",
|
|
67
|
+
properties: {
|
|
68
|
+
id: { type: "string", format: "uuid" },
|
|
69
|
+
email: { type: "string", format: "email" },
|
|
70
|
+
age: { type: "integer", minimum: 18, maximum: 100 },
|
|
71
|
+
tags: {
|
|
72
|
+
type: "array",
|
|
73
|
+
items: { type: "string", pattern: "^[a-z]+$" },
|
|
74
|
+
maxItems: 5,
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
required: ["id", "email"],
|
|
78
|
+
},
|
|
79
|
+
minItems: 10,
|
|
80
|
+
maxItems: 10,
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const { duration } = await perf.measure(() =>
|
|
86
|
+
generateFromSchema({ schema }),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
expect(duration).toBeLessThan(300); // Complex but still reasonable
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("Scaling Behavior", () => {
|
|
94
|
+
it("scales linearly with array size", async () => {
|
|
95
|
+
const smallSchema = schemas.simple.array(schemas.simple.string(), {
|
|
96
|
+
minItems: 50,
|
|
97
|
+
maxItems: 50,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const largeSchema = schemas.simple.array(schemas.simple.string(), {
|
|
101
|
+
minItems: 500,
|
|
102
|
+
maxItems: 500,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Warmup runs to stabilize JIT
|
|
106
|
+
for (let i = 0; i < 3; i++) {
|
|
107
|
+
generateFromSchema({ schema: smallSchema });
|
|
108
|
+
generateFromSchema({ schema: largeSchema });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Multiple measurement runs for statistical stability
|
|
112
|
+
const smallTimes: number[] = [];
|
|
113
|
+
const largeTimes: number[] = [];
|
|
114
|
+
|
|
115
|
+
for (let i = 0; i < 10; i++) {
|
|
116
|
+
const start1 = performance.now();
|
|
117
|
+
generateFromSchema({ schema: smallSchema });
|
|
118
|
+
const small = performance.now() - start1;
|
|
119
|
+
smallTimes.push(small);
|
|
120
|
+
|
|
121
|
+
const start2 = performance.now();
|
|
122
|
+
generateFromSchema({ schema: largeSchema });
|
|
123
|
+
const large = performance.now() - start2;
|
|
124
|
+
largeTimes.push(large);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Remove outliers and calculate medians for stability
|
|
128
|
+
smallTimes.sort((a, b) => a - b);
|
|
129
|
+
largeTimes.sort((a, b) => a - b);
|
|
130
|
+
const medianSmall = smallTimes[Math.floor(smallTimes.length / 2)];
|
|
131
|
+
const medianLarge = largeTimes[Math.floor(largeTimes.length / 2)];
|
|
132
|
+
|
|
133
|
+
// Both should complete in reasonable time (main goal is to ensure it works, not strict timing)
|
|
134
|
+
expect(medianSmall).toBeLessThan(50); // Small arrays should be fast
|
|
135
|
+
expect(medianLarge).toBeLessThan(200); // Large arrays should still be reasonable
|
|
136
|
+
|
|
137
|
+
// Optional: Check scaling if timing is meaningful
|
|
138
|
+
if (medianSmall > 0.1) {
|
|
139
|
+
// Only check scaling if we have measurable timing
|
|
140
|
+
expect(medianLarge).toBeGreaterThan(medianSmall * 0.5); // Should take at least half as long
|
|
141
|
+
expect(medianLarge).toBeLessThan(medianSmall * 50); // But not extremely longer
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("handles wide objects efficiently", async () => {
|
|
146
|
+
const narrowSchema = schemas.nested.wide(20);
|
|
147
|
+
const wideSchema = schemas.nested.wide(100);
|
|
148
|
+
|
|
149
|
+
// Warmup
|
|
150
|
+
for (let i = 0; i < 3; i++) {
|
|
151
|
+
generateFromSchema({ schema: narrowSchema });
|
|
152
|
+
generateFromSchema({ schema: wideSchema });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Measure with multiple runs
|
|
156
|
+
const narrowTimes: number[] = [];
|
|
157
|
+
const wideTimes: number[] = [];
|
|
158
|
+
|
|
159
|
+
for (let i = 0; i < 8; i++) {
|
|
160
|
+
const start1 = performance.now();
|
|
161
|
+
generateFromSchema({ schema: narrowSchema });
|
|
162
|
+
narrowTimes.push(performance.now() - start1);
|
|
163
|
+
|
|
164
|
+
const start2 = performance.now();
|
|
165
|
+
generateFromSchema({ schema: wideSchema });
|
|
166
|
+
wideTimes.push(performance.now() - start2);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const avgNarrow =
|
|
170
|
+
narrowTimes.reduce((a, b) => a + b, 0) / narrowTimes.length;
|
|
171
|
+
const avgWide = wideTimes.reduce((a, b) => a + b, 0) / wideTimes.length;
|
|
172
|
+
|
|
173
|
+
// 5x more properties should take more time but scale reasonably
|
|
174
|
+
expect(avgWide).toBeGreaterThan(avgNarrow);
|
|
175
|
+
expect(avgWide).toBeLessThan(avgNarrow * 15); // Linear-ish scaling, not exponential
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe("Plugin Performance", () => {
|
|
180
|
+
it("plugin creation is fast", async () => {
|
|
181
|
+
const schema = schemas.complex.user();
|
|
182
|
+
|
|
183
|
+
const { duration } = await perf.measure(() => schemaPlugin({ schema }));
|
|
184
|
+
|
|
185
|
+
expect(duration).toBeLessThan(10); // Plugin creation should be instant
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("plugin processing adds minimal overhead", async () => {
|
|
189
|
+
const schema = schemas.complex.apiResponse();
|
|
190
|
+
const plugin = schemaPlugin({ schema });
|
|
191
|
+
|
|
192
|
+
const context = {
|
|
193
|
+
method: "GET",
|
|
194
|
+
path: "/test",
|
|
195
|
+
params: {},
|
|
196
|
+
query: {},
|
|
197
|
+
state: {},
|
|
198
|
+
headers: {},
|
|
199
|
+
body: null,
|
|
200
|
+
route: {},
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const times: number[] = [];
|
|
204
|
+
for (let i = 0; i < 10; i++) {
|
|
205
|
+
const { duration } = await perf.measure(() => plugin.process(context));
|
|
206
|
+
times.push(duration);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const avgTime = times.reduce((a, b) => a + b, 0) / times.length;
|
|
210
|
+
expect(avgTime).toBeLessThan(100);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("template processing is efficient", async () => {
|
|
214
|
+
const schema = schemas.simple.object({
|
|
215
|
+
id: schemas.simple.string(),
|
|
216
|
+
userId: schemas.simple.string(),
|
|
217
|
+
timestamp: schemas.simple.string(),
|
|
218
|
+
message: schemas.simple.string(),
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const overrides = {
|
|
222
|
+
id: "{{params.id}}",
|
|
223
|
+
userId: "{{state.user.id}}",
|
|
224
|
+
timestamp: "{{state.timestamp}}",
|
|
225
|
+
message: "User {{params.id}} at {{state.timestamp}}",
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const { duration } = await perf.measure(() =>
|
|
229
|
+
generateFromSchema({
|
|
230
|
+
schema,
|
|
231
|
+
overrides,
|
|
232
|
+
params: { id: "123" },
|
|
233
|
+
state: {
|
|
234
|
+
user: { id: "user-456" },
|
|
235
|
+
timestamp: new Date().toISOString(),
|
|
236
|
+
},
|
|
237
|
+
}),
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
expect(duration).toBeLessThan(50); // Template processing should be fast
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
describe("Concurrent Generation", () => {
|
|
245
|
+
it("handles multiple concurrent generations", async () => {
|
|
246
|
+
const schema = schemas.complex.user();
|
|
247
|
+
|
|
248
|
+
const { duration } = await perf.measure(async () => {
|
|
249
|
+
const promises = Array.from({ length: 20 }, () =>
|
|
250
|
+
generateFromSchema({ schema }),
|
|
251
|
+
);
|
|
252
|
+
await Promise.all(promises);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
expect(duration).toBeLessThan(500); // Should handle concurrency well
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("maintains performance under load", async () => {
|
|
259
|
+
const schema = schemas.simple.object({
|
|
260
|
+
id: schemas.simple.number(),
|
|
261
|
+
data: schemas.simple.string(),
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// Warm up
|
|
265
|
+
generateFromSchema({ schema });
|
|
266
|
+
|
|
267
|
+
// Test under load
|
|
268
|
+
const iterations = 100;
|
|
269
|
+
const { duration } = await perf.measure(async () => {
|
|
270
|
+
for (let i = 0; i < iterations; i++) {
|
|
271
|
+
generateFromSchema({ schema });
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
const avgTime = duration / iterations;
|
|
276
|
+
expect(avgTime).toBeLessThan(10); // Should maintain speed
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
describe("Memory Efficiency", () => {
|
|
281
|
+
it("doesn't leak memory on repeated generation", () => {
|
|
282
|
+
const schema = schemas.simple.object({
|
|
283
|
+
id: schemas.simple.number(),
|
|
284
|
+
name: schemas.simple.string(),
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// Generate many times
|
|
288
|
+
for (let i = 0; i < 1000; i++) {
|
|
289
|
+
const result = generateFromSchema({ schema });
|
|
290
|
+
// Result should be garbage collectable
|
|
291
|
+
expect(result).toBeDefined();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// If we got here without crashing, memory is managed well
|
|
295
|
+
expect(true).toBe(true);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("handles large data structures without excessive memory", () => {
|
|
299
|
+
const schema = schemas.simple.array(
|
|
300
|
+
schemas.simple.object({
|
|
301
|
+
id: schemas.simple.string(),
|
|
302
|
+
data: schemas.simple.string(),
|
|
303
|
+
}),
|
|
304
|
+
{ minItems: 1000, maxItems: 1000 },
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
// Should be able to generate without memory issues
|
|
308
|
+
const result = generateFromSchema({ schema });
|
|
309
|
+
expect(result).toHaveLength(1000);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("cleans up after schema validation errors", () => {
|
|
313
|
+
// Generate errors repeatedly
|
|
314
|
+
for (let i = 0; i < 100; i++) {
|
|
315
|
+
try {
|
|
316
|
+
generateFromSchema({ schema: { type: "invalid" as any } });
|
|
317
|
+
} catch (_e) {
|
|
318
|
+
// Expected
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Should not have memory leaks from error objects
|
|
323
|
+
expect(true).toBe(true);
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
describe("Optimization Opportunities", () => {
|
|
328
|
+
it("generates data consistently across multiple calls", async () => {
|
|
329
|
+
const schema = schemas.simple.object({
|
|
330
|
+
email: schemas.simple.string(),
|
|
331
|
+
firstName: schemas.simple.string(),
|
|
332
|
+
phone: schemas.simple.string(),
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// Generate multiple times to ensure consistent behavior
|
|
336
|
+
const results: any[] = [];
|
|
337
|
+
for (let i = 0; i < 10; i++) {
|
|
338
|
+
const result = generateFromSchema({ schema });
|
|
339
|
+
results.push(result);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Verify all results have the expected structure
|
|
343
|
+
results.forEach((result) => {
|
|
344
|
+
expect(result).toHaveProperty("email");
|
|
345
|
+
expect(result).toHaveProperty("firstName");
|
|
346
|
+
expect(result).toHaveProperty("phone");
|
|
347
|
+
expect(typeof result.email).toBe("string");
|
|
348
|
+
expect(typeof result.firstName).toBe("string");
|
|
349
|
+
expect(typeof result.phone).toBe("string");
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
// Verify that smart field mapping worked across all calls
|
|
353
|
+
const emails = results.map((r) => r.email);
|
|
354
|
+
const firstNames = results.map((r) => r.firstName);
|
|
355
|
+
|
|
356
|
+
// Should have good diversity (no exact duplicates expected)
|
|
357
|
+
const uniqueEmails = new Set(emails);
|
|
358
|
+
const uniqueFirstNames = new Set(firstNames);
|
|
359
|
+
expect(uniqueEmails.size).toBeGreaterThan(1);
|
|
360
|
+
expect(uniqueFirstNames.size).toBeGreaterThan(1);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it("reuses schema enhancement for repeated generations", async () => {
|
|
364
|
+
const schema = schemas.complex.user();
|
|
365
|
+
const plugin = schemaPlugin({ schema });
|
|
366
|
+
|
|
367
|
+
const context = {
|
|
368
|
+
method: "GET",
|
|
369
|
+
path: "/test",
|
|
370
|
+
params: {},
|
|
371
|
+
query: {},
|
|
372
|
+
state: {},
|
|
373
|
+
headers: {},
|
|
374
|
+
body: null,
|
|
375
|
+
route: {},
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
// Warmup
|
|
379
|
+
for (let i = 0; i < 3; i++) {
|
|
380
|
+
plugin.process(context);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Multiple calls through same plugin with proper timing
|
|
384
|
+
const times: number[] = [];
|
|
385
|
+
for (let i = 0; i < 15; i++) {
|
|
386
|
+
const start = performance.now();
|
|
387
|
+
plugin.process(context);
|
|
388
|
+
times.push(performance.now() - start);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Calculate statistics
|
|
392
|
+
const avgTime = times.reduce((a, b) => a + b, 0) / times.length;
|
|
393
|
+
const maxTime = Math.max(...times);
|
|
394
|
+
const minTime = Math.min(...times);
|
|
395
|
+
|
|
396
|
+
// Performance should be consistent and reasonable
|
|
397
|
+
expect(avgTime).toBeLessThan(50); // Should be reasonably fast
|
|
398
|
+
expect(maxTime).toBeLessThan(100); // No extreme outliers
|
|
399
|
+
expect(minTime).toBeGreaterThanOrEqual(0); // Valid timing
|
|
400
|
+
|
|
401
|
+
// All measurements should be reasonable - no extreme variance
|
|
402
|
+
const reasonableMaxTime = Math.max(avgTime * 5, 10); // At least 10ms tolerance
|
|
403
|
+
expect(maxTime).toBeLessThan(reasonableMaxTime);
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
describe("Edge Case Performance", () => {
|
|
408
|
+
it("handles empty schemas efficiently", async () => {
|
|
409
|
+
const schema = schemas.simple.object({});
|
|
410
|
+
|
|
411
|
+
const { duration } = await perf.measure(() =>
|
|
412
|
+
generateFromSchema({ schema }),
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
expect(duration).toBeLessThan(10); // Empty should be instant
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it("handles schemas with many enum values", async () => {
|
|
419
|
+
const schema = schemas.simple.object({
|
|
420
|
+
country: {
|
|
421
|
+
type: "string",
|
|
422
|
+
enum: Array.from({ length: 200 }, (_, i) => `country-${i}`),
|
|
423
|
+
},
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
const { duration } = await perf.measure(() =>
|
|
427
|
+
generateFromSchema({ schema }),
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
expect(duration).toBeLessThan(50); // Large enum shouldn't be slow
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it("handles complex regex patterns efficiently", async () => {
|
|
434
|
+
const schema = schemas.simple.object({
|
|
435
|
+
code: {
|
|
436
|
+
type: "string",
|
|
437
|
+
pattern: "^[A-Z]{2}-[0-9]{4}-[a-z]{2}-[0-9A-F]{8}$",
|
|
438
|
+
},
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
const { duration } = await perf.measure(() =>
|
|
442
|
+
generate.samples(schema, 10),
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
expect(duration).toBeLessThan(100); // Complex patterns OK
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it("handles mixed constraint schemas", async () => {
|
|
449
|
+
const schema: JSONSchema7 = {
|
|
450
|
+
anyOf: [
|
|
451
|
+
{
|
|
452
|
+
type: "object",
|
|
453
|
+
properties: {
|
|
454
|
+
type: { const: "A" },
|
|
455
|
+
data: { type: "string", minLength: 100 },
|
|
456
|
+
},
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
type: "array",
|
|
460
|
+
items: { type: "number", minimum: 0, maximum: 1000 },
|
|
461
|
+
minItems: 50,
|
|
462
|
+
},
|
|
463
|
+
{
|
|
464
|
+
type: "string",
|
|
465
|
+
pattern: "^[A-Za-z0-9+/]{100,}={0,2}$",
|
|
466
|
+
},
|
|
467
|
+
],
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
const { duration } = await perf.measure(() =>
|
|
471
|
+
generateFromSchema({ schema }),
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
expect(duration).toBeLessThan(200); // Complex but manageable
|
|
475
|
+
});
|
|
476
|
+
});
|
|
477
|
+
});
|