@amritk/generate-examples 0.5.2 → 0.5.4
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 +37 -0
- package/dist/generators/build-schema.js +23 -50
- package/dist/generators/collect-example-imports.js +64 -94
- package/dist/generators/derive-example.js +507 -680
- package/dist/generators/find-schema-cycles.js +91 -115
- package/dist/generators/generate-arbitrary.js +316 -471
- package/dist/generators/generate-files.js +28 -45
- package/dist/generators/schema-validation.js +55 -78
- package/dist/index.js +10 -3
- package/package.json +6 -5
|
@@ -1,506 +1,351 @@
|
|
|
1
|
-
import { getMjstInstanceOf, getMjstPrimitive } from
|
|
2
|
-
import { refToFilename } from
|
|
3
|
-
import { refToName } from
|
|
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
|
|
5
|
-
import { mergeAllOf } from
|
|
6
|
-
import { needsValidationFilter, withResolvableDefs } from
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
|
|
40
|
+
checks.push(`s.length >= ${schema.minLength}`);
|
|
77
41
|
if (hasMaxLength(schema))
|
|
78
|
-
|
|
79
|
-
return
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
313
|
-
|
|
314
|
-
for (const
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
205
|
+
}
|
|
206
|
+
if (hasRequired(sub))
|
|
207
|
+
for (const key of sub.required)
|
|
208
|
+
required.add(key);
|
|
328
209
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
|
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
|
-
|
|
351
|
-
|
|
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
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
-
|
|
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
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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
|
-
|
|
443
|
-
|
|
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
|
|
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
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
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
|
};
|