@amritk/generate-examples 0.6.3 → 0.7.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,6 +1,11 @@
1
1
  import { getMjstInstanceOf, getMjstPrimitive } from "@amritk/helpers/mjst-extension";
2
+ import { readKey } from "@amritk/helpers/read-key";
2
3
  import { resolveRef } from "@amritk/helpers/resolve-ref";
3
4
  import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasConst, hasDefault, hasDependentRequired, hasDependentSchemas, hasEnum, hasExamples, 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";
5
+ import { assertGeneratorDepth } from "./assert-generator-depth.js";
6
+ import { closedValueSet } from "./closed-value-set.js";
7
+ import { declaresOwnShape, isObjectLike, typesConflict } from "./is-object-like.js";
8
+ import { satisfiesScalarConstraints } from "./satisfies-scalar-constraints.js";
4
9
  import { makeInstanceCheck, needsValidationFilter } from "./schema-validation.js";
5
10
  const lowerFirst = (name) => name.charAt(0).toLowerCase() + name.slice(1);
6
11
  const exampleName = (typeName) => `${lowerFirst(typeName)}Example`;
@@ -67,6 +72,13 @@ const topLevelAlternatives = (s) => {
67
72
  parts.push(cur);
68
73
  return parts;
69
74
  };
75
+ const matchesPattern = (pattern, value) => {
76
+ try {
77
+ return new RegExp(pattern).test(value);
78
+ } catch {
79
+ return false;
80
+ }
81
+ };
70
82
  const sampleFromPattern = (pattern, minLength) => {
71
83
  let body = pattern;
72
84
  if (body.startsWith("^"))
@@ -178,16 +190,17 @@ const FORMAT_EXAMPLES = {
178
190
  // The `regex` format asks for a string that *compiles* as a regular expression.
179
191
  regex: "^example$"
180
192
  };
193
+ const MAX_DERIVED_SIZE = 1e4;
181
194
  const exampleString = (schema) => {
182
195
  if (hasFormat(schema)) {
183
- const formatted = FORMAT_EXAMPLES[schema.format];
196
+ const formatted = readKey(FORMAT_EXAMPLES, schema.format);
184
197
  if (formatted !== void 0)
185
198
  return formatted;
186
199
  }
187
- const minLength = hasMinLength(schema) ? schema.minLength : 0;
200
+ const minLength = Math.min(hasMinLength(schema) ? schema.minLength : 0, MAX_DERIVED_SIZE);
188
201
  if (hasPattern(schema)) {
189
202
  const sampled = sampleFromPattern(schema.pattern, minLength);
190
- if (sampled !== void 0 && sampled.length >= minLength && new RegExp(schema.pattern).test(sampled)) {
203
+ if (sampled !== void 0 && sampled.length >= minLength && matchesPattern(schema.pattern, sampled)) {
191
204
  if (!(hasMaxLength(schema) && sampled.length > schema.maxLength))
192
205
  return sampled;
193
206
  }
@@ -204,7 +217,9 @@ const newContext = (rootSchema) => ({
204
217
  rootSchema,
205
218
  values: /* @__PURE__ */ new Map(),
206
219
  active: /* @__PURE__ */ new Set(),
207
- cycleBroken: false
220
+ cycleBroken: false,
221
+ needsAssertion: false,
222
+ depth: 0
208
223
  });
209
224
  const contextFor = (rootSchema) => {
210
225
  if (rootSchema === void 0)
@@ -216,52 +231,76 @@ const contextFor = (rootSchema) => {
216
231
  contexts.set(rootSchema, created);
217
232
  return created;
218
233
  };
219
- const deriveExample = (schema, rootSchema) => {
234
+ const deriveExample = (schema, rootSchema) => deriveReported(schema, rootSchema).value;
235
+ const deriveReported = (schema, rootSchema) => {
220
236
  const ctx = contextFor(rootSchema);
221
237
  ctx.active.clear();
222
238
  ctx.cycleBroken = false;
223
- return derive(schema, ctx);
239
+ ctx.needsAssertion = false;
240
+ ctx.depth = 0;
241
+ const value = derive(schema, ctx);
242
+ return { value, needsAssertion: ctx.needsAssertion, cycleBroken: ctx.cycleBroken };
224
243
  };
225
244
  const derive = (schema, ctx) => {
245
+ assertGeneratorDepth(ctx.depth, "deriveExample");
226
246
  if (!isSchemaObject(schema))
227
247
  return null;
228
- const base = deriveBase(schema, ctx);
229
- return needsValidationFilter(schema) ? refineExample(schema, base, ctx) : base;
248
+ ctx.depth++;
249
+ try {
250
+ const base = deriveBase(schema, ctx);
251
+ return needsValidationFilter(schema) ? refineExample(schema, base, ctx) : base;
252
+ } finally {
253
+ ctx.depth--;
254
+ }
230
255
  };
231
256
  const deriveRef = (ref, ctx) => {
232
257
  if (ctx.active.has(ref)) {
233
258
  ctx.cycleBroken = true;
234
259
  return null;
235
260
  }
236
- if (ctx.values.has(ref))
237
- return ctx.values.get(ref);
261
+ const memoized = ctx.values.get(ref);
262
+ if (memoized !== void 0) {
263
+ if (memoized.needsAssertion)
264
+ ctx.needsAssertion = true;
265
+ return memoized.value;
266
+ }
238
267
  if (!ctx.rootSchema)
239
268
  return null;
240
269
  const resolved = resolveRef(ref, ctx.rootSchema);
241
270
  if (!resolved)
242
271
  return null;
243
272
  const outerBroken = ctx.cycleBroken;
273
+ const outerUndeclared = ctx.needsAssertion;
244
274
  ctx.cycleBroken = false;
275
+ ctx.needsAssertion = false;
245
276
  ctx.active.add(ref);
246
277
  try {
247
278
  const value = derive(resolved, ctx);
248
279
  if (!ctx.cycleBroken)
249
- ctx.values.set(ref, value);
280
+ ctx.values.set(ref, { value, needsAssertion: ctx.needsAssertion });
250
281
  ctx.cycleBroken = outerBroken || ctx.cycleBroken;
282
+ ctx.needsAssertion = outerUndeclared || ctx.needsAssertion;
251
283
  return value;
252
284
  } finally {
253
285
  ctx.active.delete(ref);
254
286
  }
255
287
  };
288
+ const authoredHint = (schema) => {
289
+ if (hasExamples(schema) && Array.isArray(schema.examples) && schema.examples.length > 0) {
290
+ return { value: schema.examples[0] };
291
+ }
292
+ if (hasDefault(schema))
293
+ return { value: schema.default };
294
+ return void 0;
295
+ };
256
296
  const deriveBase = (schema, ctx) => {
257
297
  if (!isSchemaObject(schema))
258
298
  return null;
259
299
  if (hasConst(schema))
260
300
  return schema.const;
261
- if (hasExamples(schema) && Array.isArray(schema.examples) && schema.examples.length > 0)
262
- return schema.examples[0];
263
- if (hasDefault(schema))
264
- return schema.default;
301
+ const hint = authoredHint(schema);
302
+ if (hint !== void 0 && makeInstanceCheck(schema, ctx.rootSchema)(hint.value))
303
+ return hint.value;
265
304
  if (hasEnum(schema) && schema.enum.length > 0) {
266
305
  const fitting = schema.enum.find((value) => satisfiesScalarConstraints(schema, value));
267
306
  return fitting !== void 0 ? fitting : schema.enum[0];
@@ -276,10 +315,16 @@ const deriveBase = (schema, ctx) => {
276
315
  return 0n;
277
316
  if (hasAllOf(schema))
278
317
  return derive(mergeAllOf(schema), ctx);
279
- if (hasOneOf(schema) && schema.oneOf[0] !== void 0)
280
- return derive(schema.oneOf[0], ctx);
281
- if (hasAnyOf(schema) && schema.anyOf[0] !== void 0)
282
- return derive(schema.anyOf[0], ctx);
318
+ const branches = hasOneOf(schema) ? schema.oneOf : hasAnyOf(schema) ? schema.anyOf : void 0;
319
+ if (branches !== void 0 && branches.length > 0) {
320
+ const rest = withoutCombinators(schema);
321
+ if (!declaresOwnShape(rest))
322
+ return derive(branches[0], ctx);
323
+ const chosen = branches.find((branch) => !typesConflict(rest, branch)) ?? branches[0];
324
+ return derive({ allOf: [rest, chosen] }, ctx);
325
+ }
326
+ if (isObjectLike(schema))
327
+ return deriveObject(schema, ctx);
283
328
  if (hasType(schema))
284
329
  return deriveForType(schema.type, schema, ctx);
285
330
  if (Array.isArray(schema.type) && schema.type.length > 0) {
@@ -287,6 +332,12 @@ const deriveBase = (schema, ctx) => {
287
332
  }
288
333
  return null;
289
334
  };
335
+ const withoutCombinators = (schema) => {
336
+ const rest = { ...schema };
337
+ delete rest["oneOf"];
338
+ delete rest["anyOf"];
339
+ return rest;
340
+ };
290
341
  const REFINED_APPLICATORS = ["if", "then", "else", "not", "oneOf"];
291
342
  const structuralOnly = (schema) => {
292
343
  const clone = { ...schema };
@@ -336,26 +387,6 @@ const refineExample = (schema, base, ctx) => {
336
387
  return candidate;
337
388
  return base;
338
389
  };
339
- const satisfiesScalarConstraints = (schema, value) => {
340
- if (typeof value === "string") {
341
- if (hasMinLength(schema) && value.length < schema.minLength)
342
- return false;
343
- if (hasMaxLength(schema) && value.length > schema.maxLength)
344
- return false;
345
- } else if (typeof value === "number") {
346
- if (hasMinimum(schema) && value < schema.minimum)
347
- return false;
348
- if (hasMaximum(schema) && value > schema.maximum)
349
- return false;
350
- if (hasExclusiveMinimum(schema) && value <= schema.exclusiveMinimum)
351
- return false;
352
- if (hasExclusiveMaximum(schema) && value >= schema.exclusiveMaximum)
353
- return false;
354
- if (hasMultipleOf(schema) && schema.multipleOf > 0 && value % schema.multipleOf !== 0)
355
- return false;
356
- }
357
- return true;
358
- };
359
390
  const deriveForType = (type, schema, ctx) => {
360
391
  switch (type) {
361
392
  case "string":
@@ -396,6 +427,15 @@ const deriveObject = (schema, ctx) => {
396
427
  const additionalClosed = hasAdditionalProperties(schema) && schema.additionalProperties === false;
397
428
  const extrasAllowed = !additionalClosed || patternEntries.length > 0;
398
429
  const nameCheck = hasPropertyNames(schema) ? makeInstanceCheck(schema.propertyNames, ctx.rootSchema) : void 0;
430
+ const declaredKeys = new Set(hasProperties(schema) ? Object.keys(schema.properties) : []);
431
+ const hasIndexSignature = additionalSchema !== void 0 || hasPatternProperties(schema) && Object.keys(schema.patternProperties).length > 0;
432
+ const noteKey = (key) => {
433
+ if (!hasIndexSignature && !declaredKeys.has(key))
434
+ ctx.needsAssertion = true;
435
+ };
436
+ const raw = schema;
437
+ if (Object.hasOwn(raw, "if") && Object.hasOwn(raw, "then"))
438
+ ctx.needsAssertion = true;
399
439
  const valueSchemaFor = (key) => {
400
440
  const matches = patternEntries.filter(([re]) => re.test(key)).map(([, sub]) => sub);
401
441
  if (matches.length === 1)
@@ -408,6 +448,7 @@ const deriveObject = (schema, ctx) => {
408
448
  const sub = valueSchemaFor(key);
409
449
  if (sub === void 0 && additionalClosed)
410
450
  return;
451
+ noteKey(key);
411
452
  setProperty(out, key, sub !== void 0 ? derive(sub, ctx) : null);
412
453
  };
413
454
  if (hasProperties(schema)) {
@@ -416,9 +457,10 @@ const deriveObject = (schema, ctx) => {
416
457
  }
417
458
  }
418
459
  if (hasRequired(schema)) {
419
- for (const key of schema.required)
420
- if (!Object.hasOwn(out, key))
460
+ for (const key of schema.required) {
461
+ if (typeof key === "string" && !Object.hasOwn(out, key))
421
462
  addKey(key);
463
+ }
422
464
  }
423
465
  if (hasDependentRequired(schema)) {
424
466
  for (const [trigger, deps] of Object.entries(schema.dependentRequired)) {
@@ -432,63 +474,78 @@ const deriveObject = (schema, ctx) => {
432
474
  if (hasDependentSchemas(schema)) {
433
475
  for (const [trigger, sub] of Object.entries(schema.dependentSchemas)) {
434
476
  if (Object.hasOwn(out, trigger) && isSchemaObject(sub))
435
- applyDependentSchema(out, sub, ctx);
477
+ applyDependentSchema(out, sub, ctx, noteKey);
436
478
  }
437
479
  }
438
480
  if (hasMinProperties(schema) && extrasAllowed) {
481
+ const target = Math.min(schema.minProperties, MAX_DERIVED_SIZE);
482
+ let count = Object.keys(out).length;
439
483
  let n = 0;
440
- let guard = 0;
441
- while (Object.keys(out).length < schema.minProperties && guard++ < schema.minProperties + 50) {
484
+ let attempts = 0;
485
+ while (count < target && attempts++ < target + 50) {
442
486
  const key = synthKey(n++, patternEntries, schema, nameCheck);
443
- if (key === void 0 || Object.hasOwn(out, key))
487
+ if (key === void 0)
488
+ break;
489
+ if (Object.hasOwn(out, key))
444
490
  continue;
445
491
  addKey(key);
492
+ if (Object.hasOwn(out, key))
493
+ count++;
446
494
  }
447
495
  }
448
496
  if (hasMaxProperties(schema))
449
497
  enforceMaxProperties(out, schema, schema.maxProperties);
450
498
  return out;
451
499
  };
452
- const applyDependentSchema = (out, sub, ctx) => {
500
+ const applyDependentSchema = (out, sub, ctx, noteKey) => {
501
+ const add = (key, propSchema) => {
502
+ noteKey(key);
503
+ setProperty(out, key, propSchema !== void 0 ? derive(propSchema, ctx) : null);
504
+ };
453
505
  if (hasProperties(sub)) {
454
506
  for (const [key, propSchema] of Object.entries(sub.properties)) {
455
507
  if (!Object.hasOwn(out, key))
456
- setProperty(out, key, derive(propSchema, ctx));
508
+ add(key, propSchema);
457
509
  }
458
510
  }
459
511
  if (hasRequired(sub)) {
460
512
  const propSchemas = hasProperties(sub) ? sub.properties : {};
461
513
  for (const key of sub.required) {
462
- if (Object.hasOwn(out, key))
514
+ if (typeof key !== "string" || Object.hasOwn(out, key))
463
515
  continue;
464
- const propSchema = propSchemas[key];
465
- setProperty(out, key, propSchema !== void 0 ? derive(propSchema, ctx) : null);
516
+ add(key, propSchemas[key]);
466
517
  }
467
518
  }
468
519
  };
520
+ const MAX_SYNTH_KEY_LENGTH = 64;
469
521
  const synthKey = (i, patternEntries, schema, nameCheck) => {
470
- const vary = (seed) => seed.repeat(i + 1);
522
+ const variants = (seed) => i === 0 ? [seed] : [`${seed}${i}`, seed.repeat(i + 1)].filter((key) => key.length <= MAX_SYNTH_KEY_LENGTH);
471
523
  const seeds = [];
472
524
  for (const [re] of patternEntries) {
473
525
  const sampled = sampleFromPattern(re.source, 0);
474
- if (sampled !== void 0 && sampled.length > 0)
475
- seeds.push(vary(sampled));
526
+ if (sampled !== void 0 && sampled.length > 0) {
527
+ seeds.push({ seed: sampled, matches: (key) => re.test(key) });
528
+ }
476
529
  }
477
530
  const propertyNames = isSchemaObject(schema) && hasPropertyNames(schema) ? schema.propertyNames : void 0;
478
531
  if (propertyNames !== void 0 && isSchemaObject(propertyNames) && hasPattern(propertyNames)) {
479
532
  const sampled = sampleFromPattern(propertyNames.pattern, 0);
480
533
  if (sampled !== void 0 && sampled.length > 0)
481
- seeds.push(vary(sampled));
534
+ seeds.push({ seed: sampled, matches: () => true });
482
535
  }
483
- seeds.push(vary("extra"));
484
- for (const seed of seeds) {
485
- if (!nameCheck || nameCheck(seed))
486
- return seed;
536
+ seeds.push({ seed: "extra", matches: () => true });
537
+ for (const { seed, matches } of seeds) {
538
+ for (const key of variants(seed)) {
539
+ if (matches(key) && (!nameCheck || nameCheck(key)))
540
+ return key;
541
+ }
487
542
  }
488
543
  return void 0;
489
544
  };
490
545
  const enforceMaxProperties = (out, schema, max) => {
491
- if (Object.keys(out).length <= max)
546
+ const keys = Object.keys(out);
547
+ let count = keys.length;
548
+ if (count <= max)
492
549
  return;
493
550
  const protectedKeys = new Set(hasRequired(schema) ? schema.required : []);
494
551
  if (hasDependentRequired(schema)) {
@@ -498,11 +555,13 @@ const enforceMaxProperties = (out, schema, max) => {
498
555
  protectedKeys.add(dep);
499
556
  }
500
557
  }
501
- for (const key of Object.keys(out)) {
502
- if (Object.keys(out).length <= max)
558
+ for (const key of keys) {
559
+ if (count <= max)
503
560
  break;
504
- if (!protectedKeys.has(key))
505
- delete out[key];
561
+ if (protectedKeys.has(key))
562
+ continue;
563
+ delete out[key];
564
+ count--;
506
565
  }
507
566
  };
508
567
  const deriveNumber = (schema, isInteger) => {
@@ -516,7 +575,7 @@ const deriveNumber = (schema, isInteger) => {
516
575
  let value = Number.isFinite(lo) ? lo : Number.isFinite(hi) ? Math.min(0, hi) : 0;
517
576
  if (isInteger)
518
577
  value = Math.ceil(value);
519
- if (hasMultipleOf(schema) && schema.multipleOf > 0) {
578
+ if (hasMultipleOf(schema) && Number.isFinite(schema.multipleOf) && schema.multipleOf > 0) {
520
579
  const m = schema.multipleOf;
521
580
  value = Math.ceil(value / m - 1e-9) * m;
522
581
  if (value > hi && Number.isFinite(hi))
@@ -551,35 +610,27 @@ const deriveArray = (schema, ctx) => {
551
610
  const unique = hasUniqueItems(schema) && schema.uniqueItems === true;
552
611
  const elem = rest ?? contains;
553
612
  const choices = unique && contains === void 0 ? closedValueSet(elem) : void 0;
554
- const wanted = Math.min(Math.max(min, minContains, max === 0 ? 0 : 1), max);
613
+ const wanted = Math.min(Math.max(min, minContains, max === 0 ? 0 : 1), max, MAX_DERIVED_SIZE);
555
614
  const count = choices !== void 0 ? Math.min(wanted, choices.length) : wanted;
615
+ const itemsCheck = rest !== void 0 && contains !== void 0 ? makeInstanceCheck(rest, ctx.rootSchema) : void 0;
556
616
  const result = [];
557
617
  for (let i = 0; i < count; i++) {
558
618
  if (choices !== void 0) {
559
619
  result.push(choices[i]);
560
620
  continue;
561
621
  }
562
- const itemSchema = contains !== void 0 && i < minContains ? contains : elem;
563
- const base = itemSchema !== void 0 ? derive(itemSchema, ctx) : null;
564
- result.push(unique ? distinctify(base, i, itemSchema) : base);
622
+ const useContains = contains !== void 0 && i < minContains;
623
+ const itemSchema = useContains ? contains : elem;
624
+ let chosen = itemSchema;
625
+ let base = itemSchema !== void 0 ? derive(itemSchema, ctx) : null;
626
+ if (useContains && itemsCheck !== void 0 && !itemsCheck(base)) {
627
+ chosen = rest;
628
+ base = rest !== void 0 ? derive(rest, ctx) : null;
629
+ }
630
+ result.push(unique ? distinctify(base, i, chosen) : base);
565
631
  }
566
632
  return result;
567
633
  };
568
- const closedValueSet = (schema) => {
569
- if (schema === void 0 || !isSchemaObject(schema))
570
- return void 0;
571
- if (hasConst(schema))
572
- return [schema.const];
573
- if (hasEnum(schema)) {
574
- const fitting = schema.enum.filter((value) => satisfiesScalarConstraints(schema, value));
575
- return fitting.length > 0 ? fitting : [...schema.enum];
576
- }
577
- if (hasType(schema) && schema.type === "boolean")
578
- return [true, false];
579
- if (hasType(schema) && schema.type === "null")
580
- return [null];
581
- return void 0;
582
- };
583
634
  const distinctify = (base, i, itemSchema) => {
584
635
  if (i === 0)
585
636
  return base;
@@ -605,10 +656,20 @@ const TIGHTEST = /* @__PURE__ */ new Map([
605
656
  ["maxItems", "min"],
606
657
  ["maxProperties", "min"]
607
658
  ]);
659
+ const flattenBranches = (nodes, depth = 0) => {
660
+ assertGeneratorDepth(depth, "mergeAllOf");
661
+ return nodes.flatMap((node) => {
662
+ if (!isSchemaObject(node) || !hasAllOf(node))
663
+ return [node];
664
+ const own = { ...node };
665
+ delete own["allOf"];
666
+ return [...flattenBranches(node.allOf, depth + 1), own];
667
+ });
668
+ };
608
669
  const mergeAllOf = (schema) => {
609
- const branches = hasAllOf(schema) ? schema.allOf : [];
610
- const merged = {};
611
- const properties = {};
670
+ const branches = hasAllOf(schema) ? flattenBranches(schema.allOf) : [];
671
+ const merged = /* @__PURE__ */ Object.create(null);
672
+ const properties = /* @__PURE__ */ Object.create(null);
612
673
  const required = /* @__PURE__ */ new Set();
613
674
  for (const branch of [...branches, schema]) {
614
675
  if (!isSchemaObject(branch))
@@ -636,7 +697,7 @@ const mergeAllOf = (schema) => {
636
697
  }
637
698
  }
638
699
  }
639
- const mergedProps = {};
700
+ const mergedProps = /* @__PURE__ */ Object.create(null);
640
701
  for (const [prop, schemas] of Object.entries(properties)) {
641
702
  mergedProps[prop] = schemas.length === 1 ? schemas[0] : { allOf: schemas };
642
703
  }
@@ -671,9 +732,13 @@ const warnInvalidExample = (schema, typeName, value, rootSchema) => {
671
732
  console.warn(`Warning: the derived example for ${typeName} does not validate against its own schema \u2014 ${quoted}. It is emitted anyway so the file still compiles, but treat it as a placeholder: either the schema has no instance, or it uses a constraint the deriver cannot satisfy structurally (see the README's "Known limits"). ${arbitraryName(typeName)} is unaffected.`);
672
733
  };
673
734
  const generateExampleConst = (schema, typeName, rootSchema) => {
674
- const value = deriveExample(schema, rootSchema);
735
+ const { value, needsAssertion, cycleBroken } = deriveReported(schema, rootSchema);
675
736
  warnInvalidExample(schema, typeName, value, rootSchema);
676
- return `export const ${exampleName(typeName)}: ${typeName} = ${serializeValue(value)}`;
737
+ const literal = serializeValue(value);
738
+ const admitsNothing = schema === false;
739
+ const assertion = cycleBroken || admitsNothing ? ` as unknown as ${typeName}` : needsAssertion ? ` as ${typeName}` : "";
740
+ const expression = `${literal}${assertion}`;
741
+ return `export const ${exampleName(typeName)}: ${typeName} = ${expression}`;
677
742
  };
678
743
  export {
679
744
  deriveExample,