@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.
package/AI.md CHANGED
@@ -47,6 +47,25 @@ const files = await buildExampleSchema(schema, 'User') // → user.ts, index.ts
47
47
  5. **`deriveExample` memoizes each `$ref` per root document**, so the returned
48
48
  value can share sub-objects with the value derived for a sibling schema.
49
49
  Treat it as read-only.
50
+ 6. **`fooExample` is sometimes emitted as `… as Foo`.** A schema can require a
51
+ key the generated type never declares (`required` naming something absent from
52
+ `properties`, a `dependentRequired`/`dependentSchemas` dependency, a
53
+ `minProperties` filler on an object with no index signature). The value keeps
54
+ the key — dropping it would ship a fixture missing what its schema demands —
55
+ so the assertion is what lets the file compile. Do not "clean it up" into a
56
+ bare literal; TypeScript rejects the excess property, and `satisfies` runs the
57
+ very same check.
58
+ 7. **Derived values are capped at 10,000** characters / elements / keys, so a
59
+ schema with an enormous `minLength`/`minItems`/`minProperties` gets a
60
+ short value plus a `console.warn` rather than a file nobody can open.
61
+ `FooArbitrary` still honours the real bound — reach for it when the exact size
62
+ matters.
63
+
64
+ 8. **A `default`/`examples` hint that contradicts its own schema is ignored.**
65
+ The generated type follows the schema, so emitting the hint verbatim produced
66
+ a file that would not compile (`const fooExample: string = 42`). `const` is
67
+ always honoured — the type is its literal type. Do not "restore" a mismatched
68
+ hint; fix the schema instead.
50
69
 
51
70
  Exports: `buildExampleSchema`, `generateArbitrary`, `generateExampleConst`,
52
71
  `deriveExample`, `serializeValue`, `GeneratedFile`. Only the `.` entry.
package/README.md CHANGED
@@ -163,7 +163,7 @@ compiles — but the generator prints a `console.warn` naming the type. Reach fo
163
163
  `FooArbitrary` in those cases: the arbitrary carries a runtime validating filter
164
164
  and stays correct where the static value cannot.
165
165
 
166
- The value falls short for two reasons:
166
+ The value falls short for three reasons:
167
167
 
168
168
  - **The schema has no instance.** `{ pattern: '^ab$', minLength: 5 }`,
169
169
  `uniqueItems` over booleans with `minItems: 3`, a `required` key that
@@ -173,6 +173,61 @@ The value falls short for two reasons:
173
173
  - **The constraint is beyond the deriver.** `pattern` is sampled by a
174
174
  best-effort recursive-descent walk of the regex, so lookarounds and
175
175
  backreferences fall back to `"string"`; an unrecognized `format` does the same.
176
+ - **The bound is larger than any fixture should be.** A derived string, array, or
177
+ object stops growing at 10,000 characters / elements / keys, so a document
178
+ asking for `minLength: 50000000` yields a capped value and a warning rather than
179
+ a 50 MB literal. `FooArbitrary` still honours the real bound.
180
+
181
+ Two more shapes worth knowing about, both of which keep the generated file
182
+ compiling rather than making it correct:
183
+
184
+ - A schema can require a key its **generated type never declares** — `required`
185
+ naming something absent from `properties`, a `dependentRequired` /
186
+ `dependentSchemas` dependency, or a `minProperties` filler on an object with no
187
+ index signature. The example keeps the key (a fixture missing what its schema
188
+ demands is broken data) and is emitted as `… as Foo`, since a bare object
189
+ literal with an excess property fails to compile.
190
+ - An authored `default` or `examples[0]` is used **only when it satisfies its own
191
+ schema**. A hint that does not (`{ type: 'string', default: 42 }` — common in
192
+ documents whose field types changed after the hint was written) is ignored in
193
+ favour of a structurally derived value, because the generated type follows the
194
+ schema and would reject the hint outright. `const` is always honoured: the type
195
+ is the const's own literal type, so the two cannot disagree.
196
+ - An **unsatisfiable range** (`minLength: 10, maxLength: 2`) collapses onto its
197
+ upper bound in the arbitrary. Every bounded `fc.*` combinator asserts
198
+ `min <= max` and throws at *import*, which would take down every other export in
199
+ the file alongside it. Integer bounds are also confined to fast-check's own
200
+ 32-bit range, and length/count bounds to non-negative integers.
201
+ - A **recursive definition's** example has to stop somewhere and stops with
202
+ `null`, which the non-nullable type does not admit — so it is emitted as
203
+ `… as unknown as Node`. `NodeArbitrary` ties the recursion properly through
204
+ `fc.letrec` and needs no such escape.
205
+ - A `pattern` that is **not a valid JavaScript regex**, or that uses a lookahead
206
+ or lookbehind, falls back to a plain `fc.string()`. `fc.stringMatching` compiles
207
+ the pattern at module scope and cannot generate from an assertion, so honouring
208
+ it would throw where the whole file becomes unusable rather than just that one
209
+ arbitrary being loose.
210
+ - **Nesting deeper than 400 levels** is refused with an error naming the limit.
211
+ Building an arbitrary costs several stack frames per schema level, so a deeper
212
+ document exhausts the stack — the cap turns that into a message that says what
213
+ is wrong.
214
+
215
+ Two shapes stay impossible to generate from, and the arbitrary will retry
216
+ forever if you sample it. Both are schemas with no instance, and the example
217
+ warns:
218
+
219
+ - A `pattern` no string of the required length can match
220
+ (`{ pattern: '^[a-z]{2}$', minLength: 5 }`). A satisfiable-but-narrow pairing
221
+ (`{ pattern: '^[a-f0-9]+$', minLength: 32, maxLength: 32 }`) is slow for the
222
+ same reason — `fc.stringMatching` rarely lands on the exact length.
223
+ - A `minLength`/`minItems` so large that no value of that size can be built.
224
+
225
+ One more gap is not this package's to close: a `$ref` that resolves nowhere in
226
+ the document is typed by its name (`Nope`) but never imported, because it was
227
+ never generated as a file. The arbitrary degrades to `fc.anything()`, but the
228
+ type still names it, so the file does not compile. Same for `{ "type": [] }`,
229
+ which types as `export type Foo = ;`. Both come from
230
+ `@amritk/helpers/generate-type-definition`.
176
231
 
177
232
  > [!TIP]
178
233
  > The example for a `$ref` is inlined by value, so a definition graph with wide
@@ -0,0 +1,28 @@
1
+ /**
2
+ * How deep a schema may nest before the two generator recursions give up.
3
+ *
4
+ * Lower than `@amritk/helpers`' `MAX_SCHEMA_DEPTH` of 2000, and deliberately so.
5
+ * That cap suits the helpers' walkers, which spend roughly two stack frames per
6
+ * schema level. Building an arbitrary or deriving a value costs far more —
7
+ * `derive` → `deriveBase` → `deriveForType` → `deriveObject` → `derive` is five
8
+ * frames before the next level even starts, plus whatever `needsValidationFilter`
9
+ * adds — so the real stack runs out first: measured, the arbitrary generator dies
10
+ * somewhere between 500 and 1000 levels and the deriver between 1000 and 1500,
11
+ * both well under 2000. A cap the stack beats to the punch is no cap at all.
12
+ *
13
+ * 400 sits inside the range both were measured to handle, with room for a
14
+ * shallower stack than this one, and it is still an order of magnitude past
15
+ * anything a real document nests — hand-written schemas rarely pass ten levels,
16
+ * and generated ones rarely pass fifty.
17
+ */
18
+ export declare const MAX_GENERATOR_DEPTH = 400;
19
+ /**
20
+ * Throws a message naming the nesting limit once a generator recursion passes it.
21
+ *
22
+ * Call this at the top of each recursive step with the *current* depth. Without
23
+ * it a pathologically nested document — a fuzz case, or a document that lost a
24
+ * closing brace — died with a bare `RangeError: Maximum call stack size exceeded`
25
+ * pointing at whichever helper happened to be deepest, which says nothing about
26
+ * the input that caused it.
27
+ */
28
+ export declare const assertGeneratorDepth: (depth: number, generator: string) => void;
@@ -0,0 +1,10 @@
1
+ const MAX_GENERATOR_DEPTH = 400;
2
+ const assertGeneratorDepth = (depth, generator) => {
3
+ if (depth > MAX_GENERATOR_DEPTH) {
4
+ throw new Error(`Schema nesting exceeds ${MAX_GENERATOR_DEPTH} levels while running ${generator}. Flatten the document (or split the deepest subschema into a $defs entry and $ref it) and try again.`);
5
+ }
6
+ };
7
+ export {
8
+ MAX_GENERATOR_DEPTH,
9
+ assertGeneratorDepth
10
+ };
@@ -0,0 +1,14 @@
1
+ import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
2
+ /**
3
+ * The complete set of values an item schema admits, when that set is finite and
4
+ * small enough to enumerate: `const`, `enum`, `boolean`, `null`. `uniqueItems`
5
+ * needs distinct elements, and for these schemas perturbing one value would step
6
+ * straight out of the schema, so the deriver walks this set instead of nudging.
7
+ * `undefined` means the value space is open and perturbing is fine.
8
+ *
9
+ * The arbitrary generator asks the same question for a different reason: a set
10
+ * of N values cannot fill a `uniqueArray` of more than N slots, and fast-check
11
+ * retries forever rather than admitting it — so `{ items: { type: 'boolean' },
12
+ * minItems: 5, uniqueItems: true }` produced an arbitrary that hung when sampled.
13
+ */
14
+ export declare const closedValueSet: (schema: JSONSchema | undefined) => unknown[] | undefined;
@@ -0,0 +1,20 @@
1
+ import { hasConst, hasEnum, hasType, isSchemaObject } from "@amritk/helpers/schema-guards";
2
+ import { satisfiesScalarConstraints } from "./satisfies-scalar-constraints.js";
3
+ const closedValueSet = (schema) => {
4
+ if (schema === void 0 || !isSchemaObject(schema))
5
+ return void 0;
6
+ if (hasConst(schema))
7
+ return [schema.const];
8
+ if (hasEnum(schema)) {
9
+ const fitting = schema.enum.filter((value) => satisfiesScalarConstraints(schema, value));
10
+ return fitting.length > 0 ? fitting : [...schema.enum];
11
+ }
12
+ if (hasType(schema) && schema.type === "boolean")
13
+ return [true, false];
14
+ if (hasType(schema) && schema.type === "null")
15
+ return [null];
16
+ return void 0;
17
+ };
18
+ export {
19
+ closedValueSet
20
+ };
@@ -1,20 +1,22 @@
1
1
  import { refToFilename } from "@amritk/helpers/ref-to-filename";
2
2
  import { refToName } from "@amritk/helpers/ref-to-name";
3
3
  import { resolveRef } from "@amritk/helpers/resolve-ref";
4
- import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasOneOf, hasProperties, hasRef, isSchemaObject } from "@amritk/helpers/schema-guards";
4
+ import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasContains, hasDependentSchemas, hasOneOf, hasProperties, hasRef, isSchemaObject } from "@amritk/helpers/schema-guards";
5
+ import { assertGeneratorDepth } from "./assert-generator-depth.js";
5
6
  const buildImport = (ref, suffix) => {
6
7
  const filename = refToFilename(ref);
7
8
  const typeName = refToName(ref, suffix);
8
9
  return `import { type ${typeName}, ${typeName}Arbitrary } from './${filename}.js'`;
9
10
  };
10
- const collectRefs = (schema) => {
11
+ const collectRefs = (schema, depth = 0) => {
12
+ assertGeneratorDepth(depth, "collectExampleImports");
11
13
  if (!isSchemaObject(schema))
12
14
  return [];
13
15
  if (hasRef(schema))
14
16
  return [schema.$ref];
15
17
  const refs = [];
16
18
  const visit = (sub) => {
17
- refs.push(...collectRefs(sub));
19
+ refs.push(...collectRefs(sub, depth + 1));
18
20
  };
19
21
  if (hasOneOf(schema))
20
22
  schema.oneOf.forEach(visit);
@@ -32,6 +34,8 @@ const collectRefs = (schema) => {
32
34
  if (hasAdditionalProperties(schema) && isSchemaObject(schema.additionalProperties)) {
33
35
  visit(schema.additionalProperties);
34
36
  }
37
+ if (hasDependentSchemas(schema))
38
+ Object.values(schema.dependentSchemas).forEach(visit);
35
39
  const prefixItems = raw["prefixItems"];
36
40
  if (Array.isArray(prefixItems))
37
41
  prefixItems.forEach(visit);
@@ -40,6 +44,8 @@ const collectRefs = (schema) => {
40
44
  items.forEach(visit);
41
45
  else if (isSchemaObject(items))
42
46
  visit(items);
47
+ if (hasContains(schema) && isSchemaObject(schema.contains))
48
+ visit(schema.contains);
43
49
  return refs;
44
50
  };
45
51
  const collectExampleImports = (schema, options) => {