@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.
@@ -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 { needsValidationFilter, withResolvableDefs } from "./schema-validation.js";
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
- if (hasPattern(schema)) {
38
- const base = `fc.stringMatching(new RegExp(${JSON.stringify(schema.pattern)}))`;
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 (hasMinLength(schema))
41
- checks.push(`s.length >= ${schema.minLength}`);
42
- if (hasMaxLength(schema))
43
- checks.push(`s.length <= ${schema.maxLength}`);
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 (hasMinLength(schema))
48
- opts.push(`minLength: ${schema.minLength}`);
49
- if (hasMaxLength(schema))
50
- opts.push(`maxLength: ${schema.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 integerExpr = (schema) => {
54
- const opts = [];
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 (hasMinimum(schema))
57
- mins.push(Math.ceil(Number(schema.minimum)));
58
- if (hasExclusiveMinimum(schema))
59
- mins.push(Math.floor(Number(schema.exclusiveMinimum)) + 1);
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 (hasMaximum(schema))
64
- maxs.push(Math.floor(Number(schema.maximum)));
65
- if (hasExclusiveMaximum(schema))
66
- maxs.push(Math.ceil(Number(schema.exclusiveMaximum)) - 1);
67
- if (maxs.length > 0)
68
- opts.push(`max: ${Math.min(...maxs)}`);
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 hasMultipleOf(schema) ? `${base}.filter((n) => n % ${schema.multipleOf} === 0)` : base;
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
- if (hasMultipleOf(schema) && schema.multipleOf > 0)
118
- return numberMultipleOfExpr(schema);
166
+ const multipleOf = usableMultipleOf(schema);
167
+ if (multipleOf !== void 0)
168
+ return numberMultipleOfExpr(schema, multipleOf);
119
169
  const opts = ["noNaN: true", "noDefaultInfinity: true"];
120
- if (hasMinimum(schema) && (!hasExclusiveMinimum(schema) || Number(schema.minimum) > Number(schema.exclusiveMinimum))) {
121
- opts.push(`min: ${schema.minimum}`);
122
- } else if (hasExclusiveMinimum(schema)) {
123
- opts.push(`min: ${schema.exclusiveMinimum}`, "minExcluded: true");
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
- if (hasMaximum(schema) && (!hasExclusiveMaximum(schema) || Number(schema.maximum) < Number(schema.exclusiveMaximum))) {
126
- opts.push(`max: ${schema.maximum}`);
127
- } else if (hasExclusiveMaximum(schema)) {
128
- opts.push(`max: ${schema.exclusiveMaximum}`, "maxExcluded: true");
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 items = hasItems(schema) && isSchemaObject(schema.items) ? arbitraryExpr(schema.items, ctx) : containsSchema ? arbitraryExpr(containsSchema, ctx) : "fc.anything()";
142
- const minContains = containsSchema !== void 0 && typeof raw["minContains"] === "number" ? raw["minContains"] : 1;
143
- const minLength = Math.max(hasMinItems(schema) ? schema.minItems : 0, containsSchema !== void 0 ? Math.max(1, minContains) : 0);
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 (minLength > 0)
146
- opts.push(`minLength: ${minLength}`);
147
- if (hasMaxItems(schema))
148
- opts.push(`maxLength: ${schema.maxItems}`);
149
- const fn = hasUniqueItems(schema) && schema.uniqueItems === true ? "fc.uniqueArray" : "fc.array";
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
- if (firstPatternSource !== void 0)
154
- return `fc.stringMatching(new RegExp(${JSON.stringify(firstPatternSource)}))`;
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
- return `fc.stringMatching(new RegExp(${JSON.stringify(propertyNames.pattern)}))`;
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 (minKeys !== void 0 && minKeys > 0)
176
- opts.push(`minKeys: ${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(hasRequired(schema) ? schema.required : []);
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
- if (hasRequired(sub))
208
- for (const key of sub.required)
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
- const exprs = branches.map((branch) => arbitraryExpr(branch, ctx));
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) => enumMemberFits(schema, 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
- if (hasOneOf(schema))
323
- return oneofExpr(schema.oneOf, ctx);
324
- if (hasAnyOf(schema))
325
- return oneofExpr(schema.anyOf, ctx);
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
- const exprs = schema.type.map((type) => scalarExpr(type, schema, ctx));
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 = { suffix, selfArbName, usedTie: { value: false }, lazyRefFilenames };
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, VALIDATE_IMPORT_NAME, VALIDATE_IMPORT_STATEMENT } from "./generate-arbitrary.js";
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 (arbitrary.includes(`${VALIDATE_IMPORT_NAME}(`)) {
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.6.4",
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.15.4",
59
- "@amritk/runtime-validators": "^0.11.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"