@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.
Files changed (39) hide show
  1. package/README.md +60 -1
  2. package/dist/cli.js +17 -1
  3. package/dist/reference/http.d.ts.map +1 -1
  4. package/dist/reference/http.js +2 -2
  5. package/dist/reference/http.js.map +1 -1
  6. package/dist/report/console.d.ts.map +1 -1
  7. package/dist/report/console.js +18 -0
  8. package/dist/report/console.js.map +1 -1
  9. package/dist/runtime/checks.d.ts.map +1 -1
  10. package/dist/runtime/checks.js +122 -31
  11. package/dist/runtime/checks.js.map +1 -1
  12. package/dist/runtime/feature-gate.d.ts +23 -0
  13. package/dist/runtime/feature-gate.d.ts.map +1 -0
  14. package/dist/runtime/feature-gate.js +68 -0
  15. package/dist/runtime/feature-gate.js.map +1 -0
  16. package/dist/runtime/fixture.d.ts +28 -1
  17. package/dist/runtime/fixture.d.ts.map +1 -1
  18. package/dist/runtime/fixture.js +551 -90
  19. package/dist/runtime/fixture.js.map +1 -1
  20. package/dist/runtime/run.d.ts.map +1 -1
  21. package/dist/runtime/run.js +135 -41
  22. package/dist/runtime/run.js.map +1 -1
  23. package/dist/runtime/world.d.ts +17 -1
  24. package/dist/runtime/world.d.ts.map +1 -1
  25. package/dist/runtime/world.js +56 -4
  26. package/dist/runtime/world.js.map +1 -1
  27. package/dist/spec/extensions.d.ts +7 -0
  28. package/dist/spec/extensions.d.ts.map +1 -1
  29. package/dist/spec/extensions.js +10 -0
  30. package/dist/spec/extensions.js.map +1 -1
  31. package/dist/spec/graph.d.ts +2 -0
  32. package/dist/spec/graph.d.ts.map +1 -1
  33. package/dist/spec/graph.js +7 -2
  34. package/dist/spec/graph.js.map +1 -1
  35. package/dist/spec/load.d.ts.map +1 -1
  36. package/dist/spec/load.js +19 -4
  37. package/dist/spec/load.js.map +1 -1
  38. package/labs/annotated-openapi.yaml +6 -0
  39. package/package.json +3 -2
@@ -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
- export function buildCohort(bodySchema, seed, variants = COHORT) {
38
- return variants.map((variant, index) => ({
39
- body: generateObject(bodySchema, variant, mulberry32(seed + index * 7919), index),
40
- variant,
41
- }));
42
- }
43
- function generateObject(schema, variant, rand, index, depth = 0) {
44
- if (depth > 4)
45
- return {};
46
- const properties = schema.properties;
47
- if (properties === null || typeof properties !== "object")
48
- return {};
49
- const required = Array.isArray(schema.required) ? schema.required : [];
50
- const out = {};
51
- for (const [name, raw] of Object.entries(properties)) {
52
- if (raw === null || typeof raw !== "object")
53
- continue;
54
- if (raw.readOnly === true)
55
- continue;
56
- const value = generateValue(name, raw, variant, rand, index, depth);
57
- if (value === undefined) {
58
- if (required.includes(name))
59
- out[name] = fallbackFor(raw);
60
- continue;
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[name] = value;
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, schema, variant, rand, index, depth = 0) {
67
- /* Recursive schemas are shared objects after dereferencing, so generation must be bounded. */
68
- if (depth > 4)
69
- return undefined;
70
- const nullable = isNullable(schema);
71
- if (variant === "null-heavy" && nullable)
72
- return null;
73
- const concrete = concreteBranch(schema);
74
- const type = typeOf(concrete);
75
- if (Array.isArray(concrete.enum) && concrete.enum.length > 0) {
76
- /* Walk the enum across the cohort so every value is represented and filters on it
77
- * partition the set non-trivially. */
78
- return concrete.enum[index % concrete.enum.length];
79
- }
80
- switch (type) {
81
- case "string": {
82
- const max = typeof concrete.maxLength === "number" ? concrete.maxLength : 64;
83
- if (variant === "boundary")
84
- return "B".repeat(Math.max(1, Math.min(max, 512)));
85
- const base = STRINGS[variant];
86
- /* Suffix keeps values distinct so equality filters select exactly one instance. */
87
- const text = `${base} ${index}`;
88
- return text.length > max ? text.slice(0, max) : text;
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
- case "integer":
91
- case "number": {
92
- if (variant === "boundary" && typeof concrete.maximum === "number")
93
- return concrete.maximum;
94
- const min = typeof concrete.minimum === "number" ? concrete.minimum : 0;
95
- /*
96
- * A ladder whose lexical order differs from its numeric order: as text these sort
97
- * 1, 10, 100, 2, 20, 5, 50 — nothing like their numeric order.
98
- *
99
- * An evenly spaced sequence (10, 20, 30…) sorts identically either way, so a backend
100
- * comparing numbers as strings would look correct. Values have to disagree for the
101
- * assertion to carry any signal.
102
- */
103
- const ladder = [1, 2, 5, 10, 20, 50, 100];
104
- const step = ladder[index % ladder.length] ?? 1;
105
- return type === "integer" ? min + step : min + step + Math.round(rand() * 9) / 10;
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
- case "boolean":
108
- return index % 2 === 0;
109
- case "array": {
110
- /* An empty array is not a safe default: `minItems` is common on required collections
111
- * (column definitions, line items), and sending [] fails validation on exactly the
112
- * endpoints most worth testing. */
113
- const min = typeof concrete.minItems === "number" ? concrete.minItems : 0;
114
- const count = Math.max(min, 1);
115
- const items = concrete.items;
116
- if (items === null || typeof items !== "object")
117
- return [];
118
- return Array.from({ length: count }, (_, position) => generateValue(`${name}_item`, items, variant, rand, index + position, depth + 1));
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
- case "object":
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 fallbackFor(schema) {
127
- switch (typeOf(concreteBranch(schema))) {
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
- const union = schema.oneOf ?? schema.anyOf;
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 schema;
146
- const branch = union.find((candidate) => candidate !== null && typeof candidate === "object" && candidate.type !== "null");
147
- return branch ?? schema;
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