@amritk/generate-examples 0.6.4 → 0.8.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/AI.md +19 -0
- package/README.md +56 -1
- package/dist/generators/assert-generator-depth.d.ts +28 -0
- package/dist/generators/assert-generator-depth.js +10 -0
- package/dist/generators/closed-value-set.d.ts +14 -0
- package/dist/generators/closed-value-set.js +20 -0
- package/dist/generators/collect-example-imports.js +9 -3
- package/dist/generators/derive-example.js +144 -87
- package/dist/generators/generate-arbitrary.js +187 -96
- package/dist/generators/generate-files.js +3 -2
- package/dist/generators/is-object-like.d.ts +47 -0
- package/dist/generators/is-object-like.js +23 -0
- package/dist/generators/satisfies-scalar-constraints.d.ts +17 -0
- package/dist/generators/satisfies-scalar-constraints.js +34 -0
- package/dist/generators/schema-validation.d.ts +12 -0
- package/dist/generators/schema-validation.js +37 -0
- package/package.json +3 -3
|
@@ -1,15 +1,40 @@
|
|
|
1
1
|
import { getMjstInstanceOf, getMjstPrimitive } from "@amritk/helpers/mjst-extension";
|
|
2
2
|
import { refToFilename } from "@amritk/helpers/ref-to-filename";
|
|
3
3
|
import { refToName } from "@amritk/helpers/ref-to-name";
|
|
4
|
+
import { resolveRef } from "@amritk/helpers/resolve-ref";
|
|
4
5
|
import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasConst, hasContains, hasDependentRequired, hasDependentSchemas, hasEnum, hasExclusiveMaximum, hasExclusiveMinimum, hasFormat, hasItems, hasMaxItems, hasMaximum, hasMaxLength, hasMaxProperties, hasMinItems, hasMinimum, hasMinLength, hasMinProperties, hasMultipleOf, hasOneOf, hasPattern, hasPatternProperties, hasProperties, hasPropertyNames, hasRef, hasRequired, hasType, hasUniqueItems, isSchemaObject } from "@amritk/helpers/schema-guards";
|
|
6
|
+
import { assertGeneratorDepth } from "./assert-generator-depth.js";
|
|
7
|
+
import { closedValueSet } from "./closed-value-set.js";
|
|
5
8
|
import { mergeAllOf } from "./derive-example.js";
|
|
6
|
-
import {
|
|
9
|
+
import { declaresOwnShape, isObjectLike, typesConflict } from "./is-object-like.js";
|
|
10
|
+
import { satisfiesScalarConstraints } from "./satisfies-scalar-constraints.js";
|
|
11
|
+
import { canBuildGuard, needsValidationFilter, withResolvableDefs } from "./schema-validation.js";
|
|
7
12
|
const arbitraryName = (typeName) => `${typeName}Arbitrary`;
|
|
8
13
|
const VALIDATE_IMPORT_NAME = "__mjstValidate";
|
|
9
14
|
const VALIDATE_IMPORT_STATEMENT = `import { validate as ${VALIDATE_IMPORT_NAME} } from '@amritk/runtime-validators'`;
|
|
10
15
|
const lazyRef = (arbName) => `fc.constant(null).chain(() => ${arbName})`;
|
|
11
16
|
const SELF_KEY = "self";
|
|
12
17
|
const safeLiteralKey = (key) => key === "__proto__" ? '["__proto__"]' : JSON.stringify(key);
|
|
18
|
+
const finiteNumber = (value) => value !== void 0 && Number.isFinite(value) ? value : void 0;
|
|
19
|
+
const countBound = (value, side) => {
|
|
20
|
+
const finite = finiteNumber(value);
|
|
21
|
+
if (finite === void 0)
|
|
22
|
+
return void 0;
|
|
23
|
+
return Math.max(0, side === "lower" ? Math.ceil(finite) : Math.floor(finite));
|
|
24
|
+
};
|
|
25
|
+
const usableMultipleOf = (schema) => hasMultipleOf(schema) && Number.isFinite(schema.multipleOf) && schema.multipleOf > 0 ? schema.multipleOf : void 0;
|
|
26
|
+
const clampLow = (low, high) => low !== void 0 && high !== void 0 && low > high ? high : low;
|
|
27
|
+
const UNSUPPORTED_ASSERTION = /\(\?<?[=!]/;
|
|
28
|
+
const usablePattern = (pattern) => {
|
|
29
|
+
if (UNSUPPORTED_ASSERTION.test(pattern))
|
|
30
|
+
return void 0;
|
|
31
|
+
try {
|
|
32
|
+
new RegExp(pattern);
|
|
33
|
+
return pattern;
|
|
34
|
+
} catch {
|
|
35
|
+
return void 0;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
13
38
|
const stringExpr = (schema) => {
|
|
14
39
|
if (hasFormat(schema)) {
|
|
15
40
|
switch (schema.format) {
|
|
@@ -34,43 +59,67 @@ const stringExpr = (schema) => {
|
|
|
34
59
|
return "fc.ipV6()";
|
|
35
60
|
}
|
|
36
61
|
}
|
|
37
|
-
|
|
38
|
-
|
|
62
|
+
const maxLength = countBound(hasMaxLength(schema) ? schema.maxLength : void 0, "upper");
|
|
63
|
+
const minLength = clampLow(countBound(hasMinLength(schema) ? schema.minLength : void 0, "lower"), maxLength);
|
|
64
|
+
const pattern = hasPattern(schema) ? usablePattern(schema.pattern) : void 0;
|
|
65
|
+
if (pattern !== void 0) {
|
|
66
|
+
const base = `fc.stringMatching(new RegExp(${JSON.stringify(pattern)}))`;
|
|
39
67
|
const checks = [];
|
|
40
|
-
if (
|
|
41
|
-
checks.push(`s.length >= ${
|
|
42
|
-
if (
|
|
43
|
-
checks.push(`s.length <= ${
|
|
68
|
+
if (minLength !== void 0)
|
|
69
|
+
checks.push(`s.length >= ${minLength}`);
|
|
70
|
+
if (maxLength !== void 0)
|
|
71
|
+
checks.push(`s.length <= ${maxLength}`);
|
|
44
72
|
return checks.length > 0 ? `${base}.filter((s) => ${checks.join(" && ")})` : base;
|
|
45
73
|
}
|
|
46
74
|
const opts = [];
|
|
47
|
-
if (
|
|
48
|
-
opts.push(`minLength: ${
|
|
49
|
-
if (
|
|
50
|
-
opts.push(`maxLength: ${
|
|
75
|
+
if (minLength !== void 0)
|
|
76
|
+
opts.push(`minLength: ${minLength}`);
|
|
77
|
+
if (maxLength !== void 0)
|
|
78
|
+
opts.push(`maxLength: ${maxLength}`);
|
|
51
79
|
return opts.length > 0 ? `fc.string({ ${opts.join(", ")} })` : "fc.string()";
|
|
52
80
|
};
|
|
53
|
-
const
|
|
54
|
-
const
|
|
81
|
+
const integerBounds = (schema) => {
|
|
82
|
+
const minimum = finiteNumber(hasMinimum(schema) ? Number(schema.minimum) : void 0);
|
|
83
|
+
const exclusiveMinimum = finiteNumber(hasExclusiveMinimum(schema) ? Number(schema.exclusiveMinimum) : void 0);
|
|
84
|
+
const maximum = finiteNumber(hasMaximum(schema) ? Number(schema.maximum) : void 0);
|
|
85
|
+
const exclusiveMaximum = finiteNumber(hasExclusiveMaximum(schema) ? Number(schema.exclusiveMaximum) : void 0);
|
|
55
86
|
const mins = [];
|
|
56
|
-
if (
|
|
57
|
-
mins.push(Math.ceil(
|
|
58
|
-
if (
|
|
59
|
-
mins.push(Math.floor(
|
|
60
|
-
if (mins.length > 0)
|
|
61
|
-
opts.push(`min: ${Math.max(...mins)}`);
|
|
87
|
+
if (minimum !== void 0)
|
|
88
|
+
mins.push(Math.ceil(minimum));
|
|
89
|
+
if (exclusiveMinimum !== void 0)
|
|
90
|
+
mins.push(Math.floor(exclusiveMinimum) + 1);
|
|
62
91
|
const maxs = [];
|
|
63
|
-
if (
|
|
64
|
-
maxs.push(Math.floor(
|
|
65
|
-
if (
|
|
66
|
-
maxs.push(Math.ceil(
|
|
67
|
-
|
|
68
|
-
|
|
92
|
+
if (maximum !== void 0)
|
|
93
|
+
maxs.push(Math.floor(maximum));
|
|
94
|
+
if (exclusiveMaximum !== void 0)
|
|
95
|
+
maxs.push(Math.ceil(exclusiveMaximum) - 1);
|
|
96
|
+
const max = clampIntegerBound(maxs.length > 0 ? Math.min(...maxs) : void 0);
|
|
97
|
+
const min = clampIntegerBound(mins.length > 0 ? Math.max(...mins) : void 0);
|
|
98
|
+
return { min: clampLow(min, max), max };
|
|
99
|
+
};
|
|
100
|
+
const DEFAULT_INTEGER_MAGNITUDE = 2 ** 31;
|
|
101
|
+
const clampIntegerBound = (value) => value === void 0 ? void 0 : Math.max(-DEFAULT_INTEGER_MAGNITUDE, Math.min(DEFAULT_INTEGER_MAGNITUDE - 1, value));
|
|
102
|
+
const integerMultipleOfExpr = (m, min, max) => {
|
|
103
|
+
const bound = Math.max(1, Math.floor((DEFAULT_INTEGER_MAGNITUDE - 1) / m));
|
|
104
|
+
const kMin = min !== void 0 ? Math.ceil(min / m) : -bound;
|
|
105
|
+
const kMax = Math.max(kMin, max !== void 0 ? Math.floor(max / m) : bound);
|
|
106
|
+
return `fc.integer({ min: ${kMin}, max: ${kMax} }).map((k) => k * ${m})`;
|
|
107
|
+
};
|
|
108
|
+
const integerExpr = (schema) => {
|
|
109
|
+
const { min, max } = integerBounds(schema);
|
|
110
|
+
const multipleOf = usableMultipleOf(schema);
|
|
111
|
+
if (multipleOf !== void 0 && Number.isInteger(multipleOf)) {
|
|
112
|
+
return integerMultipleOfExpr(multipleOf, min, max);
|
|
113
|
+
}
|
|
114
|
+
const opts = [];
|
|
115
|
+
if (min !== void 0)
|
|
116
|
+
opts.push(`min: ${min}`);
|
|
117
|
+
if (max !== void 0)
|
|
118
|
+
opts.push(`max: ${max}`);
|
|
69
119
|
const base = opts.length > 0 ? `fc.integer({ ${opts.join(", ")} })` : "fc.integer()";
|
|
70
|
-
return
|
|
120
|
+
return multipleOf !== void 0 ? `${base}.filter((n) => n % ${multipleOf} === 0)` : base;
|
|
71
121
|
};
|
|
72
|
-
const numberMultipleOfExpr = (schema) => {
|
|
73
|
-
const m = Number(schema.multipleOf);
|
|
122
|
+
const numberMultipleOfExpr = (schema, m) => {
|
|
74
123
|
const EPS = 1e-9;
|
|
75
124
|
let lo = Number.NEGATIVE_INFINITY;
|
|
76
125
|
let loExclusive = false;
|
|
@@ -114,19 +163,39 @@ const numberMultipleOfExpr = (schema) => {
|
|
|
114
163
|
return `${k}.map((k) => ${value})`;
|
|
115
164
|
};
|
|
116
165
|
const numberExpr = (schema) => {
|
|
117
|
-
|
|
118
|
-
|
|
166
|
+
const multipleOf = usableMultipleOf(schema);
|
|
167
|
+
if (multipleOf !== void 0)
|
|
168
|
+
return numberMultipleOfExpr(schema, multipleOf);
|
|
119
169
|
const opts = ["noNaN: true", "noDefaultInfinity: true"];
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
170
|
+
const minimum = finiteNumber(hasMinimum(schema) ? Number(schema.minimum) : void 0);
|
|
171
|
+
const exclusiveMinimum = finiteNumber(hasExclusiveMinimum(schema) ? Number(schema.exclusiveMinimum) : void 0);
|
|
172
|
+
const maximum = finiteNumber(hasMaximum(schema) ? Number(schema.maximum) : void 0);
|
|
173
|
+
const exclusiveMaximum = finiteNumber(hasExclusiveMaximum(schema) ? Number(schema.exclusiveMaximum) : void 0);
|
|
174
|
+
let low;
|
|
175
|
+
let lowExcluded = false;
|
|
176
|
+
if (minimum !== void 0 && (exclusiveMinimum === void 0 || minimum > exclusiveMinimum)) {
|
|
177
|
+
low = minimum;
|
|
178
|
+
} else if (exclusiveMinimum !== void 0) {
|
|
179
|
+
low = exclusiveMinimum;
|
|
180
|
+
lowExcluded = true;
|
|
181
|
+
}
|
|
182
|
+
let high;
|
|
183
|
+
let highExcluded = false;
|
|
184
|
+
if (maximum !== void 0 && (exclusiveMaximum === void 0 || maximum < exclusiveMaximum)) {
|
|
185
|
+
high = maximum;
|
|
186
|
+
} else if (exclusiveMaximum !== void 0) {
|
|
187
|
+
high = exclusiveMaximum;
|
|
188
|
+
highExcluded = true;
|
|
124
189
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
190
|
+
low = clampLow(low, high);
|
|
191
|
+
if (low !== void 0 && low === high) {
|
|
192
|
+
lowExcluded = false;
|
|
193
|
+
highExcluded = false;
|
|
129
194
|
}
|
|
195
|
+
if (low !== void 0)
|
|
196
|
+
opts.push(`min: ${low}`, ...lowExcluded ? ["minExcluded: true"] : []);
|
|
197
|
+
if (high !== void 0)
|
|
198
|
+
opts.push(`max: ${high}`, ...highExcluded ? ["maxExcluded: true"] : []);
|
|
130
199
|
return `fc.double({ ${opts.join(", ")} })`;
|
|
131
200
|
};
|
|
132
201
|
const arrayExpr = (schema, ctx) => {
|
|
@@ -138,26 +207,36 @@ const arrayExpr = (schema, ctx) => {
|
|
|
138
207
|
return `fc.tuple(${exprs.join(", ")})`;
|
|
139
208
|
}
|
|
140
209
|
const containsSchema = hasContains(schema) && isSchemaObject(schema.contains) ? schema.contains : void 0;
|
|
141
|
-
const
|
|
142
|
-
const
|
|
143
|
-
const
|
|
210
|
+
const unique = hasUniqueItems(schema) && schema.uniqueItems === true;
|
|
211
|
+
const containsExpr = containsSchema !== void 0 ? arbitraryExpr(containsSchema, ctx) : void 0;
|
|
212
|
+
const items = hasItems(schema) && isSchemaObject(schema.items) ? arbitraryExpr(schema.items, ctx) : containsExpr === void 0 ? "fc.anything()" : unique ? `fc.oneof(${containsExpr}, fc.anything())` : containsExpr;
|
|
213
|
+
const minContains = countBound(typeof raw["minContains"] === "number" ? raw["minContains"] : void 0, "lower") ?? 1;
|
|
214
|
+
const minLength = Math.max(countBound(hasMinItems(schema) ? schema.minItems : void 0, "lower") ?? 0, containsSchema !== void 0 ? Math.max(1, minContains) : 0);
|
|
215
|
+
const distinctCap = unique && containsSchema === void 0 ? closedValueSet(hasItems(schema) && isSchemaObject(schema.items) ? schema.items : void 0)?.length : void 0;
|
|
216
|
+
const declaredMax = countBound(hasMaxItems(schema) ? schema.maxItems : void 0, "upper");
|
|
217
|
+
const maxLength = distinctCap !== void 0 ? Math.min(declaredMax ?? distinctCap, distinctCap) : declaredMax;
|
|
218
|
+
const clampedMin = clampLow(minLength, maxLength) ?? minLength;
|
|
144
219
|
const opts = [];
|
|
145
|
-
if (
|
|
146
|
-
opts.push(`minLength: ${
|
|
147
|
-
if (
|
|
148
|
-
opts.push(`maxLength: ${
|
|
149
|
-
const fn =
|
|
220
|
+
if (clampedMin > 0)
|
|
221
|
+
opts.push(`minLength: ${clampedMin}`);
|
|
222
|
+
if (maxLength !== void 0)
|
|
223
|
+
opts.push(`maxLength: ${maxLength}`);
|
|
224
|
+
const fn = unique ? "fc.uniqueArray" : "fc.array";
|
|
150
225
|
return opts.length > 0 ? `${fn}(${items}, { ${opts.join(", ")} })` : `${fn}(${items})`;
|
|
151
226
|
};
|
|
152
227
|
const extraKeyArb = (schema, firstPatternSource) => {
|
|
153
|
-
|
|
154
|
-
|
|
228
|
+
const fromPattern = firstPatternSource !== void 0 ? usablePattern(firstPatternSource) : void 0;
|
|
229
|
+
if (fromPattern !== void 0)
|
|
230
|
+
return `fc.stringMatching(new RegExp(${JSON.stringify(fromPattern)}))`;
|
|
155
231
|
const propertyNames = hasPropertyNames(schema) ? schema.propertyNames : void 0;
|
|
156
232
|
if (propertyNames !== void 0 && isSchemaObject(propertyNames) && hasPattern(propertyNames)) {
|
|
157
|
-
|
|
233
|
+
const fromNames = usablePattern(propertyNames.pattern);
|
|
234
|
+
if (fromNames !== void 0)
|
|
235
|
+
return `fc.stringMatching(new RegExp(${JSON.stringify(fromNames)}))`;
|
|
158
236
|
}
|
|
159
237
|
return "fc.string()";
|
|
160
238
|
};
|
|
239
|
+
const requiredKeyNames = (schema) => hasRequired(schema) ? schema.required.filter((key) => typeof key === "string") : [];
|
|
161
240
|
const objectExpr = (schema, ctx) => {
|
|
162
241
|
const additional = hasAdditionalProperties(schema) ? schema.additionalProperties : false;
|
|
163
242
|
const additionalArb = isSchemaObject(additional) ? arbitraryExpr(additional, ctx) : void 0;
|
|
@@ -168,12 +247,13 @@ const objectExpr = (schema, ctx) => {
|
|
|
168
247
|
const extrasAllowed = !additionalClosed || patternEntries.length > 0;
|
|
169
248
|
const extraValueArb = additionalArb ?? patternValueArb;
|
|
170
249
|
const keyArb = extraKeyArb(schema, firstPattern?.[0]);
|
|
171
|
-
const minProps = hasMinProperties(schema) ? schema.minProperties : void 0;
|
|
172
|
-
const maxProps = hasMaxProperties(schema) ? schema.maxProperties : void 0;
|
|
250
|
+
const minProps = countBound(hasMinProperties(schema) ? schema.minProperties : void 0, "lower");
|
|
251
|
+
const maxProps = countBound(hasMaxProperties(schema) ? schema.maxProperties : void 0, "upper");
|
|
173
252
|
const dictKeyOpts = (minKeys, maxKeys) => {
|
|
253
|
+
const low = clampLow(minKeys, maxKeys);
|
|
174
254
|
const opts = [];
|
|
175
|
-
if (
|
|
176
|
-
opts.push(`minKeys: ${
|
|
255
|
+
if (low !== void 0 && low > 0)
|
|
256
|
+
opts.push(`minKeys: ${low}`);
|
|
177
257
|
if (maxKeys !== void 0)
|
|
178
258
|
opts.push(`maxKeys: ${maxKeys}`);
|
|
179
259
|
return opts.length > 0 ? `, { ${opts.join(", ")} }` : "";
|
|
@@ -183,7 +263,7 @@ const objectExpr = (schema, ctx) => {
|
|
|
183
263
|
for (const [key, propSchema] of Object.entries(schema.properties))
|
|
184
264
|
propArbs.set(key, arbitraryExpr(propSchema, ctx));
|
|
185
265
|
}
|
|
186
|
-
const required = new Set(
|
|
266
|
+
const required = new Set(requiredKeyNames(schema));
|
|
187
267
|
const openValueArb = extraValueArb ?? "fc.anything()";
|
|
188
268
|
if (hasDependentRequired(schema)) {
|
|
189
269
|
for (const [, deps] of Object.entries(schema.dependentRequired)) {
|
|
@@ -204,9 +284,8 @@ const objectExpr = (schema, ctx) => {
|
|
|
204
284
|
propArbs.set(key, arbitraryExpr(propSchema, ctx));
|
|
205
285
|
}
|
|
206
286
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
required.add(key);
|
|
287
|
+
for (const key of requiredKeyNames(sub))
|
|
288
|
+
required.add(key);
|
|
210
289
|
}
|
|
211
290
|
}
|
|
212
291
|
for (const key of required)
|
|
@@ -232,8 +311,12 @@ const objectExpr = (schema, ctx) => {
|
|
|
232
311
|
}
|
|
233
312
|
return record;
|
|
234
313
|
};
|
|
235
|
-
const oneofExpr = (branches, ctx) =>
|
|
236
|
-
|
|
314
|
+
const oneofExpr = (branches, ctx) => chooseExpr(branches.map((branch) => arbitraryExpr(branch, ctx)));
|
|
315
|
+
const chooseExpr = (exprs) => {
|
|
316
|
+
if (exprs.length === 0)
|
|
317
|
+
return "fc.anything()";
|
|
318
|
+
if (exprs.length === 1)
|
|
319
|
+
return exprs[0];
|
|
237
320
|
return `fc.oneof(${exprs.join(", ")})`;
|
|
238
321
|
};
|
|
239
322
|
const scalarExpr = (type, schema, ctx) => {
|
|
@@ -256,37 +339,21 @@ const scalarExpr = (type, schema, ctx) => {
|
|
|
256
339
|
return "fc.anything()";
|
|
257
340
|
}
|
|
258
341
|
};
|
|
259
|
-
const enumMemberFits = (schema, value) => {
|
|
260
|
-
if (typeof value === "string") {
|
|
261
|
-
if (hasMinLength(schema) && value.length < schema.minLength)
|
|
262
|
-
return false;
|
|
263
|
-
if (hasMaxLength(schema) && value.length > schema.maxLength)
|
|
264
|
-
return false;
|
|
265
|
-
if (hasPattern(schema)) {
|
|
266
|
-
try {
|
|
267
|
-
if (!new RegExp(schema.pattern).test(value))
|
|
268
|
-
return false;
|
|
269
|
-
} catch {
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
} else if (typeof value === "number") {
|
|
273
|
-
if (hasMinimum(schema) && value < schema.minimum)
|
|
274
|
-
return false;
|
|
275
|
-
if (hasMaximum(schema) && value > schema.maximum)
|
|
276
|
-
return false;
|
|
277
|
-
if (hasExclusiveMinimum(schema) && value <= schema.exclusiveMinimum)
|
|
278
|
-
return false;
|
|
279
|
-
if (hasExclusiveMaximum(schema) && value >= schema.exclusiveMaximum)
|
|
280
|
-
return false;
|
|
281
|
-
if (hasMultipleOf(schema) && schema.multipleOf > 0 && value % schema.multipleOf !== 0)
|
|
282
|
-
return false;
|
|
283
|
-
}
|
|
284
|
-
return true;
|
|
285
|
-
};
|
|
286
342
|
const arbitraryExpr = (schema, ctx) => {
|
|
343
|
+
assertGeneratorDepth(ctx.depth.value, "generateArbitrary");
|
|
287
344
|
if (!isSchemaObject(schema))
|
|
288
345
|
return "fc.anything()";
|
|
346
|
+
ctx.depth.value++;
|
|
347
|
+
try {
|
|
348
|
+
return arbitraryExprAtDepth(schema, ctx);
|
|
349
|
+
} finally {
|
|
350
|
+
ctx.depth.value--;
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
const arbitraryExprAtDepth = (schema, ctx) => {
|
|
289
354
|
if (hasRef(schema)) {
|
|
355
|
+
if (ctx.rootSchema !== void 0 && !resolveRef(schema.$ref, ctx.rootSchema))
|
|
356
|
+
return "fc.anything()";
|
|
290
357
|
const name = arbitraryName(refToName(schema.$ref, ctx.suffix));
|
|
291
358
|
if (name === ctx.selfArbName) {
|
|
292
359
|
ctx.usedTie.value = true;
|
|
@@ -301,8 +368,10 @@ const arbitraryExpr = (schema, ctx) => {
|
|
|
301
368
|
return `fc.constant(${JSON.stringify(schema.const)})`;
|
|
302
369
|
if (hasEnum(schema)) {
|
|
303
370
|
const members = schema.enum;
|
|
304
|
-
const fitting = members.filter((value) =>
|
|
371
|
+
const fitting = members.filter((value) => satisfiesScalarConstraints(schema, value));
|
|
305
372
|
const chosen = fitting.length > 0 ? fitting : members;
|
|
373
|
+
if (chosen.length === 0)
|
|
374
|
+
return "fc.anything()";
|
|
306
375
|
const values = chosen.map((value) => JSON.stringify(value)).join(", ");
|
|
307
376
|
const literalSafe = chosen.every((value) => value === null || typeof value !== "object");
|
|
308
377
|
return literalSafe ? `fc.constantFrom(...([${values}] as const))` : `fc.constantFrom(${values})`;
|
|
@@ -319,30 +388,52 @@ const arbitraryExpr = (schema, ctx) => {
|
|
|
319
388
|
return "fc.anything()";
|
|
320
389
|
if (hasAllOf(schema))
|
|
321
390
|
return arbitraryExpr(mergeAllOf(schema), ctx);
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
391
|
+
const branches = hasOneOf(schema) ? schema.oneOf : hasAnyOf(schema) ? schema.anyOf : void 0;
|
|
392
|
+
if (branches !== void 0) {
|
|
393
|
+
const rest = withoutCombinators(schema);
|
|
394
|
+
if (!declaresOwnShape(rest))
|
|
395
|
+
return oneofExpr(branches, ctx);
|
|
396
|
+
const viable = branches.filter((branch) => !typesConflict(rest, branch));
|
|
397
|
+
const kept = viable.length > 0 ? viable : branches;
|
|
398
|
+
return chooseExpr(kept.map((branch) => arbitraryExpr({ allOf: [rest, branch] }, ctx)));
|
|
399
|
+
}
|
|
400
|
+
if (isObjectLike(schema))
|
|
401
|
+
return objectExpr(schema, ctx);
|
|
326
402
|
if (hasType(schema))
|
|
327
403
|
return scalarExpr(schema.type, schema, ctx);
|
|
328
404
|
if (Array.isArray(schema.type)) {
|
|
329
|
-
|
|
330
|
-
return exprs.length === 1 ? exprs[0] : `fc.oneof(${exprs.join(", ")})`;
|
|
405
|
+
return chooseExpr(schema.type.map((type) => scalarExpr(type, schema, ctx)));
|
|
331
406
|
}
|
|
332
407
|
return "fc.anything()";
|
|
333
408
|
};
|
|
409
|
+
const withoutCombinators = (schema) => {
|
|
410
|
+
const rest = { ...schema };
|
|
411
|
+
delete rest["oneOf"];
|
|
412
|
+
delete rest["anyOf"];
|
|
413
|
+
return rest;
|
|
414
|
+
};
|
|
334
415
|
const generateArbitrary = (schema, typeName, suffix = "", lazyRefFilenames = /* @__PURE__ */ new Set(), rootSchema) => {
|
|
335
416
|
const selfArbName = arbitraryName(typeName);
|
|
336
|
-
const ctx = {
|
|
417
|
+
const ctx = {
|
|
418
|
+
suffix,
|
|
419
|
+
selfArbName,
|
|
420
|
+
usedTie: { value: false },
|
|
421
|
+
lazyRefFilenames,
|
|
422
|
+
depth: { value: 0 },
|
|
423
|
+
rootSchema
|
|
424
|
+
};
|
|
337
425
|
const expr = arbitraryExpr(schema, ctx);
|
|
338
426
|
const body = ctx.usedTie.value ? `fc.letrec<{ ${SELF_KEY}: ${typeName} }>((tie) => ({
|
|
339
427
|
${SELF_KEY}: ${expr},
|
|
340
428
|
})).${SELF_KEY}` : expr;
|
|
341
|
-
if (needsValidationFilter(schema)) {
|
|
429
|
+
if (needsValidationFilter(schema) && canBuildGuard(schema, rootSchema)) {
|
|
342
430
|
const validatorName = `${selfArbName}Validator`;
|
|
343
431
|
const embedded = JSON.stringify(withResolvableDefs(schema, rootSchema));
|
|
344
432
|
return `const ${validatorName} = ${VALIDATE_IMPORT_NAME}(${embedded})
|
|
345
|
-
export const ${selfArbName}: fc.Arbitrary<${typeName}> = (${body}).filter((value): value is ${typeName} => ${validatorName}(value) === true)`;
|
|
433
|
+
export const ${selfArbName}: fc.Arbitrary<${typeName}> = (${body} as fc.Arbitrary<unknown>).filter((value): value is ${typeName} => ${validatorName}(value) === true)`;
|
|
434
|
+
}
|
|
435
|
+
if (schema === false) {
|
|
436
|
+
return `export const ${selfArbName}: fc.Arbitrary<${typeName}> = ${body} as fc.Arbitrary<${typeName}>`;
|
|
346
437
|
}
|
|
347
438
|
return `export const ${selfArbName}: fc.Arbitrary<${typeName}> = ${body}`;
|
|
348
439
|
};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { generateTypeDefinition } from "@amritk/helpers/generate-type-definition";
|
|
2
2
|
import { collectExampleImports } from "./collect-example-imports.js";
|
|
3
3
|
import { generateExampleConst } from "./derive-example.js";
|
|
4
|
-
import { generateArbitrary,
|
|
4
|
+
import { generateArbitrary, VALIDATE_IMPORT_STATEMENT } from "./generate-arbitrary.js";
|
|
5
|
+
import { canBuildGuard, needsValidationFilter } from "./schema-validation.js";
|
|
5
6
|
const generateExampleFile = (schema, typeName, options) => {
|
|
6
7
|
const typeSuffix = options?.typeSuffix ?? "";
|
|
7
8
|
const refImports = collectExampleImports(schema, {
|
|
@@ -17,7 +18,7 @@ const generateExampleFile = (schema, typeName, options) => {
|
|
|
17
18
|
const example = generateExampleConst(schema, typeName, options?.rootSchema);
|
|
18
19
|
let result = `import * as fc from 'fast-check'
|
|
19
20
|
`;
|
|
20
|
-
if (
|
|
21
|
+
if (needsValidationFilter(schema) && canBuildGuard(schema, options?.rootSchema)) {
|
|
21
22
|
result += VALIDATE_IMPORT_STATEMENT + "\n";
|
|
22
23
|
}
|
|
23
24
|
for (const imp of refImports) {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
|
|
2
|
+
/**
|
|
3
|
+
* True when a schema describes an object even though it never says `type:
|
|
4
|
+
* 'object'`.
|
|
5
|
+
*
|
|
6
|
+
* This mirrors `isObjectLikeSchema` in `@amritk/helpers/generate-type-definition`
|
|
7
|
+
* — deliberately, and it has to keep mirroring it *exactly*. That function
|
|
8
|
+
* decides the *type* a generated file declares; this one decides the arbitrary
|
|
9
|
+
* and the example. Wherever they disagree, the file does not compile.
|
|
10
|
+
*
|
|
11
|
+
* So the tests below are the presence of the key, not the shape of its value:
|
|
12
|
+
* the helper asks `'patternProperties' in schema`, so a malformed
|
|
13
|
+
* `"patternProperties": null` still makes the type an object, and answering "not
|
|
14
|
+
* object-like" here emitted `null` against it. Likewise `properties` wins over
|
|
15
|
+
* `type`, so `{ type: 'string', properties: … }` is typed as an object however
|
|
16
|
+
* odd that reads — matching it is what keeps the file compiling.
|
|
17
|
+
*
|
|
18
|
+
* The helper is not exported, so the rule is restated here rather than
|
|
19
|
+
* duplicated by accident — the same arrangement `schema-validation.ts` has with
|
|
20
|
+
* that package's keyword sets.
|
|
21
|
+
*/
|
|
22
|
+
export declare const isObjectLike: (schema: JSONSchema) => boolean;
|
|
23
|
+
/**
|
|
24
|
+
* True when a schema says something about its own value, rather than delegating
|
|
25
|
+
* entirely to a combinator.
|
|
26
|
+
*
|
|
27
|
+
* `oneOf`/`anyOf` are constraints that sit *alongside* the node's other
|
|
28
|
+
* keywords, not replacements for them, so a node that also declares a `type` or
|
|
29
|
+
* an object shape has to contribute that to whatever branch is chosen. Reading
|
|
30
|
+
* the branch on its own dropped the node's own `properties`, and the example
|
|
31
|
+
* came out `null` against an object type.
|
|
32
|
+
*/
|
|
33
|
+
export declare const declaresOwnShape: (schema: JSONSchema) => boolean;
|
|
34
|
+
/**
|
|
35
|
+
* True when two schemas declare `type`s no single value can satisfy at once.
|
|
36
|
+
*
|
|
37
|
+
* Merging a node's own keywords into each `oneOf`/`anyOf` branch can produce a
|
|
38
|
+
* branch that contradicts them — `{ anyOf: [integer, null], type: 'integer' }`
|
|
39
|
+
* arises whenever an `allOf` narrows a property that carried a nullable union.
|
|
40
|
+
* `mergeAllOf` resolves a clash by letting the later branch win, so the `null`
|
|
41
|
+
* branch survived as `fc.constant(null)` under a type the intersection had
|
|
42
|
+
* already narrowed to `number`. A branch that cannot be satisfied is not a
|
|
43
|
+
* choice; dropping it is what makes the union match the declared type.
|
|
44
|
+
*
|
|
45
|
+
* `integer` and `number` are not a clash — every integer is a number.
|
|
46
|
+
*/
|
|
47
|
+
export declare const typesConflict: (a: JSONSchema, b: JSONSchema) => boolean;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { hasType, isSchemaObject } from "@amritk/helpers/schema-guards";
|
|
2
|
+
const isObjectLike = (schema) => {
|
|
3
|
+
if (!isSchemaObject(schema))
|
|
4
|
+
return false;
|
|
5
|
+
const raw = schema;
|
|
6
|
+
if (raw["type"] === "object" || Object.hasOwn(raw, "properties"))
|
|
7
|
+
return true;
|
|
8
|
+
if (Object.hasOwn(raw, "patternProperties") || Object.hasOwn(raw, "additionalProperties"))
|
|
9
|
+
return true;
|
|
10
|
+
return Object.hasOwn(raw, "if") && Object.hasOwn(raw, "then");
|
|
11
|
+
};
|
|
12
|
+
const declaresOwnShape = (schema) => hasType(schema) || Array.isArray(schema.type) || isObjectLike(schema);
|
|
13
|
+
const typesConflict = (a, b) => {
|
|
14
|
+
if (!hasType(a) || !hasType(b) || a.type === b.type)
|
|
15
|
+
return false;
|
|
16
|
+
const numeric = /* @__PURE__ */ new Set(["integer", "number"]);
|
|
17
|
+
return !(numeric.has(a.type) && numeric.has(b.type));
|
|
18
|
+
};
|
|
19
|
+
export {
|
|
20
|
+
declaresOwnShape,
|
|
21
|
+
isObjectLike,
|
|
22
|
+
typesConflict
|
|
23
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
|
|
2
|
+
/**
|
|
3
|
+
* True when an already-chosen scalar value satisfies the node's own
|
|
4
|
+
* length/range/pattern keywords.
|
|
5
|
+
*
|
|
6
|
+
* Both generators face the same question from opposite directions: the deriver
|
|
7
|
+
* picks the first `enum`/`const` member that fits a sibling `minLength`, and the
|
|
8
|
+
* arbitrary drops the members that do not fit before handing the rest to
|
|
9
|
+
* `fc.constantFrom`. Answering it in two places let the two drift — the
|
|
10
|
+
* arbitrary learned about `pattern` and the deriver did not, so the same schema
|
|
11
|
+
* could yield an example the arbitrary would have rejected.
|
|
12
|
+
*
|
|
13
|
+
* Only *scalar* keywords are checked. A value that is neither a string nor a
|
|
14
|
+
* number has nothing here to violate, so it passes; the full judgement is the
|
|
15
|
+
* validator's job (see `makeInstanceCheck`).
|
|
16
|
+
*/
|
|
17
|
+
export declare const satisfiesScalarConstraints: (schema: JSONSchema, value: unknown) => boolean;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { hasExclusiveMaximum, hasExclusiveMinimum, hasMaximum, hasMaxLength, hasMinimum, hasMinLength, hasMultipleOf, hasPattern } from "@amritk/helpers/schema-guards";
|
|
2
|
+
const satisfiesScalarConstraints = (schema, value) => {
|
|
3
|
+
if (typeof value === "string") {
|
|
4
|
+
if (hasMinLength(schema) && value.length < schema.minLength)
|
|
5
|
+
return false;
|
|
6
|
+
if (hasMaxLength(schema) && value.length > schema.maxLength)
|
|
7
|
+
return false;
|
|
8
|
+
if (hasPattern(schema)) {
|
|
9
|
+
try {
|
|
10
|
+
if (!new RegExp(schema.pattern).test(value))
|
|
11
|
+
return false;
|
|
12
|
+
} catch {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
if (typeof value === "number") {
|
|
19
|
+
if (hasMinimum(schema) && value < schema.minimum)
|
|
20
|
+
return false;
|
|
21
|
+
if (hasMaximum(schema) && value > schema.maximum)
|
|
22
|
+
return false;
|
|
23
|
+
if (hasExclusiveMinimum(schema) && value <= schema.exclusiveMinimum)
|
|
24
|
+
return false;
|
|
25
|
+
if (hasExclusiveMaximum(schema) && value >= schema.exclusiveMaximum)
|
|
26
|
+
return false;
|
|
27
|
+
if (hasMultipleOf(schema) && schema.multipleOf > 0 && value % schema.multipleOf !== 0)
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
return true;
|
|
31
|
+
};
|
|
32
|
+
export {
|
|
33
|
+
satisfiesScalarConstraints
|
|
34
|
+
};
|
|
@@ -33,6 +33,18 @@ export declare const withResolvableDefs: (schema: JSONSchema, rootSchema?: Recor
|
|
|
33
33
|
export type InstanceCheckOptions = {
|
|
34
34
|
readonly checkFormats?: boolean;
|
|
35
35
|
};
|
|
36
|
+
/**
|
|
37
|
+
* Whether a runtime validator can be built for `schema` at all.
|
|
38
|
+
*
|
|
39
|
+
* The arbitrary generator embeds `__mjstValidate(<schema>)` as a top-level
|
|
40
|
+
* `const` in the file it emits, and nothing there catches a throw — so a schema
|
|
41
|
+
* the interpreter refuses would kill the module at import rather than at the one
|
|
42
|
+
* call site that wanted an opinion. Asking first turns that into a warning and an
|
|
43
|
+
* unfiltered arbitrary. Failures are reported once by {@link tryGuard}, and the
|
|
44
|
+
* built guard is cached on the schema object, so asking costs nothing the
|
|
45
|
+
* eventual validation would not have paid anyway.
|
|
46
|
+
*/
|
|
47
|
+
export declare const canBuildGuard: (schema: JSONSchema, rootSchema?: Record<string, unknown>) => boolean;
|
|
36
48
|
/**
|
|
37
49
|
* Compiles a boolean validator for `schema` (with the definitions its `$ref`s
|
|
38
50
|
* need spliced in). Used at generation time to accept/reject candidate example
|
|
@@ -162,6 +162,7 @@ const spliceDefs = (schema, rootSchema) => {
|
|
|
162
162
|
};
|
|
163
163
|
const WARNING_SCHEMA_LIMIT = 240;
|
|
164
164
|
const reportedGuardFailures = /* @__PURE__ */ new Set();
|
|
165
|
+
const MAX_REPORTED_GUARD_FAILURES = 1e3;
|
|
165
166
|
const describeSchema = (schema) => {
|
|
166
167
|
const serialized = (() => {
|
|
167
168
|
try {
|
|
@@ -179,6 +180,8 @@ const warnUndecidableSchema = (schema, reason) => {
|
|
|
179
180
|
${described}`;
|
|
180
181
|
if (reportedGuardFailures.has(key))
|
|
181
182
|
return;
|
|
183
|
+
if (reportedGuardFailures.size >= MAX_REPORTED_GUARD_FAILURES)
|
|
184
|
+
reportedGuardFailures.clear();
|
|
182
185
|
reportedGuardFailures.add(key);
|
|
183
186
|
console.warn(`Warning: cannot validate generated values against ${described} \u2014 ${message}. Every candidate value is accepted for this subschema, so constraints only the validator enforces (\`oneOf\`, \`not\`, \`pattern\`, \`if\`/\`then\`, \u2026) go unchecked there and the emitted example may not satisfy its own schema. Generation continues regardless; the usual causes are a \`$ref\` pointing outside this fragment and a \`pattern\` the ReDoS screen rejects.`);
|
|
184
187
|
};
|
|
@@ -190,6 +193,39 @@ const tryGuard = (schema, checkFormats) => {
|
|
|
190
193
|
return void 0;
|
|
191
194
|
}
|
|
192
195
|
};
|
|
196
|
+
const everyPatternCompiles = (node) => {
|
|
197
|
+
if (node === null || typeof node !== "object")
|
|
198
|
+
return true;
|
|
199
|
+
if (Array.isArray(node))
|
|
200
|
+
return node.every(everyPatternCompiles);
|
|
201
|
+
const compiles = (source) => {
|
|
202
|
+
try {
|
|
203
|
+
new RegExp(source);
|
|
204
|
+
return true;
|
|
205
|
+
} catch {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
for (const [key, value] of Object.entries(node)) {
|
|
210
|
+
if (key === "pattern" && typeof value === "string" && !compiles(value))
|
|
211
|
+
return false;
|
|
212
|
+
if (key === "patternProperties" && value !== null && typeof value === "object") {
|
|
213
|
+
if (!Object.keys(value).every(compiles))
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
if (!everyPatternCompiles(value))
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
return true;
|
|
220
|
+
};
|
|
221
|
+
const canBuildGuard = (schema, rootSchema) => {
|
|
222
|
+
const resolved = withResolvableDefs(schema, rootSchema);
|
|
223
|
+
if (!everyPatternCompiles(resolved)) {
|
|
224
|
+
warnUndecidableSchema(resolved, "the schema carries a `pattern` that is not a valid regular expression");
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
return tryGuard(resolved, false) !== void 0;
|
|
228
|
+
};
|
|
193
229
|
const makeInstanceCheck = (schema, rootSchema, options) => {
|
|
194
230
|
const resolved = withResolvableDefs(schema, rootSchema);
|
|
195
231
|
const guard = tryGuard(resolved, options?.checkFormats === true);
|
|
@@ -205,6 +241,7 @@ const makeInstanceCheck = (schema, rootSchema, options) => {
|
|
|
205
241
|
};
|
|
206
242
|
};
|
|
207
243
|
export {
|
|
244
|
+
canBuildGuard,
|
|
208
245
|
makeInstanceCheck,
|
|
209
246
|
needsValidationFilter,
|
|
210
247
|
withResolvableDefs
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amritk/generate-examples",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Generate fast-check arbitraries and example values from JSON Schemas.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -55,8 +55,8 @@
|
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
57
|
"json-schema-typed": "^8.0.1",
|
|
58
|
-
"@amritk/helpers": "^0.
|
|
59
|
-
"@amritk/runtime-validators": "^0.
|
|
58
|
+
"@amritk/helpers": "^0.17.0",
|
|
59
|
+
"@amritk/runtime-validators": "^0.12.1"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"ajv": "^8.17.1"
|