@amritk/generate-examples 0.6.4 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +144 -87
- 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 +37 -0
- 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) => {
|
|
@@ -2,6 +2,10 @@ import { getMjstInstanceOf, getMjstPrimitive } from "@amritk/helpers/mjst-extens
|
|
|
2
2
|
import { readKey } from "@amritk/helpers/read-key";
|
|
3
3
|
import { resolveRef } from "@amritk/helpers/resolve-ref";
|
|
4
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";
|
|
5
9
|
import { makeInstanceCheck, needsValidationFilter } from "./schema-validation.js";
|
|
6
10
|
const lowerFirst = (name) => name.charAt(0).toLowerCase() + name.slice(1);
|
|
7
11
|
const exampleName = (typeName) => `${lowerFirst(typeName)}Example`;
|
|
@@ -186,13 +190,14 @@ const FORMAT_EXAMPLES = {
|
|
|
186
190
|
// The `regex` format asks for a string that *compiles* as a regular expression.
|
|
187
191
|
regex: "^example$"
|
|
188
192
|
};
|
|
193
|
+
const MAX_DERIVED_SIZE = 1e4;
|
|
189
194
|
const exampleString = (schema) => {
|
|
190
195
|
if (hasFormat(schema)) {
|
|
191
196
|
const formatted = readKey(FORMAT_EXAMPLES, schema.format);
|
|
192
197
|
if (formatted !== void 0)
|
|
193
198
|
return formatted;
|
|
194
199
|
}
|
|
195
|
-
const minLength = hasMinLength(schema) ? schema.minLength : 0;
|
|
200
|
+
const minLength = Math.min(hasMinLength(schema) ? schema.minLength : 0, MAX_DERIVED_SIZE);
|
|
196
201
|
if (hasPattern(schema)) {
|
|
197
202
|
const sampled = sampleFromPattern(schema.pattern, minLength);
|
|
198
203
|
if (sampled !== void 0 && sampled.length >= minLength && matchesPattern(schema.pattern, sampled)) {
|
|
@@ -212,7 +217,9 @@ const newContext = (rootSchema) => ({
|
|
|
212
217
|
rootSchema,
|
|
213
218
|
values: /* @__PURE__ */ new Map(),
|
|
214
219
|
active: /* @__PURE__ */ new Set(),
|
|
215
|
-
cycleBroken: false
|
|
220
|
+
cycleBroken: false,
|
|
221
|
+
needsAssertion: false,
|
|
222
|
+
depth: 0
|
|
216
223
|
});
|
|
217
224
|
const contextFor = (rootSchema) => {
|
|
218
225
|
if (rootSchema === void 0)
|
|
@@ -224,52 +231,76 @@ const contextFor = (rootSchema) => {
|
|
|
224
231
|
contexts.set(rootSchema, created);
|
|
225
232
|
return created;
|
|
226
233
|
};
|
|
227
|
-
const deriveExample = (schema, rootSchema) =>
|
|
234
|
+
const deriveExample = (schema, rootSchema) => deriveReported(schema, rootSchema).value;
|
|
235
|
+
const deriveReported = (schema, rootSchema) => {
|
|
228
236
|
const ctx = contextFor(rootSchema);
|
|
229
237
|
ctx.active.clear();
|
|
230
238
|
ctx.cycleBroken = false;
|
|
231
|
-
|
|
239
|
+
ctx.needsAssertion = false;
|
|
240
|
+
ctx.depth = 0;
|
|
241
|
+
const value = derive(schema, ctx);
|
|
242
|
+
return { value, needsAssertion: ctx.needsAssertion, cycleBroken: ctx.cycleBroken };
|
|
232
243
|
};
|
|
233
244
|
const derive = (schema, ctx) => {
|
|
245
|
+
assertGeneratorDepth(ctx.depth, "deriveExample");
|
|
234
246
|
if (!isSchemaObject(schema))
|
|
235
247
|
return null;
|
|
236
|
-
|
|
237
|
-
|
|
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
|
+
}
|
|
238
255
|
};
|
|
239
256
|
const deriveRef = (ref, ctx) => {
|
|
240
257
|
if (ctx.active.has(ref)) {
|
|
241
258
|
ctx.cycleBroken = true;
|
|
242
259
|
return null;
|
|
243
260
|
}
|
|
244
|
-
|
|
245
|
-
|
|
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
|
+
}
|
|
246
267
|
if (!ctx.rootSchema)
|
|
247
268
|
return null;
|
|
248
269
|
const resolved = resolveRef(ref, ctx.rootSchema);
|
|
249
270
|
if (!resolved)
|
|
250
271
|
return null;
|
|
251
272
|
const outerBroken = ctx.cycleBroken;
|
|
273
|
+
const outerUndeclared = ctx.needsAssertion;
|
|
252
274
|
ctx.cycleBroken = false;
|
|
275
|
+
ctx.needsAssertion = false;
|
|
253
276
|
ctx.active.add(ref);
|
|
254
277
|
try {
|
|
255
278
|
const value = derive(resolved, ctx);
|
|
256
279
|
if (!ctx.cycleBroken)
|
|
257
|
-
ctx.values.set(ref, value);
|
|
280
|
+
ctx.values.set(ref, { value, needsAssertion: ctx.needsAssertion });
|
|
258
281
|
ctx.cycleBroken = outerBroken || ctx.cycleBroken;
|
|
282
|
+
ctx.needsAssertion = outerUndeclared || ctx.needsAssertion;
|
|
259
283
|
return value;
|
|
260
284
|
} finally {
|
|
261
285
|
ctx.active.delete(ref);
|
|
262
286
|
}
|
|
263
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
|
+
};
|
|
264
296
|
const deriveBase = (schema, ctx) => {
|
|
265
297
|
if (!isSchemaObject(schema))
|
|
266
298
|
return null;
|
|
267
299
|
if (hasConst(schema))
|
|
268
300
|
return schema.const;
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
return schema.default;
|
|
301
|
+
const hint = authoredHint(schema);
|
|
302
|
+
if (hint !== void 0 && makeInstanceCheck(schema, ctx.rootSchema)(hint.value))
|
|
303
|
+
return hint.value;
|
|
273
304
|
if (hasEnum(schema) && schema.enum.length > 0) {
|
|
274
305
|
const fitting = schema.enum.find((value) => satisfiesScalarConstraints(schema, value));
|
|
275
306
|
return fitting !== void 0 ? fitting : schema.enum[0];
|
|
@@ -284,10 +315,16 @@ const deriveBase = (schema, ctx) => {
|
|
|
284
315
|
return 0n;
|
|
285
316
|
if (hasAllOf(schema))
|
|
286
317
|
return derive(mergeAllOf(schema), ctx);
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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);
|
|
291
328
|
if (hasType(schema))
|
|
292
329
|
return deriveForType(schema.type, schema, ctx);
|
|
293
330
|
if (Array.isArray(schema.type) && schema.type.length > 0) {
|
|
@@ -295,6 +332,12 @@ const deriveBase = (schema, ctx) => {
|
|
|
295
332
|
}
|
|
296
333
|
return null;
|
|
297
334
|
};
|
|
335
|
+
const withoutCombinators = (schema) => {
|
|
336
|
+
const rest = { ...schema };
|
|
337
|
+
delete rest["oneOf"];
|
|
338
|
+
delete rest["anyOf"];
|
|
339
|
+
return rest;
|
|
340
|
+
};
|
|
298
341
|
const REFINED_APPLICATORS = ["if", "then", "else", "not", "oneOf"];
|
|
299
342
|
const structuralOnly = (schema) => {
|
|
300
343
|
const clone = { ...schema };
|
|
@@ -344,26 +387,6 @@ const refineExample = (schema, base, ctx) => {
|
|
|
344
387
|
return candidate;
|
|
345
388
|
return base;
|
|
346
389
|
};
|
|
347
|
-
const satisfiesScalarConstraints = (schema, value) => {
|
|
348
|
-
if (typeof value === "string") {
|
|
349
|
-
if (hasMinLength(schema) && value.length < schema.minLength)
|
|
350
|
-
return false;
|
|
351
|
-
if (hasMaxLength(schema) && value.length > schema.maxLength)
|
|
352
|
-
return false;
|
|
353
|
-
} else if (typeof value === "number") {
|
|
354
|
-
if (hasMinimum(schema) && value < schema.minimum)
|
|
355
|
-
return false;
|
|
356
|
-
if (hasMaximum(schema) && value > schema.maximum)
|
|
357
|
-
return false;
|
|
358
|
-
if (hasExclusiveMinimum(schema) && value <= schema.exclusiveMinimum)
|
|
359
|
-
return false;
|
|
360
|
-
if (hasExclusiveMaximum(schema) && value >= schema.exclusiveMaximum)
|
|
361
|
-
return false;
|
|
362
|
-
if (hasMultipleOf(schema) && schema.multipleOf > 0 && value % schema.multipleOf !== 0)
|
|
363
|
-
return false;
|
|
364
|
-
}
|
|
365
|
-
return true;
|
|
366
|
-
};
|
|
367
390
|
const deriveForType = (type, schema, ctx) => {
|
|
368
391
|
switch (type) {
|
|
369
392
|
case "string":
|
|
@@ -404,6 +427,15 @@ const deriveObject = (schema, ctx) => {
|
|
|
404
427
|
const additionalClosed = hasAdditionalProperties(schema) && schema.additionalProperties === false;
|
|
405
428
|
const extrasAllowed = !additionalClosed || patternEntries.length > 0;
|
|
406
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;
|
|
407
439
|
const valueSchemaFor = (key) => {
|
|
408
440
|
const matches = patternEntries.filter(([re]) => re.test(key)).map(([, sub]) => sub);
|
|
409
441
|
if (matches.length === 1)
|
|
@@ -416,6 +448,7 @@ const deriveObject = (schema, ctx) => {
|
|
|
416
448
|
const sub = valueSchemaFor(key);
|
|
417
449
|
if (sub === void 0 && additionalClosed)
|
|
418
450
|
return;
|
|
451
|
+
noteKey(key);
|
|
419
452
|
setProperty(out, key, sub !== void 0 ? derive(sub, ctx) : null);
|
|
420
453
|
};
|
|
421
454
|
if (hasProperties(schema)) {
|
|
@@ -424,9 +457,10 @@ const deriveObject = (schema, ctx) => {
|
|
|
424
457
|
}
|
|
425
458
|
}
|
|
426
459
|
if (hasRequired(schema)) {
|
|
427
|
-
for (const key of schema.required)
|
|
428
|
-
if (!Object.hasOwn(out, key))
|
|
460
|
+
for (const key of schema.required) {
|
|
461
|
+
if (typeof key === "string" && !Object.hasOwn(out, key))
|
|
429
462
|
addKey(key);
|
|
463
|
+
}
|
|
430
464
|
}
|
|
431
465
|
if (hasDependentRequired(schema)) {
|
|
432
466
|
for (const [trigger, deps] of Object.entries(schema.dependentRequired)) {
|
|
@@ -440,63 +474,78 @@ const deriveObject = (schema, ctx) => {
|
|
|
440
474
|
if (hasDependentSchemas(schema)) {
|
|
441
475
|
for (const [trigger, sub] of Object.entries(schema.dependentSchemas)) {
|
|
442
476
|
if (Object.hasOwn(out, trigger) && isSchemaObject(sub))
|
|
443
|
-
applyDependentSchema(out, sub, ctx);
|
|
477
|
+
applyDependentSchema(out, sub, ctx, noteKey);
|
|
444
478
|
}
|
|
445
479
|
}
|
|
446
480
|
if (hasMinProperties(schema) && extrasAllowed) {
|
|
481
|
+
const target = Math.min(schema.minProperties, MAX_DERIVED_SIZE);
|
|
482
|
+
let count = Object.keys(out).length;
|
|
447
483
|
let n = 0;
|
|
448
|
-
let
|
|
449
|
-
while (
|
|
484
|
+
let attempts = 0;
|
|
485
|
+
while (count < target && attempts++ < target + 50) {
|
|
450
486
|
const key = synthKey(n++, patternEntries, schema, nameCheck);
|
|
451
|
-
if (key === void 0
|
|
487
|
+
if (key === void 0)
|
|
488
|
+
break;
|
|
489
|
+
if (Object.hasOwn(out, key))
|
|
452
490
|
continue;
|
|
453
491
|
addKey(key);
|
|
492
|
+
if (Object.hasOwn(out, key))
|
|
493
|
+
count++;
|
|
454
494
|
}
|
|
455
495
|
}
|
|
456
496
|
if (hasMaxProperties(schema))
|
|
457
497
|
enforceMaxProperties(out, schema, schema.maxProperties);
|
|
458
498
|
return out;
|
|
459
499
|
};
|
|
460
|
-
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
|
+
};
|
|
461
505
|
if (hasProperties(sub)) {
|
|
462
506
|
for (const [key, propSchema] of Object.entries(sub.properties)) {
|
|
463
507
|
if (!Object.hasOwn(out, key))
|
|
464
|
-
|
|
508
|
+
add(key, propSchema);
|
|
465
509
|
}
|
|
466
510
|
}
|
|
467
511
|
if (hasRequired(sub)) {
|
|
468
512
|
const propSchemas = hasProperties(sub) ? sub.properties : {};
|
|
469
513
|
for (const key of sub.required) {
|
|
470
|
-
if (Object.hasOwn(out, key))
|
|
514
|
+
if (typeof key !== "string" || Object.hasOwn(out, key))
|
|
471
515
|
continue;
|
|
472
|
-
|
|
473
|
-
setProperty(out, key, propSchema !== void 0 ? derive(propSchema, ctx) : null);
|
|
516
|
+
add(key, propSchemas[key]);
|
|
474
517
|
}
|
|
475
518
|
}
|
|
476
519
|
};
|
|
520
|
+
const MAX_SYNTH_KEY_LENGTH = 64;
|
|
477
521
|
const synthKey = (i, patternEntries, schema, nameCheck) => {
|
|
478
|
-
const
|
|
522
|
+
const variants = (seed) => i === 0 ? [seed] : [`${seed}${i}`, seed.repeat(i + 1)].filter((key) => key.length <= MAX_SYNTH_KEY_LENGTH);
|
|
479
523
|
const seeds = [];
|
|
480
524
|
for (const [re] of patternEntries) {
|
|
481
525
|
const sampled = sampleFromPattern(re.source, 0);
|
|
482
|
-
if (sampled !== void 0 && sampled.length > 0)
|
|
483
|
-
seeds.push(
|
|
526
|
+
if (sampled !== void 0 && sampled.length > 0) {
|
|
527
|
+
seeds.push({ seed: sampled, matches: (key) => re.test(key) });
|
|
528
|
+
}
|
|
484
529
|
}
|
|
485
530
|
const propertyNames = isSchemaObject(schema) && hasPropertyNames(schema) ? schema.propertyNames : void 0;
|
|
486
531
|
if (propertyNames !== void 0 && isSchemaObject(propertyNames) && hasPattern(propertyNames)) {
|
|
487
532
|
const sampled = sampleFromPattern(propertyNames.pattern, 0);
|
|
488
533
|
if (sampled !== void 0 && sampled.length > 0)
|
|
489
|
-
seeds.push(
|
|
534
|
+
seeds.push({ seed: sampled, matches: () => true });
|
|
490
535
|
}
|
|
491
|
-
seeds.push(
|
|
492
|
-
for (const seed of seeds) {
|
|
493
|
-
|
|
494
|
-
|
|
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
|
+
}
|
|
495
542
|
}
|
|
496
543
|
return void 0;
|
|
497
544
|
};
|
|
498
545
|
const enforceMaxProperties = (out, schema, max) => {
|
|
499
|
-
|
|
546
|
+
const keys = Object.keys(out);
|
|
547
|
+
let count = keys.length;
|
|
548
|
+
if (count <= max)
|
|
500
549
|
return;
|
|
501
550
|
const protectedKeys = new Set(hasRequired(schema) ? schema.required : []);
|
|
502
551
|
if (hasDependentRequired(schema)) {
|
|
@@ -506,11 +555,13 @@ const enforceMaxProperties = (out, schema, max) => {
|
|
|
506
555
|
protectedKeys.add(dep);
|
|
507
556
|
}
|
|
508
557
|
}
|
|
509
|
-
for (const key of
|
|
510
|
-
if (
|
|
558
|
+
for (const key of keys) {
|
|
559
|
+
if (count <= max)
|
|
511
560
|
break;
|
|
512
|
-
if (
|
|
513
|
-
|
|
561
|
+
if (protectedKeys.has(key))
|
|
562
|
+
continue;
|
|
563
|
+
delete out[key];
|
|
564
|
+
count--;
|
|
514
565
|
}
|
|
515
566
|
};
|
|
516
567
|
const deriveNumber = (schema, isInteger) => {
|
|
@@ -524,7 +575,7 @@ const deriveNumber = (schema, isInteger) => {
|
|
|
524
575
|
let value = Number.isFinite(lo) ? lo : Number.isFinite(hi) ? Math.min(0, hi) : 0;
|
|
525
576
|
if (isInteger)
|
|
526
577
|
value = Math.ceil(value);
|
|
527
|
-
if (hasMultipleOf(schema) && schema.multipleOf > 0) {
|
|
578
|
+
if (hasMultipleOf(schema) && Number.isFinite(schema.multipleOf) && schema.multipleOf > 0) {
|
|
528
579
|
const m = schema.multipleOf;
|
|
529
580
|
value = Math.ceil(value / m - 1e-9) * m;
|
|
530
581
|
if (value > hi && Number.isFinite(hi))
|
|
@@ -559,35 +610,27 @@ const deriveArray = (schema, ctx) => {
|
|
|
559
610
|
const unique = hasUniqueItems(schema) && schema.uniqueItems === true;
|
|
560
611
|
const elem = rest ?? contains;
|
|
561
612
|
const choices = unique && contains === void 0 ? closedValueSet(elem) : void 0;
|
|
562
|
-
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);
|
|
563
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;
|
|
564
616
|
const result = [];
|
|
565
617
|
for (let i = 0; i < count; i++) {
|
|
566
618
|
if (choices !== void 0) {
|
|
567
619
|
result.push(choices[i]);
|
|
568
620
|
continue;
|
|
569
621
|
}
|
|
570
|
-
const
|
|
571
|
-
const
|
|
572
|
-
|
|
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);
|
|
573
631
|
}
|
|
574
632
|
return result;
|
|
575
633
|
};
|
|
576
|
-
const closedValueSet = (schema) => {
|
|
577
|
-
if (schema === void 0 || !isSchemaObject(schema))
|
|
578
|
-
return void 0;
|
|
579
|
-
if (hasConst(schema))
|
|
580
|
-
return [schema.const];
|
|
581
|
-
if (hasEnum(schema)) {
|
|
582
|
-
const fitting = schema.enum.filter((value) => satisfiesScalarConstraints(schema, value));
|
|
583
|
-
return fitting.length > 0 ? fitting : [...schema.enum];
|
|
584
|
-
}
|
|
585
|
-
if (hasType(schema) && schema.type === "boolean")
|
|
586
|
-
return [true, false];
|
|
587
|
-
if (hasType(schema) && schema.type === "null")
|
|
588
|
-
return [null];
|
|
589
|
-
return void 0;
|
|
590
|
-
};
|
|
591
634
|
const distinctify = (base, i, itemSchema) => {
|
|
592
635
|
if (i === 0)
|
|
593
636
|
return base;
|
|
@@ -613,10 +656,20 @@ const TIGHTEST = /* @__PURE__ */ new Map([
|
|
|
613
656
|
["maxItems", "min"],
|
|
614
657
|
["maxProperties", "min"]
|
|
615
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
|
+
};
|
|
616
669
|
const mergeAllOf = (schema) => {
|
|
617
|
-
const branches = hasAllOf(schema) ? schema.allOf : [];
|
|
618
|
-
const merged =
|
|
619
|
-
const properties =
|
|
670
|
+
const branches = hasAllOf(schema) ? flattenBranches(schema.allOf) : [];
|
|
671
|
+
const merged = /* @__PURE__ */ Object.create(null);
|
|
672
|
+
const properties = /* @__PURE__ */ Object.create(null);
|
|
620
673
|
const required = /* @__PURE__ */ new Set();
|
|
621
674
|
for (const branch of [...branches, schema]) {
|
|
622
675
|
if (!isSchemaObject(branch))
|
|
@@ -644,7 +697,7 @@ const mergeAllOf = (schema) => {
|
|
|
644
697
|
}
|
|
645
698
|
}
|
|
646
699
|
}
|
|
647
|
-
const mergedProps =
|
|
700
|
+
const mergedProps = /* @__PURE__ */ Object.create(null);
|
|
648
701
|
for (const [prop, schemas] of Object.entries(properties)) {
|
|
649
702
|
mergedProps[prop] = schemas.length === 1 ? schemas[0] : { allOf: schemas };
|
|
650
703
|
}
|
|
@@ -679,9 +732,13 @@ const warnInvalidExample = (schema, typeName, value, rootSchema) => {
|
|
|
679
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.`);
|
|
680
733
|
};
|
|
681
734
|
const generateExampleConst = (schema, typeName, rootSchema) => {
|
|
682
|
-
const value =
|
|
735
|
+
const { value, needsAssertion, cycleBroken } = deriveReported(schema, rootSchema);
|
|
683
736
|
warnInvalidExample(schema, typeName, value, rootSchema);
|
|
684
|
-
|
|
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}`;
|
|
685
742
|
};
|
|
686
743
|
export {
|
|
687
744
|
deriveExample,
|