@amritk/generate-examples 0.5.5 → 0.6.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 CHANGED
@@ -24,13 +24,29 @@ const files = await buildExampleSchema(schema, 'User') // → user.ts, index.ts
24
24
 
25
25
  1. **Generated arbitrary files `import * as fc from 'fast-check'`** — `fast-check`
26
26
  (`>=3`) is an **optional peer dependency** consumers must install. The static
27
- `fooExample` values have no runtime deps.
27
+ `fooExample` values have no runtime deps. A generated file whose schema uses
28
+ `if`/`then`/`else`, `not`, `oneOf`, `patternProperties`, `propertyNames`,
29
+ `dependentRequired`, `dependentSchemas`, `dependencies`, `minProperties`,
30
+ `maxProperties`, or `contains` **also** imports `@amritk/runtime-validators`
31
+ for its validating filter. That one is a `dependency` of this package, not a
32
+ peer — the generator imports it too — so it resolves for the generator but not
33
+ necessarily from the consumer's own tree, where the generated file lives.
34
+ Under pnpm-strict or Yarn PnP, install it directly. It cannot be declared a
35
+ peer as well: Bun rejects a workspace package listed as both, and
36
+ `--frozen-lockfile` then fails repo-wide.
28
37
  2. **`generateArbitrary` / `generateExampleConst` return source-code STRINGS**;
29
38
  **`deriveExample` returns an actual runtime VALUE.** Easy to confuse.
30
- 3. **A static example constrained only by `pattern` may not match the pattern**
31
- use the arbitrary when pattern fidelity matters.
39
+ 3. **A static example is validated against its own schema before it is emitted.**
40
+ If it fails, it is written anyway (the module must compile) and the generator
41
+ `console.warn`s, naming the type. That happens when the schema has no instance
42
+ at all, or when the constraint is beyond the deriver (a `pattern` with
43
+ lookarounds/backreferences, an unrecognized `format`). Use `FooArbitrary`
44
+ there — it carries a runtime validating filter and stays correct.
32
45
  4. **Unsupported keywords degrade silently:** `fc.anything()` in arbitraries,
33
46
  `null` in static examples — no error thrown.
47
+ 5. **`deriveExample` memoizes each `$ref` per root document**, so the returned
48
+ value can share sub-objects with the value derived for a sibling schema.
49
+ Treat it as read-only.
34
50
 
35
51
  Exports: `buildExampleSchema`, `generateArbitrary`, `generateExampleConst`,
36
52
  `deriveExample`, `serializeValue`, `GeneratedFile`. Only the `.` entry.
package/README.md CHANGED
@@ -5,9 +5,11 @@
5
5
  **Programmatic API for generating fast-check arbitraries and example values from JSON Schemas.**
6
6
 
7
7
  ![status](https://img.shields.io/badge/status-pre--alpha-ef4444?style=flat-square) 
8
+ ![version](https://img.shields.io/npm/v/@amritk/generate-examples?style=flat-square&logo=npm&logoColor=white&label=version&color=6366f1) 
8
9
  ![license](https://img.shields.io/badge/license-MIT-22c55e?style=flat-square) 
9
10
  ![JSON Schema](https://img.shields.io/badge/JSON%20Schema-2020--12-f97316?style=flat-square) 
10
- ![node](https://img.shields.io/badge/node-%E2%89%A520-339933?style=flat-square&logo=node.js&logoColor=white)
11
+ ![node](https://img.shields.io/badge/node-%E2%89%A520-339933?style=flat-square&logo=node.js&logoColor=white) 
12
+ ![vibe coded](https://img.shields.io/badge/vibe-coded-a855f7?style=flat-square)
11
13
 
12
14
  </div>
13
15
 
@@ -37,6 +39,15 @@ An `index.ts` barrel re-exports everything.
37
39
  > and the presence-gated object keywords) also imports `@amritk/runtime-validators`
38
40
  > for a post-generation validating filter; files that need no such filter don't.
39
41
  > The static `fooExample` values have no runtime dependencies.
42
+ >
43
+ > `@amritk/runtime-validators` is a `dependency` here rather than a peer, because
44
+ > this generator imports it itself. That resolves it for the generator, but *not*
45
+ > necessarily for the generated file — that file lands in **your** source tree, so
46
+ > under pnpm's strict layout or Yarn PnP it resolves from your project, not from
47
+ > this package's. If your schemas use any of those keywords, install it directly
48
+ > (`npm i @amritk/runtime-validators`). It cannot also be declared a peer: Bun
49
+ > rejects a workspace package listed as both, and `--frozen-lockfile` then fails
50
+ > for the whole repo.
40
51
 
41
52
  ---
42
53
 
@@ -126,8 +137,7 @@ const res = await fetch('/users', { method: 'POST', body: JSON.stringify(userExa
126
137
  `type` — including multi-type unions like `['string', 'null']` —
127
138
  (string/number/integer/boolean/null/array/object), `properties`,
128
139
  `required`, `items`, `minItems`/`maxItems`, `uniqueItems`,
129
- `minLength`/`maxLength`, `pattern`, `format` (`email`, `uuid`, `uri`/`url`,
130
- `date`, `date-time`, `time`, `hostname`, `ipv4`, `ipv6`), `minimum`/`maximum`,
140
+ `minLength`/`maxLength`, `pattern`, `format`, `minimum`/`maximum`,
131
141
  `exclusiveMinimum`/`exclusiveMaximum`, `multipleOf`, `enum` (filtered by sibling
132
142
  constraints), `const`, `minProperties`/`maxProperties`, `patternProperties`,
133
143
  `propertyNames`, `dependentRequired`, `dependentSchemas`, `contains`,
@@ -137,9 +147,37 @@ enforced by validating generated candidates against the schema and
137
147
  retrying/rejecting. Unsupported constructs degrade to `fc.anything()` in
138
148
  arbitraries and `null` in static examples.
139
149
 
150
+ Static examples cover every `format` `@amritk/runtime-validators` knows how to
151
+ check: `email`, `idn-email`, `date`, `date-time`, `time`, `duration`, `uuid`,
152
+ `uri`, `iri`, `uri-reference`, `iri-reference`, `uri-template`, `json-pointer`,
153
+ `relative-json-pointer`, `hostname`, `idn-hostname`, `ipv4`, `ipv6`, `regex`,
154
+ plus OpenAPI's `url`. An unrecognized `format` falls back to `"string"`.
155
+
156
+ ---
157
+
158
+ ## Known limits
159
+
160
+ Every `fooExample` is validated against its own schema before it is written. When
161
+ the value does not satisfy the schema it is still emitted — so the module always
162
+ compiles — but the generator prints a `console.warn` naming the type. Reach for
163
+ `FooArbitrary` in those cases: the arbitrary carries a runtime validating filter
164
+ and stays correct where the static value cannot.
165
+
166
+ The value falls short for two reasons:
167
+
168
+ - **The schema has no instance.** `{ pattern: '^ab$', minLength: 5 }`,
169
+ `uniqueItems` over booleans with `minItems: 3`, a `required` key that
170
+ `additionalProperties: false` forbids, or a `oneOf` whose branches every value
171
+ matches twice. Nothing correct exists to emit; the warning is pointing at the
172
+ schema, not the generator.
173
+ - **The constraint is beyond the deriver.** `pattern` is sampled by a
174
+ best-effort recursive-descent walk of the regex, so lookarounds and
175
+ backreferences fall back to `"string"`; an unrecognized `format` does the same.
176
+
140
177
  > [!TIP]
141
- > A static example constrained only by `pattern` is not guaranteed to match the
142
- > pattern reach for the arbitrary when pattern fidelity matters.
178
+ > The example for a `$ref` is inlined by value, so a definition graph with wide
179
+ > fan-out produces a correspondingly large literal. That cost is in the output
180
+ > size, not in generation time — each definition is derived once per document.
143
181
 
144
182
  ---
145
183
 
@@ -4,13 +4,21 @@ import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
4
4
  *
5
5
  * Prefers explicit hints in this order: `const`, `examples[0]`, `default`,
6
6
  * `enum[0]`; otherwise produces a canonical value for the declared type.
7
- * `$ref`s are resolved and inlined by value; recursive refs short-circuit to
8
- * `null` (tracked via `seen`).
7
+ * `$ref`s are resolved and inlined by value; a recursive ref short-circuits to
8
+ * `null` where the cycle closes.
9
9
  *
10
- * Note: values constrained only by `pattern` are not guaranteed to match the
11
- * pattern use the generated arbitrary when pattern fidelity matters.
10
+ * Derived values are memoized per `$ref` per root document, so the returned
11
+ * value may share sub-objects with a value returned for a sibling schema. That
12
+ * is safe for the generator (it serializes and never mutates), but treat the
13
+ * result as read-only.
14
+ *
15
+ * Some schemas have no derivable instance (an unsatisfiable `pattern` +
16
+ * `minLength`, a `oneOf` every candidate matches twice); this returns the
17
+ * closest structural value it built. {@link generateExampleConst} validates the
18
+ * final value and warns when that happens, rather than letting an invalid
19
+ * example ship silently.
12
20
  */
13
- export declare const deriveExample: (schema: JSONSchema, rootSchema?: Record<string, unknown>, seen?: ReadonlySet<string>) => unknown;
21
+ export declare const deriveExample: (schema: JSONSchema, rootSchema?: Record<string, unknown>) => unknown;
14
22
  export declare const mergeAllOf: (schema: JSONSchema) => JSONSchema;
15
23
  /**
16
24
  * Serializes a derived value into a TypeScript source expression. Handles the
@@ -21,6 +29,10 @@ export declare const serializeValue: (value: unknown) => string;
21
29
  /**
22
30
  * Generates an exported const holding a concrete, schema-valid example value.
23
31
  *
32
+ * The derived value is validated against its own schema before being emitted; a
33
+ * value that fails is still written (so the generated file compiles) but is
34
+ * reported via `console.warn` rather than shipping as a silently-wrong fixture.
35
+ *
24
36
  * @example
25
37
  * ```typescript
26
38
  * generateExampleConst({ type: 'object', properties: { name: { type: 'string' } } }, 'Info')
@@ -14,6 +14,13 @@ const charForClass = (inner) => {
14
14
  const first = inner.replace(/^\^/, "")[0];
15
15
  return first && first !== "\\" ? first : "a";
16
16
  };
17
+ const CONTROL_ESCAPES = {
18
+ n: "\n",
19
+ r: "\r",
20
+ t: " ",
21
+ f: "\f",
22
+ v: "\v"
23
+ };
17
24
  const charForEscape = (esc) => {
18
25
  if (esc === "\\d")
19
26
  return "5";
@@ -21,7 +28,10 @@ const charForEscape = (esc) => {
21
28
  return "a";
22
29
  if (esc === "\\s")
23
30
  return " ";
24
- return esc[1] ?? "a";
31
+ const char = esc[1];
32
+ if (char === void 0)
33
+ return "a";
34
+ return CONTROL_ESCAPES[char] ?? char;
25
35
  };
26
36
  const topLevelAlternatives = (s) => {
27
37
  const parts = [];
@@ -144,34 +154,40 @@ const sampleFromPattern = (pattern, minLength) => {
144
154
  };
145
155
  return sampleAlt(body);
146
156
  };
157
+ const FORMAT_EXAMPLES = {
158
+ email: "user@example.com",
159
+ "idn-email": "user@example.com",
160
+ uuid: "00000000-0000-0000-0000-000000000000",
161
+ uri: "https://example.com",
162
+ iri: "https://example.com",
163
+ // `url` is not a JSON Schema format, but OpenAPI documents use it constantly.
164
+ url: "https://example.com",
165
+ "uri-reference": "https://example.com",
166
+ "iri-reference": "https://example.com",
167
+ "uri-template": "https://example.com/{id}",
168
+ "date-time": "1970-01-01T00:00:00.000Z",
169
+ date: "1970-01-01",
170
+ time: "00:00:00.000Z",
171
+ duration: "P1D",
172
+ "json-pointer": "/example",
173
+ "relative-json-pointer": "0/example",
174
+ hostname: "example.com",
175
+ "idn-hostname": "example.com",
176
+ ipv4: "127.0.0.1",
177
+ ipv6: "::1",
178
+ // The `regex` format asks for a string that *compiles* as a regular expression.
179
+ regex: "^example$"
180
+ };
147
181
  const exampleString = (schema) => {
148
182
  if (hasFormat(schema)) {
149
- switch (schema.format) {
150
- case "email":
151
- return "user@example.com";
152
- case "uuid":
153
- return "00000000-0000-0000-0000-000000000000";
154
- case "uri":
155
- case "url":
156
- return "https://example.com";
157
- case "date-time":
158
- return "1970-01-01T00:00:00.000Z";
159
- case "date":
160
- return "1970-01-01";
161
- case "time":
162
- return "00:00:00.000Z";
163
- case "hostname":
164
- return "example.com";
165
- case "ipv4":
166
- return "127.0.0.1";
167
- case "ipv6":
168
- return "::1";
169
- }
183
+ const formatted = FORMAT_EXAMPLES[schema.format];
184
+ if (formatted !== void 0)
185
+ return formatted;
170
186
  }
171
187
  const minLength = hasMinLength(schema) ? schema.minLength : 0;
172
188
  if (hasPattern(schema)) {
173
189
  const sampled = sampleFromPattern(schema.pattern, minLength);
174
- if (sampled !== void 0 && new RegExp(schema.pattern).test(sampled)) {
190
+ if (sampled !== void 0 && sampled.length >= minLength && new RegExp(schema.pattern).test(sampled)) {
175
191
  if (!(hasMaxLength(schema) && sampled.length > schema.maxLength))
176
192
  return sampled;
177
193
  }
@@ -183,13 +199,61 @@ const exampleString = (schema) => {
183
199
  value = value.slice(0, schema.maxLength);
184
200
  return value;
185
201
  };
186
- const deriveExample = (schema, rootSchema, seen = /* @__PURE__ */ new Set()) => {
202
+ const contexts = /* @__PURE__ */ new WeakMap();
203
+ const newContext = (rootSchema) => ({
204
+ rootSchema,
205
+ values: /* @__PURE__ */ new Map(),
206
+ active: /* @__PURE__ */ new Set(),
207
+ cycleBroken: false
208
+ });
209
+ const contextFor = (rootSchema) => {
210
+ if (rootSchema === void 0)
211
+ return newContext(void 0);
212
+ const existing = contexts.get(rootSchema);
213
+ if (existing)
214
+ return existing;
215
+ const created = newContext(rootSchema);
216
+ contexts.set(rootSchema, created);
217
+ return created;
218
+ };
219
+ const deriveExample = (schema, rootSchema) => {
220
+ const ctx = contextFor(rootSchema);
221
+ ctx.active.clear();
222
+ ctx.cycleBroken = false;
223
+ return derive(schema, ctx);
224
+ };
225
+ const derive = (schema, ctx) => {
187
226
  if (!isSchemaObject(schema))
188
227
  return null;
189
- const base = deriveBase(schema, rootSchema, seen);
190
- return needsValidationFilter(schema) ? refineExample(schema, base, rootSchema, seen) : base;
228
+ const base = deriveBase(schema, ctx);
229
+ return needsValidationFilter(schema) ? refineExample(schema, base, ctx) : base;
191
230
  };
192
- const deriveBase = (schema, rootSchema, seen) => {
231
+ const deriveRef = (ref, ctx) => {
232
+ if (ctx.active.has(ref)) {
233
+ ctx.cycleBroken = true;
234
+ return null;
235
+ }
236
+ if (ctx.values.has(ref))
237
+ return ctx.values.get(ref);
238
+ if (!ctx.rootSchema)
239
+ return null;
240
+ const resolved = resolveRef(ref, ctx.rootSchema);
241
+ if (!resolved)
242
+ return null;
243
+ const outerBroken = ctx.cycleBroken;
244
+ ctx.cycleBroken = false;
245
+ ctx.active.add(ref);
246
+ try {
247
+ const value = derive(resolved, ctx);
248
+ if (!ctx.cycleBroken)
249
+ ctx.values.set(ref, value);
250
+ ctx.cycleBroken = outerBroken || ctx.cycleBroken;
251
+ return value;
252
+ } finally {
253
+ ctx.active.delete(ref);
254
+ }
255
+ };
256
+ const deriveBase = (schema, ctx) => {
193
257
  if (!isSchemaObject(schema))
194
258
  return null;
195
259
  if (hasConst(schema))
@@ -202,15 +266,8 @@ const deriveBase = (schema, rootSchema, seen) => {
202
266
  const fitting = schema.enum.find((value) => satisfiesScalarConstraints(schema, value));
203
267
  return fitting !== void 0 ? fitting : schema.enum[0];
204
268
  }
205
- if (hasRef(schema)) {
206
- const ref = schema.$ref;
207
- if (seen.has(ref) || !rootSchema)
208
- return null;
209
- const resolved = resolveRef(ref, rootSchema);
210
- if (!resolved)
211
- return null;
212
- return deriveExample(resolved, rootSchema, /* @__PURE__ */ new Set([...seen, ref]));
213
- }
269
+ if (hasRef(schema))
270
+ return deriveRef(schema.$ref, ctx);
214
271
  const instanceOf = getMjstInstanceOf(schema);
215
272
  if (instanceOf === "Date")
216
273
  return /* @__PURE__ */ new Date(0);
@@ -218,15 +275,15 @@ const deriveBase = (schema, rootSchema, seen) => {
218
275
  if (primitive === "bigint")
219
276
  return 0n;
220
277
  if (hasAllOf(schema))
221
- return deriveExample(mergeAllOf(schema), rootSchema, seen);
278
+ return derive(mergeAllOf(schema), ctx);
222
279
  if (hasOneOf(schema) && schema.oneOf[0] !== void 0)
223
- return deriveExample(schema.oneOf[0], rootSchema, seen);
280
+ return derive(schema.oneOf[0], ctx);
224
281
  if (hasAnyOf(schema) && schema.anyOf[0] !== void 0)
225
- return deriveExample(schema.anyOf[0], rootSchema, seen);
282
+ return derive(schema.anyOf[0], ctx);
226
283
  if (hasType(schema))
227
- return deriveForType(schema.type, schema, rootSchema, seen);
284
+ return deriveForType(schema.type, schema, ctx);
228
285
  if (Array.isArray(schema.type) && schema.type.length > 0) {
229
- return deriveForType(schema.type[0], schema, rootSchema, seen);
286
+ return deriveForType(schema.type[0], schema, ctx);
230
287
  }
231
288
  return null;
232
289
  };
@@ -237,13 +294,28 @@ const structuralOnly = (schema) => {
237
294
  delete clone[key];
238
295
  return clone;
239
296
  };
240
- const refineExample = (schema, base, rootSchema, seen) => {
241
- const check = makeInstanceCheck(schema, rootSchema);
297
+ const perturb = (value) => {
298
+ if (typeof value === "string")
299
+ return [`${value}-1`, ""];
300
+ if (typeof value === "number")
301
+ return [value + 1, value - 1];
302
+ if (typeof value === "boolean")
303
+ return [!value];
304
+ if (value === null)
305
+ return ["string", 0];
306
+ if (Array.isArray(value))
307
+ return [[], [...value, null]];
308
+ if (typeof value === "object")
309
+ return [{}];
310
+ return [];
311
+ };
312
+ const refineExample = (schema, base, ctx) => {
313
+ const check = makeInstanceCheck(schema, ctx.rootSchema);
242
314
  if (check(base))
243
315
  return base;
244
316
  const raw = schema;
245
317
  const structural = structuralOnly(schema);
246
- const combine = (branch) => deriveExample({ allOf: [structural, branch] }, rootSchema, seen);
318
+ const combine = (branch) => derive({ allOf: [structural, branch] }, ctx);
247
319
  const candidates = [];
248
320
  if (hasOneOf(schema)) {
249
321
  for (const branch of schema.oneOf)
@@ -255,8 +327,10 @@ const refineExample = (schema, base, rootSchema, seen) => {
255
327
  candidates.push(combine(raw["then"]));
256
328
  if (raw["else"] !== void 0)
257
329
  candidates.push(combine(raw["else"]));
258
- candidates.push(deriveExample(structural, rootSchema, seen));
330
+ candidates.push(derive(structural, ctx));
259
331
  }
332
+ if ("not" in raw)
333
+ candidates.push(...perturb(base));
260
334
  for (const candidate of candidates)
261
335
  if (check(candidate))
262
336
  return candidate;
@@ -282,7 +356,7 @@ const satisfiesScalarConstraints = (schema, value) => {
282
356
  }
283
357
  return true;
284
358
  };
285
- const deriveForType = (type, schema, rootSchema, seen) => {
359
+ const deriveForType = (type, schema, ctx) => {
286
360
  switch (type) {
287
361
  case "string":
288
362
  return exampleString(schema);
@@ -294,14 +368,21 @@ const deriveForType = (type, schema, rootSchema, seen) => {
294
368
  case "null":
295
369
  return null;
296
370
  case "array":
297
- return deriveArray(schema, rootSchema, seen);
371
+ return deriveArray(schema, ctx);
298
372
  case "object":
299
- return deriveObject(schema, rootSchema, seen);
373
+ return deriveObject(schema, ctx);
300
374
  default:
301
375
  return null;
302
376
  }
303
377
  };
304
- const deriveObject = (schema, rootSchema, seen) => {
378
+ const setProperty = (out, key, value) => {
379
+ if (key === "__proto__") {
380
+ Object.defineProperty(out, key, { value, writable: true, enumerable: true, configurable: true });
381
+ return;
382
+ }
383
+ out[key] = value;
384
+ };
385
+ const deriveObject = (schema, ctx) => {
305
386
  const out = {};
306
387
  const patternEntries = hasPatternProperties(schema) ? Object.entries(schema.patternProperties).flatMap(([source, sub]) => {
307
388
  try {
@@ -314,7 +395,7 @@ const deriveObject = (schema, rootSchema, seen) => {
314
395
  const additionalSchema = isSchemaObject(additional) ? additional : void 0;
315
396
  const additionalClosed = hasAdditionalProperties(schema) && schema.additionalProperties === false;
316
397
  const extrasAllowed = !additionalClosed || patternEntries.length > 0;
317
- const nameCheck = hasPropertyNames(schema) ? makeInstanceCheck(schema.propertyNames, rootSchema) : void 0;
398
+ const nameCheck = hasPropertyNames(schema) ? makeInstanceCheck(schema.propertyNames, ctx.rootSchema) : void 0;
318
399
  const valueSchemaFor = (key) => {
319
400
  const matches = patternEntries.filter(([re]) => re.test(key)).map(([, sub]) => sub);
320
401
  if (matches.length === 1)
@@ -325,31 +406,33 @@ const deriveObject = (schema, rootSchema, seen) => {
325
406
  };
326
407
  const addKey = (key) => {
327
408
  const sub = valueSchemaFor(key);
328
- out[key] = sub !== void 0 ? deriveExample(sub, rootSchema, seen) : null;
409
+ if (sub === void 0 && additionalClosed)
410
+ return;
411
+ setProperty(out, key, sub !== void 0 ? derive(sub, ctx) : null);
329
412
  };
330
413
  if (hasProperties(schema)) {
331
414
  for (const [key, propSchema] of Object.entries(schema.properties)) {
332
- out[key] = deriveExample(propSchema, rootSchema, seen);
415
+ setProperty(out, key, derive(propSchema, ctx));
333
416
  }
334
417
  }
335
418
  if (hasRequired(schema)) {
336
419
  for (const key of schema.required)
337
- if (!(key in out))
420
+ if (!Object.hasOwn(out, key))
338
421
  addKey(key);
339
422
  }
340
423
  if (hasDependentRequired(schema)) {
341
424
  for (const [trigger, deps] of Object.entries(schema.dependentRequired)) {
342
- if (!(trigger in out))
425
+ if (!Object.hasOwn(out, trigger))
343
426
  continue;
344
427
  for (const dep of deps)
345
- if (!(dep in out))
428
+ if (!Object.hasOwn(out, dep))
346
429
  addKey(dep);
347
430
  }
348
431
  }
349
432
  if (hasDependentSchemas(schema)) {
350
433
  for (const [trigger, sub] of Object.entries(schema.dependentSchemas)) {
351
- if (trigger in out && isSchemaObject(sub))
352
- applyDependentSchema(out, sub, rootSchema, seen);
434
+ if (Object.hasOwn(out, trigger) && isSchemaObject(sub))
435
+ applyDependentSchema(out, sub, ctx);
353
436
  }
354
437
  }
355
438
  if (hasMinProperties(schema) && extrasAllowed) {
@@ -357,7 +440,7 @@ const deriveObject = (schema, rootSchema, seen) => {
357
440
  let guard = 0;
358
441
  while (Object.keys(out).length < schema.minProperties && guard++ < schema.minProperties + 50) {
359
442
  const key = synthKey(n++, patternEntries, schema, nameCheck);
360
- if (key === void 0 || key in out)
443
+ if (key === void 0 || Object.hasOwn(out, key))
361
444
  continue;
362
445
  addKey(key);
363
446
  }
@@ -366,20 +449,20 @@ const deriveObject = (schema, rootSchema, seen) => {
366
449
  enforceMaxProperties(out, schema, schema.maxProperties);
367
450
  return out;
368
451
  };
369
- const applyDependentSchema = (out, sub, rootSchema, seen) => {
452
+ const applyDependentSchema = (out, sub, ctx) => {
370
453
  if (hasProperties(sub)) {
371
454
  for (const [key, propSchema] of Object.entries(sub.properties)) {
372
- if (!(key in out))
373
- out[key] = deriveExample(propSchema, rootSchema, seen);
455
+ if (!Object.hasOwn(out, key))
456
+ setProperty(out, key, derive(propSchema, ctx));
374
457
  }
375
458
  }
376
459
  if (hasRequired(sub)) {
377
460
  const propSchemas = hasProperties(sub) ? sub.properties : {};
378
461
  for (const key of sub.required) {
379
- if (key in out)
462
+ if (Object.hasOwn(out, key))
380
463
  continue;
381
464
  const propSchema = propSchemas[key];
382
- out[key] = propSchema !== void 0 ? deriveExample(propSchema, rootSchema, seen) : null;
465
+ setProperty(out, key, propSchema !== void 0 ? derive(propSchema, ctx) : null);
383
466
  }
384
467
  }
385
468
  };
@@ -410,7 +493,7 @@ const enforceMaxProperties = (out, schema, max) => {
410
493
  const protectedKeys = new Set(hasRequired(schema) ? schema.required : []);
411
494
  if (hasDependentRequired(schema)) {
412
495
  for (const [trigger, deps] of Object.entries(schema.dependentRequired)) {
413
- if (trigger in out)
496
+ if (Object.hasOwn(out, trigger))
414
497
  for (const dep of deps)
415
498
  protectedKeys.add(dep);
416
499
  }
@@ -441,7 +524,7 @@ const deriveNumber = (schema, isInteger) => {
441
524
  }
442
525
  return (isInteger ? Math.round(value) : value) + 0;
443
526
  };
444
- const deriveArray = (schema, rootSchema, seen) => {
527
+ const deriveArray = (schema, ctx) => {
445
528
  const items = hasItems(schema) ? schema.items : void 0;
446
529
  const prefixItems = schema["prefixItems"];
447
530
  const prefix = Array.isArray(prefixItems) ? prefixItems : Array.isArray(items) ? items : void 0;
@@ -449,11 +532,11 @@ const deriveArray = (schema, rootSchema, seen) => {
449
532
  const max = hasMaxItems(schema) ? schema.maxItems : Number.POSITIVE_INFINITY;
450
533
  const rest = items !== void 0 && !Array.isArray(items) && isSchemaObject(items) ? items : void 0;
451
534
  if (prefix) {
452
- const tuple = prefix.map((item) => deriveExample(item, rootSchema, seen));
535
+ const tuple = prefix.map((item) => derive(item, ctx));
453
536
  const itemsClosed = schema["items"] === false;
454
537
  while (tuple.length < min && tuple.length < max) {
455
538
  if (rest !== void 0)
456
- tuple.push(deriveExample(rest, rootSchema, seen));
539
+ tuple.push(derive(rest, ctx));
457
540
  else if (itemsClosed)
458
541
  break;
459
542
  else
@@ -467,15 +550,36 @@ const deriveArray = (schema, rootSchema, seen) => {
467
550
  const minContains = contains !== void 0 ? typeof raw["minContains"] === "number" ? raw["minContains"] : 1 : 0;
468
551
  const unique = hasUniqueItems(schema) && schema.uniqueItems === true;
469
552
  const elem = rest ?? contains;
470
- const count = Math.min(Math.max(min, minContains, max === 0 ? 0 : 1), max);
553
+ const choices = unique && contains === void 0 ? closedValueSet(elem) : void 0;
554
+ const wanted = Math.min(Math.max(min, minContains, max === 0 ? 0 : 1), max);
555
+ const count = choices !== void 0 ? Math.min(wanted, choices.length) : wanted;
471
556
  const result = [];
472
557
  for (let i = 0; i < count; i++) {
558
+ if (choices !== void 0) {
559
+ result.push(choices[i]);
560
+ continue;
561
+ }
473
562
  const itemSchema = contains !== void 0 && i < minContains ? contains : elem;
474
- const base = itemSchema !== void 0 ? deriveExample(itemSchema, rootSchema, seen) : null;
563
+ const base = itemSchema !== void 0 ? derive(itemSchema, ctx) : null;
475
564
  result.push(unique ? distinctify(base, i, itemSchema) : base);
476
565
  }
477
566
  return result;
478
567
  };
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
+ };
479
583
  const distinctify = (base, i, itemSchema) => {
480
584
  if (i === 0)
481
585
  return base;
@@ -542,6 +646,7 @@ const mergeAllOf = (schema) => {
542
646
  merged["required"] = [...required];
543
647
  return merged;
544
648
  };
649
+ const serializeKey = (key) => key === "__proto__" ? '["__proto__"]' : JSON.stringify(key);
545
650
  const serializeValue = (value) => {
546
651
  if (typeof value === "bigint")
547
652
  return `${value}n`;
@@ -550,13 +655,24 @@ const serializeValue = (value) => {
550
655
  if (Array.isArray(value))
551
656
  return `[${value.map(serializeValue).join(", ")}]`;
552
657
  if (value !== null && typeof value === "object") {
553
- const entries = Object.entries(value).filter(([, v]) => v !== void 0).map(([key, v]) => `${JSON.stringify(key)}: ${serializeValue(v)}`);
658
+ const entries = Object.entries(value).filter(([, v]) => v !== void 0).map(([key, v]) => `${serializeKey(key)}: ${serializeValue(v)}`);
554
659
  return `{ ${entries.join(", ")} }`;
555
660
  }
556
661
  return JSON.stringify(value);
557
662
  };
663
+ const arbitraryName = (typeName) => `${typeName}Arbitrary`;
664
+ const WARNING_VALUE_LIMIT = 240;
665
+ const warnInvalidExample = (schema, typeName, value, rootSchema) => {
666
+ const check = makeInstanceCheck(schema, rootSchema, { checkFormats: true });
667
+ if (check(value))
668
+ return;
669
+ const serialized = serializeValue(value);
670
+ const quoted = serialized.length > WARNING_VALUE_LIMIT ? `${serialized.slice(0, WARNING_VALUE_LIMIT)}\u2026` : serialized;
671
+ 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
+ };
558
673
  const generateExampleConst = (schema, typeName, rootSchema) => {
559
674
  const value = deriveExample(schema, rootSchema);
675
+ warnInvalidExample(schema, typeName, value, rootSchema);
560
676
  return `export const ${exampleName(typeName)}: ${typeName} = ${serializeValue(value)}`;
561
677
  };
562
678
  export {
@@ -9,6 +9,7 @@ const VALIDATE_IMPORT_NAME = "__mjstValidate";
9
9
  const VALIDATE_IMPORT_STATEMENT = `import { validate as ${VALIDATE_IMPORT_NAME} } from '@amritk/runtime-validators'`;
10
10
  const lazyRef = (arbName) => `fc.constant(null).chain(() => ${arbName})`;
11
11
  const SELF_KEY = "self";
12
+ const safeLiteralKey = (key) => key === "__proto__" ? '["__proto__"]' : JSON.stringify(key);
12
13
  const stringExpr = (schema) => {
13
14
  if (hasFormat(schema)) {
14
15
  switch (schema.format) {
@@ -220,7 +221,7 @@ const objectExpr = (schema, ctx) => {
220
221
  }
221
222
  return "fc.object()";
222
223
  }
223
- const entries = keys.map((key) => `${JSON.stringify(key)}: ${propArbs.get(key)}`);
224
+ const entries = keys.map((key) => `${safeLiteralKey(key)}: ${propArbs.get(key)}`);
224
225
  const model = `{ ${entries.join(", ")} }`;
225
226
  const record = keys.every((key) => required.has(key)) ? `fc.record(${model})` : `fc.record(${model}, { requiredKeys: [${[...required].map((key) => JSON.stringify(key)).join(", ")}] })`;
226
227
  const needExtras = extrasAllowed && (extraValueArb !== void 0 || minProps !== void 0 && minProps > required.size);
@@ -303,7 +304,8 @@ const arbitraryExpr = (schema, ctx) => {
303
304
  const fitting = members.filter((value) => enumMemberFits(schema, value));
304
305
  const chosen = fitting.length > 0 ? fitting : members;
305
306
  const values = chosen.map((value) => JSON.stringify(value)).join(", ");
306
- return `fc.constantFrom(${values})`;
307
+ const literalSafe = chosen.every((value) => value === null || typeof value !== "object");
308
+ return literalSafe ? `fc.constantFrom(...([${values}] as const))` : `fc.constantFrom(${values})`;
307
309
  }
308
310
  const instanceOf = getMjstInstanceOf(schema);
309
311
  if (instanceOf === "Date")
@@ -340,7 +342,7 @@ const generateArbitrary = (schema, typeName, suffix = "", lazyRefFilenames = /*
340
342
  const validatorName = `${selfArbName}Validator`;
341
343
  const embedded = JSON.stringify(withResolvableDefs(schema, rootSchema));
342
344
  return `const ${validatorName} = ${VALIDATE_IMPORT_NAME}(${embedded})
343
- export const ${selfArbName}: fc.Arbitrary<${typeName}> = (${body}).filter((value) => ${validatorName}(value) === true)`;
345
+ export const ${selfArbName}: fc.Arbitrary<${typeName}> = (${body}).filter((value): value is ${typeName} => ${validatorName}(value) === true)`;
344
346
  }
345
347
  return `export const ${selfArbName}: fc.Arbitrary<${typeName}> = ${body}`;
346
348
  };
@@ -9,7 +9,10 @@ const generateExampleFile = (schema, typeName, options) => {
9
9
  rootSchema: options?.rootSchema,
10
10
  typeSuffix
11
11
  });
12
- const typeDefinition = generateTypeDefinition(schema, typeName, { typeSuffix });
12
+ const typeDefinition = generateTypeDefinition(schema, typeName, {
13
+ typeSuffix,
14
+ ...options?.rootSchema !== void 0 ? { rootSchema: options.rootSchema } : {}
15
+ });
13
16
  const arbitrary = generateArbitrary(schema, typeName, typeSuffix, options?.lazyRefFilenames, options?.rootSchema);
14
17
  const example = generateExampleConst(schema, typeName, options?.rootSchema);
15
18
  let result = `import * as fc from 'fast-check'
@@ -7,14 +7,44 @@ import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
7
7
  */
8
8
  export declare const needsValidationFilter: (schema: JSONSchema) => boolean;
9
9
  /**
10
- * Returns `schema` augmented with the root document's `$defs`/`definitions` so its
11
- * local `$ref`s (`#/$defs/…`) resolve when it is validated in isolation. The
12
- * schema's own definitions win on collision.
10
+ * Returns `schema` augmented with the definitions its local `$ref`s need, so it
11
+ * can be validated (or embedded in generated output) on its own.
12
+ *
13
+ * Only the *reachable* definitions come along. Splicing in the root's entire
14
+ * `$defs` used to make every check quadratic in the size of the document — the
15
+ * validator screens every `pattern` in whatever it is handed, so a 959-definition
16
+ * OpenAPI schema paid for all 959 on each of the thousand-odd checks a single
17
+ * generation run makes — and it also embedded the whole document into every
18
+ * generated file that carries a validating filter. A ref we cannot pin to one
19
+ * definition (an `$anchor` name, or any `$dynamicRef`/`$recursiveRef`) still
20
+ * falls back to the full set, since correctness beats size. The schema's own
21
+ * definitions win on collision.
13
22
  */
14
23
  export declare const withResolvableDefs: (schema: JSONSchema, rootSchema?: Record<string, unknown>) => Record<string, unknown>;
15
24
  /**
16
- * Compiles a boolean validator for `schema` (with the root document's definitions
17
- * spliced in so local `$ref`s resolve). Used at generation time to accept/reject
18
- * candidate example values for keywords the deriver can't satisfy structurally.
25
+ * How strictly a check judges a candidate value.
26
+ *
27
+ * `format` is off for the checks that *steer* derivation (`refineExample` picks
28
+ * among candidates; rejecting one over a format it cannot influence would only
29
+ * discard a better value), and on for the final audit of an emitted example,
30
+ * where an unsupported `format` falling back to the bare `"string"` is exactly
31
+ * the kind of quiet wrongness worth reporting.
19
32
  */
20
- export declare const makeInstanceCheck: (schema: JSONSchema, rootSchema?: Record<string, unknown>) => ((value: unknown) => boolean);
33
+ export type InstanceCheckOptions = {
34
+ readonly checkFormats?: boolean;
35
+ };
36
+ /**
37
+ * Compiles a boolean validator for `schema` (with the definitions its `$ref`s
38
+ * need spliced in). Used at generation time to accept/reject candidate example
39
+ * values for keywords the deriver can't satisfy structurally.
40
+ *
41
+ * A schema the interpreter refuses — a `$ref` pointing outside the document
42
+ * (`#/components/schemas/…` in a bare OpenAPI fragment), or a `pattern` its
43
+ * ReDoS screen rejects — answers `true` for everything instead of throwing.
44
+ * This check is an *opinion* about a candidate value; it must never be the
45
+ * reason a whole generation run dies, and an undecidable schema is not grounds
46
+ * for discarding an otherwise reasonable example. It does warn on the way past,
47
+ * though — a filter that turns itself off without saying so is indistinguishable
48
+ * from one that ran and approved of everything.
49
+ */
50
+ export declare const makeInstanceCheck: (schema: JSONSchema, rootSchema?: Record<string, unknown>, options?: InstanceCheckOptions) => ((value: unknown) => boolean);
@@ -1,3 +1,4 @@
1
+ import { extractRefs } from "@amritk/helpers/extract-refs";
1
2
  import { isSchemaObject } from "@amritk/helpers/schema-guards";
2
3
  import { validateGuard } from "@amritk/runtime-validators";
3
4
  const FILTER_KEYWORDS = /* @__PURE__ */ new Set([
@@ -16,6 +17,7 @@ const FILTER_KEYWORDS = /* @__PURE__ */ new Set([
16
17
  "contains"
17
18
  ]);
18
19
  const SKIP_RECURSE = /* @__PURE__ */ new Set(["enum", "const", "examples", "default", "$ref", "required", "$defs", "definitions"]);
20
+ const filterAnswers = /* @__PURE__ */ new WeakMap();
19
21
  const needsValidationFilter = (schema) => {
20
22
  const walk = (node) => {
21
23
  if (Array.isArray(node))
@@ -35,23 +37,148 @@ const needsValidationFilter = (schema) => {
35
37
  }
36
38
  return false;
37
39
  };
38
- return walk(schema);
40
+ if (typeof schema !== "object" || schema === null)
41
+ return walk(schema);
42
+ const cached = filterAnswers.get(schema);
43
+ if (cached !== void 0)
44
+ return cached;
45
+ const answer = walk(schema);
46
+ filterAnswers.set(schema, answer);
47
+ return answer;
39
48
  };
49
+ const defTargetFor = (ref) => {
50
+ if (ref.startsWith("#")) {
51
+ const fragment = ref.slice(1);
52
+ if (fragment === "")
53
+ return null;
54
+ if (!fragment.startsWith("/"))
55
+ return "all";
56
+ const [container, name] = fragment.slice(1).split("/");
57
+ if (name === void 0 || container !== "$defs" && container !== "definitions")
58
+ return null;
59
+ return { container, name: name.replace(/~1/g, "/").replace(/~0/g, "~") };
60
+ }
61
+ const hash = ref.indexOf("#");
62
+ return { container: "$defs", name: hash === -1 ? ref : ref.slice(0, hash) };
63
+ };
64
+ const DYNAMIC_REF_KEYWORDS = /* @__PURE__ */ new Set(["$dynamicRef", "$recursiveRef"]);
65
+ const dynamicRefAnswers = /* @__PURE__ */ new WeakMap();
66
+ const usesDynamicRef = (node) => {
67
+ if (node === null || typeof node !== "object")
68
+ return false;
69
+ const cached = dynamicRefAnswers.get(node);
70
+ if (cached !== void 0)
71
+ return cached;
72
+ const answer = Array.isArray(node) ? node.some(usesDynamicRef) : Object.entries(node).some(([key, value]) => DYNAMIC_REF_KEYWORDS.has(key) && typeof value === "string" || usesDynamicRef(value));
73
+ dynamicRefAnswers.set(node, answer);
74
+ return answer;
75
+ };
76
+ const reachableDefs = (schema, rootSchema) => {
77
+ if (usesDynamicRef(schema))
78
+ return "all";
79
+ const picked = {
80
+ $defs: /* @__PURE__ */ Object.create(null),
81
+ definitions: /* @__PURE__ */ Object.create(null)
82
+ };
83
+ const seen = /* @__PURE__ */ new Set();
84
+ const queue = [...extractRefs(schema)];
85
+ for (let head = 0; head < queue.length; head++) {
86
+ const ref = queue[head];
87
+ if (ref === void 0 || seen.has(ref))
88
+ continue;
89
+ seen.add(ref);
90
+ const target = defTargetFor(ref);
91
+ if (target === "all")
92
+ return "all";
93
+ if (target === null)
94
+ continue;
95
+ const container = rootSchema[target.container];
96
+ if (container === null || typeof container !== "object")
97
+ continue;
98
+ const definition = container[target.name];
99
+ if (definition === void 0 || Object.hasOwn(picked[target.container], target.name))
100
+ continue;
101
+ if (usesDynamicRef(definition))
102
+ return "all";
103
+ picked[target.container][target.name] = definition;
104
+ for (const nested of extractRefs(definition)) {
105
+ if (!seen.has(nested))
106
+ queue.push(nested);
107
+ }
108
+ }
109
+ return picked;
110
+ };
111
+ const resolvableCache = /* @__PURE__ */ new WeakMap();
40
112
  const withResolvableDefs = (schema, rootSchema) => {
113
+ if (!rootSchema || typeof schema !== "object" || schema === null)
114
+ return spliceDefs(schema, rootSchema);
115
+ const perRoot = resolvableCache.get(rootSchema) ?? /* @__PURE__ */ new WeakMap();
116
+ resolvableCache.set(rootSchema, perRoot);
117
+ const existing = perRoot.get(schema);
118
+ if (existing)
119
+ return existing;
120
+ const spliced = spliceDefs(schema, rootSchema);
121
+ perRoot.set(schema, spliced);
122
+ return spliced;
123
+ };
124
+ const spliceDefs = (schema, rootSchema) => {
41
125
  const base = isSchemaObject(schema) ? { ...schema } : { const: schema };
42
126
  if (!rootSchema)
43
127
  return base;
128
+ const reachable = reachableDefs(schema, rootSchema);
44
129
  for (const key of ["$defs", "definitions"]) {
45
- const rootDefs = rootSchema[key];
46
- if (rootDefs && typeof rootDefs === "object") {
47
- base[key] = { ...rootDefs, ...base[key] ?? {} };
48
- }
130
+ const rootDefs = reachable === "all" ? rootSchema[key] : reachable[key];
131
+ if (!rootDefs || typeof rootDefs !== "object")
132
+ continue;
133
+ if (reachable !== "all" && Object.keys(rootDefs).length === 0 && base[key] === void 0)
134
+ continue;
135
+ base[key] = { ...rootDefs, ...base[key] ?? {} };
49
136
  }
50
137
  return base;
51
138
  };
52
- const makeInstanceCheck = (schema, rootSchema) => {
53
- const guard = validateGuard(withResolvableDefs(schema, rootSchema));
54
- return (value) => guard(value) === true;
139
+ const WARNING_SCHEMA_LIMIT = 240;
140
+ const reportedGuardFailures = /* @__PURE__ */ new Set();
141
+ const describeSchema = (schema) => {
142
+ const serialized = (() => {
143
+ try {
144
+ return JSON.stringify(schema) ?? String(schema);
145
+ } catch {
146
+ return String(schema);
147
+ }
148
+ })();
149
+ return serialized.length > WARNING_SCHEMA_LIMIT ? `${serialized.slice(0, WARNING_SCHEMA_LIMIT)}\u2026` : serialized;
150
+ };
151
+ const warnUndecidableSchema = (schema, reason) => {
152
+ const described = describeSchema(schema);
153
+ const message = reason instanceof Error ? reason.message : String(reason);
154
+ const key = `${message}
155
+ ${described}`;
156
+ if (reportedGuardFailures.has(key))
157
+ return;
158
+ reportedGuardFailures.add(key);
159
+ 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.`);
160
+ };
161
+ const tryGuard = (schema, checkFormats) => {
162
+ try {
163
+ return checkFormats ? validateGuard(schema, { formats: "all" }) : validateGuard(schema);
164
+ } catch (error) {
165
+ warnUndecidableSchema(schema, error);
166
+ return void 0;
167
+ }
168
+ };
169
+ const makeInstanceCheck = (schema, rootSchema, options) => {
170
+ const resolved = withResolvableDefs(schema, rootSchema);
171
+ const guard = tryGuard(resolved, options?.checkFormats === true);
172
+ if (guard === void 0)
173
+ return () => true;
174
+ return (value) => {
175
+ try {
176
+ return guard(value) === true;
177
+ } catch (error) {
178
+ warnUndecidableSchema(resolved, error);
179
+ return true;
180
+ }
181
+ };
55
182
  };
56
183
  export {
57
184
  makeInstanceCheck,
package/package.json CHANGED
@@ -1,9 +1,15 @@
1
1
  {
2
2
  "name": "@amritk/generate-examples",
3
- "version": "0.5.5",
3
+ "version": "0.6.0",
4
4
  "description": "Generate fast-check arbitraries and example values from JSON Schemas.",
5
+ "main": "./dist/index.js",
5
6
  "module": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
6
8
  "type": "module",
9
+ "sideEffects": false,
10
+ "engines": {
11
+ "node": ">=20"
12
+ },
7
13
  "license": "MIT",
8
14
  "author": "amritk",
9
15
  "keywords": [
@@ -48,8 +54,8 @@
48
54
  },
49
55
  "dependencies": {
50
56
  "json-schema-typed": "^8.0.1",
51
- "@amritk/helpers": "0.13.5",
52
- "@amritk/runtime-validators": "0.9.0"
57
+ "@amritk/helpers": "^0.15.0",
58
+ "@amritk/runtime-validators": "^0.10.0"
53
59
  },
54
60
  "devDependencies": {
55
61
  "ajv": "^8.17.1"