@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,59 @@
|
|
|
1
|
+
import type { JSONSchema7 } from "json-schema";
|
|
2
|
+
export declare const schemas: {
|
|
3
|
+
simple: {
|
|
4
|
+
string: () => JSONSchema7;
|
|
5
|
+
number: () => JSONSchema7;
|
|
6
|
+
object: (properties?: Record<string, JSONSchema7>) => JSONSchema7;
|
|
7
|
+
array: (items: JSONSchema7, constraints?: {
|
|
8
|
+
minItems?: number;
|
|
9
|
+
maxItems?: number;
|
|
10
|
+
}) => JSONSchema7;
|
|
11
|
+
};
|
|
12
|
+
withFaker: (type: JSONSchema7["type"], fakerMethod: string) => JSONSchema7;
|
|
13
|
+
nested: {
|
|
14
|
+
deep: (depth: number, leafSchema?: JSONSchema7) => JSONSchema7;
|
|
15
|
+
wide: (width: number, propertySchema?: JSONSchema7) => JSONSchema7;
|
|
16
|
+
};
|
|
17
|
+
complex: {
|
|
18
|
+
user: () => JSONSchema7;
|
|
19
|
+
apiResponse: () => JSONSchema7;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
export declare const validators: {
|
|
23
|
+
isFieldMapped: (fieldName: string, fieldType?: JSONSchema7["type"]) => Promise<boolean>;
|
|
24
|
+
uniquenessRatio: (samples: any[]) => number;
|
|
25
|
+
allMatch: (samples: any[], validator: (sample: any) => boolean) => boolean;
|
|
26
|
+
appearsToBeFromCategory: (samples: string[], category: "email" | "name" | "phone" | "address" | "uuid" | "date") => boolean;
|
|
27
|
+
};
|
|
28
|
+
export declare const performance: {
|
|
29
|
+
measure: <T>(fn: () => T | Promise<T>) => Promise<{
|
|
30
|
+
result: T;
|
|
31
|
+
duration: number;
|
|
32
|
+
}>;
|
|
33
|
+
measureMemory: (fn: () => void) => number;
|
|
34
|
+
benchmark: (_name: string, fn: () => any, iterations?: number) => Promise<{
|
|
35
|
+
mean: number;
|
|
36
|
+
min: number;
|
|
37
|
+
max: number;
|
|
38
|
+
}>;
|
|
39
|
+
};
|
|
40
|
+
export declare const generate: {
|
|
41
|
+
samples: <T>(schema: JSONSchema7, count?: number, options?: any) => T[];
|
|
42
|
+
withSeed: (schema: JSONSchema7, _seed?: number) => any;
|
|
43
|
+
};
|
|
44
|
+
export declare const stats: {
|
|
45
|
+
distribution: (samples: any[]) => Map<any, number>;
|
|
46
|
+
entropy: (samples: any[]) => number;
|
|
47
|
+
};
|
|
48
|
+
export declare const schemaTests: {
|
|
49
|
+
expectValid: (schema: JSONSchema7) => void;
|
|
50
|
+
expectInvalid: (schema: any, errorMessage?: string | RegExp) => void;
|
|
51
|
+
expectSchemaError: (schema: any, path: string, issue?: string) => void;
|
|
52
|
+
};
|
|
53
|
+
export declare const mocks: {
|
|
54
|
+
trackFakerCalls: () => {
|
|
55
|
+
calls: string[];
|
|
56
|
+
reset: () => void;
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
//# sourceMappingURL=test-utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-utils.d.ts","sourceRoot":"","sources":["../src/test-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAK/C,eAAO,MAAM,OAAO;;sBAEJ,WAAW;sBAIX,WAAW;8BAIF,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,KAAQ,WAAW;uBAM1D,WAAW,gBACJ;YAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;YAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;SAAE,KACrD,WAAW;;sBAOE,WAAW,CAAC,MAAM,CAAC,eAAe,MAAM,KAAG,WAAW;;sBAQ7D,MAAM,eACD,WAAW,KACtB,WAAW;sBAWL,MAAM,mBACG,WAAW,KAC1B,WAAW;;;oBASJ,WAAW;2BAYJ,WAAW;;CAkB/B,CAAC;AAGF,eAAO,MAAM,UAAU;+BAGR,MAAM,cACN,WAAW,CAAC,MAAM,CAAC,KAC7B,OAAO,CAAC,OAAO,CAAC;+BAoCQ,GAAG,EAAE,KAAG,MAAM;wBAMrB,GAAG,EAAE,aAAa,CAAC,MAAM,EAAE,GAAG,KAAK,OAAO,KAAG,OAAO;uCAM7D,MAAM,EAAE,YACP,OAAO,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,KACjE,OAAO;CAgDX,CAAC;AAGF,eAAO,MAAM,WAAW;cACN,CAAC,MACX,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,KACvB,OAAO,CAAC;QAAE,MAAM,EAAE,CAAC,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;wBAOvB,MAAM,IAAI,KAAG,MAAM;uBAW9B,MAAM,MACT,MAAM,GAAG,0BAEZ,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;CAgBvD,CAAC;AAGF,eAAO,MAAM,QAAQ;cACT,CAAC,UAAU,WAAW,4BAAwB,GAAG,KAAG,CAAC,EAAE;uBAM9C,WAAW,UAAU,MAAM,KAAG,GAAG;CAKrD,CAAC;AAGF,eAAO,MAAM,KAAK;4BACQ,GAAG,EAAE,KAAG,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC;uBAS7B,GAAG,EAAE,KAAG,MAAM;CAclC,CAAC;AAGF,eAAO,MAAM,WAAW;0BACA,WAAW,KAAG,IAAI;4BAIhB,GAAG,iBAAiB,MAAM,GAAG,MAAM,KAAG,IAAI;gCAQtC,GAAG,QAAQ,MAAM,UAAU,MAAM,KAAG,IAAI;CAerE,CAAC;AA8BF,eAAO,MAAM,KAAK;;;;;CAYjB,CAAC"}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { expect } from "vitest";
|
|
2
|
+
import { generateFromSchema } from "./index";
|
|
3
|
+
// Schema Factory Functions
|
|
4
|
+
export const schemas = {
|
|
5
|
+
simple: {
|
|
6
|
+
string: () => ({
|
|
7
|
+
type: "string",
|
|
8
|
+
}),
|
|
9
|
+
number: () => ({
|
|
10
|
+
type: "number",
|
|
11
|
+
}),
|
|
12
|
+
object: (properties = {}) => ({
|
|
13
|
+
type: "object",
|
|
14
|
+
properties,
|
|
15
|
+
}),
|
|
16
|
+
array: (items, constraints) => ({
|
|
17
|
+
type: "array",
|
|
18
|
+
items,
|
|
19
|
+
...constraints,
|
|
20
|
+
}),
|
|
21
|
+
},
|
|
22
|
+
withFaker: (type, fakerMethod) => ({
|
|
23
|
+
type: type,
|
|
24
|
+
faker: fakerMethod,
|
|
25
|
+
}),
|
|
26
|
+
nested: {
|
|
27
|
+
deep: (depth, leafSchema = schemas.simple.string()) => {
|
|
28
|
+
if (depth <= 0)
|
|
29
|
+
return leafSchema;
|
|
30
|
+
return {
|
|
31
|
+
type: "object",
|
|
32
|
+
properties: {
|
|
33
|
+
nested: schemas.nested.deep(depth - 1, leafSchema),
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
wide: (width, propertySchema = schemas.simple.string()) => ({
|
|
38
|
+
type: "object",
|
|
39
|
+
properties: Object.fromEntries(Array.from({ length: width }, (_, i) => [`prop${i}`, propertySchema])),
|
|
40
|
+
}),
|
|
41
|
+
},
|
|
42
|
+
complex: {
|
|
43
|
+
user: () => ({
|
|
44
|
+
type: "object",
|
|
45
|
+
properties: {
|
|
46
|
+
id: { type: "string", format: "uuid" },
|
|
47
|
+
email: { type: "string" },
|
|
48
|
+
firstName: { type: "string" },
|
|
49
|
+
lastName: { type: "string" },
|
|
50
|
+
createdAt: { type: "string" },
|
|
51
|
+
},
|
|
52
|
+
required: ["id", "email"],
|
|
53
|
+
}),
|
|
54
|
+
apiResponse: () => ({
|
|
55
|
+
type: "object",
|
|
56
|
+
properties: {
|
|
57
|
+
success: { type: "boolean" },
|
|
58
|
+
data: {
|
|
59
|
+
type: "array",
|
|
60
|
+
items: schemas.complex.user(),
|
|
61
|
+
},
|
|
62
|
+
meta: {
|
|
63
|
+
type: "object",
|
|
64
|
+
properties: {
|
|
65
|
+
page: { type: "number" },
|
|
66
|
+
total: { type: "number" },
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
}),
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
// Validation Helpers
|
|
74
|
+
export const validators = {
|
|
75
|
+
// Check if a field was mapped to a faker method by comparing with unmapped behavior
|
|
76
|
+
isFieldMapped: async (fieldName, fieldType = "string") => {
|
|
77
|
+
const mappedSchema = {
|
|
78
|
+
type: "object",
|
|
79
|
+
properties: {
|
|
80
|
+
[fieldName]: { type: fieldType },
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
const unmappedSchema = {
|
|
84
|
+
type: "object",
|
|
85
|
+
properties: {
|
|
86
|
+
unmappedRandomField12345: { type: fieldType },
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
// Generate multiple samples to check for patterns
|
|
90
|
+
const mappedSamples = Array.from({ length: 10 }, () => generateFromSchema({ schema: mappedSchema })[fieldName]);
|
|
91
|
+
const unmappedSamples = Array.from({ length: 10 }, () => generateFromSchema({ schema: unmappedSchema }).unmappedRandomField12345);
|
|
92
|
+
// If field is mapped to a specific faker method, it should have different characteristics
|
|
93
|
+
// than the generic unmapped field
|
|
94
|
+
return (analyzeDataCharacteristics(mappedSamples) !==
|
|
95
|
+
analyzeDataCharacteristics(unmappedSamples));
|
|
96
|
+
},
|
|
97
|
+
// Analyze uniqueness of generated data
|
|
98
|
+
uniquenessRatio: (samples) => {
|
|
99
|
+
const unique = new Set(samples);
|
|
100
|
+
return unique.size / samples.length;
|
|
101
|
+
},
|
|
102
|
+
// Check if all samples match a basic pattern without being too specific
|
|
103
|
+
allMatch: (samples, validator) => {
|
|
104
|
+
return samples.every(validator);
|
|
105
|
+
},
|
|
106
|
+
// Check if data appears to be from a specific faker category
|
|
107
|
+
appearsToBeFromCategory: (samples, category) => {
|
|
108
|
+
switch (category) {
|
|
109
|
+
case "email":
|
|
110
|
+
return validators.allMatch(samples, (s) => typeof s === "string" && s.includes("@") && s.includes("."));
|
|
111
|
+
case "name":
|
|
112
|
+
return validators.allMatch(samples, (s) => typeof s === "string" &&
|
|
113
|
+
s.length > 1 &&
|
|
114
|
+
s.length < 50 &&
|
|
115
|
+
/^[A-Z]/.test(s));
|
|
116
|
+
case "phone":
|
|
117
|
+
return validators.allMatch(samples, (s) => typeof s === "string" && /\d/.test(s) && s.length > 10);
|
|
118
|
+
case "address":
|
|
119
|
+
return validators.allMatch(samples, (s) => typeof s === "string" &&
|
|
120
|
+
s.length > 10 &&
|
|
121
|
+
/\d/.test(s) &&
|
|
122
|
+
/[A-Z]/.test(s));
|
|
123
|
+
case "uuid":
|
|
124
|
+
return validators.allMatch(samples, (s) => typeof s === "string" &&
|
|
125
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(s));
|
|
126
|
+
case "date":
|
|
127
|
+
return validators.allMatch(samples, (s) => typeof s === "string" && !Number.isNaN(Date.parse(s)));
|
|
128
|
+
default:
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
// Performance Testing Utilities
|
|
134
|
+
export const performance = {
|
|
135
|
+
measure: async (fn) => {
|
|
136
|
+
const start = Date.now();
|
|
137
|
+
const result = await fn();
|
|
138
|
+
const duration = Date.now() - start;
|
|
139
|
+
return { result, duration };
|
|
140
|
+
},
|
|
141
|
+
measureMemory: (fn) => {
|
|
142
|
+
if (globalThis.gc) {
|
|
143
|
+
globalThis.gc();
|
|
144
|
+
}
|
|
145
|
+
const before = process.memoryUsage().heapUsed;
|
|
146
|
+
fn();
|
|
147
|
+
const after = process.memoryUsage().heapUsed;
|
|
148
|
+
return after - before;
|
|
149
|
+
},
|
|
150
|
+
benchmark: async (_name, fn, iterations = 100) => {
|
|
151
|
+
const times = [];
|
|
152
|
+
for (let i = 0; i < iterations; i++) {
|
|
153
|
+
const start = Date.now();
|
|
154
|
+
await fn();
|
|
155
|
+
const duration = Date.now() - start;
|
|
156
|
+
times.push(duration);
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
mean: times.reduce((a, b) => a + b, 0) / times.length,
|
|
160
|
+
min: Math.min(...times),
|
|
161
|
+
max: Math.max(...times),
|
|
162
|
+
};
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
// Test Data Generators
|
|
166
|
+
export const generate = {
|
|
167
|
+
samples: (schema, count = 10, options) => {
|
|
168
|
+
return Array.from({ length: count }, () => generateFromSchema({ schema, ...options }));
|
|
169
|
+
},
|
|
170
|
+
withSeed: (schema, _seed) => {
|
|
171
|
+
// Note: faker.js doesn't support seeding in the same way,
|
|
172
|
+
// but we can at least ensure consistent test behavior
|
|
173
|
+
return generateFromSchema({ schema });
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
// Statistical Analysis
|
|
177
|
+
export const stats = {
|
|
178
|
+
distribution: (samples) => {
|
|
179
|
+
const dist = new Map();
|
|
180
|
+
for (const sample of samples) {
|
|
181
|
+
const key = JSON.stringify(sample);
|
|
182
|
+
dist.set(key, (dist.get(key) || 0) + 1);
|
|
183
|
+
}
|
|
184
|
+
return dist;
|
|
185
|
+
},
|
|
186
|
+
entropy: (samples) => {
|
|
187
|
+
const dist = stats.distribution(samples);
|
|
188
|
+
const total = samples.length;
|
|
189
|
+
let entropy = 0;
|
|
190
|
+
for (const count of dist.values()) {
|
|
191
|
+
const p = count / total;
|
|
192
|
+
if (p > 0) {
|
|
193
|
+
entropy -= p * Math.log2(p);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return entropy;
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
// Schema Validation Test Helpers
|
|
200
|
+
export const schemaTests = {
|
|
201
|
+
expectValid: (schema) => {
|
|
202
|
+
expect(() => generateFromSchema({ schema })).not.toThrow();
|
|
203
|
+
},
|
|
204
|
+
expectInvalid: (schema, errorMessage) => {
|
|
205
|
+
if (errorMessage) {
|
|
206
|
+
expect(() => generateFromSchema({ schema })).toThrow(errorMessage);
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
expect(() => generateFromSchema({ schema })).toThrow();
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
expectSchemaError: (schema, path, issue) => {
|
|
213
|
+
try {
|
|
214
|
+
generateFromSchema({ schema });
|
|
215
|
+
throw new Error("Expected schema validation to fail");
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
expect(error.name).toBe("SchemaValidationError");
|
|
219
|
+
// The schemaPath is in the context
|
|
220
|
+
if (error.context?.schemaPath) {
|
|
221
|
+
expect(error.context.schemaPath).toBe(path);
|
|
222
|
+
}
|
|
223
|
+
if (issue) {
|
|
224
|
+
expect(error.message).toContain(issue);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
// Helper to analyze data characteristics without hardcoding patterns
|
|
230
|
+
function analyzeDataCharacteristics(samples) {
|
|
231
|
+
if (samples.length === 0)
|
|
232
|
+
return "empty";
|
|
233
|
+
const first = samples[0];
|
|
234
|
+
const type = typeof first;
|
|
235
|
+
if (type !== "string")
|
|
236
|
+
return type;
|
|
237
|
+
// Analyze string characteristics
|
|
238
|
+
const characteristics = [type];
|
|
239
|
+
// Check common patterns without being too specific
|
|
240
|
+
if (samples.every((s) => s.includes("@")))
|
|
241
|
+
characteristics.push("has-at");
|
|
242
|
+
if (samples.every((s) => /^\d+$/.test(s)))
|
|
243
|
+
characteristics.push("numeric");
|
|
244
|
+
if (samples.every((s) => /^[0-9a-f-]+$/i.test(s)))
|
|
245
|
+
characteristics.push("hex-like");
|
|
246
|
+
if (samples.every((s) => s.length > 50))
|
|
247
|
+
characteristics.push("long");
|
|
248
|
+
if (samples.every((s) => s.length < 10))
|
|
249
|
+
characteristics.push("short");
|
|
250
|
+
if (validators.uniquenessRatio(samples) > 0.8)
|
|
251
|
+
characteristics.push("high-entropy");
|
|
252
|
+
if (validators.uniquenessRatio(samples) < 0.2)
|
|
253
|
+
characteristics.push("low-entropy");
|
|
254
|
+
return characteristics.join("-");
|
|
255
|
+
}
|
|
256
|
+
// Mock/Spy utilities for testing faker integration
|
|
257
|
+
export const mocks = {
|
|
258
|
+
trackFakerCalls: () => {
|
|
259
|
+
const calls = [];
|
|
260
|
+
// This would need actual implementation with faker.js internals
|
|
261
|
+
// For now, it's a placeholder for the concept
|
|
262
|
+
return {
|
|
263
|
+
calls,
|
|
264
|
+
reset: () => {
|
|
265
|
+
calls.length = 0;
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
},
|
|
269
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@schmock/schema",
|
|
3
|
+
"description": "JSON Schema-based automatic data generation for Schmock",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"src"
|
|
11
|
+
],
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js",
|
|
16
|
+
"default": "./dist/index.js"
|
|
17
|
+
},
|
|
18
|
+
"./package.json": "./package.json"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "bun build:lib && bun build:types",
|
|
22
|
+
"build:lib": "bun build --minify --outdir=dist src/index.ts",
|
|
23
|
+
"build:types": "tsc -p tsconfig.json",
|
|
24
|
+
"test": "vitest",
|
|
25
|
+
"test:watch": "vitest --watch",
|
|
26
|
+
"lint": "biome check src/*.ts",
|
|
27
|
+
"lint:fix": "biome check --write --unsafe src/*.ts"
|
|
28
|
+
},
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"json-schema-faker": "^0.5.6",
|
|
32
|
+
"@faker-js/faker": "^10.1.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/json-schema": "^7.0.15",
|
|
36
|
+
"@types/node": "^24.9.1",
|
|
37
|
+
"vitest": "^4.0.15"
|
|
38
|
+
}
|
|
39
|
+
}
|