@amritk/generate-examples 0.5.6 → 0.6.1
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 -3
- package/README.md +40 -4
- package/dist/generators/derive-example.d.ts +17 -5
- package/dist/generators/derive-example.js +186 -70
- package/dist/generators/generate-arbitrary.js +5 -3
- package/dist/generators/schema-validation.d.ts +37 -7
- package/dist/generators/schema-validation.js +135 -8
- package/package.json +6 -3
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
|
|
31
|
-
|
|
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
|
@@ -39,6 +39,15 @@ An `index.ts` barrel re-exports everything.
|
|
|
39
39
|
> and the presence-gated object keywords) also imports `@amritk/runtime-validators`
|
|
40
40
|
> for a post-generation validating filter; files that need no such filter don't.
|
|
41
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.
|
|
42
51
|
|
|
43
52
|
---
|
|
44
53
|
|
|
@@ -128,8 +137,7 @@ const res = await fetch('/users', { method: 'POST', body: JSON.stringify(userExa
|
|
|
128
137
|
`type` — including multi-type unions like `['string', 'null']` —
|
|
129
138
|
(string/number/integer/boolean/null/array/object), `properties`,
|
|
130
139
|
`required`, `items`, `minItems`/`maxItems`, `uniqueItems`,
|
|
131
|
-
`minLength`/`maxLength`, `pattern`, `format
|
|
132
|
-
`date`, `date-time`, `time`, `hostname`, `ipv4`, `ipv6`), `minimum`/`maximum`,
|
|
140
|
+
`minLength`/`maxLength`, `pattern`, `format`, `minimum`/`maximum`,
|
|
133
141
|
`exclusiveMinimum`/`exclusiveMaximum`, `multipleOf`, `enum` (filtered by sibling
|
|
134
142
|
constraints), `const`, `minProperties`/`maxProperties`, `patternProperties`,
|
|
135
143
|
`propertyNames`, `dependentRequired`, `dependentSchemas`, `contains`,
|
|
@@ -139,9 +147,37 @@ enforced by validating generated candidates against the schema and
|
|
|
139
147
|
retrying/rejecting. Unsupported constructs degrade to `fc.anything()` in
|
|
140
148
|
arbitraries and `null` in static examples.
|
|
141
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
|
+
|
|
142
177
|
> [!TIP]
|
|
143
|
-
>
|
|
144
|
-
>
|
|
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.
|
|
145
181
|
|
|
146
182
|
---
|
|
147
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
|
|
8
|
-
* `null`
|
|
7
|
+
* `$ref`s are resolved and inlined by value; a recursive ref short-circuits to
|
|
8
|
+
* `null` where the cycle closes.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
|
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,
|
|
190
|
-
return needsValidationFilter(schema) ? refineExample(schema, base,
|
|
228
|
+
const base = deriveBase(schema, ctx);
|
|
229
|
+
return needsValidationFilter(schema) ? refineExample(schema, base, ctx) : base;
|
|
191
230
|
};
|
|
192
|
-
const
|
|
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
|
-
|
|
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
|
|
278
|
+
return derive(mergeAllOf(schema), ctx);
|
|
222
279
|
if (hasOneOf(schema) && schema.oneOf[0] !== void 0)
|
|
223
|
-
return
|
|
280
|
+
return derive(schema.oneOf[0], ctx);
|
|
224
281
|
if (hasAnyOf(schema) && schema.anyOf[0] !== void 0)
|
|
225
|
-
return
|
|
282
|
+
return derive(schema.anyOf[0], ctx);
|
|
226
283
|
if (hasType(schema))
|
|
227
|
-
return deriveForType(schema.type, schema,
|
|
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,
|
|
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
|
|
241
|
-
|
|
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) =>
|
|
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(
|
|
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,
|
|
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,
|
|
371
|
+
return deriveArray(schema, ctx);
|
|
298
372
|
case "object":
|
|
299
|
-
return deriveObject(schema,
|
|
373
|
+
return deriveObject(schema, ctx);
|
|
300
374
|
default:
|
|
301
375
|
return null;
|
|
302
376
|
}
|
|
303
377
|
};
|
|
304
|
-
const
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
425
|
+
if (!Object.hasOwn(out, trigger))
|
|
343
426
|
continue;
|
|
344
427
|
for (const dep of deps)
|
|
345
|
-
if (!(dep
|
|
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
|
|
352
|
-
applyDependentSchema(out, sub,
|
|
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
|
|
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,
|
|
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
|
|
373
|
-
out
|
|
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
|
|
462
|
+
if (Object.hasOwn(out, key))
|
|
380
463
|
continue;
|
|
381
464
|
const propSchema = propSchemas[key];
|
|
382
|
-
out
|
|
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
|
|
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,
|
|
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) =>
|
|
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(
|
|
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
|
|
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 ?
|
|
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]) => `${
|
|
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) => `${
|
|
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
|
-
|
|
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
|
};
|
|
@@ -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
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
|
47
|
-
|
|
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
|
|
53
|
-
|
|
54
|
-
|
|
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,8 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amritk/generate-examples",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
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",
|
|
7
9
|
"sideEffects": false,
|
|
8
10
|
"engines": {
|
|
@@ -38,6 +40,7 @@
|
|
|
38
40
|
},
|
|
39
41
|
"scripts": {
|
|
40
42
|
"build": "tsgo -p tsconfig.build.json && tsc-alias -p tsconfig.build.json -f && node ../../scripts/strip-comments.mjs",
|
|
43
|
+
"prepublishOnly": "node ../../scripts/check-publishable.mjs",
|
|
41
44
|
"types:check": "tsgo -p . --noEmit",
|
|
42
45
|
"test": "NODE_ENV=production vitest run --root ../.. generate-examples"
|
|
43
46
|
},
|
|
@@ -52,8 +55,8 @@
|
|
|
52
55
|
},
|
|
53
56
|
"dependencies": {
|
|
54
57
|
"json-schema-typed": "^8.0.1",
|
|
55
|
-
"@amritk/helpers": "0.
|
|
56
|
-
"@amritk/runtime-validators": "0.
|
|
58
|
+
"@amritk/helpers": "^0.15.1",
|
|
59
|
+
"@amritk/runtime-validators": "^0.10.1"
|
|
57
60
|
},
|
|
58
61
|
"devDependencies": {
|
|
59
62
|
"ajv": "^8.17.1"
|