@amritk/generate-examples 0.4.1 → 0.4.2
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.
|
@@ -9,7 +9,8 @@ import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasItems, hasOneOf, hasRef
|
|
|
9
9
|
const buildImport = (ref, suffix) => {
|
|
10
10
|
const filename = refToFilename(ref);
|
|
11
11
|
const typeName = refToName(ref, suffix);
|
|
12
|
-
|
|
12
|
+
// `.js` extension so the emitted import resolves under Node ESM, not only Bun.
|
|
13
|
+
return `import { type ${typeName}, ${typeName}Arbitrary } from './${filename}.js'`;
|
|
13
14
|
};
|
|
14
15
|
/**
|
|
15
16
|
* Walks one level of the schema and yields all direct $ref strings that should
|
|
@@ -11,6 +11,7 @@ import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
|
|
|
11
11
|
* pattern — use the generated arbitrary when pattern fidelity matters.
|
|
12
12
|
*/
|
|
13
13
|
export declare const deriveExample: (schema: JSONSchema, rootSchema?: Record<string, unknown>, seen?: ReadonlySet<string>) => unknown;
|
|
14
|
+
export declare const mergeAllOf: (schema: JSONSchema) => JSONSchema;
|
|
14
15
|
/**
|
|
15
16
|
* Serializes a derived value into a TypeScript source expression. Handles the
|
|
16
17
|
* non-JSON values `deriveExample` can produce (`Date`, `bigint`) in addition to
|
|
@@ -472,7 +472,7 @@ const TIGHTEST = new Map([
|
|
|
472
472
|
['maxItems', 'min'],
|
|
473
473
|
['maxProperties', 'min'],
|
|
474
474
|
]);
|
|
475
|
-
const mergeAllOf = (schema) => {
|
|
475
|
+
export const mergeAllOf = (schema) => {
|
|
476
476
|
const branches = hasAllOf(schema) ? schema.allOf : [];
|
|
477
477
|
const merged = {};
|
|
478
478
|
const properties = {};
|
|
@@ -2,6 +2,10 @@ import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
|
|
|
2
2
|
/**
|
|
3
3
|
* Generates a `fast-check` arbitrary that produces schema-valid values.
|
|
4
4
|
*
|
|
5
|
+
* A schema that references itself is wrapped in `fc.letrec` so the recursion is
|
|
6
|
+
* tied lazily — a plain `const NodeArbitrary = fc.record({ next: NodeArbitrary })`
|
|
7
|
+
* would throw a TDZ `ReferenceError` the moment the module is imported.
|
|
8
|
+
*
|
|
5
9
|
* @example
|
|
6
10
|
* ```typescript
|
|
7
11
|
* generateArbitrary({ type: 'object', properties: { name: { type: 'string' } }, required: ['name'] }, 'Info')
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { getMjstInstanceOf, getMjstPrimitive } from '@amritk/helpers/mjst-extension';
|
|
2
2
|
import { refToName } from '@amritk/helpers/ref-to-name';
|
|
3
|
-
import { hasAnyOf, hasConst, hasEnum, hasExclusiveMaximum, hasExclusiveMinimum, hasFormat, hasItems, hasMaxItems, hasMaximum, hasMaxLength, hasMinItems, hasMinimum, hasMinLength, hasMultipleOf, hasOneOf, hasPattern, hasProperties, hasRef, hasRequired, hasType, hasUniqueItems, isSchemaObject, } from '@amritk/helpers/schema-guards';
|
|
3
|
+
import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasConst, hasEnum, hasExclusiveMaximum, hasExclusiveMinimum, hasFormat, hasItems, hasMaxItems, hasMaximum, hasMaxLength, hasMinItems, hasMinimum, hasMinLength, hasMultipleOf, hasOneOf, hasPattern, hasProperties, hasRef, hasRequired, hasType, hasUniqueItems, isSchemaObject, } from '@amritk/helpers/schema-guards';
|
|
4
|
+
import { mergeAllOf } from './derive-example.js';
|
|
4
5
|
/**
|
|
5
6
|
* Derives the arbitrary const name from a type name.
|
|
6
7
|
* e.g. "User" → "UserArbitrary"
|
|
7
8
|
*/
|
|
8
9
|
const arbitraryName = (typeName) => `${typeName}Arbitrary`;
|
|
10
|
+
/** The letrec key used for a type's own (self-referential) arbitrary. */
|
|
11
|
+
const SELF_KEY = 'self';
|
|
9
12
|
/** Builds a `fc.string({ ... })` expression honouring format and length constraints. */
|
|
10
13
|
const stringExpr = (schema) => {
|
|
11
14
|
if (hasFormat(schema)) {
|
|
@@ -31,8 +34,20 @@ const stringExpr = (schema) => {
|
|
|
31
34
|
return 'fc.ipV6()';
|
|
32
35
|
}
|
|
33
36
|
}
|
|
34
|
-
if (hasPattern(schema))
|
|
35
|
-
|
|
37
|
+
if (hasPattern(schema)) {
|
|
38
|
+
// Build the regex via `new RegExp(<json-string>)` rather than inlining the
|
|
39
|
+
// pattern into a `/.../ ` literal: a pattern containing `/` (e.g. `^/api/v\d+$`)
|
|
40
|
+
// would otherwise close the literal early and emit invalid TypeScript.
|
|
41
|
+
const base = `fc.stringMatching(new RegExp(${JSON.stringify(schema.pattern)}))`;
|
|
42
|
+
// `stringMatching` takes no length bounds, so honour any min/maxLength with a
|
|
43
|
+
// filter instead of silently dropping them. Only emit it when a bound exists.
|
|
44
|
+
const checks = [];
|
|
45
|
+
if (hasMinLength(schema))
|
|
46
|
+
checks.push(`s.length >= ${schema.minLength}`);
|
|
47
|
+
if (hasMaxLength(schema))
|
|
48
|
+
checks.push(`s.length <= ${schema.maxLength}`);
|
|
49
|
+
return checks.length > 0 ? `${base}.filter((s) => ${checks.join(' && ')})` : base;
|
|
50
|
+
}
|
|
36
51
|
const opts = [];
|
|
37
52
|
if (hasMinLength(schema))
|
|
38
53
|
opts.push(`minLength: ${schema.minLength}`);
|
|
@@ -43,14 +58,23 @@ const stringExpr = (schema) => {
|
|
|
43
58
|
/** Builds a `fc.integer({ ... })` expression honouring range and multiple-of constraints. */
|
|
44
59
|
const integerExpr = (schema) => {
|
|
45
60
|
const opts = [];
|
|
61
|
+
// With both `minimum` and `exclusiveMinimum` present the effective lower bound
|
|
62
|
+
// is the tighter (larger) of the two, so combine them rather than letting one
|
|
63
|
+
// shadow the other via else-if.
|
|
64
|
+
const mins = [];
|
|
46
65
|
if (hasMinimum(schema))
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
66
|
+
mins.push(Number(schema.minimum));
|
|
67
|
+
if (hasExclusiveMinimum(schema))
|
|
68
|
+
mins.push(Number(schema.exclusiveMinimum) + 1);
|
|
69
|
+
if (mins.length > 0)
|
|
70
|
+
opts.push(`min: ${Math.max(...mins)}`);
|
|
71
|
+
const maxs = [];
|
|
50
72
|
if (hasMaximum(schema))
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
73
|
+
maxs.push(Number(schema.maximum));
|
|
74
|
+
if (hasExclusiveMaximum(schema))
|
|
75
|
+
maxs.push(Number(schema.exclusiveMaximum) - 1);
|
|
76
|
+
if (maxs.length > 0)
|
|
77
|
+
opts.push(`max: ${Math.min(...maxs)}`);
|
|
54
78
|
const base = opts.length > 0 ? `fc.integer({ ${opts.join(', ')} })` : 'fc.integer()';
|
|
55
79
|
return hasMultipleOf(schema) ? `${base}.filter((n) => n % ${schema.multipleOf} === 0)` : base;
|
|
56
80
|
};
|
|
@@ -68,9 +92,24 @@ const numberExpr = (schema) => {
|
|
|
68
92
|
const base = `fc.double({ ${opts.join(', ')} })`;
|
|
69
93
|
return hasMultipleOf(schema) ? `${base}.filter((n) => n % ${schema.multipleOf} === 0)` : base;
|
|
70
94
|
};
|
|
71
|
-
/** Builds a `fc.array(...)` / `fc.uniqueArray(...)` expression for an array schema. */
|
|
72
|
-
const arrayExpr = (schema,
|
|
73
|
-
const
|
|
95
|
+
/** Builds a `fc.array(...)` / `fc.uniqueArray(...)` / `fc.tuple(...)` expression for an array schema. */
|
|
96
|
+
const arrayExpr = (schema, ctx) => {
|
|
97
|
+
const raw = schema;
|
|
98
|
+
// A tuple is `prefixItems` (2020-12) or the draft-07 array-form `items`. Both
|
|
99
|
+
// describe one schema per position, so map them to `fc.tuple(...)`. Extra items
|
|
100
|
+
// beyond the prefix are unconstrained (or forbidden by `items: false`); the
|
|
101
|
+
// minimal tuple is schema-valid either way.
|
|
102
|
+
const prefixItems = raw['prefixItems'];
|
|
103
|
+
const tuple = Array.isArray(prefixItems)
|
|
104
|
+
? prefixItems
|
|
105
|
+
: Array.isArray(raw['items'])
|
|
106
|
+
? raw['items']
|
|
107
|
+
: undefined;
|
|
108
|
+
if (tuple) {
|
|
109
|
+
const exprs = tuple.map((item) => arbitraryExpr(item, ctx));
|
|
110
|
+
return `fc.tuple(${exprs.join(', ')})`;
|
|
111
|
+
}
|
|
112
|
+
const items = hasItems(schema) && isSchemaObject(schema.items) ? arbitraryExpr(schema.items, ctx) : 'fc.anything()';
|
|
74
113
|
const opts = [];
|
|
75
114
|
if (hasMinItems(schema))
|
|
76
115
|
opts.push(`minLength: ${schema.minItems}`);
|
|
@@ -79,30 +118,43 @@ const arrayExpr = (schema, suffix) => {
|
|
|
79
118
|
const fn = hasUniqueItems(schema) && schema.uniqueItems === true ? 'fc.uniqueArray' : 'fc.array';
|
|
80
119
|
return opts.length > 0 ? `${fn}(${items}, { ${opts.join(', ')} })` : `${fn}(${items})`;
|
|
81
120
|
};
|
|
82
|
-
/** Builds a `fc.record(...)` expression for an object schema. */
|
|
83
|
-
const objectExpr = (schema,
|
|
84
|
-
|
|
85
|
-
|
|
121
|
+
/** Builds a `fc.record(...)` / `fc.dictionary(...)` expression for an object schema. */
|
|
122
|
+
const objectExpr = (schema, ctx) => {
|
|
123
|
+
// The `additionalProperties` value schema (when it constrains extra keys with a
|
|
124
|
+
// real subschema rather than the boolean true/false form).
|
|
125
|
+
const additional = hasAdditionalProperties(schema) ? schema.additionalProperties : false;
|
|
126
|
+
const additionalArb = isSchemaObject(additional) ? arbitraryExpr(additional, ctx) : undefined;
|
|
127
|
+
if (!hasProperties(schema)) {
|
|
128
|
+
// A map-style object (no declared properties) with a typed `additionalProperties`
|
|
129
|
+
// is a dictionary of that value type; otherwise fall back to a free-form object.
|
|
130
|
+
return additionalArb ? `fc.dictionary(fc.string(), ${additionalArb})` : 'fc.object()';
|
|
131
|
+
}
|
|
86
132
|
const required = new Set(hasRequired(schema) ? schema.required : []);
|
|
87
133
|
const keys = Object.keys(schema.properties);
|
|
88
|
-
const entries = Object.entries(schema.properties).map(([key, propSchema]) => `${JSON.stringify(key)}: ${arbitraryExpr(propSchema,
|
|
134
|
+
const entries = Object.entries(schema.properties).map(([key, propSchema]) => `${JSON.stringify(key)}: ${arbitraryExpr(propSchema, ctx)}`);
|
|
89
135
|
if (entries.length === 0)
|
|
90
136
|
return 'fc.record({})';
|
|
91
137
|
const model = `{ ${entries.join(', ')} }`;
|
|
92
138
|
// fc.record treats all keys as required by default. Only emit requiredKeys
|
|
93
139
|
// when at least one property is optional.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
140
|
+
const record = keys.every((key) => required.has(key))
|
|
141
|
+
? `fc.record(${model})`
|
|
142
|
+
: `fc.record(${model}, { requiredKeys: [${[...required].map((key) => JSON.stringify(key)).join(', ')}] })`;
|
|
143
|
+
// When both declared properties and a typed `additionalProperties` are present,
|
|
144
|
+
// fold in a dictionary of extra keys so the value exercises the open-map part of
|
|
145
|
+
// the schema too. The declared keys win on collision (merged last).
|
|
146
|
+
if (additionalArb) {
|
|
147
|
+
return `fc.tuple(${record}, fc.dictionary(fc.string(), ${additionalArb})).map(([base, extra]) => ({ ...extra, ...base }))`;
|
|
148
|
+
}
|
|
149
|
+
return record;
|
|
98
150
|
};
|
|
99
151
|
/** Builds a `fc.oneof(...)` expression from a list of branch schemas. */
|
|
100
|
-
const oneofExpr = (branches,
|
|
101
|
-
const exprs = branches.map((branch) => arbitraryExpr(branch,
|
|
152
|
+
const oneofExpr = (branches, ctx) => {
|
|
153
|
+
const exprs = branches.map((branch) => arbitraryExpr(branch, ctx));
|
|
102
154
|
return `fc.oneof(${exprs.join(', ')})`;
|
|
103
155
|
};
|
|
104
156
|
/** Builds the fast-check expression for a single (non-union) JSON Schema type. */
|
|
105
|
-
const scalarExpr = (type, schema,
|
|
157
|
+
const scalarExpr = (type, schema, ctx) => {
|
|
106
158
|
switch (type) {
|
|
107
159
|
case 'string':
|
|
108
160
|
return stringExpr(schema);
|
|
@@ -115,23 +167,32 @@ const scalarExpr = (type, schema, suffix) => {
|
|
|
115
167
|
case 'null':
|
|
116
168
|
return 'fc.constant(null)';
|
|
117
169
|
case 'array':
|
|
118
|
-
return arrayExpr(schema,
|
|
170
|
+
return arrayExpr(schema, ctx);
|
|
119
171
|
case 'object':
|
|
120
|
-
return objectExpr(schema,
|
|
172
|
+
return objectExpr(schema, ctx);
|
|
121
173
|
default:
|
|
122
174
|
return 'fc.anything()';
|
|
123
175
|
}
|
|
124
176
|
};
|
|
125
177
|
/**
|
|
126
178
|
* Recursively builds the fast-check arbitrary expression for a schema node.
|
|
127
|
-
* `$ref`s resolve to the referenced file's exported arbitrary;
|
|
128
|
-
*
|
|
179
|
+
* `$ref`s resolve to the referenced file's exported arbitrary; a self-`$ref`
|
|
180
|
+
* resolves to `tie('self')` so recursive schemas tie lazily via `fc.letrec`.
|
|
181
|
+
* Everything else maps to the appropriate `fc.*` combinator.
|
|
129
182
|
*/
|
|
130
|
-
const arbitraryExpr = (schema,
|
|
183
|
+
const arbitraryExpr = (schema, ctx) => {
|
|
131
184
|
if (!isSchemaObject(schema))
|
|
132
185
|
return 'fc.anything()';
|
|
133
|
-
if (hasRef(schema))
|
|
134
|
-
|
|
186
|
+
if (hasRef(schema)) {
|
|
187
|
+
const name = arbitraryName(refToName(schema.$ref, ctx.suffix));
|
|
188
|
+
// A reference back to the type being generated must be tied lazily; an eager
|
|
189
|
+
// identifier would touch a still-uninitialized const (TDZ) at import time.
|
|
190
|
+
if (name === ctx.selfArbName) {
|
|
191
|
+
ctx.usedTie.value = true;
|
|
192
|
+
return `tie(${JSON.stringify(SELF_KEY)})`;
|
|
193
|
+
}
|
|
194
|
+
return name;
|
|
195
|
+
}
|
|
135
196
|
if (hasConst(schema))
|
|
136
197
|
return `fc.constant(${JSON.stringify(schema.const)})`;
|
|
137
198
|
if (hasEnum(schema)) {
|
|
@@ -148,16 +209,21 @@ const arbitraryExpr = (schema, suffix) => {
|
|
|
148
209
|
return 'fc.bigInt()';
|
|
149
210
|
if (primitive)
|
|
150
211
|
return 'fc.anything()';
|
|
212
|
+
// `allOf` must satisfy every branch at once. fast-check has no generic
|
|
213
|
+
// intersection combinator, so flatten the branches into one merged schema
|
|
214
|
+
// (tightest bounds, unioned required, merged properties) and generate from it.
|
|
215
|
+
if (hasAllOf(schema))
|
|
216
|
+
return arbitraryExpr(mergeAllOf(schema), ctx);
|
|
151
217
|
if (hasOneOf(schema))
|
|
152
|
-
return oneofExpr(schema.oneOf,
|
|
218
|
+
return oneofExpr(schema.oneOf, ctx);
|
|
153
219
|
if (hasAnyOf(schema))
|
|
154
|
-
return oneofExpr(schema.anyOf,
|
|
220
|
+
return oneofExpr(schema.anyOf, ctx);
|
|
155
221
|
if (hasType(schema))
|
|
156
|
-
return scalarExpr(schema.type, schema,
|
|
222
|
+
return scalarExpr(schema.type, schema, ctx);
|
|
157
223
|
// Multi-type schemas (`type: ['string', 'null']`) become a oneof over each
|
|
158
224
|
// member type; `hasType` only matches a single string `type`.
|
|
159
225
|
if (Array.isArray(schema.type)) {
|
|
160
|
-
const exprs = schema.type.map((type) => scalarExpr(type, schema,
|
|
226
|
+
const exprs = schema.type.map((type) => scalarExpr(type, schema, ctx));
|
|
161
227
|
return exprs.length === 1 ? exprs[0] : `fc.oneof(${exprs.join(', ')})`;
|
|
162
228
|
}
|
|
163
229
|
return 'fc.anything()';
|
|
@@ -165,6 +231,10 @@ const arbitraryExpr = (schema, suffix) => {
|
|
|
165
231
|
/**
|
|
166
232
|
* Generates a `fast-check` arbitrary that produces schema-valid values.
|
|
167
233
|
*
|
|
234
|
+
* A schema that references itself is wrapped in `fc.letrec` so the recursion is
|
|
235
|
+
* tied lazily — a plain `const NodeArbitrary = fc.record({ next: NodeArbitrary })`
|
|
236
|
+
* would throw a TDZ `ReferenceError` the moment the module is imported.
|
|
237
|
+
*
|
|
168
238
|
* @example
|
|
169
239
|
* ```typescript
|
|
170
240
|
* generateArbitrary({ type: 'object', properties: { name: { type: 'string' } }, required: ['name'] }, 'Info')
|
|
@@ -172,6 +242,11 @@ const arbitraryExpr = (schema, suffix) => {
|
|
|
172
242
|
* ```
|
|
173
243
|
*/
|
|
174
244
|
export const generateArbitrary = (schema, typeName, suffix = '') => {
|
|
175
|
-
const
|
|
176
|
-
|
|
245
|
+
const selfArbName = arbitraryName(typeName);
|
|
246
|
+
const ctx = { suffix, selfArbName, usedTie: { value: false } };
|
|
247
|
+
const expr = arbitraryExpr(schema, ctx);
|
|
248
|
+
if (ctx.usedTie.value) {
|
|
249
|
+
return `export const ${selfArbName}: fc.Arbitrary<${typeName}> = fc.letrec<{ ${SELF_KEY}: ${typeName} }>((tie) => ({\n ${SELF_KEY}: ${expr},\n})).${SELF_KEY}`;
|
|
250
|
+
}
|
|
251
|
+
return `export const ${selfArbName}: fc.Arbitrary<${typeName}> = ${expr}`;
|
|
177
252
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amritk/generate-examples",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "Generate fast-check arbitraries and example values from JSON Schemas.",
|
|
5
5
|
"module": "./dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -41,13 +41,13 @@
|
|
|
41
41
|
},
|
|
42
42
|
"exports": {
|
|
43
43
|
".": {
|
|
44
|
-
"
|
|
45
|
-
"
|
|
44
|
+
"types": "./dist/index.d.ts",
|
|
45
|
+
"default": "./dist/index.js"
|
|
46
46
|
}
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"json-schema-typed": "^8.0.1",
|
|
50
|
-
"@amritk/helpers": "0.10.
|
|
50
|
+
"@amritk/helpers": "0.10.2"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"ajv": "^8.17.1"
|