@amritk/generate-examples 0.3.2 → 0.4.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/dist/generators/derive-example.js +421 -12
- package/package.json +4 -1
|
@@ -1,11 +1,173 @@
|
|
|
1
1
|
import { getMjstInstanceOf, getMjstPrimitive } from '@amritk/helpers/mjst-extension';
|
|
2
2
|
import { resolveRef } from '@amritk/helpers/resolve-ref';
|
|
3
|
-
import { hasAnyOf, hasConst, hasDefault, hasEnum, hasExamples, hasFormat, hasItems, hasMaxLength, hasMinItems, hasMinimum, hasMinLength, hasOneOf, hasProperties, hasRef, hasType, isSchemaObject, } from '@amritk/helpers/schema-guards';
|
|
3
|
+
import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasConst, hasDefault, hasEnum, hasExamples, hasExclusiveMaximum, hasExclusiveMinimum, hasFormat, hasItems, hasMaxItems, hasMaximum, hasMaxLength, hasMinItems, hasMinimum, hasMinLength, hasMinProperties, hasMultipleOf, hasOneOf, hasPattern, hasProperties, hasRef, hasRequired, hasType, hasUniqueItems, isSchemaObject, } from '@amritk/helpers/schema-guards';
|
|
4
4
|
/** Lowercases the first character of a name. e.g. "User" → "user" */
|
|
5
5
|
const lowerFirst = (name) => name.charAt(0).toLowerCase() + name.slice(1);
|
|
6
6
|
/** Derives the example const name from a type name. e.g. "User" → "userExample" */
|
|
7
7
|
const exampleName = (typeName) => `${lowerFirst(typeName)}Example`;
|
|
8
|
-
/**
|
|
8
|
+
/** A representative character for a single regex atom (class body / escape). */
|
|
9
|
+
const charForClass = (inner) => {
|
|
10
|
+
if (/a-z/.test(inner))
|
|
11
|
+
return 'a';
|
|
12
|
+
if (/A-Z/.test(inner))
|
|
13
|
+
return 'A';
|
|
14
|
+
if (/0-9|\\d/.test(inner))
|
|
15
|
+
return '5';
|
|
16
|
+
const first = inner.replace(/^\^/, '')[0];
|
|
17
|
+
return first && first !== '\\' ? first : 'a';
|
|
18
|
+
};
|
|
19
|
+
const charForEscape = (esc) => {
|
|
20
|
+
if (esc === '\\d')
|
|
21
|
+
return '5';
|
|
22
|
+
if (esc === '\\w')
|
|
23
|
+
return 'a';
|
|
24
|
+
if (esc === '\\s')
|
|
25
|
+
return ' ';
|
|
26
|
+
return esc[1] ?? 'a';
|
|
27
|
+
};
|
|
28
|
+
/** Splits `s` on top-level `|`, respecting `[...]` and `(...)` nesting. */
|
|
29
|
+
const topLevelAlternatives = (s) => {
|
|
30
|
+
const parts = [];
|
|
31
|
+
let depth = 0;
|
|
32
|
+
let inClass = false;
|
|
33
|
+
let cur = '';
|
|
34
|
+
for (let i = 0; i < s.length; i++) {
|
|
35
|
+
const c = s[i];
|
|
36
|
+
if (c === '\\') {
|
|
37
|
+
cur += c + (s[i + 1] ?? '');
|
|
38
|
+
i++;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (inClass) {
|
|
42
|
+
cur += c;
|
|
43
|
+
if (c === ']')
|
|
44
|
+
inClass = false;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (c === '[')
|
|
48
|
+
inClass = true;
|
|
49
|
+
else if (c === '(')
|
|
50
|
+
depth++;
|
|
51
|
+
else if (c === ')')
|
|
52
|
+
depth--;
|
|
53
|
+
else if (c === '|' && depth === 0) {
|
|
54
|
+
parts.push(cur);
|
|
55
|
+
cur = '';
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
cur += c;
|
|
59
|
+
}
|
|
60
|
+
parts.push(cur);
|
|
61
|
+
return parts;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Best-effort generator of a string matching a `pattern`, via recursive descent:
|
|
65
|
+
* anchors, literals, `.`, escapes (`\d`/`\w`/`\s`), character classes, groups
|
|
66
|
+
* (capturing / non-capturing / named), alternation (`a|b` — picks the first
|
|
67
|
+
* usable branch), and the `+`/`*`/`?`/`{n}`/`{n,m}` quantifiers. Lookarounds and
|
|
68
|
+
* backreferences fall through to `undefined`. The caller verifies the result
|
|
69
|
+
* against the real regex and only uses it on a match, so a partial sampler never
|
|
70
|
+
* makes the example worse — it just upgrades the cases it understands.
|
|
71
|
+
*/
|
|
72
|
+
const sampleFromPattern = (pattern, minLength) => {
|
|
73
|
+
let body = pattern;
|
|
74
|
+
if (body.startsWith('^'))
|
|
75
|
+
body = body.slice(1);
|
|
76
|
+
if (body.endsWith('$') && !body.endsWith('\\$'))
|
|
77
|
+
body = body.slice(0, -1);
|
|
78
|
+
// Samples one alternation, preferring the first branch that samples cleanly.
|
|
79
|
+
const sampleAlt = (s) => {
|
|
80
|
+
for (const alt of topLevelAlternatives(s)) {
|
|
81
|
+
const r = sampleSeq(alt);
|
|
82
|
+
if (r !== undefined)
|
|
83
|
+
return r;
|
|
84
|
+
}
|
|
85
|
+
return undefined;
|
|
86
|
+
};
|
|
87
|
+
// Samples one concatenation (no top-level `|`).
|
|
88
|
+
const sampleSeq = (seq) => {
|
|
89
|
+
let out = '';
|
|
90
|
+
let i = 0;
|
|
91
|
+
while (i < seq.length) {
|
|
92
|
+
let unit;
|
|
93
|
+
const c = seq[i];
|
|
94
|
+
if (c === '(') {
|
|
95
|
+
// Find the matching close paren.
|
|
96
|
+
let depth = 1;
|
|
97
|
+
let j = i + 1;
|
|
98
|
+
for (; j < seq.length && depth > 0; j++) {
|
|
99
|
+
const cj = seq[j];
|
|
100
|
+
if (cj === '\\') {
|
|
101
|
+
j++;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (cj === '(')
|
|
105
|
+
depth++;
|
|
106
|
+
else if (cj === ')')
|
|
107
|
+
depth--;
|
|
108
|
+
}
|
|
109
|
+
if (depth !== 0)
|
|
110
|
+
return undefined;
|
|
111
|
+
let inner = seq.slice(i + 1, j - 1);
|
|
112
|
+
if (/^\?[=!]/.test(inner) || /^\?<[=!]/.test(inner))
|
|
113
|
+
return undefined; // lookaround
|
|
114
|
+
inner = inner.replace(/^\?:/, '').replace(/^\?<[^>]*>/, '');
|
|
115
|
+
unit = sampleAlt(inner);
|
|
116
|
+
if (unit === undefined)
|
|
117
|
+
return undefined;
|
|
118
|
+
i = j;
|
|
119
|
+
}
|
|
120
|
+
else if (c === '[') {
|
|
121
|
+
const end = seq.indexOf(']', i + 1);
|
|
122
|
+
if (end === -1)
|
|
123
|
+
return undefined;
|
|
124
|
+
unit = charForClass(seq.slice(i + 1, end));
|
|
125
|
+
i = end + 1;
|
|
126
|
+
}
|
|
127
|
+
else if (c === '\\') {
|
|
128
|
+
const esc = seq.slice(i, i + 2);
|
|
129
|
+
if (/\d/.test(esc[1] ?? ''))
|
|
130
|
+
return undefined; // backreference
|
|
131
|
+
unit = charForEscape(esc);
|
|
132
|
+
i += 2;
|
|
133
|
+
}
|
|
134
|
+
else if (c === '.') {
|
|
135
|
+
unit = 'a';
|
|
136
|
+
i++;
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
unit = c;
|
|
140
|
+
i++;
|
|
141
|
+
}
|
|
142
|
+
// Optional quantifier.
|
|
143
|
+
let reps = 1;
|
|
144
|
+
const q = seq[i];
|
|
145
|
+
if (q === '+') {
|
|
146
|
+
reps = Math.max(1, minLength);
|
|
147
|
+
i++;
|
|
148
|
+
}
|
|
149
|
+
else if (q === '*') {
|
|
150
|
+
reps = Math.max(0, minLength);
|
|
151
|
+
i++;
|
|
152
|
+
}
|
|
153
|
+
else if (q === '?') {
|
|
154
|
+
reps = 1;
|
|
155
|
+
i++;
|
|
156
|
+
}
|
|
157
|
+
else if (q === '{') {
|
|
158
|
+
const end = seq.indexOf('}', i + 1);
|
|
159
|
+
if (end === -1)
|
|
160
|
+
return undefined;
|
|
161
|
+
reps = Number.parseInt(seq.slice(i + 1, end), 10) || 0;
|
|
162
|
+
i = end + 1;
|
|
163
|
+
}
|
|
164
|
+
out += unit.repeat(reps);
|
|
165
|
+
}
|
|
166
|
+
return out;
|
|
167
|
+
};
|
|
168
|
+
return sampleAlt(body);
|
|
169
|
+
};
|
|
170
|
+
/** Returns a representative string honouring `format`, `pattern`, and length. */
|
|
9
171
|
const exampleString = (schema) => {
|
|
10
172
|
if (hasFormat(schema)) {
|
|
11
173
|
switch (schema.format) {
|
|
@@ -30,9 +192,18 @@ const exampleString = (schema) => {
|
|
|
30
192
|
return '::1';
|
|
31
193
|
}
|
|
32
194
|
}
|
|
195
|
+
const minLength = hasMinLength(schema) ? schema.minLength : 0;
|
|
196
|
+
if (hasPattern(schema)) {
|
|
197
|
+
const sampled = sampleFromPattern(schema.pattern, minLength);
|
|
198
|
+
// Only trust the sampler when it actually matches and fits the length bound.
|
|
199
|
+
if (sampled !== undefined && new RegExp(schema.pattern).test(sampled)) {
|
|
200
|
+
if (!(hasMaxLength(schema) && sampled.length > schema.maxLength))
|
|
201
|
+
return sampled;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
33
204
|
let value = 'string';
|
|
34
|
-
if (
|
|
35
|
-
value = value.padEnd(
|
|
205
|
+
if (value.length < minLength)
|
|
206
|
+
value = value.padEnd(minLength, 'x');
|
|
36
207
|
if (hasMaxLength(schema) && value.length > schema.maxLength)
|
|
37
208
|
value = value.slice(0, schema.maxLength);
|
|
38
209
|
return value;
|
|
@@ -57,8 +228,12 @@ export const deriveExample = (schema, rootSchema, seen = new Set()) => {
|
|
|
57
228
|
return schema.examples[0];
|
|
58
229
|
if (hasDefault(schema))
|
|
59
230
|
return schema.default;
|
|
60
|
-
if (hasEnum(schema) && schema.enum.length > 0)
|
|
61
|
-
|
|
231
|
+
if (hasEnum(schema) && schema.enum.length > 0) {
|
|
232
|
+
// Prefer the first member that also satisfies any sibling length/range
|
|
233
|
+
// constraints (e.g. `enum` + `minLength`), falling back to the first member.
|
|
234
|
+
const fitting = schema.enum.find((value) => satisfiesScalarConstraints(schema, value));
|
|
235
|
+
return fitting !== undefined ? fitting : schema.enum[0];
|
|
236
|
+
}
|
|
62
237
|
if (hasRef(schema)) {
|
|
63
238
|
const ref = schema.$ref;
|
|
64
239
|
if (seen.has(ref) || !rootSchema)
|
|
@@ -74,6 +249,11 @@ export const deriveExample = (schema, rootSchema, seen = new Set()) => {
|
|
|
74
249
|
const primitive = getMjstPrimitive(schema);
|
|
75
250
|
if (primitive === 'bigint')
|
|
76
251
|
return 0n;
|
|
252
|
+
// `allOf` must satisfy every branch at once, so derive from a single schema
|
|
253
|
+
// that merges the branches (and the node's own keywords) rather than picking
|
|
254
|
+
// one branch — picking one would ignore the others' constraints.
|
|
255
|
+
if (hasAllOf(schema))
|
|
256
|
+
return deriveExample(mergeAllOf(schema), rootSchema, seen);
|
|
77
257
|
if (hasOneOf(schema) && schema.oneOf[0] !== undefined)
|
|
78
258
|
return deriveExample(schema.oneOf[0], rootSchema, seen);
|
|
79
259
|
if (hasAnyOf(schema) && schema.anyOf[0] !== undefined)
|
|
@@ -87,6 +267,32 @@ export const deriveExample = (schema, rootSchema, seen = new Set()) => {
|
|
|
87
267
|
}
|
|
88
268
|
return null;
|
|
89
269
|
};
|
|
270
|
+
/**
|
|
271
|
+
* True when a candidate value (e.g. an `enum`/`const` member) satisfies the
|
|
272
|
+
* node's simple string-length and numeric-range constraints. Used to pick an
|
|
273
|
+
* `enum` member that also meets a sibling `minLength`/`minimum`/etc.
|
|
274
|
+
*/
|
|
275
|
+
const satisfiesScalarConstraints = (schema, value) => {
|
|
276
|
+
if (typeof value === 'string') {
|
|
277
|
+
if (hasMinLength(schema) && value.length < schema.minLength)
|
|
278
|
+
return false;
|
|
279
|
+
if (hasMaxLength(schema) && value.length > schema.maxLength)
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
else if (typeof value === 'number') {
|
|
283
|
+
if (hasMinimum(schema) && value < schema.minimum)
|
|
284
|
+
return false;
|
|
285
|
+
if (hasMaximum(schema) && value > schema.maximum)
|
|
286
|
+
return false;
|
|
287
|
+
if (hasExclusiveMinimum(schema) && value <= schema.exclusiveMinimum)
|
|
288
|
+
return false;
|
|
289
|
+
if (hasExclusiveMaximum(schema) && value >= schema.exclusiveMaximum)
|
|
290
|
+
return false;
|
|
291
|
+
if (hasMultipleOf(schema) && schema.multipleOf > 0 && value % schema.multipleOf !== 0)
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
return true;
|
|
295
|
+
};
|
|
90
296
|
/** Derives a canonical value for a single declared `type`. */
|
|
91
297
|
const deriveForType = (type, schema, rootSchema, seen) => {
|
|
92
298
|
switch (type) {
|
|
@@ -94,16 +300,13 @@ const deriveForType = (type, schema, rootSchema, seen) => {
|
|
|
94
300
|
return exampleString(schema);
|
|
95
301
|
case 'number':
|
|
96
302
|
case 'integer':
|
|
97
|
-
return
|
|
303
|
+
return deriveNumber(schema, type === 'integer');
|
|
98
304
|
case 'boolean':
|
|
99
305
|
return true;
|
|
100
306
|
case 'null':
|
|
101
307
|
return null;
|
|
102
|
-
case 'array':
|
|
103
|
-
|
|
104
|
-
const count = hasMinItems(schema) ? Math.max(schema.minItems, 1) : 1;
|
|
105
|
-
return Array.from({ length: count }, () => item);
|
|
106
|
-
}
|
|
308
|
+
case 'array':
|
|
309
|
+
return deriveArray(schema, rootSchema, seen);
|
|
107
310
|
case 'object': {
|
|
108
311
|
const out = {};
|
|
109
312
|
if (hasProperties(schema)) {
|
|
@@ -111,12 +314,218 @@ const deriveForType = (type, schema, rootSchema, seen) => {
|
|
|
111
314
|
out[key] = deriveExample(propSchema, rootSchema, seen);
|
|
112
315
|
}
|
|
113
316
|
}
|
|
317
|
+
const additional = hasAdditionalProperties(schema) ? schema.additionalProperties : false;
|
|
318
|
+
const additionalSchema = isSchemaObject(additional) ? additional : undefined;
|
|
319
|
+
// Extra keys are allowed unless `additionalProperties: false` forbids them.
|
|
320
|
+
const extrasAllowed = !(hasAdditionalProperties(schema) && schema.additionalProperties === false);
|
|
321
|
+
// A required key with no `properties` entry still needs a value. Use the
|
|
322
|
+
// `additionalProperties` schema when one constrains it, else a null.
|
|
323
|
+
if (hasRequired(schema)) {
|
|
324
|
+
for (const key of schema.required) {
|
|
325
|
+
if (key in out)
|
|
326
|
+
continue;
|
|
327
|
+
out[key] = additionalSchema ? deriveExample(additionalSchema, rootSchema, seen) : null;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
// Synthesize filler keys to reach `minProperties` when extras are allowed.
|
|
331
|
+
if (hasMinProperties(schema) && extrasAllowed) {
|
|
332
|
+
let n = 0;
|
|
333
|
+
while (Object.keys(out).length < schema.minProperties) {
|
|
334
|
+
const key = `extra${n++}`;
|
|
335
|
+
if (key in out)
|
|
336
|
+
continue;
|
|
337
|
+
out[key] = additionalSchema ? deriveExample(additionalSchema, rootSchema, seen) : null;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
114
340
|
return out;
|
|
115
341
|
}
|
|
116
342
|
default:
|
|
117
343
|
return null;
|
|
118
344
|
}
|
|
119
345
|
};
|
|
346
|
+
/**
|
|
347
|
+
* Picks a number satisfying the node's bounds and `multipleOf`. Starts at the
|
|
348
|
+
* lower bound (or 0 when unbounded), nudges past an exclusive bound, then rounds
|
|
349
|
+
* up to the nearest multiple. An unsatisfiable range (e.g. `minimum > maximum`)
|
|
350
|
+
* can't be met and falls back to the lower bound.
|
|
351
|
+
*/
|
|
352
|
+
const deriveNumber = (schema, isInteger) => {
|
|
353
|
+
const step = isInteger ? 1 : 0.5;
|
|
354
|
+
let lo = -Infinity;
|
|
355
|
+
if (hasMinimum(schema))
|
|
356
|
+
lo = Math.max(lo, schema.minimum);
|
|
357
|
+
if (hasExclusiveMinimum(schema))
|
|
358
|
+
lo = Math.max(lo, schema.exclusiveMinimum + step);
|
|
359
|
+
const hi = hasMaximum(schema)
|
|
360
|
+
? schema.maximum
|
|
361
|
+
: hasExclusiveMaximum(schema)
|
|
362
|
+
? schema.exclusiveMaximum - step
|
|
363
|
+
: Number.POSITIVE_INFINITY;
|
|
364
|
+
// Base candidate: the lower bound, or 0 (or the upper bound) when unbounded below.
|
|
365
|
+
let value = Number.isFinite(lo) ? lo : Number.isFinite(hi) ? Math.min(0, hi) : 0;
|
|
366
|
+
if (isInteger)
|
|
367
|
+
value = Math.ceil(value);
|
|
368
|
+
if (hasMultipleOf(schema) && schema.multipleOf > 0) {
|
|
369
|
+
const m = schema.multipleOf;
|
|
370
|
+
value = Math.ceil(value / m - 1e-9) * m;
|
|
371
|
+
// Rounding up can overshoot the upper bound; drop to the largest multiple
|
|
372
|
+
// that fits. (If even that falls below `lo`, the range has no multiple — an
|
|
373
|
+
// unsatisfiable schema — and we return the in-range candidate as best effort.)
|
|
374
|
+
if (value > hi && Number.isFinite(hi))
|
|
375
|
+
value = Math.floor(hi / m + 1e-9) * m;
|
|
376
|
+
}
|
|
377
|
+
// `+ 0` normalizes a `-0` (which `Math.ceil`/`Math.floor` can produce) to `0`.
|
|
378
|
+
return (isInteger ? Math.round(value) : value) + 0;
|
|
379
|
+
};
|
|
380
|
+
/**
|
|
381
|
+
* Derives an array value. A tuple schema (`prefixItems`, or the draft-07
|
|
382
|
+
* array-form `items`) derives one value per position; a uniform array repeats a
|
|
383
|
+
* single item value. The count is clamped into `[minItems, maxItems]` so a
|
|
384
|
+
* `maxItems: 0` yields `[]` and a `minItems` is always met.
|
|
385
|
+
*/
|
|
386
|
+
const deriveArray = (schema, rootSchema, seen) => {
|
|
387
|
+
const items = hasItems(schema) ? schema.items : undefined;
|
|
388
|
+
const prefixItems = schema['prefixItems'];
|
|
389
|
+
// A tuple is `prefixItems` (2020-12) or the draft-07 array-form `items`.
|
|
390
|
+
const prefix = Array.isArray(prefixItems)
|
|
391
|
+
? prefixItems
|
|
392
|
+
: Array.isArray(items)
|
|
393
|
+
? items
|
|
394
|
+
: undefined;
|
|
395
|
+
const min = hasMinItems(schema) ? schema.minItems : 0;
|
|
396
|
+
const max = hasMaxItems(schema) ? schema.maxItems : Number.POSITIVE_INFINITY;
|
|
397
|
+
// The rest/uniform element schema (the singular object-form `items`). A boolean
|
|
398
|
+
// `items` (`true`/`false`) is not a value-producing schema, so it is not `rest`.
|
|
399
|
+
const rest = items !== undefined && !Array.isArray(items) && isSchemaObject(items) ? items : undefined;
|
|
400
|
+
if (prefix) {
|
|
401
|
+
const tuple = prefix.map((item) => deriveExample(item, rootSchema, seen));
|
|
402
|
+
// Pad up to `minItems`: with the rest schema when one is present, otherwise
|
|
403
|
+
// (in 2020-12 additional items past `prefixItems` are unconstrained unless
|
|
404
|
+
// `items: false`) with a plain `null`.
|
|
405
|
+
const itemsClosed = schema['items'] === false;
|
|
406
|
+
while (tuple.length < min && tuple.length < max) {
|
|
407
|
+
if (rest !== undefined)
|
|
408
|
+
tuple.push(deriveExample(rest, rootSchema, seen));
|
|
409
|
+
else if (itemsClosed)
|
|
410
|
+
break;
|
|
411
|
+
else
|
|
412
|
+
tuple.push(null);
|
|
413
|
+
}
|
|
414
|
+
return tuple.length > max ? tuple.slice(0, max) : tuple;
|
|
415
|
+
}
|
|
416
|
+
const raw = schema;
|
|
417
|
+
const containsRaw = raw['contains'];
|
|
418
|
+
const contains = containsRaw !== undefined && isSchemaObject(containsRaw) ? containsRaw : undefined;
|
|
419
|
+
const minContains = contains !== undefined ? (typeof raw['minContains'] === 'number' ? raw['minContains'] : 1) : 0;
|
|
420
|
+
const unique = hasUniqueItems(schema) && schema.uniqueItems === true;
|
|
421
|
+
// The element schema: the uniform `items`, else the `contains` subschema.
|
|
422
|
+
const elem = rest ?? contains;
|
|
423
|
+
// Prefer a non-empty example, satisfy `minItems` and `minContains`, never exceed `maxItems`.
|
|
424
|
+
const count = Math.min(Math.max(min, minContains, max === 0 ? 0 : 1), max);
|
|
425
|
+
const result = [];
|
|
426
|
+
for (let i = 0; i < count; i++) {
|
|
427
|
+
// Make the first `minContains` items satisfy `contains`; the rest use `items`.
|
|
428
|
+
const itemSchema = contains !== undefined && i < minContains ? contains : elem;
|
|
429
|
+
const base = itemSchema !== undefined ? deriveExample(itemSchema, rootSchema, seen) : null;
|
|
430
|
+
result.push(unique ? distinctify(base, i, itemSchema) : base);
|
|
431
|
+
}
|
|
432
|
+
return result;
|
|
433
|
+
};
|
|
434
|
+
/**
|
|
435
|
+
* Returns a value distinct from earlier ones for index `i`, used to satisfy
|
|
436
|
+
* `uniqueItems`, while staying within the item schema's constraints: numbers step
|
|
437
|
+
* by `multipleOf` (so the perturbed values remain valid multiples) rather than by
|
|
438
|
+
* 1, strings are suffixed, booleans alternated. Values that can't be cheaply
|
|
439
|
+
* varied are returned as-is (a best-effort the generated `fast-check` arbitrary
|
|
440
|
+
* covers fully).
|
|
441
|
+
*/
|
|
442
|
+
const distinctify = (base, i, itemSchema) => {
|
|
443
|
+
if (i === 0)
|
|
444
|
+
return base;
|
|
445
|
+
if (typeof base === 'number') {
|
|
446
|
+
const step = itemSchema && isSchemaObject(itemSchema) && hasMultipleOf(itemSchema) && itemSchema.multipleOf > 0
|
|
447
|
+
? itemSchema.multipleOf
|
|
448
|
+
: 1;
|
|
449
|
+
return base + i * step;
|
|
450
|
+
}
|
|
451
|
+
if (typeof base === 'string')
|
|
452
|
+
return `${base}${i}`;
|
|
453
|
+
if (typeof base === 'boolean')
|
|
454
|
+
return i % 2 === 1 ? !base : base;
|
|
455
|
+
return base;
|
|
456
|
+
};
|
|
457
|
+
/**
|
|
458
|
+
* Flattens an `allOf` into a single schema: object `properties` are merged and
|
|
459
|
+
* `required` unioned across branches, while scalar keywords from later branches
|
|
460
|
+
* (and the node's own keywords) win. `allOf` itself is dropped so the merged
|
|
461
|
+
* schema derives directly.
|
|
462
|
+
*/
|
|
463
|
+
const TIGHTEST = new Map([
|
|
464
|
+
['minimum', 'max'],
|
|
465
|
+
['exclusiveMinimum', 'max'],
|
|
466
|
+
['minLength', 'max'],
|
|
467
|
+
['minItems', 'max'],
|
|
468
|
+
['minProperties', 'max'],
|
|
469
|
+
['maximum', 'min'],
|
|
470
|
+
['exclusiveMaximum', 'min'],
|
|
471
|
+
['maxLength', 'min'],
|
|
472
|
+
['maxItems', 'min'],
|
|
473
|
+
['maxProperties', 'min'],
|
|
474
|
+
]);
|
|
475
|
+
const mergeAllOf = (schema) => {
|
|
476
|
+
const branches = hasAllOf(schema) ? schema.allOf : [];
|
|
477
|
+
const merged = {};
|
|
478
|
+
const properties = {};
|
|
479
|
+
const required = new Set();
|
|
480
|
+
for (const branch of [...branches, schema]) {
|
|
481
|
+
if (!isSchemaObject(branch))
|
|
482
|
+
continue;
|
|
483
|
+
for (const [key, value] of Object.entries(branch)) {
|
|
484
|
+
if (key === 'allOf')
|
|
485
|
+
continue;
|
|
486
|
+
if (key === 'properties' && value && typeof value === 'object') {
|
|
487
|
+
// The same property constrained by several branches must satisfy all of
|
|
488
|
+
// them, so collect each schema and combine them below rather than letting
|
|
489
|
+
// a later branch's schema silently replace an earlier one.
|
|
490
|
+
for (const [prop, propSchema] of Object.entries(value)) {
|
|
491
|
+
const bucket = properties[prop];
|
|
492
|
+
if (bucket)
|
|
493
|
+
bucket.push(propSchema);
|
|
494
|
+
else
|
|
495
|
+
properties[prop] = [propSchema];
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
else if (key === 'required' && Array.isArray(value)) {
|
|
499
|
+
for (const r of value)
|
|
500
|
+
required.add(r);
|
|
501
|
+
}
|
|
502
|
+
else if (key === 'enum' && Array.isArray(value)) {
|
|
503
|
+
// A value must be in *every* branch's enum, so intersect rather than let
|
|
504
|
+
// a later branch's enum replace an earlier one (which could pick a member
|
|
505
|
+
// the earlier branch rejects).
|
|
506
|
+
merged['enum'] = Array.isArray(merged['enum'])
|
|
507
|
+
? merged['enum'].filter((member) => value.includes(member))
|
|
508
|
+
: value;
|
|
509
|
+
}
|
|
510
|
+
else if (TIGHTEST.has(key) && typeof value === 'number' && typeof merged[key] === 'number') {
|
|
511
|
+
// Numeric bounds from different branches combine to the tightest one.
|
|
512
|
+
merged[key] = TIGHTEST.get(key) === 'max' ? Math.max(merged[key], value) : Math.min(merged[key], value);
|
|
513
|
+
}
|
|
514
|
+
else {
|
|
515
|
+
merged[key] = value;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
const mergedProps = {};
|
|
520
|
+
for (const [prop, schemas] of Object.entries(properties)) {
|
|
521
|
+
mergedProps[prop] = schemas.length === 1 ? schemas[0] : { allOf: schemas };
|
|
522
|
+
}
|
|
523
|
+
if (Object.keys(mergedProps).length > 0)
|
|
524
|
+
merged['properties'] = mergedProps;
|
|
525
|
+
if (required.size > 0)
|
|
526
|
+
merged['required'] = [...required];
|
|
527
|
+
return merged;
|
|
528
|
+
};
|
|
120
529
|
/**
|
|
121
530
|
* Serializes a derived value into a TypeScript source expression. Handles the
|
|
122
531
|
* non-JSON values `deriveExample` can produce (`Date`, `bigint`) in addition to
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amritk/generate-examples",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Generate fast-check arbitraries and example values from JSON Schemas.",
|
|
5
5
|
"module": "./dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -49,6 +49,9 @@
|
|
|
49
49
|
"json-schema-typed": "^8.0.1",
|
|
50
50
|
"@amritk/helpers": "0.10.0"
|
|
51
51
|
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"ajv": "^8.17.1"
|
|
54
|
+
},
|
|
52
55
|
"peerDependencies": {
|
|
53
56
|
"fast-check": ">=3"
|
|
54
57
|
},
|