@schmock/faker 1.9.5 → 1.12.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/constants.d.ts +1 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +2 -0
- package/dist/field-mappings.d.ts +11 -0
- package/dist/field-mappings.d.ts.map +1 -0
- package/dist/field-mappings.js +1107 -0
- package/dist/field-name-matcher.d.ts +31 -0
- package/dist/field-name-matcher.d.ts.map +1 -0
- package/dist/field-name-matcher.js +136 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/jsf-config.d.ts.map +1 -1
- package/dist/jsf-config.js +2 -2
- package/dist/schema-enhancement.d.ts.map +1 -1
- package/dist/schema-enhancement.js +42 -53
- package/package.json +6 -6
- package/src/constants.ts +3 -0
- package/src/field-mappings.test.ts +340 -0
- package/src/field-mappings.ts +1141 -0
- package/src/field-name-matcher.test.ts +252 -0
- package/src/field-name-matcher.ts +172 -0
- package/src/index.ts +58 -5
- package/src/jsf-config.ts +2 -2
- package/src/schema-enhancement.ts +58 -55
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { ALL_FIELD_MAPPINGS, type FieldMapping } from "./field-mappings";
|
|
3
|
+
import { findBestMapping } from "./field-name-matcher";
|
|
4
|
+
import { generateFromSchema } from "./index";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Generate a value for a field by placing it inside an object schema
|
|
8
|
+
* and calling generateFromSchema. Returns the generated value.
|
|
9
|
+
*/
|
|
10
|
+
function generateField(fieldName: string, schemaType: string): unknown {
|
|
11
|
+
const schema = {
|
|
12
|
+
type: "object" as const,
|
|
13
|
+
properties: {
|
|
14
|
+
[fieldName]: { type: schemaType as "string" | "number" | "boolean" },
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
const result = generateFromSchema({ schema });
|
|
18
|
+
return result[fieldName];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ─── Matcher coverage: every mapping's first keyword resolves ───────
|
|
22
|
+
|
|
23
|
+
describe("Field mapping matcher coverage", () => {
|
|
24
|
+
for (const mapping of ALL_FIELD_MAPPINGS) {
|
|
25
|
+
const keyword = mapping.keywords[0];
|
|
26
|
+
const type = mapping.schemaType ?? "string";
|
|
27
|
+
|
|
28
|
+
it(`"${keyword}" → ${mapping.fakerMethod}`, () => {
|
|
29
|
+
const match = findBestMapping(keyword, {
|
|
30
|
+
type: type as "string" | "number" | "boolean",
|
|
31
|
+
});
|
|
32
|
+
expect(match).toBeDefined();
|
|
33
|
+
expect(match?.mapping.fakerMethod).toBe(mapping.fakerMethod);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// ─── Generation: every mapping produces a value of the right type ──
|
|
39
|
+
|
|
40
|
+
describe("Field mapping generation", () => {
|
|
41
|
+
// Group mappings by category for readable output
|
|
42
|
+
const stringMappings = ALL_FIELD_MAPPINGS.filter(
|
|
43
|
+
(m) => !m.schemaType || m.schemaType === "string",
|
|
44
|
+
);
|
|
45
|
+
const numberMappings = ALL_FIELD_MAPPINGS.filter(
|
|
46
|
+
(m) => m.schemaType === "number",
|
|
47
|
+
);
|
|
48
|
+
const booleanMappings = ALL_FIELD_MAPPINGS.filter(
|
|
49
|
+
(m) => m.schemaType === "boolean",
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
describe("string fields", () => {
|
|
53
|
+
for (const mapping of stringMappings) {
|
|
54
|
+
const keyword = mapping.keywords[0];
|
|
55
|
+
|
|
56
|
+
it(`"${keyword}" generates a non-empty string`, () => {
|
|
57
|
+
const value = generateField(keyword, "string");
|
|
58
|
+
expect(typeof value).toBe("string");
|
|
59
|
+
expect((value as string).length).toBeGreaterThan(0);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("number fields", () => {
|
|
65
|
+
for (const mapping of numberMappings) {
|
|
66
|
+
const keyword = mapping.keywords[0];
|
|
67
|
+
|
|
68
|
+
it(`"${keyword}" generates a number`, () => {
|
|
69
|
+
const value = generateField(keyword, "number");
|
|
70
|
+
expect(typeof value).toBe("number");
|
|
71
|
+
expect(Number.isFinite(value as number)).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("boolean fields", () => {
|
|
77
|
+
for (const mapping of booleanMappings) {
|
|
78
|
+
const keyword = mapping.keywords[0];
|
|
79
|
+
|
|
80
|
+
it(`"${keyword}" generates a boolean`, () => {
|
|
81
|
+
const value = generateField(keyword, "boolean");
|
|
82
|
+
expect(typeof value).toBe("boolean");
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// ─── All keyword variants resolve to the same faker method ─────────
|
|
89
|
+
|
|
90
|
+
describe("All keyword variants match", () => {
|
|
91
|
+
for (const mapping of ALL_FIELD_MAPPINGS) {
|
|
92
|
+
if (mapping.keywords.length <= 1) continue;
|
|
93
|
+
|
|
94
|
+
describe(`${mapping.fakerMethod}`, () => {
|
|
95
|
+
for (const keyword of mapping.keywords) {
|
|
96
|
+
it(`"${keyword}" matches`, () => {
|
|
97
|
+
const match = findBestMapping(keyword, {
|
|
98
|
+
type: (mapping.schemaType ?? "string") as
|
|
99
|
+
| "string"
|
|
100
|
+
| "number"
|
|
101
|
+
| "boolean",
|
|
102
|
+
});
|
|
103
|
+
expect(match).toBeDefined();
|
|
104
|
+
expect(match?.mapping.fakerMethod).toBe(mapping.fakerMethod);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// ─── Category-specific quality checks ──────────────────────────────
|
|
112
|
+
|
|
113
|
+
describe("Category quality checks", () => {
|
|
114
|
+
function samples(fieldName: string, type: string, count = 10): unknown[] {
|
|
115
|
+
return Array.from({ length: count }, () => generateField(fieldName, type));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
describe("Identity & Auth", () => {
|
|
119
|
+
it("email contains @ and .", () => {
|
|
120
|
+
for (const v of samples("email", "string")) {
|
|
121
|
+
expect(v).toMatch(/@.*\./);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("username is a non-empty string", () => {
|
|
126
|
+
for (const v of samples("username", "string")) {
|
|
127
|
+
expect(typeof v).toBe("string");
|
|
128
|
+
expect((v as string).length).toBeGreaterThan(0);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe("Person", () => {
|
|
134
|
+
it("firstName starts with uppercase", () => {
|
|
135
|
+
for (const v of samples("firstName", "string")) {
|
|
136
|
+
expect(v).toMatch(/^[A-Z]/);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("lastName starts with uppercase", () => {
|
|
141
|
+
for (const v of samples("lastName", "string")) {
|
|
142
|
+
expect(v).toMatch(/^[A-Z]/);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("name (fullName) contains a space", () => {
|
|
147
|
+
for (const v of samples("name", "string")) {
|
|
148
|
+
expect(v).toContain(" ");
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe("Contact", () => {
|
|
154
|
+
it("phone contains digits", () => {
|
|
155
|
+
for (const v of samples("phone", "string")) {
|
|
156
|
+
expect(v).toMatch(/\d/);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe("Address", () => {
|
|
162
|
+
it("city is a capitalized string", () => {
|
|
163
|
+
for (const v of samples("city", "string")) {
|
|
164
|
+
expect(v).toMatch(/^[A-Z]/);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("zipcode matches US format", () => {
|
|
169
|
+
for (const v of samples("zipcode", "string")) {
|
|
170
|
+
expect(v).toMatch(/^\d{5}/);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("latitude is a valid number", () => {
|
|
175
|
+
for (const v of samples("latitude", "number")) {
|
|
176
|
+
expect(typeof v).toBe("number");
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("longitude is a valid number", () => {
|
|
181
|
+
for (const v of samples("longitude", "number")) {
|
|
182
|
+
expect(typeof v).toBe("number");
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe("Internet", () => {
|
|
188
|
+
it("url starts with http", () => {
|
|
189
|
+
for (const v of samples("url", "string")) {
|
|
190
|
+
expect(v).toMatch(/^https?:\/\//);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("ip_address looks like an IP", () => {
|
|
195
|
+
for (const v of samples("ip_address", "string")) {
|
|
196
|
+
expect(v).toMatch(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe("IDs", () => {
|
|
202
|
+
it("uuid matches UUID pattern", () => {
|
|
203
|
+
for (const v of samples("uuid", "string")) {
|
|
204
|
+
expect(v).toMatch(
|
|
205
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("userId (suffix rule) generates UUID", () => {
|
|
211
|
+
for (const v of samples("userId", "string")) {
|
|
212
|
+
expect(v).toMatch(
|
|
213
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("order_id (suffix rule) generates UUID", () => {
|
|
219
|
+
for (const v of samples("order_id", "string")) {
|
|
220
|
+
expect(v).toMatch(
|
|
221
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
describe("Text/Content", () => {
|
|
228
|
+
it("description is a paragraph (> 20 chars)", () => {
|
|
229
|
+
for (const v of samples("description", "string")) {
|
|
230
|
+
expect((v as string).length).toBeGreaterThan(20);
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("title is sentence-length", () => {
|
|
235
|
+
for (const v of samples("title", "string")) {
|
|
236
|
+
expect((v as string).length).toBeGreaterThan(5);
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
describe("Business", () => {
|
|
242
|
+
it("company generates a non-empty string", () => {
|
|
243
|
+
for (const v of samples("company", "string")) {
|
|
244
|
+
expect((v as string).length).toBeGreaterThan(0);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
describe("Date/Time", () => {
|
|
250
|
+
it("createdAt generates parseable dates", () => {
|
|
251
|
+
for (const v of samples("createdAt", "string")) {
|
|
252
|
+
expect(Date.parse(v as string)).not.toBeNaN();
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("updatedAt generates parseable dates", () => {
|
|
257
|
+
for (const v of samples("updatedAt", "string")) {
|
|
258
|
+
expect(Date.parse(v as string)).not.toBeNaN();
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("expires_at generates parseable dates", () => {
|
|
263
|
+
for (const v of samples("expires_at", "string")) {
|
|
264
|
+
expect(Date.parse(v as string)).not.toBeNaN();
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
describe("Boolean weighting", () => {
|
|
270
|
+
it("isActive is mostly true (~90%)", () => {
|
|
271
|
+
const values = samples("isActive", "boolean", 200) as boolean[];
|
|
272
|
+
const trueCount = values.filter(Boolean).length;
|
|
273
|
+
// With p=0.9 and n=200, expect 160-200 trues
|
|
274
|
+
expect(trueCount).toBeGreaterThan(140);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("isDeleted is mostly false (~5% true)", () => {
|
|
278
|
+
const values = samples("isDeleted", "boolean", 200) as boolean[];
|
|
279
|
+
const trueCount = values.filter(Boolean).length;
|
|
280
|
+
// With p=0.05 and n=200, expect 0-30 trues
|
|
281
|
+
expect(trueCount).toBeLessThan(40);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("isVerified is mostly true (~80%)", () => {
|
|
285
|
+
const values = samples("isVerified", "boolean", 200) as boolean[];
|
|
286
|
+
const trueCount = values.filter(Boolean).length;
|
|
287
|
+
expect(trueCount).toBeGreaterThan(120);
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
describe("Nullable post-processing", () => {
|
|
292
|
+
it("schmockNullable fields are null ~5% of the time", () => {
|
|
293
|
+
const schema = {
|
|
294
|
+
type: "object" as const,
|
|
295
|
+
properties: {
|
|
296
|
+
value: {
|
|
297
|
+
type: "string" as const,
|
|
298
|
+
schmockNullable: true,
|
|
299
|
+
} as any,
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
let nullCount = 0;
|
|
304
|
+
const total = 500;
|
|
305
|
+
for (let i = 0; i < total; i++) {
|
|
306
|
+
const result = generateFromSchema({ schema });
|
|
307
|
+
if (result.value === null) nullCount++;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// With p=0.05 and n=500, expect 10-45 nulls (generous bounds)
|
|
311
|
+
expect(nullCount).toBeGreaterThan(5);
|
|
312
|
+
expect(nullCount).toBeLessThan(60);
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
describe("Composition recursion", () => {
|
|
317
|
+
it("enhances fields inside allOf branches", () => {
|
|
318
|
+
const schema = {
|
|
319
|
+
type: "object" as const,
|
|
320
|
+
properties: {
|
|
321
|
+
data: {
|
|
322
|
+
allOf: [
|
|
323
|
+
{
|
|
324
|
+
type: "object" as const,
|
|
325
|
+
properties: {
|
|
326
|
+
email: { type: "string" as const },
|
|
327
|
+
city: { type: "string" as const },
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
],
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const result = generateFromSchema({ schema });
|
|
336
|
+
expect(result.data.email).toMatch(/@/);
|
|
337
|
+
expect(result.data.city).toMatch(/^[A-Z]/);
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
});
|