@amritk/generate-examples 0.5.2 → 0.5.3

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.
@@ -1,506 +1,351 @@
1
- import { getMjstInstanceOf, getMjstPrimitive } from '@amritk/helpers/mjst-extension';
2
- import { refToFilename } from '@amritk/helpers/ref-to-filename';
3
- import { refToName } from '@amritk/helpers/ref-to-name';
4
- import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasConst, hasContains, hasDependentRequired, hasDependentSchemas, hasEnum, 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 { mergeAllOf } from './derive-example.js';
6
- import { needsValidationFilter, withResolvableDefs } from './schema-validation.js';
7
- /**
8
- * Derives the arbitrary const name from a type name.
9
- * e.g. "User" → "UserArbitrary"
10
- */
1
+ import { getMjstInstanceOf, getMjstPrimitive } from "@amritk/helpers/mjst-extension";
2
+ import { refToFilename } from "@amritk/helpers/ref-to-filename";
3
+ import { refToName } from "@amritk/helpers/ref-to-name";
4
+ import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasConst, hasContains, hasDependentRequired, hasDependentSchemas, hasEnum, 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 { mergeAllOf } from "./derive-example.js";
6
+ import { needsValidationFilter, withResolvableDefs } from "./schema-validation.js";
11
7
  const arbitraryName = (typeName) => `${typeName}Arbitrary`;
12
- /**
13
- * Local alias the generated file binds `@amritk/runtime-validators`' `validate`
14
- * to. Namespaced (leading underscores) so it can't collide with a schema-derived
15
- * type name. {@link generateArbitrary} emits references to it; the file assembler
16
- * adds the matching import when any arbitrary uses it.
17
- */
18
- export const VALIDATE_IMPORT_NAME = '__mjstValidate';
19
- /**
20
- * The import line the generated file needs when an arbitrary embeds a validating
21
- * filter. Emitted by the file assembler only when {@link VALIDATE_IMPORT_NAME}
22
- * appears in the generated source.
23
- */
24
- export const VALIDATE_IMPORT_STATEMENT = `import { validate as ${VALIDATE_IMPORT_NAME} } from '@amritk/runtime-validators'`;
25
- /**
26
- * Wraps a cross-module arbitrary reference so the imported binding is read at
27
- * generation time rather than at module-init time. `fc.constant(null).chain`
28
- * stores the thunk and only invokes it when a value is generated — by which
29
- * point every module in the cycle has finished initializing — so the otherwise
30
- * eager identifier never touches a `const` in its TDZ.
31
- */
8
+ const VALIDATE_IMPORT_NAME = "__mjstValidate";
9
+ const VALIDATE_IMPORT_STATEMENT = `import { validate as ${VALIDATE_IMPORT_NAME} } from '@amritk/runtime-validators'`;
32
10
  const lazyRef = (arbName) => `fc.constant(null).chain(() => ${arbName})`;
33
- /** The letrec key used for a type's own (self-referential) arbitrary. */
34
- const SELF_KEY = 'self';
35
- /** Builds a `fc.string({ ... })` expression honouring format and length constraints. */
11
+ const SELF_KEY = "self";
36
12
  const stringExpr = (schema) => {
37
- if (hasFormat(schema)) {
38
- switch (schema.format) {
39
- case 'email':
40
- return 'fc.emailAddress()';
41
- case 'uuid':
42
- return 'fc.uuid()';
43
- case 'uri':
44
- case 'url':
45
- return 'fc.webUrl()';
46
- case 'date-time':
47
- return 'fc.date({ noInvalidDate: true }).map((d) => d.toISOString())';
48
- case 'date':
49
- return 'fc.date({ noInvalidDate: true }).map((d) => d.toISOString().slice(0, 10))';
50
- case 'time':
51
- return 'fc.date({ noInvalidDate: true }).map((d) => d.toISOString().slice(11))';
52
- case 'hostname':
53
- return 'fc.domain()';
54
- case 'ipv4':
55
- return 'fc.ipV4()';
56
- case 'ipv6':
57
- return 'fc.ipV6()';
58
- }
59
- }
60
- if (hasPattern(schema)) {
61
- // Build the regex via `new RegExp(<json-string>)` rather than inlining the
62
- // pattern into a `/.../ ` literal: a pattern containing `/` (e.g. `^/api/v\d+$`)
63
- // would otherwise close the literal early and emit invalid TypeScript.
64
- const base = `fc.stringMatching(new RegExp(${JSON.stringify(schema.pattern)}))`;
65
- // `stringMatching` takes no length bounds, so honour any min/maxLength with a
66
- // filter instead of silently dropping them. Only emit it when a bound exists.
67
- const checks = [];
68
- if (hasMinLength(schema))
69
- checks.push(`s.length >= ${schema.minLength}`);
70
- if (hasMaxLength(schema))
71
- checks.push(`s.length <= ${schema.maxLength}`);
72
- return checks.length > 0 ? `${base}.filter((s) => ${checks.join(' && ')})` : base;
13
+ if (hasFormat(schema)) {
14
+ switch (schema.format) {
15
+ case "email":
16
+ return "fc.emailAddress()";
17
+ case "uuid":
18
+ return "fc.uuid()";
19
+ case "uri":
20
+ case "url":
21
+ return "fc.webUrl()";
22
+ case "date-time":
23
+ return "fc.date({ noInvalidDate: true }).map((d) => d.toISOString())";
24
+ case "date":
25
+ return "fc.date({ noInvalidDate: true }).map((d) => d.toISOString().slice(0, 10))";
26
+ case "time":
27
+ return "fc.date({ noInvalidDate: true }).map((d) => d.toISOString().slice(11))";
28
+ case "hostname":
29
+ return "fc.domain()";
30
+ case "ipv4":
31
+ return "fc.ipV4()";
32
+ case "ipv6":
33
+ return "fc.ipV6()";
73
34
  }
74
- const opts = [];
35
+ }
36
+ if (hasPattern(schema)) {
37
+ const base = `fc.stringMatching(new RegExp(${JSON.stringify(schema.pattern)}))`;
38
+ const checks = [];
75
39
  if (hasMinLength(schema))
76
- opts.push(`minLength: ${schema.minLength}`);
40
+ checks.push(`s.length >= ${schema.minLength}`);
77
41
  if (hasMaxLength(schema))
78
- opts.push(`maxLength: ${schema.maxLength}`);
79
- return opts.length > 0 ? `fc.string({ ${opts.join(', ')} })` : 'fc.string()';
42
+ checks.push(`s.length <= ${schema.maxLength}`);
43
+ return checks.length > 0 ? `${base}.filter((s) => ${checks.join(" && ")})` : base;
44
+ }
45
+ const opts = [];
46
+ if (hasMinLength(schema))
47
+ opts.push(`minLength: ${schema.minLength}`);
48
+ if (hasMaxLength(schema))
49
+ opts.push(`maxLength: ${schema.maxLength}`);
50
+ return opts.length > 0 ? `fc.string({ ${opts.join(", ")} })` : "fc.string()";
80
51
  };
81
- /** Builds a `fc.integer({ ... })` expression honouring range and multiple-of constraints. */
82
52
  const integerExpr = (schema) => {
83
- const opts = [];
84
- // With both `minimum` and `exclusiveMinimum` present the effective lower bound
85
- // is the tighter (larger) of the two, so combine them rather than letting one
86
- // shadow the other via else-if. `fc.integer` also requires integral bounds, but
87
- // the schema keywords may be fractional (`minimum: 2.5`, `exclusiveMinimum: 5.5`),
88
- // so round each toward the satisfiable side: the smallest integer that still
89
- // meets the lower bound, and the largest that still meets the upper.
90
- // `Math.floor(x) + 1` is the smallest integer strictly greater than `x` (so it
91
- // also handles integral exclusives); `Math.ceil(x) - 1` is the largest strictly less.
92
- const mins = [];
93
- if (hasMinimum(schema))
94
- mins.push(Math.ceil(Number(schema.minimum)));
95
- if (hasExclusiveMinimum(schema))
96
- mins.push(Math.floor(Number(schema.exclusiveMinimum)) + 1);
97
- if (mins.length > 0)
98
- opts.push(`min: ${Math.max(...mins)}`);
99
- const maxs = [];
100
- if (hasMaximum(schema))
101
- maxs.push(Math.floor(Number(schema.maximum)));
102
- if (hasExclusiveMaximum(schema))
103
- maxs.push(Math.ceil(Number(schema.exclusiveMaximum)) - 1);
104
- if (maxs.length > 0)
105
- opts.push(`max: ${Math.min(...maxs)}`);
106
- const base = opts.length > 0 ? `fc.integer({ ${opts.join(', ')} })` : 'fc.integer()';
107
- return hasMultipleOf(schema) ? `${base}.filter((n) => n % ${schema.multipleOf} === 0)` : base;
53
+ const opts = [];
54
+ const mins = [];
55
+ if (hasMinimum(schema))
56
+ mins.push(Math.ceil(Number(schema.minimum)));
57
+ if (hasExclusiveMinimum(schema))
58
+ mins.push(Math.floor(Number(schema.exclusiveMinimum)) + 1);
59
+ if (mins.length > 0)
60
+ opts.push(`min: ${Math.max(...mins)}`);
61
+ const maxs = [];
62
+ if (hasMaximum(schema))
63
+ maxs.push(Math.floor(Number(schema.maximum)));
64
+ if (hasExclusiveMaximum(schema))
65
+ maxs.push(Math.ceil(Number(schema.exclusiveMaximum)) - 1);
66
+ if (maxs.length > 0)
67
+ opts.push(`max: ${Math.min(...maxs)}`);
68
+ const base = opts.length > 0 ? `fc.integer({ ${opts.join(", ")} })` : "fc.integer()";
69
+ return hasMultipleOf(schema) ? `${base}.filter((n) => n % ${schema.multipleOf} === 0)` : base;
108
70
  };
109
- /**
110
- * Builds a multiple-of-respecting number arbitrary analytically: pick an integer
111
- * `k` whose multiple `k * multipleOf` lands inside the (possibly exclusive)
112
- * bounds, then emit that product. Random doubles essentially never satisfy
113
- * `n % m === 0`, so a `.filter` here starves fast-check ("too many filtered
114
- * values") at sample time; deriving the multiple directly cannot fail. This
115
- * mirrors the static path's `deriveNumber`.
116
- *
117
- * The trailing `.map` clamps `k * m` back inside the finite bounds to absorb
118
- * floating-point drift (e.g. `3 * 0.1 === 0.30000000000000004`, which would
119
- * otherwise slip just past a `maximum` of `0.3`).
120
- */
121
71
  const numberMultipleOfExpr = (schema) => {
122
- const m = Number(schema.multipleOf);
123
- const EPS = 1e-9;
124
- // Effective lower bound: the tighter (larger) of minimum / exclusiveMinimum,
125
- // tracking whether the binding bound is exclusive.
126
- let lo = Number.NEGATIVE_INFINITY;
127
- let loExclusive = false;
128
- if (hasMinimum(schema))
129
- lo = Number(schema.minimum);
130
- if (hasExclusiveMinimum(schema) && Number(schema.exclusiveMinimum) >= lo) {
131
- lo = Number(schema.exclusiveMinimum);
132
- loExclusive = true;
133
- }
134
- // Effective upper bound: the tighter (smaller) of maximum / exclusiveMaximum.
135
- let hi = Number.POSITIVE_INFINITY;
136
- let hiExclusive = false;
137
- if (hasMaximum(schema))
138
- hi = Number(schema.maximum);
139
- if (hasExclusiveMaximum(schema) && Number(schema.exclusiveMaximum) <= hi) {
140
- hi = Number(schema.exclusiveMaximum);
141
- hiExclusive = true;
142
- }
143
- // Translate value bounds into integer-`k` bounds, where the emitted value is
144
- // `k * m`. An exclusive bound must be strictly cleared, so a `k` landing exactly
145
- // on it is nudged one step inward; `EPS` keeps a mathematically-integer ratio
146
- // (e.g. `0.3 / 0.1`) from being mis-rounded by floating-point error.
147
- let kMin;
148
- let kMax;
149
- if (Number.isFinite(lo)) {
150
- const raw = lo / m;
151
- kMin = loExclusive ? Math.floor(raw + EPS) + 1 : Math.ceil(raw - EPS);
152
- }
153
- if (Number.isFinite(hi)) {
154
- const raw = hi / m;
155
- kMax = hiExclusive ? Math.ceil(raw - EPS) - 1 : Math.floor(raw + EPS);
156
- }
157
- // An unsatisfiable range (no multiple fits) would make `fc.integer` throw on
158
- // `min > max`; collapse to a single best-effort value instead.
159
- if (kMin !== undefined && kMax !== undefined && kMin > kMax)
160
- kMax = kMin;
161
- const kOpts = [];
162
- if (kMin !== undefined)
163
- kOpts.push(`min: ${kMin}`);
164
- if (kMax !== undefined)
165
- kOpts.push(`max: ${kMax}`);
166
- const k = kOpts.length > 0 ? `fc.integer({ ${kOpts.join(', ')} })` : 'fc.integer()';
167
- let value = `k * ${m}`;
168
- if (Number.isFinite(lo))
169
- value = `Math.max(${value}, ${lo})`;
170
- if (Number.isFinite(hi))
171
- value = `Math.min(${value}, ${hi})`;
172
- return `${k}.map((k) => ${value})`;
72
+ const m = Number(schema.multipleOf);
73
+ const EPS = 1e-9;
74
+ let lo = Number.NEGATIVE_INFINITY;
75
+ let loExclusive = false;
76
+ if (hasMinimum(schema))
77
+ lo = Number(schema.minimum);
78
+ if (hasExclusiveMinimum(schema) && Number(schema.exclusiveMinimum) >= lo) {
79
+ lo = Number(schema.exclusiveMinimum);
80
+ loExclusive = true;
81
+ }
82
+ let hi = Number.POSITIVE_INFINITY;
83
+ let hiExclusive = false;
84
+ if (hasMaximum(schema))
85
+ hi = Number(schema.maximum);
86
+ if (hasExclusiveMaximum(schema) && Number(schema.exclusiveMaximum) <= hi) {
87
+ hi = Number(schema.exclusiveMaximum);
88
+ hiExclusive = true;
89
+ }
90
+ let kMin;
91
+ let kMax;
92
+ if (Number.isFinite(lo)) {
93
+ const raw = lo / m;
94
+ kMin = loExclusive ? Math.floor(raw + EPS) + 1 : Math.ceil(raw - EPS);
95
+ }
96
+ if (Number.isFinite(hi)) {
97
+ const raw = hi / m;
98
+ kMax = hiExclusive ? Math.ceil(raw - EPS) - 1 : Math.floor(raw + EPS);
99
+ }
100
+ if (kMin !== void 0 && kMax !== void 0 && kMin > kMax)
101
+ kMax = kMin;
102
+ const kOpts = [];
103
+ if (kMin !== void 0)
104
+ kOpts.push(`min: ${kMin}`);
105
+ if (kMax !== void 0)
106
+ kOpts.push(`max: ${kMax}`);
107
+ const k = kOpts.length > 0 ? `fc.integer({ ${kOpts.join(", ")} })` : "fc.integer()";
108
+ let value = `k * ${m}`;
109
+ if (Number.isFinite(lo))
110
+ value = `Math.max(${value}, ${lo})`;
111
+ if (Number.isFinite(hi))
112
+ value = `Math.min(${value}, ${hi})`;
113
+ return `${k}.map((k) => ${value})`;
173
114
  };
174
- /** Builds a `fc.double({ ... })` expression honouring range and multiple-of constraints. */
175
115
  const numberExpr = (schema) => {
176
- // A positive `multipleOf` is satisfied analytically rather than by filtering
177
- // random doubles, which would starve fast-check at sample time.
178
- if (hasMultipleOf(schema) && schema.multipleOf > 0)
179
- return numberMultipleOfExpr(schema);
180
- const opts = ['noNaN: true', 'noDefaultInfinity: true'];
181
- // With both an inclusive and an exclusive bound on the same side, honour the
182
- // tighter one instead of letting `minimum`/`maximum` shadow the exclusive via
183
- // else-if (which would emit values that violate the exclusive bound). The
184
- // exclusive bound wins ties, since it additionally excludes the endpoint.
185
- if (hasMinimum(schema) &&
186
- (!hasExclusiveMinimum(schema) || Number(schema.minimum) > Number(schema.exclusiveMinimum))) {
187
- opts.push(`min: ${schema.minimum}`);
188
- }
189
- else if (hasExclusiveMinimum(schema)) {
190
- opts.push(`min: ${schema.exclusiveMinimum}`, 'minExcluded: true');
191
- }
192
- if (hasMaximum(schema) &&
193
- (!hasExclusiveMaximum(schema) || Number(schema.maximum) < Number(schema.exclusiveMaximum))) {
194
- opts.push(`max: ${schema.maximum}`);
195
- }
196
- else if (hasExclusiveMaximum(schema)) {
197
- opts.push(`max: ${schema.exclusiveMaximum}`, 'maxExcluded: true');
198
- }
199
- return `fc.double({ ${opts.join(', ')} })`;
116
+ if (hasMultipleOf(schema) && schema.multipleOf > 0)
117
+ return numberMultipleOfExpr(schema);
118
+ const opts = ["noNaN: true", "noDefaultInfinity: true"];
119
+ if (hasMinimum(schema) && (!hasExclusiveMinimum(schema) || Number(schema.minimum) > Number(schema.exclusiveMinimum))) {
120
+ opts.push(`min: ${schema.minimum}`);
121
+ } else if (hasExclusiveMinimum(schema)) {
122
+ opts.push(`min: ${schema.exclusiveMinimum}`, "minExcluded: true");
123
+ }
124
+ if (hasMaximum(schema) && (!hasExclusiveMaximum(schema) || Number(schema.maximum) < Number(schema.exclusiveMaximum))) {
125
+ opts.push(`max: ${schema.maximum}`);
126
+ } else if (hasExclusiveMaximum(schema)) {
127
+ opts.push(`max: ${schema.exclusiveMaximum}`, "maxExcluded: true");
128
+ }
129
+ return `fc.double({ ${opts.join(", ")} })`;
200
130
  };
201
- /** Builds a `fc.array(...)` / `fc.uniqueArray(...)` / `fc.tuple(...)` expression for an array schema. */
202
131
  const arrayExpr = (schema, ctx) => {
203
- const raw = schema;
204
- // A tuple is `prefixItems` (2020-12) or the draft-07 array-form `items`. Both
205
- // describe one schema per position, so map them to `fc.tuple(...)`. Extra items
206
- // beyond the prefix are unconstrained (or forbidden by `items: false`); the
207
- // minimal tuple is schema-valid either way.
208
- const prefixItems = raw['prefixItems'];
209
- const tuple = Array.isArray(prefixItems)
210
- ? prefixItems
211
- : Array.isArray(raw['items'])
212
- ? raw['items']
213
- : undefined;
214
- if (tuple) {
215
- const exprs = tuple.map((item) => arbitraryExpr(item, ctx));
216
- return `fc.tuple(${exprs.join(', ')})`;
217
- }
218
- // With no `items`, a `contains` subschema is the only element constraint, so
219
- // generate from it (and guarantee at least one such element via `minLength`) —
220
- // otherwise an empty array would fail `contains`.
221
- const containsSchema = hasContains(schema) && isSchemaObject(schema.contains) ? schema.contains : undefined;
222
- const items = hasItems(schema) && isSchemaObject(schema.items)
223
- ? arbitraryExpr(schema.items, ctx)
224
- : containsSchema
225
- ? arbitraryExpr(containsSchema, ctx)
226
- : 'fc.anything()';
227
- const minContains = containsSchema !== undefined && typeof raw['minContains'] === 'number' ? raw['minContains'] : 1;
228
- const minLength = Math.max(hasMinItems(schema) ? schema.minItems : 0, containsSchema !== undefined ? Math.max(1, minContains) : 0);
229
- const opts = [];
230
- if (minLength > 0)
231
- opts.push(`minLength: ${minLength}`);
232
- if (hasMaxItems(schema))
233
- opts.push(`maxLength: ${schema.maxItems}`);
234
- const fn = hasUniqueItems(schema) && schema.uniqueItems === true ? 'fc.uniqueArray' : 'fc.array';
235
- return opts.length > 0 ? `${fn}(${items}, { ${opts.join(', ')} })` : `${fn}(${items})`;
132
+ const raw = schema;
133
+ const prefixItems = raw["prefixItems"];
134
+ const tuple = Array.isArray(prefixItems) ? prefixItems : Array.isArray(raw["items"]) ? raw["items"] : void 0;
135
+ if (tuple) {
136
+ const exprs = tuple.map((item) => arbitraryExpr(item, ctx));
137
+ return `fc.tuple(${exprs.join(", ")})`;
138
+ }
139
+ const containsSchema = hasContains(schema) && isSchemaObject(schema.contains) ? schema.contains : void 0;
140
+ const items = hasItems(schema) && isSchemaObject(schema.items) ? arbitraryExpr(schema.items, ctx) : containsSchema ? arbitraryExpr(containsSchema, ctx) : "fc.anything()";
141
+ const minContains = containsSchema !== void 0 && typeof raw["minContains"] === "number" ? raw["minContains"] : 1;
142
+ const minLength = Math.max(hasMinItems(schema) ? schema.minItems : 0, containsSchema !== void 0 ? Math.max(1, minContains) : 0);
143
+ const opts = [];
144
+ if (minLength > 0)
145
+ opts.push(`minLength: ${minLength}`);
146
+ if (hasMaxItems(schema))
147
+ opts.push(`maxLength: ${schema.maxItems}`);
148
+ const fn = hasUniqueItems(schema) && schema.uniqueItems === true ? "fc.uniqueArray" : "fc.array";
149
+ return opts.length > 0 ? `${fn}(${items}, { ${opts.join(", ")} })` : `${fn}(${items})`;
236
150
  };
237
- /** The arbitrary for keys of the open-map (extra-property) part of an object. */
238
151
  const extraKeyArb = (schema, firstPatternSource) => {
239
- // Keys must satisfy `patternProperties` (so the value schema applies) or, failing
240
- // that, a `propertyNames` pattern. Both map onto `fc.stringMatching`.
241
- if (firstPatternSource !== undefined)
242
- return `fc.stringMatching(new RegExp(${JSON.stringify(firstPatternSource)}))`;
243
- const propertyNames = hasPropertyNames(schema) ? schema.propertyNames : undefined;
244
- if (propertyNames !== undefined && isSchemaObject(propertyNames) && hasPattern(propertyNames)) {
245
- return `fc.stringMatching(new RegExp(${JSON.stringify(propertyNames.pattern)}))`;
246
- }
247
- return 'fc.string()';
152
+ if (firstPatternSource !== void 0)
153
+ return `fc.stringMatching(new RegExp(${JSON.stringify(firstPatternSource)}))`;
154
+ const propertyNames = hasPropertyNames(schema) ? schema.propertyNames : void 0;
155
+ if (propertyNames !== void 0 && isSchemaObject(propertyNames) && hasPattern(propertyNames)) {
156
+ return `fc.stringMatching(new RegExp(${JSON.stringify(propertyNames.pattern)}))`;
157
+ }
158
+ return "fc.string()";
248
159
  };
249
- /** Builds a `fc.record(...)` / `fc.dictionary(...)` expression for an object schema. */
250
160
  const objectExpr = (schema, ctx) => {
251
- // The `additionalProperties` value schema (when it constrains extra keys with a
252
- // real subschema rather than the boolean true/false form).
253
- const additional = hasAdditionalProperties(schema) ? schema.additionalProperties : false;
254
- const additionalArb = isSchemaObject(additional) ? arbitraryExpr(additional, ctx) : undefined;
255
- const additionalClosed = hasAdditionalProperties(schema) && schema.additionalProperties === false;
256
- // The first `patternProperties` entry drives the open-map value/key shape.
257
- const patternEntries = hasPatternProperties(schema) ? Object.entries(schema.patternProperties) : [];
258
- const firstPattern = patternEntries[0];
259
- const patternValueArb = firstPattern && isSchemaObject(firstPattern[1]) ? arbitraryExpr(firstPattern[1], ctx) : undefined;
260
- // Extra keys are allowed unless a fully closed object (no `additionalProperties`
261
- // and no `patternProperties` outlet) forbids them.
262
- const extrasAllowed = !additionalClosed || patternEntries.length > 0;
263
- const extraValueArb = additionalArb ?? patternValueArb;
264
- const keyArb = extraKeyArb(schema, firstPattern?.[0]);
265
- const minProps = hasMinProperties(schema) ? schema.minProperties : undefined;
266
- const maxProps = hasMaxProperties(schema) ? schema.maxProperties : undefined;
267
- const dictKeyOpts = (minKeys, maxKeys) => {
268
- const opts = [];
269
- if (minKeys !== undefined && minKeys > 0)
270
- opts.push(`minKeys: ${minKeys}`);
271
- if (maxKeys !== undefined)
272
- opts.push(`maxKeys: ${maxKeys}`);
273
- return opts.length > 0 ? `, { ${opts.join(', ')} }` : '';
274
- };
275
- // Each declared key maps to the arbitrary that generates its value.
276
- const propArbs = new Map();
277
- if (hasProperties(schema)) {
278
- for (const [key, propSchema] of Object.entries(schema.properties))
279
- propArbs.set(key, arbitraryExpr(propSchema, ctx));
280
- }
281
- const required = new Set(hasRequired(schema) ? schema.required : []);
282
- // The value arbitrary for a key not declared in `properties` (a dependency key).
283
- const openValueArb = extraValueArb ?? 'fc.anything()';
284
- // Fold presence-gated dependency keywords into the always-present set. Requiring
285
- // a dependency (or a `dependentSchemas` shape) unconditionally is stricter than
286
- // the keyword — but a value that always carries the dependency is always valid,
287
- // and it keeps the generated candidate from being rejected by the filter.
288
- if (hasDependentRequired(schema)) {
289
- for (const [, deps] of Object.entries(schema.dependentRequired)) {
290
- for (const dep of deps) {
291
- if (!propArbs.has(dep))
292
- propArbs.set(dep, openValueArb);
293
- required.add(dep);
294
- }
295
- }
296
- }
297
- if (hasDependentSchemas(schema)) {
298
- for (const [, sub] of Object.entries(schema.dependentSchemas)) {
299
- if (!isSchemaObject(sub))
300
- continue;
301
- if (hasProperties(sub)) {
302
- for (const [key, propSchema] of Object.entries(sub.properties)) {
303
- if (!propArbs.has(key))
304
- propArbs.set(key, arbitraryExpr(propSchema, ctx));
305
- }
306
- }
307
- if (hasRequired(sub))
308
- for (const key of sub.required)
309
- required.add(key);
310
- }
161
+ const additional = hasAdditionalProperties(schema) ? schema.additionalProperties : false;
162
+ const additionalArb = isSchemaObject(additional) ? arbitraryExpr(additional, ctx) : void 0;
163
+ const additionalClosed = hasAdditionalProperties(schema) && schema.additionalProperties === false;
164
+ const patternEntries = hasPatternProperties(schema) ? Object.entries(schema.patternProperties) : [];
165
+ const firstPattern = patternEntries[0];
166
+ const patternValueArb = firstPattern && isSchemaObject(firstPattern[1]) ? arbitraryExpr(firstPattern[1], ctx) : void 0;
167
+ const extrasAllowed = !additionalClosed || patternEntries.length > 0;
168
+ const extraValueArb = additionalArb ?? patternValueArb;
169
+ const keyArb = extraKeyArb(schema, firstPattern?.[0]);
170
+ const minProps = hasMinProperties(schema) ? schema.minProperties : void 0;
171
+ const maxProps = hasMaxProperties(schema) ? schema.maxProperties : void 0;
172
+ const dictKeyOpts = (minKeys, maxKeys) => {
173
+ const opts = [];
174
+ if (minKeys !== void 0 && minKeys > 0)
175
+ opts.push(`minKeys: ${minKeys}`);
176
+ if (maxKeys !== void 0)
177
+ opts.push(`maxKeys: ${maxKeys}`);
178
+ return opts.length > 0 ? `, { ${opts.join(", ")} }` : "";
179
+ };
180
+ const propArbs = /* @__PURE__ */ new Map();
181
+ if (hasProperties(schema)) {
182
+ for (const [key, propSchema] of Object.entries(schema.properties))
183
+ propArbs.set(key, arbitraryExpr(propSchema, ctx));
184
+ }
185
+ const required = new Set(hasRequired(schema) ? schema.required : []);
186
+ const openValueArb = extraValueArb ?? "fc.anything()";
187
+ if (hasDependentRequired(schema)) {
188
+ for (const [, deps] of Object.entries(schema.dependentRequired)) {
189
+ for (const dep of deps) {
190
+ if (!propArbs.has(dep))
191
+ propArbs.set(dep, openValueArb);
192
+ required.add(dep);
193
+ }
311
194
  }
312
- // A `dependentSchemas`/`dependentRequired` key may be required without a declared
313
- // value schema; give it the open-map value arbitrary.
314
- for (const key of required)
315
- if (!propArbs.has(key))
316
- propArbs.set(key, openValueArb);
317
- const keys = [...propArbs.keys()];
318
- // A map-style object (no declared keys) is a dictionary; its bounds come from
319
- // `min`/`maxProperties` and its value/key shape from `additionalProperties` /
320
- // `patternProperties` / `propertyNames`.
321
- if (keys.length === 0) {
322
- if (extraValueArb)
323
- return `fc.dictionary(${keyArb}, ${extraValueArb}${dictKeyOpts(minProps, maxProps)})`;
324
- if (minProps !== undefined || maxProps !== undefined) {
325
- return `fc.dictionary(${keyArb}, fc.anything()${dictKeyOpts(minProps, maxProps)})`;
195
+ }
196
+ if (hasDependentSchemas(schema)) {
197
+ for (const [, sub] of Object.entries(schema.dependentSchemas)) {
198
+ if (!isSchemaObject(sub))
199
+ continue;
200
+ if (hasProperties(sub)) {
201
+ for (const [key, propSchema] of Object.entries(sub.properties)) {
202
+ if (!propArbs.has(key))
203
+ propArbs.set(key, arbitraryExpr(propSchema, ctx));
326
204
  }
327
- return 'fc.object()';
205
+ }
206
+ if (hasRequired(sub))
207
+ for (const key of sub.required)
208
+ required.add(key);
328
209
  }
329
- const entries = keys.map((key) => `${JSON.stringify(key)}: ${propArbs.get(key)}`);
330
- const model = `{ ${entries.join(', ')} }`;
331
- // fc.record treats all keys as required by default. Only emit requiredKeys
332
- // when at least one property is optional.
333
- const record = keys.every((key) => required.has(key))
334
- ? `fc.record(${model})`
335
- : `fc.record(${model}, { requiredKeys: [${[...required].map((key) => JSON.stringify(key)).join(', ')}] })`;
336
- // Fold in a dictionary of extra keys when the open-map part is typed, or when
337
- // `minProperties` needs more keys than the declared set guarantees. `minKeys`
338
- // fills only the gap above the always-present (required) keys so the floor is met
339
- // without overshooting. Declared keys win on collision (merged last).
340
- const needExtras = extrasAllowed && (extraValueArb !== undefined || (minProps !== undefined && minProps > required.size));
341
- if (needExtras) {
342
- const valueArb = extraValueArb ?? 'fc.anything()';
343
- const minKeys = minProps !== undefined ? Math.max(0, minProps - required.size) : undefined;
344
- return `fc.tuple(${record}, fc.dictionary(${keyArb}, ${valueArb}${dictKeyOpts(minKeys, undefined)})).map(([base, extra]) => ({ ...extra, ...base }))`;
210
+ }
211
+ for (const key of required)
212
+ if (!propArbs.has(key))
213
+ propArbs.set(key, openValueArb);
214
+ const keys = [...propArbs.keys()];
215
+ if (keys.length === 0) {
216
+ if (extraValueArb)
217
+ return `fc.dictionary(${keyArb}, ${extraValueArb}${dictKeyOpts(minProps, maxProps)})`;
218
+ if (minProps !== void 0 || maxProps !== void 0) {
219
+ return `fc.dictionary(${keyArb}, fc.anything()${dictKeyOpts(minProps, maxProps)})`;
345
220
  }
346
- return record;
221
+ return "fc.object()";
222
+ }
223
+ const entries = keys.map((key) => `${JSON.stringify(key)}: ${propArbs.get(key)}`);
224
+ const model = `{ ${entries.join(", ")} }`;
225
+ const record = keys.every((key) => required.has(key)) ? `fc.record(${model})` : `fc.record(${model}, { requiredKeys: [${[...required].map((key) => JSON.stringify(key)).join(", ")}] })`;
226
+ const needExtras = extrasAllowed && (extraValueArb !== void 0 || minProps !== void 0 && minProps > required.size);
227
+ if (needExtras) {
228
+ const valueArb = extraValueArb ?? "fc.anything()";
229
+ const minKeys = minProps !== void 0 ? Math.max(0, minProps - required.size) : void 0;
230
+ return `fc.tuple(${record}, fc.dictionary(${keyArb}, ${valueArb}${dictKeyOpts(minKeys, void 0)})).map(([base, extra]) => ({ ...extra, ...base }))`;
231
+ }
232
+ return record;
347
233
  };
348
- /** Builds a `fc.oneof(...)` expression from a list of branch schemas. */
349
234
  const oneofExpr = (branches, ctx) => {
350
- const exprs = branches.map((branch) => arbitraryExpr(branch, ctx));
351
- return `fc.oneof(${exprs.join(', ')})`;
235
+ const exprs = branches.map((branch) => arbitraryExpr(branch, ctx));
236
+ return `fc.oneof(${exprs.join(", ")})`;
352
237
  };
353
- /** Builds the fast-check expression for a single (non-union) JSON Schema type. */
354
238
  const scalarExpr = (type, schema, ctx) => {
355
- switch (type) {
356
- case 'string':
357
- return stringExpr(schema);
358
- case 'integer':
359
- return integerExpr(schema);
360
- case 'number':
361
- return numberExpr(schema);
362
- case 'boolean':
363
- return 'fc.boolean()';
364
- case 'null':
365
- return 'fc.constant(null)';
366
- case 'array':
367
- return arrayExpr(schema, ctx);
368
- case 'object':
369
- return objectExpr(schema, ctx);
370
- default:
371
- return 'fc.anything()';
372
- }
239
+ switch (type) {
240
+ case "string":
241
+ return stringExpr(schema);
242
+ case "integer":
243
+ return integerExpr(schema);
244
+ case "number":
245
+ return numberExpr(schema);
246
+ case "boolean":
247
+ return "fc.boolean()";
248
+ case "null":
249
+ return "fc.constant(null)";
250
+ case "array":
251
+ return arrayExpr(schema, ctx);
252
+ case "object":
253
+ return objectExpr(schema, ctx);
254
+ default:
255
+ return "fc.anything()";
256
+ }
373
257
  };
374
- /** True when an `enum` member satisfies the node's sibling length/range/pattern constraints. */
375
258
  const enumMemberFits = (schema, value) => {
376
- if (typeof value === 'string') {
377
- if (hasMinLength(schema) && value.length < schema.minLength)
378
- return false;
379
- if (hasMaxLength(schema) && value.length > schema.maxLength)
380
- return false;
381
- if (hasPattern(schema)) {
382
- try {
383
- if (!new RegExp(schema.pattern).test(value))
384
- return false;
385
- }
386
- catch {
387
- // An invalid pattern can't reject anything.
388
- }
389
- }
390
- }
391
- else if (typeof value === 'number') {
392
- if (hasMinimum(schema) && value < schema.minimum)
393
- return false;
394
- if (hasMaximum(schema) && value > schema.maximum)
395
- return false;
396
- if (hasExclusiveMinimum(schema) && value <= schema.exclusiveMinimum)
397
- return false;
398
- if (hasExclusiveMaximum(schema) && value >= schema.exclusiveMaximum)
399
- return false;
400
- if (hasMultipleOf(schema) && schema.multipleOf > 0 && value % schema.multipleOf !== 0)
401
- return false;
259
+ if (typeof value === "string") {
260
+ if (hasMinLength(schema) && value.length < schema.minLength)
261
+ return false;
262
+ if (hasMaxLength(schema) && value.length > schema.maxLength)
263
+ return false;
264
+ if (hasPattern(schema)) {
265
+ try {
266
+ if (!new RegExp(schema.pattern).test(value))
267
+ return false;
268
+ } catch {
269
+ }
402
270
  }
403
- return true;
271
+ } else if (typeof value === "number") {
272
+ if (hasMinimum(schema) && value < schema.minimum)
273
+ return false;
274
+ if (hasMaximum(schema) && value > schema.maximum)
275
+ return false;
276
+ if (hasExclusiveMinimum(schema) && value <= schema.exclusiveMinimum)
277
+ return false;
278
+ if (hasExclusiveMaximum(schema) && value >= schema.exclusiveMaximum)
279
+ return false;
280
+ if (hasMultipleOf(schema) && schema.multipleOf > 0 && value % schema.multipleOf !== 0)
281
+ return false;
282
+ }
283
+ return true;
404
284
  };
405
- /**
406
- * Recursively builds the fast-check arbitrary expression for a schema node.
407
- * `$ref`s resolve to the referenced file's exported arbitrary; a self-`$ref`
408
- * resolves to `tie('self')` so recursive schemas tie lazily via `fc.letrec`.
409
- * Everything else maps to the appropriate `fc.*` combinator.
410
- */
411
285
  const arbitraryExpr = (schema, ctx) => {
412
- if (!isSchemaObject(schema))
413
- return 'fc.anything()';
414
- if (hasRef(schema)) {
415
- const name = arbitraryName(refToName(schema.$ref, ctx.suffix));
416
- // A reference back to the type being generated must be tied lazily; an eager
417
- // identifier would touch a still-uninitialized const (TDZ) at import time.
418
- if (name === ctx.selfArbName) {
419
- ctx.usedTie.value = true;
420
- return `tie(${JSON.stringify(SELF_KEY)})`;
421
- }
422
- // A reference to a sibling this type shares a cross-file cycle with is the
423
- // same TDZ hazard one module over, and `tie` cannot reach across modules —
424
- // defer the imported binding until generation time instead.
425
- if (ctx.lazyRefFilenames.has(refToFilename(schema.$ref))) {
426
- return lazyRef(name);
427
- }
428
- return name;
429
- }
430
- if (hasConst(schema))
431
- return `fc.constant(${JSON.stringify(schema.const)})`;
432
- if (hasEnum(schema)) {
433
- // Drop enum members that violate a sibling length/range/pattern constraint so
434
- // the arbitrary never emits an out-of-range member. Keep all when none fit
435
- // (an unsatisfiable schema) rather than emitting an empty `constantFrom`.
436
- const members = schema.enum;
437
- const fitting = members.filter((value) => enumMemberFits(schema, value));
438
- const chosen = fitting.length > 0 ? fitting : members;
439
- const values = chosen.map((value) => JSON.stringify(value)).join(', ');
440
- return `fc.constantFrom(${values})`;
286
+ if (!isSchemaObject(schema))
287
+ return "fc.anything()";
288
+ if (hasRef(schema)) {
289
+ const name = arbitraryName(refToName(schema.$ref, ctx.suffix));
290
+ if (name === ctx.selfArbName) {
291
+ ctx.usedTie.value = true;
292
+ return `tie(${JSON.stringify(SELF_KEY)})`;
441
293
  }
442
- const instanceOf = getMjstInstanceOf(schema);
443
- if (instanceOf === 'Date')
444
- return 'fc.date({ noInvalidDate: true })';
445
- if (instanceOf)
446
- return 'fc.anything()';
447
- const primitive = getMjstPrimitive(schema);
448
- if (primitive === 'bigint')
449
- return 'fc.bigInt()';
450
- if (primitive)
451
- return 'fc.anything()';
452
- // `allOf` must satisfy every branch at once. fast-check has no generic
453
- // intersection combinator, so flatten the branches into one merged schema
454
- // (tightest bounds, unioned required, merged properties) and generate from it.
455
- if (hasAllOf(schema))
456
- return arbitraryExpr(mergeAllOf(schema), ctx);
457
- if (hasOneOf(schema))
458
- return oneofExpr(schema.oneOf, ctx);
459
- if (hasAnyOf(schema))
460
- return oneofExpr(schema.anyOf, ctx);
461
- if (hasType(schema))
462
- return scalarExpr(schema.type, schema, ctx);
463
- // Multi-type schemas (`type: ['string', 'null']`) become a oneof over each
464
- // member type; `hasType` only matches a single string `type`.
465
- if (Array.isArray(schema.type)) {
466
- const exprs = schema.type.map((type) => scalarExpr(type, schema, ctx));
467
- return exprs.length === 1 ? exprs[0] : `fc.oneof(${exprs.join(', ')})`;
294
+ if (ctx.lazyRefFilenames.has(refToFilename(schema.$ref))) {
295
+ return lazyRef(name);
468
296
  }
469
- return 'fc.anything()';
297
+ return name;
298
+ }
299
+ if (hasConst(schema))
300
+ return `fc.constant(${JSON.stringify(schema.const)})`;
301
+ if (hasEnum(schema)) {
302
+ const members = schema.enum;
303
+ const fitting = members.filter((value) => enumMemberFits(schema, value));
304
+ const chosen = fitting.length > 0 ? fitting : members;
305
+ const values = chosen.map((value) => JSON.stringify(value)).join(", ");
306
+ return `fc.constantFrom(${values})`;
307
+ }
308
+ const instanceOf = getMjstInstanceOf(schema);
309
+ if (instanceOf === "Date")
310
+ return "fc.date({ noInvalidDate: true })";
311
+ if (instanceOf)
312
+ return "fc.anything()";
313
+ const primitive = getMjstPrimitive(schema);
314
+ if (primitive === "bigint")
315
+ return "fc.bigInt()";
316
+ if (primitive)
317
+ return "fc.anything()";
318
+ if (hasAllOf(schema))
319
+ return arbitraryExpr(mergeAllOf(schema), ctx);
320
+ if (hasOneOf(schema))
321
+ return oneofExpr(schema.oneOf, ctx);
322
+ if (hasAnyOf(schema))
323
+ return oneofExpr(schema.anyOf, ctx);
324
+ if (hasType(schema))
325
+ return scalarExpr(schema.type, schema, ctx);
326
+ if (Array.isArray(schema.type)) {
327
+ const exprs = schema.type.map((type) => scalarExpr(type, schema, ctx));
328
+ return exprs.length === 1 ? exprs[0] : `fc.oneof(${exprs.join(", ")})`;
329
+ }
330
+ return "fc.anything()";
470
331
  };
471
- /**
472
- * Generates a `fast-check` arbitrary that produces schema-valid values.
473
- *
474
- * A schema that references itself is wrapped in `fc.letrec` so the recursion is
475
- * tied lazily a plain `const NodeArbitrary = fc.record({ next: NodeArbitrary })`
476
- * would throw a TDZ `ReferenceError` the moment the module is imported.
477
- *
478
- * `lazyRefFilenames` names the sibling files this type shares a cross-file `$ref`
479
- * cycle with; references to those are deferred so mutually recursive modules do
480
- * not read each other's `const` before it is initialized (see {@link ExprCtx}).
481
- *
482
- * @example
483
- * ```typescript
484
- * generateArbitrary({ type: 'object', properties: { name: { type: 'string' } }, required: ['name'] }, 'Info')
485
- * // export const InfoArbitrary: fc.Arbitrary<Info> = fc.record({ "name": fc.string() })
486
- * ```
487
- */
488
- export const generateArbitrary = (schema, typeName, suffix = '', lazyRefFilenames = new Set(), rootSchema) => {
489
- const selfArbName = arbitraryName(typeName);
490
- const ctx = { suffix, selfArbName, usedTie: { value: false }, lazyRefFilenames };
491
- const expr = arbitraryExpr(schema, ctx);
492
- const body = ctx.usedTie.value
493
- ? `fc.letrec<{ ${SELF_KEY}: ${typeName} }>((tie) => ({\n ${SELF_KEY}: ${expr},\n})).${SELF_KEY}`
494
- : expr;
495
- // Keywords no `fc.*` combinator captures on its own (`if`/`then`/`else`, `not`,
496
- // `oneOf` exclusivity, the presence-gated object keywords) are enforced by a
497
- // post-generation filter: the arbitrary samples a candidate and rejects it
498
- // unless a runtime validator built from the same schema accepts it.
499
- if (needsValidationFilter(schema)) {
500
- const validatorName = `${selfArbName}Validator`;
501
- const embedded = JSON.stringify(withResolvableDefs(schema, rootSchema));
502
- return (`const ${validatorName} = ${VALIDATE_IMPORT_NAME}(${embedded})\n` +
503
- `export const ${selfArbName}: fc.Arbitrary<${typeName}> = (${body}).filter((value) => ${validatorName}(value) === true)`);
504
- }
505
- return `export const ${selfArbName}: fc.Arbitrary<${typeName}> = ${body}`;
332
+ const generateArbitrary = (schema, typeName, suffix = "", lazyRefFilenames = /* @__PURE__ */ new Set(), rootSchema) => {
333
+ const selfArbName = arbitraryName(typeName);
334
+ const ctx = { suffix, selfArbName, usedTie: { value: false }, lazyRefFilenames };
335
+ const expr = arbitraryExpr(schema, ctx);
336
+ const body = ctx.usedTie.value ? `fc.letrec<{ ${SELF_KEY}: ${typeName} }>((tie) => ({
337
+ ${SELF_KEY}: ${expr},
338
+ })).${SELF_KEY}` : expr;
339
+ if (needsValidationFilter(schema)) {
340
+ const validatorName = `${selfArbName}Validator`;
341
+ const embedded = JSON.stringify(withResolvableDefs(schema, rootSchema));
342
+ return `const ${validatorName} = ${VALIDATE_IMPORT_NAME}(${embedded})
343
+ export const ${selfArbName}: fc.Arbitrary<${typeName}> = (${body}).filter((value) => ${validatorName}(value) === true)`;
344
+ }
345
+ return `export const ${selfArbName}: fc.Arbitrary<${typeName}> = ${body}`;
346
+ };
347
+ export {
348
+ VALIDATE_IMPORT_NAME,
349
+ VALIDATE_IMPORT_STATEMENT,
350
+ generateArbitrary
506
351
  };