@schmock/faker 2.2.3 → 2.3.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/field-mappings.d.ts.map +1 -1
- package/dist/field-mappings.js +8 -1
- package/dist/field-name-matcher.d.ts.map +1 -1
- package/dist/field-name-matcher.js +4 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/jsf-config.d.ts +11 -1
- package/dist/jsf-config.d.ts.map +1 -1
- package/dist/jsf-config.js +93 -11
- package/dist/overrides.d.ts.map +1 -1
- package/dist/overrides.js +20 -14
- package/dist/schema-enhancement.d.ts.map +1 -1
- package/dist/schema-enhancement.js +18 -3
- package/dist/test-utils.d.ts +9 -9
- package/dist/test-utils.d.ts.map +1 -1
- package/dist/test-utils.js +25 -12
- package/dist/validation.js +1 -1
- package/package.json +4 -4
- package/src/field-mappings.ts +8 -1
- package/src/field-name-matcher.ts +4 -5
- package/src/index.test.ts +102 -0
- package/src/index.ts +59 -42
- package/src/jsf-config.test.ts +65 -0
- package/src/jsf-config.ts +129 -21
- package/src/overrides.ts +23 -20
- package/src/performance.test.ts +274 -327
- package/src/post-process.test.ts +87 -61
- package/src/schema-enhancement.test.ts +8 -2
- package/src/schema-enhancement.ts +24 -8
- package/src/steps/deterministic-seeds.steps.ts +77 -0
- package/src/steps/faker-plugin.steps.ts +198 -6
- package/src/test-utils.ts +48 -28
- package/src/validation.ts +1 -1
package/dist/test-utils.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { expect } from "vitest";
|
|
2
2
|
import { generateFromSchema } from "./index";
|
|
3
|
+
function isRecord(value) {
|
|
4
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5
|
+
}
|
|
3
6
|
// Schema Factory Functions
|
|
4
7
|
export const schemas = {
|
|
5
8
|
simple: {
|
|
@@ -89,16 +92,22 @@ export const validators = {
|
|
|
89
92
|
// Generate multiple samples to check for patterns
|
|
90
93
|
const mappedSamples = [];
|
|
91
94
|
for (let i = 0; i < 10; i++) {
|
|
92
|
-
const result =
|
|
95
|
+
const result = await generateFromSchema({
|
|
93
96
|
schema: mappedSchema,
|
|
94
|
-
})
|
|
97
|
+
});
|
|
98
|
+
if (!isRecord(result)) {
|
|
99
|
+
throw new Error("Expected mapped schema generation to return an object");
|
|
100
|
+
}
|
|
95
101
|
mappedSamples.push(result[fieldName]);
|
|
96
102
|
}
|
|
97
103
|
const unmappedSamples = [];
|
|
98
104
|
for (let i = 0; i < 10; i++) {
|
|
99
|
-
const result =
|
|
105
|
+
const result = await generateFromSchema({
|
|
100
106
|
schema: unmappedSchema,
|
|
101
|
-
})
|
|
107
|
+
});
|
|
108
|
+
if (!isRecord(result)) {
|
|
109
|
+
throw new Error("Expected unmapped schema generation to return an object");
|
|
110
|
+
}
|
|
102
111
|
unmappedSamples.push(result.unmappedRandomField12345);
|
|
103
112
|
}
|
|
104
113
|
// If field is mapped to a specific faker method, it should have different characteristics
|
|
@@ -179,14 +188,12 @@ export const generate = {
|
|
|
179
188
|
samples: async (schema, count = 10, options) => {
|
|
180
189
|
const results = [];
|
|
181
190
|
for (let i = 0; i < count; i++) {
|
|
182
|
-
results.push(
|
|
191
|
+
results.push(await generateFromSchema({ schema, ...options }));
|
|
183
192
|
}
|
|
184
193
|
return results;
|
|
185
194
|
},
|
|
186
|
-
withSeed: async (schema,
|
|
187
|
-
|
|
188
|
-
// but we can at least ensure consistent test behavior
|
|
189
|
-
return await generateFromSchema({ schema });
|
|
195
|
+
withSeed: async (schema, seed) => {
|
|
196
|
+
return await generateFromSchema({ schema, seed });
|
|
190
197
|
},
|
|
191
198
|
};
|
|
192
199
|
// Statistical Analysis
|
|
@@ -194,7 +201,7 @@ export const stats = {
|
|
|
194
201
|
distribution: (samples) => {
|
|
195
202
|
const dist = new Map();
|
|
196
203
|
for (const sample of samples) {
|
|
197
|
-
const key = JSON.stringify(sample);
|
|
204
|
+
const key = JSON.stringify(sample) ?? "undefined";
|
|
198
205
|
dist.set(key, (dist.get(key) || 0) + 1);
|
|
199
206
|
}
|
|
200
207
|
return dist;
|
|
@@ -231,9 +238,15 @@ export const schemaTests = {
|
|
|
231
238
|
throw new Error("Expected schema validation to fail");
|
|
232
239
|
}
|
|
233
240
|
catch (error) {
|
|
241
|
+
expect(error).toBeInstanceOf(Error);
|
|
242
|
+
if (!(error instanceof Error)) {
|
|
243
|
+
throw new Error("Expected schema validation to throw an Error");
|
|
244
|
+
}
|
|
234
245
|
expect(error.name).toBe("SchemaValidationError");
|
|
235
246
|
// The schemaPath is in the context
|
|
236
|
-
if (
|
|
247
|
+
if ("context" in error &&
|
|
248
|
+
isRecord(error.context) &&
|
|
249
|
+
error.context.schemaPath) {
|
|
237
250
|
expect(error.context.schemaPath).toBe(path);
|
|
238
251
|
}
|
|
239
252
|
if (issue) {
|
|
@@ -248,7 +261,7 @@ function analyzeDataCharacteristics(samples) {
|
|
|
248
261
|
return "empty";
|
|
249
262
|
const first = samples[0];
|
|
250
263
|
const type = typeof first;
|
|
251
|
-
if (
|
|
264
|
+
if (!samples.every((sample) => typeof sample === "string"))
|
|
252
265
|
return type;
|
|
253
266
|
// Analyze string characteristics
|
|
254
267
|
const characteristics = [type];
|
package/dist/validation.js
CHANGED
|
@@ -288,7 +288,7 @@ export function validateFakerMethod(fakerMethod) {
|
|
|
288
288
|
let current = faker;
|
|
289
289
|
for (const part of parts) {
|
|
290
290
|
if (current && typeof current === "object" && part in current) {
|
|
291
|
-
current = current
|
|
291
|
+
current = Reflect.get(current, part);
|
|
292
292
|
}
|
|
293
293
|
else {
|
|
294
294
|
throw new SchemaValidationError("$.faker", `Invalid faker method: "${fakerMethod}"`, "Check faker.js documentation for valid methods");
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@schmock/faker",
|
|
3
3
|
"description": "Faker-powered automatic data generation for Schmock",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.3.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"build": "bun build:lib && bun build:types",
|
|
22
22
|
"build:lib": "bun build --minify --target browser --outdir=dist src/index.ts --external @faker-js/faker --external json-schema-faker --external @schmock/core",
|
|
23
23
|
"build:types": "tsc -p tsconfig.json",
|
|
24
|
-
"test": "vitest",
|
|
24
|
+
"test": "vitest run",
|
|
25
25
|
"test:watch": "vitest --watch",
|
|
26
26
|
"test:bdd": "vitest run --config vitest.config.bdd.ts",
|
|
27
27
|
"lint": "biome check src/*.ts",
|
|
@@ -30,15 +30,15 @@
|
|
|
30
30
|
},
|
|
31
31
|
"license": "MIT",
|
|
32
32
|
"dependencies": {
|
|
33
|
+
"@types/json-schema": "^7.0.15",
|
|
33
34
|
"@faker-js/faker": "^10.5.0",
|
|
34
35
|
"json-schema-faker": "^0.6.2"
|
|
35
36
|
},
|
|
36
37
|
"peerDependencies": {
|
|
37
|
-
"@schmock/core": "^2.
|
|
38
|
+
"@schmock/core": "^2.3.0"
|
|
38
39
|
},
|
|
39
40
|
"devDependencies": {
|
|
40
41
|
"@amiceli/vitest-cucumber": "^7.0.0",
|
|
41
|
-
"@types/json-schema": "^7.0.15",
|
|
42
42
|
"@types/node": "^26.1.0",
|
|
43
43
|
"vitest": "^4.1.9"
|
|
44
44
|
}
|
package/src/field-mappings.ts
CHANGED
|
@@ -412,7 +412,14 @@ const text: FieldMapping[] = [
|
|
|
412
412
|
// ── Business/Commerce ───────────────────────────────────────────────
|
|
413
413
|
const business: FieldMapping[] = [
|
|
414
414
|
{
|
|
415
|
-
keywords: [
|
|
415
|
+
keywords: [
|
|
416
|
+
"company",
|
|
417
|
+
"organization",
|
|
418
|
+
"org",
|
|
419
|
+
"employer",
|
|
420
|
+
"brand",
|
|
421
|
+
"operator",
|
|
422
|
+
],
|
|
416
423
|
fakerMethod: "company.name",
|
|
417
424
|
schemaType: "string",
|
|
418
425
|
minScore: 0.6,
|
|
@@ -93,7 +93,6 @@ export function findBestMapping(
|
|
|
93
93
|
schema: JSONSchema7,
|
|
94
94
|
mappings: FieldMapping[] = ALL_FIELD_MAPPINGS,
|
|
95
95
|
): MatchResult | undefined {
|
|
96
|
-
const schemaAny = schema as Record<string, unknown>;
|
|
97
96
|
const schemaType = typeof schema.type === "string" ? schema.type : undefined;
|
|
98
97
|
|
|
99
98
|
// Priority: format:uuid always maps to string.uuid
|
|
@@ -115,10 +114,10 @@ export function findBestMapping(
|
|
|
115
114
|
// constraints, so they'd produce out-of-range strings ~20% of the time.
|
|
116
115
|
// Mirrors the numeric constraint skip just below.
|
|
117
116
|
if (
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
117
|
+
schema.pattern ||
|
|
118
|
+
schema.enum ||
|
|
119
|
+
("faker" in schema && schema.faker) ||
|
|
120
|
+
schema.$ref ||
|
|
122
121
|
(schemaType === "string" &&
|
|
123
122
|
(schema.minLength !== undefined || schema.maxLength !== undefined))
|
|
124
123
|
) {
|
package/src/index.test.ts
CHANGED
|
@@ -41,6 +41,108 @@ describe("Schema Generator", () => {
|
|
|
41
41
|
});
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
+
it("uses one seeded sequence across a generated array", async () => {
|
|
45
|
+
const schema: JSONSchema7 = {
|
|
46
|
+
type: "array",
|
|
47
|
+
items: {
|
|
48
|
+
type: "object",
|
|
49
|
+
properties: {
|
|
50
|
+
id: { type: "integer" },
|
|
51
|
+
name: { type: "string" },
|
|
52
|
+
},
|
|
53
|
+
required: ["id", "name"],
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const first = await generateFromSchema({ schema, count: 5, seed: 42 });
|
|
58
|
+
const second = await generateFromSchema({ schema, count: 5, seed: 42 });
|
|
59
|
+
|
|
60
|
+
expect(first).toEqual(second);
|
|
61
|
+
expect(Array.isArray(first)).toBe(true);
|
|
62
|
+
if (!Array.isArray(first)) {
|
|
63
|
+
throw new Error("Expected generated data to be an array");
|
|
64
|
+
}
|
|
65
|
+
expect(new Set(first.map((item) => JSON.stringify(item))).size).toBe(5);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("generates tuple positions from their respective schemas", async () => {
|
|
69
|
+
const schema: JSONSchema7 = {
|
|
70
|
+
type: "array",
|
|
71
|
+
items: [{ type: "string" }, { type: "integer" }, { type: "boolean" }],
|
|
72
|
+
minItems: 3,
|
|
73
|
+
maxItems: 3,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const result = await generateFromSchema({ schema, seed: 42 });
|
|
77
|
+
|
|
78
|
+
expect(Array.isArray(result)).toBe(true);
|
|
79
|
+
if (!Array.isArray(result)) {
|
|
80
|
+
throw new Error("Expected generated data to be a tuple");
|
|
81
|
+
}
|
|
82
|
+
expect(result).toHaveLength(3);
|
|
83
|
+
expect(typeof result[0]).toBe("string");
|
|
84
|
+
expect(typeof result[1]).toBe("number");
|
|
85
|
+
expect(typeof result[2]).toBe("boolean");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("generates a nested tuple reached through a definition reference", async () => {
|
|
89
|
+
const schema: JSONSchema7 = {
|
|
90
|
+
type: "object",
|
|
91
|
+
properties: {
|
|
92
|
+
payload: { $ref: "#/definitions/payloadTuple" },
|
|
93
|
+
},
|
|
94
|
+
required: ["payload"],
|
|
95
|
+
definitions: {
|
|
96
|
+
payloadTuple: {
|
|
97
|
+
type: "array",
|
|
98
|
+
items: [
|
|
99
|
+
{ type: "string" },
|
|
100
|
+
{ type: "integer" },
|
|
101
|
+
{ type: "boolean" },
|
|
102
|
+
],
|
|
103
|
+
minItems: 3,
|
|
104
|
+
maxItems: 3,
|
|
105
|
+
additionalItems: false,
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const result = await generateFromSchema({ schema, seed: 42 });
|
|
111
|
+
|
|
112
|
+
expect(result).toMatchObject({ payload: expect.any(Array) });
|
|
113
|
+
if (
|
|
114
|
+
typeof result !== "object" ||
|
|
115
|
+
result === null ||
|
|
116
|
+
!("payload" in result) ||
|
|
117
|
+
!Array.isArray(result.payload)
|
|
118
|
+
) {
|
|
119
|
+
throw new Error("Expected generated payload to be a tuple");
|
|
120
|
+
}
|
|
121
|
+
expect(result.payload).toHaveLength(3);
|
|
122
|
+
expect(typeof result.payload[0]).toBe("string");
|
|
123
|
+
expect(typeof result.payload[1]).toBe("number");
|
|
124
|
+
expect(typeof result.payload[2]).toBe("boolean");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("honors uniqueItems while generating the complete array", async () => {
|
|
128
|
+
const schema: JSONSchema7 = {
|
|
129
|
+
type: "array",
|
|
130
|
+
items: { type: "integer" },
|
|
131
|
+
minItems: 8,
|
|
132
|
+
maxItems: 8,
|
|
133
|
+
uniqueItems: true,
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const result = await generateFromSchema({ schema, seed: 42 });
|
|
137
|
+
|
|
138
|
+
expect(Array.isArray(result)).toBe(true);
|
|
139
|
+
if (!Array.isArray(result)) {
|
|
140
|
+
throw new Error("Expected generated data to be an array");
|
|
141
|
+
}
|
|
142
|
+
expect(result).toHaveLength(8);
|
|
143
|
+
expect(new Set(result).size).toBe(8);
|
|
144
|
+
});
|
|
145
|
+
|
|
44
146
|
it("respects array constraints from schema", async () => {
|
|
45
147
|
const schema = schemas.simple.array(schemas.simple.string(), {
|
|
46
148
|
minItems: 2,
|
package/src/index.ts
CHANGED
|
@@ -8,7 +8,11 @@ import {
|
|
|
8
8
|
import type { JSONSchema7 } from "json-schema";
|
|
9
9
|
import { version as packageVersion } from "../package.json";
|
|
10
10
|
import { MAX_ARRAY_SIZE, NULLABLE_NULL_PROBABILITY } from "./constants.js";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
createSeededRandom,
|
|
13
|
+
generateWithJsf,
|
|
14
|
+
resolveGenerationSeed,
|
|
15
|
+
} from "./jsf-config.js";
|
|
12
16
|
import { applyOverrides, determineArrayCount } from "./overrides.js";
|
|
13
17
|
import { enhanceSchemaWithSmartMapping } from "./schema-enhancement.js";
|
|
14
18
|
import { isJSONSchema7, validateSchema } from "./validation.js";
|
|
@@ -17,6 +21,10 @@ export type SchemaGenerationContext = Schmock.SchemaGenerationContext;
|
|
|
17
21
|
|
|
18
22
|
export type FakerPluginOptions = Schmock.FakerPluginOptions;
|
|
19
23
|
|
|
24
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
25
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
26
|
+
}
|
|
27
|
+
|
|
20
28
|
export function fakerPlugin(options: FakerPluginOptions): Schmock.Plugin {
|
|
21
29
|
// Validate schema immediately when plugin is created (fail-fast)
|
|
22
30
|
validateSchema(options.schema);
|
|
@@ -73,43 +81,39 @@ export async function generateFromSchema(
|
|
|
73
81
|
|
|
74
82
|
validateSchema(schema);
|
|
75
83
|
|
|
76
|
-
|
|
84
|
+
const generationSeed = resolveGenerationSeed(seed);
|
|
85
|
+
const random = createSeededRandom(generationSeed);
|
|
86
|
+
|
|
87
|
+
let enhancedSchema = enhanceSchemaWithSmartMapping(schema);
|
|
88
|
+
|
|
89
|
+
// Resolve the top-level array size once, then let JSF generate the complete
|
|
90
|
+
// array so tuple positions, uniqueness, and a seeded sequence are preserved.
|
|
77
91
|
if (schema.type === "array" && schema.items) {
|
|
78
|
-
const itemCount = determineArrayCount(schema, count);
|
|
92
|
+
const itemCount = determineArrayCount(schema, count, random);
|
|
79
93
|
|
|
80
|
-
// Check for resource limits
|
|
81
94
|
if (itemCount > MAX_ARRAY_SIZE) {
|
|
82
95
|
throw new ResourceLimitError("array_size", MAX_ARRAY_SIZE, itemCount);
|
|
83
96
|
}
|
|
84
97
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
:
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
"$.items",
|
|
92
|
-
"Array schema must have valid items definition",
|
|
93
|
-
);
|
|
94
|
-
}
|
|
98
|
+
enhancedSchema = {
|
|
99
|
+
...enhancedSchema,
|
|
100
|
+
minItems: itemCount,
|
|
101
|
+
maxItems: itemCount,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
95
104
|
|
|
96
|
-
|
|
97
|
-
|
|
105
|
+
let generated: unknown = await generateWithJsf(
|
|
106
|
+
enhancedSchema,
|
|
107
|
+
generationSeed,
|
|
108
|
+
);
|
|
109
|
+
generated = postProcessGenerated(generated, enhancedSchema, random);
|
|
98
110
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
item = applyOverrides(item, overrides, params, state, query);
|
|
104
|
-
items.push(item);
|
|
105
|
-
}
|
|
106
|
-
return items;
|
|
111
|
+
if (Array.isArray(generated)) {
|
|
112
|
+
return generated.map((item) =>
|
|
113
|
+
applyOverrides(item, overrides, params, state, query),
|
|
114
|
+
);
|
|
107
115
|
}
|
|
108
116
|
|
|
109
|
-
// Handle object schemas
|
|
110
|
-
const enhancedSchema = enhanceSchemaWithSmartMapping(schema);
|
|
111
|
-
let generated: unknown = await generateWithJsf(enhancedSchema, seed);
|
|
112
|
-
generated = postProcessGenerated(generated, enhancedSchema);
|
|
113
117
|
generated = applyOverrides(generated, overrides, params, state, query);
|
|
114
118
|
return generated;
|
|
115
119
|
}
|
|
@@ -120,7 +124,11 @@ export async function generateFromSchema(
|
|
|
120
124
|
* - schmockNullable: ~5% chance of null
|
|
121
125
|
* - schmockTrueProbability: weighted boolean generation
|
|
122
126
|
*/
|
|
123
|
-
function postProcessGenerated(
|
|
127
|
+
function postProcessGenerated(
|
|
128
|
+
data: unknown,
|
|
129
|
+
schema: JSONSchema7,
|
|
130
|
+
random: () => number,
|
|
131
|
+
): unknown {
|
|
124
132
|
if (
|
|
125
133
|
data === null ||
|
|
126
134
|
data === undefined ||
|
|
@@ -130,11 +138,9 @@ function postProcessGenerated(data: unknown, schema: JSONSchema7): unknown {
|
|
|
130
138
|
return data;
|
|
131
139
|
}
|
|
132
140
|
|
|
133
|
-
const schemaExt = schema as Record<string, unknown>;
|
|
134
|
-
|
|
135
141
|
// Apply nullable probability at this level
|
|
136
|
-
if (
|
|
137
|
-
if (
|
|
142
|
+
if ("schmockNullable" in schema && schema.schmockNullable === true) {
|
|
143
|
+
if (random() < NULLABLE_NULL_PROBABILITY) {
|
|
138
144
|
return null;
|
|
139
145
|
}
|
|
140
146
|
}
|
|
@@ -142,25 +148,36 @@ function postProcessGenerated(data: unknown, schema: JSONSchema7): unknown {
|
|
|
142
148
|
// Apply boolean weighting at this level
|
|
143
149
|
if (
|
|
144
150
|
schema.type === "boolean" &&
|
|
145
|
-
|
|
151
|
+
"schmockTrueProbability" in schema &&
|
|
152
|
+
typeof schema.schmockTrueProbability === "number"
|
|
146
153
|
) {
|
|
147
|
-
return
|
|
154
|
+
return random() < schema.schmockTrueProbability;
|
|
148
155
|
}
|
|
149
156
|
|
|
150
157
|
// Recurse into object properties
|
|
151
|
-
if (
|
|
152
|
-
const record = data as Record<string, unknown>;
|
|
158
|
+
if (isRecord(data) && schema.properties) {
|
|
153
159
|
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
|
154
|
-
if (key in
|
|
155
|
-
|
|
160
|
+
if (key in data && isJSONSchema7(propSchema)) {
|
|
161
|
+
data[key] = postProcessGenerated(data[key], propSchema, random);
|
|
156
162
|
}
|
|
157
163
|
}
|
|
158
164
|
}
|
|
159
165
|
|
|
160
166
|
// Recurse into array items
|
|
161
|
-
if (Array.isArray(data) &&
|
|
162
|
-
for (let
|
|
163
|
-
|
|
167
|
+
if (Array.isArray(data) && Array.isArray(schema.items)) {
|
|
168
|
+
for (let index = 0; index < data.length; index++) {
|
|
169
|
+
const itemSchema = schema.items[index];
|
|
170
|
+
if (itemSchema && isJSONSchema7(itemSchema)) {
|
|
171
|
+
data[index] = postProcessGenerated(data[index], itemSchema, random);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
} else if (
|
|
175
|
+
Array.isArray(data) &&
|
|
176
|
+
schema.items &&
|
|
177
|
+
isJSONSchema7(schema.items)
|
|
178
|
+
) {
|
|
179
|
+
for (let index = 0; index < data.length; index++) {
|
|
180
|
+
data[index] = postProcessGenerated(data[index], schema.items, random);
|
|
164
181
|
}
|
|
165
182
|
}
|
|
166
183
|
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { JSONSchema7 } from "json-schema";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { normalizeSchemaForJsf } from "./jsf-config.js";
|
|
4
|
+
|
|
5
|
+
const tuple: JSONSchema7 = {
|
|
6
|
+
type: "array",
|
|
7
|
+
items: [{ type: "string" }, { type: "integer" }],
|
|
8
|
+
minItems: 2,
|
|
9
|
+
maxItems: 2,
|
|
10
|
+
additionalItems: false,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function expectNormalizedTuple(value: unknown): void {
|
|
14
|
+
expect(value).toMatchObject({
|
|
15
|
+
type: "array",
|
|
16
|
+
prefixItems: [{ type: "string" }, { type: "integer" }],
|
|
17
|
+
items: false,
|
|
18
|
+
});
|
|
19
|
+
expect(value).not.toHaveProperty("additionalItems");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe("normalizeSchemaForJsf", () => {
|
|
23
|
+
it("recurses through Draft-7 schema-bearing keywords", () => {
|
|
24
|
+
const normalized = normalizeSchemaForJsf({
|
|
25
|
+
type: "object",
|
|
26
|
+
definitions: { legacy: tuple },
|
|
27
|
+
$defs: { modern: tuple },
|
|
28
|
+
patternProperties: { "^tuple$": tuple },
|
|
29
|
+
contains: tuple,
|
|
30
|
+
not: tuple,
|
|
31
|
+
if: tuple,
|
|
32
|
+
// biome-ignore lint/suspicious/noThenProperty: JSON Schema's conditional keyword is named "then"
|
|
33
|
+
then: tuple,
|
|
34
|
+
else: tuple,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const record: Record<string, unknown> = normalized;
|
|
38
|
+
const definitions = record.definitions;
|
|
39
|
+
const defs = record.$defs;
|
|
40
|
+
const patternProperties = record.patternProperties;
|
|
41
|
+
|
|
42
|
+
expect(definitions).toBeTypeOf("object");
|
|
43
|
+
expect(defs).toBeTypeOf("object");
|
|
44
|
+
expect(patternProperties).toBeTypeOf("object");
|
|
45
|
+
if (
|
|
46
|
+
definitions === null ||
|
|
47
|
+
typeof definitions !== "object" ||
|
|
48
|
+
defs === null ||
|
|
49
|
+
typeof defs !== "object" ||
|
|
50
|
+
patternProperties === null ||
|
|
51
|
+
typeof patternProperties !== "object"
|
|
52
|
+
) {
|
|
53
|
+
throw new Error("Expected normalized schema maps");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
expectNormalizedTuple(Reflect.get(definitions, "legacy"));
|
|
57
|
+
expectNormalizedTuple(Reflect.get(defs, "modern"));
|
|
58
|
+
expectNormalizedTuple(Reflect.get(patternProperties, "^tuple$"));
|
|
59
|
+
expectNormalizedTuple(record.contains);
|
|
60
|
+
expectNormalizedTuple(record.not);
|
|
61
|
+
expectNormalizedTuple(record.if);
|
|
62
|
+
expectNormalizedTuple(record.then);
|
|
63
|
+
expectNormalizedTuple(record.else);
|
|
64
|
+
});
|
|
65
|
+
});
|
package/src/jsf-config.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { base, en, Faker } from "@faker-js/faker";
|
|
2
|
-
import type { JSONSchema7 } from "json-schema";
|
|
2
|
+
import type { JSONSchema7, JSONSchema7Definition } from "json-schema";
|
|
3
3
|
import {
|
|
4
4
|
type GenerateOptions,
|
|
5
5
|
generate,
|
|
6
6
|
type JsonSchema,
|
|
7
7
|
} from "json-schema-faker";
|
|
8
8
|
|
|
9
|
+
const MAX_GENERATION_SEED = 2_147_483_647;
|
|
10
|
+
type JsfObjectSchema = Exclude<JsonSchema, boolean>;
|
|
11
|
+
|
|
9
12
|
/**
|
|
10
13
|
* Create isolated faker instance to avoid race conditions.
|
|
11
14
|
* Each generation gets its own faker instance to ensure thread-safety.
|
|
@@ -18,31 +21,136 @@ export function createFakerInstance(seed?: number) {
|
|
|
18
21
|
return faker;
|
|
19
22
|
}
|
|
20
23
|
|
|
24
|
+
export function resolveGenerationSeed(seed?: number): number {
|
|
25
|
+
return seed ?? Math.floor(Math.random() * MAX_GENERATION_SEED);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function createSeededRandom(seed: number): () => number {
|
|
29
|
+
let state = seed;
|
|
30
|
+
return () => {
|
|
31
|
+
state += 0x6d2b79f5;
|
|
32
|
+
let value = state;
|
|
33
|
+
value = Math.imul(value ^ (value >>> 15), value | 1);
|
|
34
|
+
value ^= value + Math.imul(value ^ (value >>> 7), value | 61);
|
|
35
|
+
return ((value ^ (value >>> 14)) >>> 0) / 4_294_967_296;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function normalizeDefinitionForJsf(
|
|
40
|
+
definition: JSONSchema7Definition,
|
|
41
|
+
): JsonSchema {
|
|
42
|
+
return typeof definition === "boolean"
|
|
43
|
+
? definition
|
|
44
|
+
: normalizeSchemaForJsf(definition);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function normalizeDefinitionMap(
|
|
48
|
+
definitions: Record<string, JSONSchema7Definition>,
|
|
49
|
+
): Record<string, JsonSchema> {
|
|
50
|
+
return Object.fromEntries(
|
|
51
|
+
Object.entries(definitions).map(([key, definition]) => [
|
|
52
|
+
key,
|
|
53
|
+
normalizeDefinitionForJsf(definition),
|
|
54
|
+
]),
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* json-schema-faker consumes tuple schemas through the 2020-12 `prefixItems`
|
|
60
|
+
* keyword. Convert Draft 7 tuple `items` recursively at the library boundary.
|
|
61
|
+
*/
|
|
62
|
+
export function normalizeSchemaForJsf(schema: JSONSchema7): JsfObjectSchema {
|
|
63
|
+
const normalized: JsfObjectSchema = {};
|
|
64
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
65
|
+
normalized[key] = value;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (schema.properties) {
|
|
69
|
+
normalized.properties = normalizeDefinitionMap(schema.properties);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (schema.definitions) {
|
|
73
|
+
normalized.definitions = normalizeDefinitionMap(schema.definitions);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (schema.$defs) {
|
|
77
|
+
normalized.$defs = normalizeDefinitionMap(schema.$defs);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (schema.patternProperties) {
|
|
81
|
+
normalized.patternProperties = normalizeDefinitionMap(
|
|
82
|
+
schema.patternProperties,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (Array.isArray(schema.items)) {
|
|
87
|
+
normalized.prefixItems = schema.items.map(normalizeDefinitionForJsf);
|
|
88
|
+
normalized.items =
|
|
89
|
+
schema.additionalItems === undefined
|
|
90
|
+
? true
|
|
91
|
+
: normalizeDefinitionForJsf(schema.additionalItems);
|
|
92
|
+
delete normalized.additionalItems;
|
|
93
|
+
} else if (schema.items !== undefined) {
|
|
94
|
+
normalized.items = normalizeDefinitionForJsf(schema.items);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
for (const keyword of ["allOf", "anyOf", "oneOf"] as const) {
|
|
98
|
+
const definitions = schema[keyword];
|
|
99
|
+
if (definitions) {
|
|
100
|
+
normalized[keyword] = definitions.map(normalizeDefinitionForJsf);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (schema.additionalProperties !== undefined) {
|
|
105
|
+
normalized.additionalProperties = normalizeDefinitionForJsf(
|
|
106
|
+
schema.additionalProperties,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
for (const keyword of [
|
|
111
|
+
"contains",
|
|
112
|
+
"not",
|
|
113
|
+
"if",
|
|
114
|
+
"then",
|
|
115
|
+
"else",
|
|
116
|
+
"propertyNames",
|
|
117
|
+
] as const) {
|
|
118
|
+
const definition = schema[keyword];
|
|
119
|
+
if (definition !== undefined) {
|
|
120
|
+
normalized[keyword] = normalizeDefinitionForJsf(definition);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (schema.dependencies) {
|
|
125
|
+
normalized.dependencies = Object.fromEntries(
|
|
126
|
+
Object.entries(schema.dependencies).map(([key, dependency]) => [
|
|
127
|
+
key,
|
|
128
|
+
Array.isArray(dependency)
|
|
129
|
+
? [...dependency]
|
|
130
|
+
: normalizeDefinitionForJsf(dependency),
|
|
131
|
+
]),
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return normalized;
|
|
136
|
+
}
|
|
137
|
+
|
|
21
138
|
/**
|
|
22
139
|
* Generate data from a JSON schema using json-schema-faker 0.6.0 async API.
|
|
23
140
|
* Stateless — each call is self-contained with its own faker instance and options.
|
|
24
141
|
*/
|
|
25
142
|
export async function generateWithJsf(
|
|
26
143
|
schema: JSONSchema7,
|
|
27
|
-
seed
|
|
144
|
+
seed: number,
|
|
28
145
|
): Promise<unknown> {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
optionalsProbability: 1.0,
|
|
40
|
-
alwaysFakeOptionals: true,
|
|
41
|
-
useDefaultValue: true,
|
|
42
|
-
ignoreMissingRefs: true,
|
|
43
|
-
failOnInvalidTypes: false,
|
|
44
|
-
failOnInvalidFormat: false,
|
|
45
|
-
extensions: { faker: createFakerInstance(seed) },
|
|
46
|
-
} as GenerateOptions & Record<string, unknown>,
|
|
47
|
-
);
|
|
146
|
+
const options: GenerateOptions = {
|
|
147
|
+
seed,
|
|
148
|
+
optionalsProbability: 1.0,
|
|
149
|
+
alwaysFakeOptionals: true,
|
|
150
|
+
useDefaultValue: true,
|
|
151
|
+
failOnInvalidTypes: false,
|
|
152
|
+
extensions: { faker: createFakerInstance(seed) },
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
return generate(normalizeSchemaForJsf(schema), options);
|
|
48
156
|
}
|