@lovrozagar/oat 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -1
- package/dist/cli.js +17 -1
- package/dist/reference/http.d.ts.map +1 -1
- package/dist/reference/http.js +2 -2
- package/dist/reference/http.js.map +1 -1
- package/dist/report/console.d.ts.map +1 -1
- package/dist/report/console.js +18 -0
- package/dist/report/console.js.map +1 -1
- package/dist/runtime/checks.d.ts.map +1 -1
- package/dist/runtime/checks.js +122 -31
- package/dist/runtime/checks.js.map +1 -1
- package/dist/runtime/feature-gate.d.ts +23 -0
- package/dist/runtime/feature-gate.d.ts.map +1 -0
- package/dist/runtime/feature-gate.js +68 -0
- package/dist/runtime/feature-gate.js.map +1 -0
- package/dist/runtime/fixture.d.ts +28 -1
- package/dist/runtime/fixture.d.ts.map +1 -1
- package/dist/runtime/fixture.js +551 -90
- package/dist/runtime/fixture.js.map +1 -1
- package/dist/runtime/run.d.ts.map +1 -1
- package/dist/runtime/run.js +135 -41
- package/dist/runtime/run.js.map +1 -1
- package/dist/runtime/world.d.ts +17 -1
- package/dist/runtime/world.d.ts.map +1 -1
- package/dist/runtime/world.js +56 -4
- package/dist/runtime/world.js.map +1 -1
- package/dist/spec/extensions.d.ts +7 -0
- package/dist/spec/extensions.d.ts.map +1 -1
- package/dist/spec/extensions.js +10 -0
- package/dist/spec/extensions.js.map +1 -1
- package/dist/spec/graph.d.ts +2 -0
- package/dist/spec/graph.d.ts.map +1 -1
- package/dist/spec/graph.js +7 -2
- package/dist/spec/graph.js.map +1 -1
- package/dist/spec/load.d.ts.map +1 -1
- package/dist/spec/load.js +19 -4
- package/dist/spec/load.js.map +1 -1
- package/labs/annotated-openapi.yaml +6 -0
- package/package.json +3 -2
package/dist/runtime/fixture.js
CHANGED
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
* order, escaping needs metacharacters. oat seeds a *discriminating cohort* — instances shaped so
|
|
6
6
|
* that every query assertion has signal — and derives everything from a seed so a failing run is
|
|
7
7
|
* exactly reproducible.
|
|
8
|
+
*
|
|
9
|
+
* After `$ref` inlining the same object identity can cycle, and empty schemas (`{}`, `true`,
|
|
10
|
+
* `additionalProperties: {}`) mean "any value", not "walk this object again". Generation therefore
|
|
11
|
+
* keys a WeakSet on every node and treats those open shapes as a scalar or `{}`.
|
|
8
12
|
*/
|
|
9
13
|
export const COHORT = [
|
|
10
14
|
"baseline",
|
|
@@ -34,97 +38,230 @@ const STRINGS = {
|
|
|
34
38
|
"null-heavy": "null heavy record",
|
|
35
39
|
unicode: "日本語 café ñandú",
|
|
36
40
|
};
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
41
|
+
const EMAIL_RE = /^[^@]+@[^@]+\.[^@]+$/;
|
|
42
|
+
const MAX_DEPTH = 8;
|
|
43
|
+
const PATTERN_ATTEMPTS = 48;
|
|
44
|
+
/** Thrown when a walk still overflows; the pointer names the node that blew the stack. */
|
|
45
|
+
export class FixtureOverflow extends Error {
|
|
46
|
+
operationId;
|
|
47
|
+
pointer;
|
|
48
|
+
constructor(operationId, pointer) {
|
|
49
|
+
super(`fixture generation overflow on ${operationId} (${pointer})`);
|
|
50
|
+
this.operationId = operationId;
|
|
51
|
+
this.pointer = pointer;
|
|
52
|
+
this.name = "FixtureOverflow";
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export function overflowFrom(error, operationId, pointer = "/") {
|
|
56
|
+
if (error instanceof FixtureOverflow) {
|
|
57
|
+
return new FixtureOverflow(operationId, error.pointer === "/" ? pointer : error.pointer);
|
|
58
|
+
}
|
|
59
|
+
return new FixtureOverflow(operationId, pointer);
|
|
60
|
+
}
|
|
61
|
+
export function isOverflowError(error) {
|
|
62
|
+
if (error instanceof FixtureOverflow)
|
|
63
|
+
return true;
|
|
64
|
+
return error instanceof RangeError;
|
|
65
|
+
}
|
|
66
|
+
export function buildCohort(bodySchema, seed, variants = COHORT, operationId = "unknown") {
|
|
67
|
+
try {
|
|
68
|
+
return variants.map((variant, index) => ({
|
|
69
|
+
body: generateObject(bodySchema, variant, mulberry32(seed + index * 7919), index, 0, "/", new WeakSet(), operationId),
|
|
70
|
+
variant,
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
if (isOverflowError(error))
|
|
75
|
+
throw overflowFrom(error, operationId);
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* One create body, or a gap if a required field cannot be honoured.
|
|
81
|
+
*
|
|
82
|
+
* Optional fields that cannot match `format`/`pattern` are omitted. Required ones must not fall
|
|
83
|
+
* back to `"Quarterly Report N"` — that is what 400s every email/handle/url field.
|
|
84
|
+
*/
|
|
85
|
+
export function generateBody(bodySchema, variant, rand, index, operationId = "unknown") {
|
|
86
|
+
const missingRequired = [];
|
|
87
|
+
const body = generateObject(bodySchema, variant, rand, index, 0, "/", new WeakSet(), operationId, missingRequired);
|
|
88
|
+
return { body, missingRequired };
|
|
89
|
+
}
|
|
90
|
+
function generateObject(schema, variant, rand, index, depth, pointer, seen, operationId, missingRequired = []) {
|
|
91
|
+
try {
|
|
92
|
+
if (depth > MAX_DEPTH)
|
|
93
|
+
return {};
|
|
94
|
+
if (isVacuous(schema))
|
|
95
|
+
return {};
|
|
96
|
+
if (schema === false)
|
|
97
|
+
return {};
|
|
98
|
+
if (typeof schema !== "object" || schema === null)
|
|
99
|
+
return {};
|
|
100
|
+
if (seen.has(schema))
|
|
101
|
+
return {};
|
|
102
|
+
seen.add(schema);
|
|
103
|
+
const concrete = concreteBranch(schema, depth, pointer, seen, operationId);
|
|
104
|
+
if (isVacuous(concrete) || typeof concrete !== "object" || concrete === null)
|
|
105
|
+
return {};
|
|
106
|
+
if (seen.has(concrete) && concrete !== schema)
|
|
107
|
+
return {};
|
|
108
|
+
const properties = concrete.properties;
|
|
109
|
+
if (properties === null || typeof properties !== "object")
|
|
110
|
+
return {};
|
|
111
|
+
const required = Array.isArray(concrete.required) ? concrete.required : [];
|
|
112
|
+
const out = {};
|
|
113
|
+
for (const [name, raw] of Object.entries(properties)) {
|
|
114
|
+
const child = `${pointer === "/" ? "" : pointer}/${escapePointer(name)}`;
|
|
115
|
+
const value = generateValue(name, raw, variant, rand, index, depth + 1, child, seen, operationId);
|
|
116
|
+
if (value === undefined) {
|
|
117
|
+
if (required.includes(name)) {
|
|
118
|
+
const fallback = requiredFallback(raw, variant, index);
|
|
119
|
+
if (fallback === undefined)
|
|
120
|
+
missingRequired.push(child);
|
|
121
|
+
else
|
|
122
|
+
out[name] = fallback;
|
|
123
|
+
}
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
out[name] = value;
|
|
61
127
|
}
|
|
62
|
-
out
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
if (isOverflowError(error))
|
|
132
|
+
throw overflowFrom(error, operationId, pointer);
|
|
133
|
+
throw error;
|
|
63
134
|
}
|
|
64
|
-
return out;
|
|
65
135
|
}
|
|
66
|
-
function generateValue(name,
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
136
|
+
function generateValue(name, raw, variant, rand, index, depth, pointer, seen, operationId) {
|
|
137
|
+
try {
|
|
138
|
+
if (depth > MAX_DEPTH)
|
|
139
|
+
return undefined;
|
|
140
|
+
if (isVacuous(raw))
|
|
141
|
+
return scalarFor(name, variant, index, undefined);
|
|
142
|
+
if (raw === false)
|
|
143
|
+
return undefined;
|
|
144
|
+
if (typeof raw !== "object" || raw === null)
|
|
145
|
+
return undefined;
|
|
146
|
+
if (seen.has(raw))
|
|
147
|
+
return {};
|
|
148
|
+
const schema = raw;
|
|
149
|
+
const nullable = isNullable(schema);
|
|
150
|
+
if (variant === "null-heavy" && nullable)
|
|
151
|
+
return null;
|
|
152
|
+
if (schema.readOnly === true)
|
|
153
|
+
return undefined;
|
|
154
|
+
const followed = followRef(schema, depth, pointer, seen, operationId);
|
|
155
|
+
if (followed === "cycle")
|
|
156
|
+
return {};
|
|
157
|
+
const concrete = concreteBranch(followed === null ? schema : followed, depth, pointer, seen, operationId);
|
|
158
|
+
if (isVacuous(concrete))
|
|
159
|
+
return scalarFor(name, variant, index, undefined);
|
|
160
|
+
if (typeof concrete !== "object" || concrete === null)
|
|
161
|
+
return scalarFor(name, variant, index, undefined);
|
|
162
|
+
if (seen.has(concrete) && concrete !== schema)
|
|
163
|
+
return {};
|
|
164
|
+
if (Array.isArray(concrete.enum) && concrete.enum.length > 0) {
|
|
165
|
+
return concrete.enum[index % concrete.enum.length];
|
|
89
166
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
return concrete
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
167
|
+
const type = typeOf(concrete);
|
|
168
|
+
switch (type) {
|
|
169
|
+
case "string":
|
|
170
|
+
return generateString(name, concrete, variant, index);
|
|
171
|
+
case "integer":
|
|
172
|
+
case "number": {
|
|
173
|
+
if (variant === "boundary" && typeof concrete.maximum === "number")
|
|
174
|
+
return concrete.maximum;
|
|
175
|
+
const min = typeof concrete.minimum === "number" ? concrete.minimum : 0;
|
|
176
|
+
/*
|
|
177
|
+
* A ladder whose lexical order differs from its numeric order: as text these sort
|
|
178
|
+
* 1, 10, 100, 2, 20, 5, 50 — nothing like their numeric order.
|
|
179
|
+
*
|
|
180
|
+
* An evenly spaced sequence (10, 20, 30…) sorts identically either way, so a backend
|
|
181
|
+
* comparing numbers as strings would look correct. Values have to disagree for the
|
|
182
|
+
* assertion to carry any signal.
|
|
183
|
+
*/
|
|
184
|
+
const ladder = [1, 2, 5, 10, 20, 50, 100];
|
|
185
|
+
const step = ladder[index % ladder.length] ?? 1;
|
|
186
|
+
return type === "integer" ? min + step : min + step + Math.round(rand() * 9) / 10;
|
|
187
|
+
}
|
|
188
|
+
case "boolean":
|
|
189
|
+
return index % 2 === 0;
|
|
190
|
+
case "array": {
|
|
191
|
+
/* An empty array is not a safe default: `minItems` is common on required collections
|
|
192
|
+
* (column definitions, line items), and sending [] fails validation on exactly the
|
|
193
|
+
* endpoints most worth testing. */
|
|
194
|
+
const min = typeof concrete.minItems === "number" ? concrete.minItems : 0;
|
|
195
|
+
const count = Math.max(min, 1);
|
|
196
|
+
const prefix = concrete.prefixItems;
|
|
197
|
+
if (Array.isArray(prefix)) {
|
|
198
|
+
return prefix.map((item, position) => generateValue(`${name}_${position}`, item, variant, rand, index + position, depth + 1, `${pointer}/prefixItems/${position}`, seen, operationId));
|
|
199
|
+
}
|
|
200
|
+
const items = concrete.items;
|
|
201
|
+
if (isVacuous(items) || items === false)
|
|
202
|
+
return Array.from({ length: count }, () => ({}));
|
|
203
|
+
if (items === null || typeof items !== "object")
|
|
204
|
+
return [];
|
|
205
|
+
return Array.from({ length: count }, (_, position) => generateValue(`${name}_item`, items, variant, rand, index + position, depth + 1, `${pointer}/items`, seen, operationId));
|
|
206
|
+
}
|
|
207
|
+
case "object":
|
|
208
|
+
return generateObject(concrete, variant, rand, index, depth + 1, pointer, seen, operationId);
|
|
209
|
+
default:
|
|
210
|
+
return undefined;
|
|
106
211
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
if (isOverflowError(error))
|
|
215
|
+
throw overflowFrom(error, operationId, pointer);
|
|
216
|
+
throw error;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function generateString(name, schema, variant, index) {
|
|
220
|
+
const max = typeof schema.maxLength === "number" ? schema.maxLength : 64;
|
|
221
|
+
const format = typeof schema.format === "string" ? schema.format.toLowerCase() : "";
|
|
222
|
+
const pattern = typeof schema.pattern === "string" ? schema.pattern : undefined;
|
|
223
|
+
let text;
|
|
224
|
+
if (format === "email")
|
|
225
|
+
text = emailOf(variant, index);
|
|
226
|
+
else if (format === "uri" || format === "url")
|
|
227
|
+
text = uriOf(variant, index);
|
|
228
|
+
else if (format === "uuid")
|
|
229
|
+
text = uuidOf(variant, index);
|
|
230
|
+
else if (pattern !== undefined)
|
|
231
|
+
text = stringMatching(pattern, max, variant, index);
|
|
232
|
+
else if (variant === "boundary")
|
|
233
|
+
text = "B".repeat(Math.max(1, Math.min(max, 512)));
|
|
234
|
+
else {
|
|
235
|
+
const base = STRINGS[variant];
|
|
236
|
+
text = `${base} ${index}`;
|
|
237
|
+
}
|
|
238
|
+
if (text === undefined)
|
|
239
|
+
return undefined;
|
|
240
|
+
if (text.length > max) {
|
|
241
|
+
if (format === "email" || format === "uri" || format === "url" || format === "uuid" || pattern !== undefined) {
|
|
242
|
+
const trimmed = fitMax(text, max, format, pattern, variant, index);
|
|
243
|
+
return trimmed;
|
|
119
244
|
}
|
|
120
|
-
|
|
121
|
-
return generateObject(concrete, variant, rand, index, depth + 1);
|
|
122
|
-
default:
|
|
123
|
-
return undefined;
|
|
245
|
+
return text.slice(0, max);
|
|
124
246
|
}
|
|
247
|
+
if (pattern !== undefined && !safeTest(pattern, text)) {
|
|
248
|
+
return stringMatching(pattern, max, variant, index);
|
|
249
|
+
}
|
|
250
|
+
if (format === "email" && !EMAIL_RE.test(text))
|
|
251
|
+
return undefined;
|
|
252
|
+
void name;
|
|
253
|
+
return text;
|
|
125
254
|
}
|
|
126
|
-
function
|
|
127
|
-
|
|
255
|
+
function requiredFallback(raw, variant, index) {
|
|
256
|
+
if (isVacuous(raw) || raw === false || raw === null || typeof raw !== "object")
|
|
257
|
+
return scalarFor("field", variant, index, undefined);
|
|
258
|
+
const schema = raw;
|
|
259
|
+
const format = typeof schema.format === "string" ? schema.format.toLowerCase() : "";
|
|
260
|
+
const pattern = typeof schema.pattern === "string" ? schema.pattern : undefined;
|
|
261
|
+
if (format === "email" || format === "uri" || format === "url" || format === "uuid" || pattern !== undefined) {
|
|
262
|
+
return generateString("field", schema, variant, index);
|
|
263
|
+
}
|
|
264
|
+
switch (typeOf(concreteBranch(schema, 0, "/", new WeakSet(), "unknown"))) {
|
|
128
265
|
case "integer":
|
|
129
266
|
case "number":
|
|
130
267
|
return 0;
|
|
@@ -135,16 +272,327 @@ function fallbackFor(schema) {
|
|
|
135
272
|
case "object":
|
|
136
273
|
return {};
|
|
137
274
|
default:
|
|
138
|
-
return "value";
|
|
275
|
+
return generateString("field", schema, variant, index) ?? "value";
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
function emailOf(variant, index) {
|
|
279
|
+
const slug = slugVariant(variant);
|
|
280
|
+
return `oat-${slug}-${index}@example.test`;
|
|
281
|
+
}
|
|
282
|
+
function uriOf(variant, index) {
|
|
283
|
+
return `https://example.test/${slugVariant(variant)}-${index}`;
|
|
284
|
+
}
|
|
285
|
+
function uuidOf(variant, index) {
|
|
286
|
+
const n = (slugVariant(variant).length * 17 + index * 7919) >>> 0;
|
|
287
|
+
const hex = (offset, width) => {
|
|
288
|
+
let out = "";
|
|
289
|
+
let x = (n + offset * 0x9e3779b9) >>> 0;
|
|
290
|
+
for (let i = 0; i < width; i++) {
|
|
291
|
+
out += (x & 0xf).toString(16);
|
|
292
|
+
x = (Math.imul(x, 1664525) + 1013904223) >>> 0;
|
|
293
|
+
}
|
|
294
|
+
return out;
|
|
295
|
+
};
|
|
296
|
+
return `${hex(1, 8)}-${hex(2, 4)}-4${hex(3, 3)}-8${hex(4, 3)}-${hex(5, 12)}`;
|
|
297
|
+
}
|
|
298
|
+
function slugVariant(variant) {
|
|
299
|
+
return variant
|
|
300
|
+
.replace(/[^a-z0-9]+/gi, "-")
|
|
301
|
+
.replace(/^-|-$/g, "")
|
|
302
|
+
.toLowerCase();
|
|
303
|
+
}
|
|
304
|
+
function fitMax(text, max, format, pattern, variant, index) {
|
|
305
|
+
if (format === "email") {
|
|
306
|
+
const short = `o${index}@e.t`;
|
|
307
|
+
if (short.length <= max && EMAIL_RE.test(short))
|
|
308
|
+
return short;
|
|
309
|
+
return undefined;
|
|
310
|
+
}
|
|
311
|
+
if (format === "uri" || format === "url") {
|
|
312
|
+
const short = "https://e.t/x";
|
|
313
|
+
if (short.length <= max)
|
|
314
|
+
return short;
|
|
315
|
+
return undefined;
|
|
316
|
+
}
|
|
317
|
+
if (format === "uuid")
|
|
318
|
+
return undefined;
|
|
319
|
+
if (pattern !== undefined)
|
|
320
|
+
return stringMatching(pattern, max, variant, index);
|
|
321
|
+
return text.slice(0, max);
|
|
322
|
+
}
|
|
323
|
+
/** Bounded attempt at a string the documented regex will accept. */
|
|
324
|
+
function stringMatching(pattern, max, variant, index) {
|
|
325
|
+
const candidates = [
|
|
326
|
+
expandPattern(pattern, max, variant, index),
|
|
327
|
+
emailOf(variant, index),
|
|
328
|
+
uriOf(variant, index),
|
|
329
|
+
uuidOf(variant, index),
|
|
330
|
+
slugVariant(variant),
|
|
331
|
+
`oat${index}`,
|
|
332
|
+
`h${index}`,
|
|
333
|
+
"a",
|
|
334
|
+
"ab",
|
|
335
|
+
"abc",
|
|
336
|
+
"handle",
|
|
337
|
+
`handle${index}`,
|
|
338
|
+
"https://example.test/x",
|
|
339
|
+
"oat@example.test",
|
|
340
|
+
];
|
|
341
|
+
for (const candidate of candidates) {
|
|
342
|
+
if (candidate === undefined)
|
|
343
|
+
continue;
|
|
344
|
+
const text = candidate.length > max ? candidate.slice(0, max) : candidate;
|
|
345
|
+
if (text.length <= max && safeTest(pattern, text))
|
|
346
|
+
return text;
|
|
347
|
+
}
|
|
348
|
+
for (let attempt = 0; attempt < PATTERN_ATTEMPTS; attempt++) {
|
|
349
|
+
const text = brutePattern(pattern, max, variant, index + attempt);
|
|
350
|
+
if (text !== undefined && safeTest(pattern, text))
|
|
351
|
+
return text;
|
|
352
|
+
}
|
|
353
|
+
return undefined;
|
|
354
|
+
}
|
|
355
|
+
function safeTest(pattern, text) {
|
|
356
|
+
try {
|
|
357
|
+
return new RegExp(pattern).test(text);
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
return false;
|
|
139
361
|
}
|
|
140
362
|
}
|
|
363
|
+
/**
|
|
364
|
+
* Expand a small, common subset of regexes (`[a-z0-9_]+`, `^handle$`, optional groups). Anything
|
|
365
|
+
* we cannot expand is left for the candidate list — better to omit than to send a junk string.
|
|
366
|
+
*/
|
|
367
|
+
function expandPattern(pattern, max, variant, index) {
|
|
368
|
+
let source = pattern;
|
|
369
|
+
if (source.startsWith("^"))
|
|
370
|
+
source = source.slice(1);
|
|
371
|
+
if (source.endsWith("$"))
|
|
372
|
+
source = source.slice(0, -1);
|
|
373
|
+
try {
|
|
374
|
+
const built = expandAtoms(source, max, variant, index);
|
|
375
|
+
if (built.length > max)
|
|
376
|
+
return built.slice(0, max);
|
|
377
|
+
return built;
|
|
378
|
+
}
|
|
379
|
+
catch {
|
|
380
|
+
return undefined;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
function expandAtoms(source, max, variant, index) {
|
|
384
|
+
let i = 0;
|
|
385
|
+
let out = "";
|
|
386
|
+
const takeQuantifier = () => {
|
|
387
|
+
if (source[i] === "+") {
|
|
388
|
+
i++;
|
|
389
|
+
return { max: 3, min: 1 };
|
|
390
|
+
}
|
|
391
|
+
if (source[i] === "*") {
|
|
392
|
+
i++;
|
|
393
|
+
return { max: 2, min: 0 };
|
|
394
|
+
}
|
|
395
|
+
if (source[i] === "?") {
|
|
396
|
+
i++;
|
|
397
|
+
return { max: 1, min: 0 };
|
|
398
|
+
}
|
|
399
|
+
if (source[i] === "{") {
|
|
400
|
+
const close = source.indexOf("}", i);
|
|
401
|
+
if (close < 0)
|
|
402
|
+
return { max: 1, min: 1 };
|
|
403
|
+
const spec = source.slice(i + 1, close);
|
|
404
|
+
i = close + 1;
|
|
405
|
+
const [lo, hi] = spec.split(",");
|
|
406
|
+
const min = Number.parseInt(lo ?? "1", 10);
|
|
407
|
+
const upper = hi === undefined ? min : hi === "" ? min + 2 : Number.parseInt(hi, 10);
|
|
408
|
+
if (!Number.isFinite(min) || !Number.isFinite(upper))
|
|
409
|
+
return { max: 1, min: 1 };
|
|
410
|
+
return { max: Math.min(upper, 16), min: Math.max(0, min) };
|
|
411
|
+
}
|
|
412
|
+
return { max: 1, min: 1 };
|
|
413
|
+
};
|
|
414
|
+
const emit = (atom, times) => {
|
|
415
|
+
for (let n = 0; n < times && out.length < max; n++)
|
|
416
|
+
out += atom;
|
|
417
|
+
};
|
|
418
|
+
while (i < source.length && out.length < max) {
|
|
419
|
+
const ch = source[i];
|
|
420
|
+
if (ch === undefined)
|
|
421
|
+
break;
|
|
422
|
+
if (ch === "(") {
|
|
423
|
+
const close = matchingParen(source, i);
|
|
424
|
+
if (close < 0)
|
|
425
|
+
break;
|
|
426
|
+
const inner = source.slice(i + 1, close).replace(/^\?:/, "");
|
|
427
|
+
i = close + 1;
|
|
428
|
+
const q = takeQuantifier();
|
|
429
|
+
const piece = expandAtoms(inner.split("|")[0] ?? inner, max, variant, index);
|
|
430
|
+
emit(piece, Math.max(q.min, Math.min(q.max, q.min === 0 ? 0 : 1)));
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
if (ch === "[") {
|
|
434
|
+
const close = source.indexOf("]", i + 1);
|
|
435
|
+
if (close < 0)
|
|
436
|
+
break;
|
|
437
|
+
const atom = firstClassChar(source.slice(i + 1, close));
|
|
438
|
+
i = close + 1;
|
|
439
|
+
const q = takeQuantifier();
|
|
440
|
+
emit(atom, Math.max(q.min, 1));
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
if (ch === "\\") {
|
|
444
|
+
const next = source[i + 1];
|
|
445
|
+
i += 2;
|
|
446
|
+
const atom = next === "d" ? "0" : next === "w" ? "a" : next === "s" ? " " : (next ?? "a");
|
|
447
|
+
const q = takeQuantifier();
|
|
448
|
+
emit(atom, Math.max(q.min, 1));
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
if (ch === ".") {
|
|
452
|
+
i++;
|
|
453
|
+
const q = takeQuantifier();
|
|
454
|
+
emit("a", Math.max(q.min, 1));
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
if (ch === "|" || ch === ")" || ch === "^" || ch === "$") {
|
|
458
|
+
i++;
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
i++;
|
|
462
|
+
const q = takeQuantifier();
|
|
463
|
+
emit(ch, Math.max(q.min, 1));
|
|
464
|
+
}
|
|
465
|
+
void variant;
|
|
466
|
+
void index;
|
|
467
|
+
return out;
|
|
468
|
+
}
|
|
469
|
+
function matchingParen(source, start) {
|
|
470
|
+
let depth = 0;
|
|
471
|
+
for (let i = start; i < source.length; i++) {
|
|
472
|
+
if (source[i] === "(")
|
|
473
|
+
depth++;
|
|
474
|
+
else if (source[i] === ")") {
|
|
475
|
+
depth--;
|
|
476
|
+
if (depth === 0)
|
|
477
|
+
return i;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return -1;
|
|
481
|
+
}
|
|
482
|
+
function firstClassChar(body) {
|
|
483
|
+
let i = 0;
|
|
484
|
+
if (body[0] === "^")
|
|
485
|
+
i = 1;
|
|
486
|
+
while (i < body.length) {
|
|
487
|
+
const ch = body[i];
|
|
488
|
+
if (ch === undefined)
|
|
489
|
+
break;
|
|
490
|
+
if (ch === "\\" && body[i + 1] !== undefined) {
|
|
491
|
+
const next = body[i + 1];
|
|
492
|
+
if (next === "d")
|
|
493
|
+
return "0";
|
|
494
|
+
if (next === "w")
|
|
495
|
+
return "a";
|
|
496
|
+
return next ?? "a";
|
|
497
|
+
}
|
|
498
|
+
if (body[i + 1] === "-" && body[i + 2] !== undefined) {
|
|
499
|
+
return ch === "^" ? (body[i + 2] ?? "a") : ch;
|
|
500
|
+
}
|
|
501
|
+
if (ch !== "^")
|
|
502
|
+
return ch;
|
|
503
|
+
i++;
|
|
504
|
+
}
|
|
505
|
+
return "a";
|
|
506
|
+
}
|
|
507
|
+
function brutePattern(pattern, max, variant, salt) {
|
|
508
|
+
const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789_-";
|
|
509
|
+
const len = Math.min(Math.max(3, (salt % 8) + 3), max);
|
|
510
|
+
let text = "";
|
|
511
|
+
let x = (salt * 1664525 + slugVariant(variant).length) >>> 0;
|
|
512
|
+
for (let i = 0; i < len; i++) {
|
|
513
|
+
text += alphabet[x % alphabet.length];
|
|
514
|
+
x = (Math.imul(x, 22695477) + 1) >>> 0;
|
|
515
|
+
}
|
|
516
|
+
return text;
|
|
517
|
+
}
|
|
518
|
+
function scalarFor(name, variant, index, schema) {
|
|
519
|
+
if (schema !== undefined) {
|
|
520
|
+
const generated = generateString(name, schema, variant, index);
|
|
521
|
+
if (generated !== undefined)
|
|
522
|
+
return generated;
|
|
523
|
+
}
|
|
524
|
+
const base = STRINGS[variant];
|
|
525
|
+
return `${base} ${index}`;
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* `{}` / `true` / a lone `additionalProperties: {}` is "any JSON", not an object to descend.
|
|
529
|
+
* Walking it again is what turns `z.unknown()` into a stack overflow.
|
|
530
|
+
*/
|
|
531
|
+
export function isVacuous(schema) {
|
|
532
|
+
if (schema === true)
|
|
533
|
+
return true;
|
|
534
|
+
if (schema === false || schema === null || typeof schema !== "object")
|
|
535
|
+
return false;
|
|
536
|
+
if (Array.isArray(schema))
|
|
537
|
+
return false;
|
|
538
|
+
const keys = Object.keys(schema).filter((key) => !isAnnotation(key));
|
|
539
|
+
if (keys.length === 0)
|
|
540
|
+
return true;
|
|
541
|
+
if (keys.length === 1 && (keys[0] === "additionalProperties" || keys[0] === "unevaluatedProperties")) {
|
|
542
|
+
return isVacuous(schema[keys[0]]);
|
|
543
|
+
}
|
|
544
|
+
if (keys.length === 1 && keys[0] === "default") {
|
|
545
|
+
const value = schema.default;
|
|
546
|
+
return (value !== null && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0);
|
|
547
|
+
}
|
|
548
|
+
return false;
|
|
549
|
+
}
|
|
550
|
+
function isAnnotation(key) {
|
|
551
|
+
return (key === "description" ||
|
|
552
|
+
key === "title" ||
|
|
553
|
+
key === "deprecated" ||
|
|
554
|
+
key === "example" ||
|
|
555
|
+
key === "examples" ||
|
|
556
|
+
key === "nullable" ||
|
|
557
|
+
key === "readOnly" ||
|
|
558
|
+
key === "writeOnly" ||
|
|
559
|
+
key === "xml" ||
|
|
560
|
+
key === "externalDocs");
|
|
561
|
+
}
|
|
141
562
|
/** `oneOf: [{type: string}, {type: null}]` is the nullable idiom; pick the non-null branch. */
|
|
142
|
-
function concreteBranch(schema) {
|
|
143
|
-
|
|
563
|
+
function concreteBranch(schema, depth, pointer, seen, operationId) {
|
|
564
|
+
if (depth > MAX_DEPTH)
|
|
565
|
+
return {};
|
|
566
|
+
const followed = followRef(schema, depth, pointer, seen, operationId);
|
|
567
|
+
const base = followed === "cycle" ? {} : (followed ?? schema);
|
|
568
|
+
if (typeof base !== "object" || base === null)
|
|
569
|
+
return {};
|
|
570
|
+
const union = base.oneOf ?? base.anyOf;
|
|
144
571
|
if (!Array.isArray(union))
|
|
145
|
-
return
|
|
146
|
-
const branch = union.find((candidate) =>
|
|
147
|
-
|
|
572
|
+
return base;
|
|
573
|
+
const branch = union.find((candidate) => {
|
|
574
|
+
if (candidate === null || typeof candidate !== "object")
|
|
575
|
+
return candidate !== null;
|
|
576
|
+
return candidate.type !== "null";
|
|
577
|
+
});
|
|
578
|
+
if (branch === null || typeof branch !== "object")
|
|
579
|
+
return base;
|
|
580
|
+
if (seen.has(branch))
|
|
581
|
+
return {};
|
|
582
|
+
return concreteBranch(branch, depth + 1, `${pointer}/oneOf`, seen, operationId);
|
|
583
|
+
}
|
|
584
|
+
function followRef(schema, depth, pointer, seen, operationId) {
|
|
585
|
+
void pointer;
|
|
586
|
+
void operationId;
|
|
587
|
+
if (depth > MAX_DEPTH)
|
|
588
|
+
return "cycle";
|
|
589
|
+
const ref = schema.$ref;
|
|
590
|
+
if (typeof ref !== "string")
|
|
591
|
+
return null;
|
|
592
|
+
/* After inlining, leftover `$ref: "#"` (or any self-ref) is the same node. Do not follow. */
|
|
593
|
+
if (ref === "#" || ref === "#/" || seen.has(schema))
|
|
594
|
+
return "cycle";
|
|
595
|
+
return "cycle";
|
|
148
596
|
}
|
|
149
597
|
function isNullable(schema) {
|
|
150
598
|
if (schema.nullable === true)
|
|
@@ -158,6 +606,16 @@ function isNullable(schema) {
|
|
|
158
606
|
return union.some((candidate) => candidate !== null && typeof candidate === "object" && candidate.type === "null");
|
|
159
607
|
}
|
|
160
608
|
function typeOf(schema) {
|
|
609
|
+
if (isVacuous(schema))
|
|
610
|
+
return "string";
|
|
611
|
+
const add = schema.additionalProperties;
|
|
612
|
+
const uneval = schema.unevaluatedProperties;
|
|
613
|
+
if (schema.type === undefined &&
|
|
614
|
+
schema.properties === undefined &&
|
|
615
|
+
schema.items === undefined &&
|
|
616
|
+
(isVacuous(add) || isVacuous(uneval))) {
|
|
617
|
+
return "object";
|
|
618
|
+
}
|
|
161
619
|
const type = schema.type;
|
|
162
620
|
if (typeof type === "string")
|
|
163
621
|
return type;
|
|
@@ -168,8 +626,11 @@ function typeOf(schema) {
|
|
|
168
626
|
}
|
|
169
627
|
if (schema.properties !== undefined)
|
|
170
628
|
return "object";
|
|
171
|
-
if (schema.items !== undefined)
|
|
629
|
+
if (schema.items !== undefined || schema.prefixItems !== undefined)
|
|
172
630
|
return "array";
|
|
173
631
|
return "string";
|
|
174
632
|
}
|
|
633
|
+
function escapePointer(name) {
|
|
634
|
+
return name.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
635
|
+
}
|
|
175
636
|
//# sourceMappingURL=fixture.js.map
|