@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 +19 -0
- package/README.md +56 -1
- package/dist/generators/assert-generator-depth.d.ts +28 -0
- package/dist/generators/assert-generator-depth.js +10 -0
- package/dist/generators/closed-value-set.d.ts +14 -0
- package/dist/generators/closed-value-set.js +20 -0
- package/dist/generators/collect-example-imports.js +9 -3
- package/dist/generators/derive-example.js +154 -89
- package/dist/generators/generate-arbitrary.js +187 -96
- package/dist/generators/generate-files.js +3 -2
- package/dist/generators/is-object-like.d.ts +47 -0
- package/dist/generators/is-object-like.js +23 -0
- package/dist/generators/satisfies-scalar-constraints.d.ts +17 -0
- package/dist/generators/satisfies-scalar-constraints.js +34 -0
- package/dist/generators/schema-validation.d.ts +12 -0
- package/dist/generators/schema-validation.js +70 -9
- package/package.json +3 -3
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
|
|
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) => {
|