openapi-contract-kit 0.0.1 → 0.0.3
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/CHANGELOG.md +10 -0
- package/README.md +13 -5
- package/dist/bin/openapi-contract-kit.d.ts +2 -0
- package/dist/bin/openapi-contract-kit.js +3 -0
- package/dist/src/cli/formatOutput.d.ts +6 -0
- package/dist/src/cli/formatOutput.js +32 -0
- package/dist/src/generator/config.d.ts +3 -0
- package/dist/src/generator/config.js +72 -0
- package/dist/src/generator/documents.d.ts +11 -0
- package/dist/src/generator/documents.js +92 -0
- package/dist/src/generator/emitEndpoints.d.ts +2 -0
- package/{src/generator/emitEndpoints.mjs → dist/src/generator/emitEndpoints.js} +48 -80
- package/dist/src/generator/emitMakers.d.ts +2 -0
- package/dist/src/generator/emitMakers.js +328 -0
- package/dist/src/generator/emitTypes.d.ts +3 -0
- package/dist/src/generator/emitTypes.js +87 -0
- package/dist/src/generator/generateOpenApiRuntime.d.ts +3 -0
- package/dist/src/generator/generateOpenApiRuntime.js +41 -0
- package/dist/src/generator/model.d.ts +2 -0
- package/dist/src/generator/model.js +278 -0
- package/dist/src/generator/schemaModel.d.ts +9 -0
- package/dist/src/generator/schemaModel.js +408 -0
- package/dist/src/generator/types.d.ts +82 -0
- package/dist/src/generator/writeOutput.d.ts +5 -0
- package/dist/src/generator/writeOutput.js +141 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +1 -0
- package/dist/src/runtime.d.ts +14 -0
- package/dist/src/runtime.js +1 -0
- package/package.json +30 -18
- package/bin/openapi-contract-kit.mjs +0 -5
- package/src/generator/config.mjs +0 -92
- package/src/generator/documents.mjs +0 -119
- package/src/generator/emitMakers.mjs +0 -459
- package/src/generator/emitTypes.mjs +0 -104
- package/src/generator/generateOpenApiRuntime.mjs +0 -48
- package/src/generator/model.mjs +0 -415
- package/src/generator/schemaModel.mjs +0 -497
- package/src/generator/writeOutput.mjs +0 -196
- package/src/index.d.ts +0 -16
- package/src/index.mjs +0 -4
- package/src/runtime.d.ts +0 -14
- /package/{src/runtime.mjs → dist/src/generator/types.js} +0 -0
|
@@ -1,459 +0,0 @@
|
|
|
1
|
-
import { join } from 'node:path';
|
|
2
|
-
|
|
3
|
-
function indent(lines, spaces = 2) {
|
|
4
|
-
const prefix = ' '.repeat(spaces);
|
|
5
|
-
return lines.map((line) => (line.length === 0 ? line : `${prefix}${line}`));
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
function typeExpression(type, value = 'value') {
|
|
9
|
-
switch (type) {
|
|
10
|
-
case 'array':
|
|
11
|
-
return `Array.isArray(${value})`;
|
|
12
|
-
case 'boolean':
|
|
13
|
-
return `typeof ${value} === 'boolean'`;
|
|
14
|
-
case 'integer':
|
|
15
|
-
return `typeof ${value} === 'number' && Number.isFinite(${value}) && Number.isInteger(${value})`;
|
|
16
|
-
case 'null':
|
|
17
|
-
return `${value} === null`;
|
|
18
|
-
case 'number':
|
|
19
|
-
return `typeof ${value} === 'number' && Number.isFinite(${value})`;
|
|
20
|
-
case 'object':
|
|
21
|
-
return `typeof ${value} === 'object' && ${value} !== null && !Array.isArray(${value})`;
|
|
22
|
-
case 'string':
|
|
23
|
-
return `typeof ${value} === 'string'`;
|
|
24
|
-
default:
|
|
25
|
-
throw new Error(`Unsupported normalised schema type "${type}"`);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
class MakerRenderer {
|
|
30
|
-
#counter = 0;
|
|
31
|
-
#functions = [];
|
|
32
|
-
#nodeNames = new WeakMap();
|
|
33
|
-
#nodeSignatures = new Map();
|
|
34
|
-
#referencedSchemas = new Set();
|
|
35
|
-
#schemaNames;
|
|
36
|
-
|
|
37
|
-
constructor(model) {
|
|
38
|
-
this.#schemaNames = new Set(model.schemas.map(({ name }) => name));
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
render(name, schema, runtimeImport) {
|
|
42
|
-
const rootValidator = this.#emitNode(schema);
|
|
43
|
-
const makerImports = [...this.#referencedSchemas]
|
|
44
|
-
.sort()
|
|
45
|
-
.map((reference) => `import { make${reference} } from './${reference}';`)
|
|
46
|
-
.join('\n');
|
|
47
|
-
|
|
48
|
-
return `/**
|
|
49
|
-
* Generated by openapi-contract-kit.
|
|
50
|
-
* Do not edit directly.
|
|
51
|
-
*/
|
|
52
|
-
|
|
53
|
-
import type { Result, ValidationIssue, ValidationPath } from ${JSON.stringify(runtimeImport)};
|
|
54
|
-
import type { ${name} } from '../quickpay-api';
|
|
55
|
-
${makerImports}${makerImports.length > 0 ? '\n' : ''}
|
|
56
|
-
|
|
57
|
-
${this.#functions.join('\n\n')}
|
|
58
|
-
|
|
59
|
-
function validate${name}(
|
|
60
|
-
value: unknown,
|
|
61
|
-
path: ValidationPath,
|
|
62
|
-
errors: ValidationIssue[]
|
|
63
|
-
): value is ${name} {
|
|
64
|
-
return ${rootValidator}(value, path, errors);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function make${name}(input: unknown): Result<${name}> {
|
|
68
|
-
const errors: ValidationIssue[] = [];
|
|
69
|
-
|
|
70
|
-
if (!validate${name}(input, [], errors)) {
|
|
71
|
-
return { ok: false, errors };
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return { ok: true, value: input };
|
|
75
|
-
}
|
|
76
|
-
`;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
#emitNode(schema) {
|
|
80
|
-
const existing = this.#nodeNames.get(schema);
|
|
81
|
-
if (existing !== undefined) {
|
|
82
|
-
return existing;
|
|
83
|
-
}
|
|
84
|
-
const signature = JSON.stringify(schema, (key, value) =>
|
|
85
|
-
key === 'location' ? undefined : value
|
|
86
|
-
);
|
|
87
|
-
const matchingNode = this.#nodeSignatures.get(signature);
|
|
88
|
-
if (matchingNode !== undefined) {
|
|
89
|
-
this.#nodeNames.set(schema, matchingNode);
|
|
90
|
-
return matchingNode;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const functionName = `validateNode${this.#counter}`;
|
|
94
|
-
this.#counter += 1;
|
|
95
|
-
this.#nodeNames.set(schema, functionName);
|
|
96
|
-
this.#nodeSignatures.set(signature, functionName);
|
|
97
|
-
|
|
98
|
-
const childFunctions = {
|
|
99
|
-
additionalProperties:
|
|
100
|
-
schema.additionalProperties !== true &&
|
|
101
|
-
schema.additionalProperties !== false &&
|
|
102
|
-
schema.additionalProperties !== undefined
|
|
103
|
-
? this.#emitNode(schema.additionalProperties)
|
|
104
|
-
: null,
|
|
105
|
-
allOf: (schema.allOf ?? []).map((child) => this.#emitNode(child)),
|
|
106
|
-
anyOf: (schema.anyOf ?? []).map((child) => this.#emitNode(child)),
|
|
107
|
-
items:
|
|
108
|
-
schema.items !== null && schema.items !== undefined
|
|
109
|
-
? this.#emitNode(schema.items)
|
|
110
|
-
: null,
|
|
111
|
-
oneOf: (schema.oneOf ?? []).map((child) => this.#emitNode(child)),
|
|
112
|
-
properties: (schema.properties ?? []).map((property) => ({
|
|
113
|
-
...property,
|
|
114
|
-
functionName: this.#emitNode(property.schema),
|
|
115
|
-
})),
|
|
116
|
-
reference:
|
|
117
|
-
schema.reference !== null && schema.reference !== undefined
|
|
118
|
-
? this.#registerReference(schema.reference)
|
|
119
|
-
: null,
|
|
120
|
-
};
|
|
121
|
-
const body = this.#renderNodeBody(schema, childFunctions);
|
|
122
|
-
this.#functions.push(
|
|
123
|
-
`function ${functionName}(\n value: unknown,\n path: ValidationPath,\n errors: ValidationIssue[]\n): boolean {\n${indent(body).join('\n')}\n}`
|
|
124
|
-
);
|
|
125
|
-
return functionName;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
#registerReference(reference) {
|
|
129
|
-
if (!this.#schemaNames.has(reference)) {
|
|
130
|
-
throw new Error(`Unknown schema model reference "${reference}"`);
|
|
131
|
-
}
|
|
132
|
-
this.#referencedSchemas.add(reference);
|
|
133
|
-
return reference;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
#renderNodeBody(schema, children) {
|
|
137
|
-
const lines = ['const errorCount = errors.length;'];
|
|
138
|
-
|
|
139
|
-
if (schema.booleanSchema === false) {
|
|
140
|
-
lines.push(
|
|
141
|
-
`errors.push({ path, keyword: 'falseSchema', message: 'Value is not allowed' });`,
|
|
142
|
-
'return false;'
|
|
143
|
-
);
|
|
144
|
-
return lines;
|
|
145
|
-
}
|
|
146
|
-
if (schema.booleanSchema === true) {
|
|
147
|
-
lines.push('return true;');
|
|
148
|
-
return lines;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
if (children.reference !== null) {
|
|
152
|
-
lines.push(
|
|
153
|
-
`const referenceResult = make${children.reference}(value);`,
|
|
154
|
-
`if (!referenceResult.ok) {`,
|
|
155
|
-
...indent([
|
|
156
|
-
`for (const issue of referenceResult.errors) {`,
|
|
157
|
-
...indent([
|
|
158
|
-
`errors.push({ ...issue, path: [...path, ...issue.path] });`,
|
|
159
|
-
]),
|
|
160
|
-
`}`,
|
|
161
|
-
]),
|
|
162
|
-
`}`
|
|
163
|
-
);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
if (schema.types !== null) {
|
|
167
|
-
const expected = schema.types
|
|
168
|
-
.map((type) => typeExpression(type))
|
|
169
|
-
.join(' || ');
|
|
170
|
-
lines.push(
|
|
171
|
-
`if (!(${expected})) {`,
|
|
172
|
-
...indent([
|
|
173
|
-
`errors.push({ path, keyword: 'type', message: ${JSON.stringify(
|
|
174
|
-
`Expected ${schema.types.join(' or ')}`
|
|
175
|
-
)} });`,
|
|
176
|
-
'return false;',
|
|
177
|
-
]),
|
|
178
|
-
'}'
|
|
179
|
-
);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
if (schema.constValue !== undefined) {
|
|
183
|
-
lines.push(
|
|
184
|
-
`if (!Object.is(value, ${JSON.stringify(schema.constValue)})) {`,
|
|
185
|
-
...indent([
|
|
186
|
-
`errors.push({ path, keyword: 'const', message: 'Expected the documented constant value' });`,
|
|
187
|
-
]),
|
|
188
|
-
'}'
|
|
189
|
-
);
|
|
190
|
-
}
|
|
191
|
-
if (schema.enumValues !== null) {
|
|
192
|
-
const values = schema.enumValues
|
|
193
|
-
.map((value) => JSON.stringify(value))
|
|
194
|
-
.join(', ');
|
|
195
|
-
lines.push(
|
|
196
|
-
`if (![${values}].some((candidate) => Object.is(candidate, value))) {`,
|
|
197
|
-
...indent([
|
|
198
|
-
`errors.push({ path, keyword: 'enum', message: 'Expected a documented enum value' });`,
|
|
199
|
-
]),
|
|
200
|
-
'}'
|
|
201
|
-
);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
for (const child of children.allOf) {
|
|
205
|
-
lines.push(`${child}(value, path, errors);`);
|
|
206
|
-
}
|
|
207
|
-
this.#renderUnionConstraint(lines, 'anyOf', children.anyOf, false);
|
|
208
|
-
this.#renderUnionConstraint(lines, 'oneOf', children.oneOf, true);
|
|
209
|
-
|
|
210
|
-
this.#renderStringConstraints(lines, schema);
|
|
211
|
-
this.#renderNumberConstraints(lines, schema);
|
|
212
|
-
this.#renderArrayConstraints(lines, children.items);
|
|
213
|
-
this.#renderObjectConstraints(lines, schema, children);
|
|
214
|
-
|
|
215
|
-
lines.push('return errors.length === errorCount;');
|
|
216
|
-
return lines;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
#renderUnionConstraint(lines, keyword, branches, isExclusive) {
|
|
220
|
-
if (branches.length === 0) {
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
const variable = `${keyword}Matches`;
|
|
225
|
-
lines.push(`let ${variable} = 0;`);
|
|
226
|
-
for (const [index, branch] of branches.entries()) {
|
|
227
|
-
const errorsName = `${keyword}Errors${index}`;
|
|
228
|
-
lines.push(
|
|
229
|
-
`const ${errorsName}: ValidationIssue[] = [];`,
|
|
230
|
-
`if (${branch}(value, path, ${errorsName})) {`,
|
|
231
|
-
...indent([`${variable} += 1;`]),
|
|
232
|
-
'}'
|
|
233
|
-
);
|
|
234
|
-
}
|
|
235
|
-
const invalidExpression = isExclusive
|
|
236
|
-
? `${variable} !== 1`
|
|
237
|
-
: `${variable} === 0`;
|
|
238
|
-
const expectation = isExclusive ? 'exactly one' : 'at least one';
|
|
239
|
-
lines.push(
|
|
240
|
-
`if (${invalidExpression}) {`,
|
|
241
|
-
...indent([
|
|
242
|
-
`errors.push({ path, keyword: '${keyword}', message: 'Expected ${expectation} matching branch' });`,
|
|
243
|
-
]),
|
|
244
|
-
'}'
|
|
245
|
-
);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
#renderStringConstraints(lines, schema) {
|
|
249
|
-
if (
|
|
250
|
-
schema.format === null &&
|
|
251
|
-
schema.maxLength === null &&
|
|
252
|
-
schema.minLength === null &&
|
|
253
|
-
schema.pattern === null
|
|
254
|
-
) {
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
lines.push(`if (typeof value === 'string') {`);
|
|
259
|
-
const checks = [];
|
|
260
|
-
if (schema.minLength !== null) {
|
|
261
|
-
checks.push(
|
|
262
|
-
`if (Array.from(value).length < ${schema.minLength}) {`,
|
|
263
|
-
...indent([
|
|
264
|
-
`errors.push({ path, keyword: 'minLength', message: 'String is shorter than ${schema.minLength} characters' });`,
|
|
265
|
-
]),
|
|
266
|
-
'}'
|
|
267
|
-
);
|
|
268
|
-
}
|
|
269
|
-
if (schema.maxLength !== null) {
|
|
270
|
-
checks.push(
|
|
271
|
-
`if (Array.from(value).length > ${schema.maxLength}) {`,
|
|
272
|
-
...indent([
|
|
273
|
-
`errors.push({ path, keyword: 'maxLength', message: 'String is longer than ${schema.maxLength} characters' });`,
|
|
274
|
-
]),
|
|
275
|
-
'}'
|
|
276
|
-
);
|
|
277
|
-
}
|
|
278
|
-
if (schema.pattern !== null) {
|
|
279
|
-
checks.push(
|
|
280
|
-
`if (!new RegExp(${JSON.stringify(schema.pattern)}, 'u').test(value)) {`,
|
|
281
|
-
...indent([
|
|
282
|
-
`errors.push({ path, keyword: 'pattern', message: 'String does not match the documented pattern' });`,
|
|
283
|
-
]),
|
|
284
|
-
'}'
|
|
285
|
-
);
|
|
286
|
-
}
|
|
287
|
-
if (schema.format === 'email') {
|
|
288
|
-
checks.push(
|
|
289
|
-
`if (!/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/u.test(value)) {`,
|
|
290
|
-
...indent([
|
|
291
|
-
`errors.push({ path, keyword: 'format', message: 'Expected email format' });`,
|
|
292
|
-
]),
|
|
293
|
-
'}'
|
|
294
|
-
);
|
|
295
|
-
}
|
|
296
|
-
lines.push(...indent(checks), '}');
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
#renderNumberConstraints(lines, schema) {
|
|
300
|
-
if (
|
|
301
|
-
schema.exclusiveMaximum === null &&
|
|
302
|
-
schema.exclusiveMinimum === null &&
|
|
303
|
-
schema.maximum === null &&
|
|
304
|
-
schema.minimum === null
|
|
305
|
-
) {
|
|
306
|
-
return;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
lines.push(`if (typeof value === 'number' && Number.isFinite(value)) {`);
|
|
310
|
-
const checks = [];
|
|
311
|
-
const addCheck = (expression, keyword, message) => {
|
|
312
|
-
checks.push(
|
|
313
|
-
`if (${expression}) {`,
|
|
314
|
-
...indent([
|
|
315
|
-
`errors.push({ path, keyword: '${keyword}', message: ${JSON.stringify(message)} });`,
|
|
316
|
-
]),
|
|
317
|
-
'}'
|
|
318
|
-
);
|
|
319
|
-
};
|
|
320
|
-
if (schema.minimum !== null) {
|
|
321
|
-
addCheck(
|
|
322
|
-
`value < ${schema.minimum}`,
|
|
323
|
-
'minimum',
|
|
324
|
-
`Number must be at least ${schema.minimum}`
|
|
325
|
-
);
|
|
326
|
-
}
|
|
327
|
-
if (schema.maximum !== null) {
|
|
328
|
-
addCheck(
|
|
329
|
-
`value > ${schema.maximum}`,
|
|
330
|
-
'maximum',
|
|
331
|
-
`Number must be at most ${schema.maximum}`
|
|
332
|
-
);
|
|
333
|
-
}
|
|
334
|
-
if (schema.exclusiveMinimum !== null) {
|
|
335
|
-
addCheck(
|
|
336
|
-
`value <= ${schema.exclusiveMinimum}`,
|
|
337
|
-
'exclusiveMinimum',
|
|
338
|
-
`Number must be greater than ${schema.exclusiveMinimum}`
|
|
339
|
-
);
|
|
340
|
-
}
|
|
341
|
-
if (schema.exclusiveMaximum !== null) {
|
|
342
|
-
addCheck(
|
|
343
|
-
`value >= ${schema.exclusiveMaximum}`,
|
|
344
|
-
'exclusiveMaximum',
|
|
345
|
-
`Number must be less than ${schema.exclusiveMaximum}`
|
|
346
|
-
);
|
|
347
|
-
}
|
|
348
|
-
lines.push(...indent(checks), '}');
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
#renderArrayConstraints(lines, itemValidator) {
|
|
352
|
-
if (itemValidator === null) {
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
lines.push(
|
|
357
|
-
'if (Array.isArray(value)) {',
|
|
358
|
-
...indent([
|
|
359
|
-
'for (const [index, item] of value.entries()) {',
|
|
360
|
-
...indent([`${itemValidator}(item, [...path, index], errors);`]),
|
|
361
|
-
'}',
|
|
362
|
-
]),
|
|
363
|
-
'}'
|
|
364
|
-
);
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
#renderObjectConstraints(lines, schema, children) {
|
|
368
|
-
if (
|
|
369
|
-
children.properties.length === 0 &&
|
|
370
|
-
children.additionalProperties === null &&
|
|
371
|
-
schema.additionalProperties !== false
|
|
372
|
-
) {
|
|
373
|
-
return;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
lines.push(
|
|
377
|
-
`if (typeof value === 'object' && value !== null && !Array.isArray(value)) {`
|
|
378
|
-
);
|
|
379
|
-
const checks = [];
|
|
380
|
-
for (const property of children.properties) {
|
|
381
|
-
const hasProperty = `Object.prototype.hasOwnProperty.call(value, ${JSON.stringify(
|
|
382
|
-
property.name
|
|
383
|
-
)})`;
|
|
384
|
-
const propertyPath = `[...path, ${JSON.stringify(property.name)}]`;
|
|
385
|
-
|
|
386
|
-
if (property.required) {
|
|
387
|
-
checks.push(
|
|
388
|
-
`if (!${hasProperty}) {`,
|
|
389
|
-
...indent([
|
|
390
|
-
`errors.push({ path: ${propertyPath}, keyword: 'required', message: 'Required property is missing' });`,
|
|
391
|
-
]),
|
|
392
|
-
'} else {',
|
|
393
|
-
...indent([
|
|
394
|
-
`${property.functionName}(Reflect.get(value, ${JSON.stringify(
|
|
395
|
-
property.name
|
|
396
|
-
)}), ${propertyPath}, errors);`,
|
|
397
|
-
]),
|
|
398
|
-
'}'
|
|
399
|
-
);
|
|
400
|
-
} else {
|
|
401
|
-
checks.push(
|
|
402
|
-
`if (${hasProperty}) {`,
|
|
403
|
-
...indent([
|
|
404
|
-
`${property.functionName}(Reflect.get(value, ${JSON.stringify(
|
|
405
|
-
property.name
|
|
406
|
-
)}), ${propertyPath}, errors);`,
|
|
407
|
-
]),
|
|
408
|
-
'}'
|
|
409
|
-
);
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
if (
|
|
414
|
-
schema.additionalProperties === false ||
|
|
415
|
-
children.additionalProperties !== null
|
|
416
|
-
) {
|
|
417
|
-
const propertyNames = children.properties.map(({ name }) => name);
|
|
418
|
-
checks.push('for (const key of Object.keys(value)) {');
|
|
419
|
-
const isAdditionalProperty =
|
|
420
|
-
propertyNames.length === 0
|
|
421
|
-
? 'true'
|
|
422
|
-
: `![${propertyNames
|
|
423
|
-
.map((name) => JSON.stringify(name))
|
|
424
|
-
.join(', ')}].includes(key)`;
|
|
425
|
-
const additionalChecks = [`if (${isAdditionalProperty}) {`];
|
|
426
|
-
if (schema.additionalProperties === false) {
|
|
427
|
-
additionalChecks.push(
|
|
428
|
-
...indent([
|
|
429
|
-
`errors.push({ path: [...path, key], keyword: 'additionalProperties', message: 'Unknown property is not allowed' });`,
|
|
430
|
-
])
|
|
431
|
-
);
|
|
432
|
-
} else {
|
|
433
|
-
additionalChecks.push(
|
|
434
|
-
...indent([
|
|
435
|
-
`${children.additionalProperties}(Reflect.get(value, key), [...path, key], errors);`,
|
|
436
|
-
])
|
|
437
|
-
);
|
|
438
|
-
}
|
|
439
|
-
additionalChecks.push('}');
|
|
440
|
-
checks.push(...indent(additionalChecks), '}');
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
lines.push(...indent(checks), '}');
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
export function emitSchemaMakers(model, { outDir, runtimeImport }) {
|
|
448
|
-
const files = new Map();
|
|
449
|
-
|
|
450
|
-
for (const { name, schema } of model.schemas) {
|
|
451
|
-
const renderer = new MakerRenderer(model);
|
|
452
|
-
files.set(
|
|
453
|
-
`schemas/${name}.ts`,
|
|
454
|
-
renderer.render(name, schema, runtimeImport)
|
|
455
|
-
);
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
return files;
|
|
459
|
-
}
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
function renderLiteral(value) {
|
|
2
|
-
return JSON.stringify(value);
|
|
3
|
-
}
|
|
4
|
-
|
|
5
|
-
function parenthesise(type) {
|
|
6
|
-
return type.includes(' | ') || type.includes(' & ') ? `(${type})` : type;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
function renderObjectType(schema) {
|
|
10
|
-
if (schema.properties.length === 0) {
|
|
11
|
-
if (
|
|
12
|
-
schema.additionalProperties !== true &&
|
|
13
|
-
schema.additionalProperties !== false
|
|
14
|
-
) {
|
|
15
|
-
return `Record<string, ${renderSchemaType(schema.additionalProperties)}>`;
|
|
16
|
-
}
|
|
17
|
-
return 'Record<string, unknown>';
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const properties = schema.properties.map((property) => {
|
|
21
|
-
const optional = property.required ? '' : '?';
|
|
22
|
-
return ` ${JSON.stringify(property.name)}${optional}: ${renderSchemaType(property.schema)};`;
|
|
23
|
-
});
|
|
24
|
-
return `{\n${properties.join('\n')}\n}`;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function renderBasicType(type, schema) {
|
|
28
|
-
switch (type) {
|
|
29
|
-
case 'array': {
|
|
30
|
-
const itemType =
|
|
31
|
-
schema.items === null ? 'unknown' : renderSchemaType(schema.items);
|
|
32
|
-
return `Array<${itemType}>`;
|
|
33
|
-
}
|
|
34
|
-
case 'boolean':
|
|
35
|
-
return 'boolean';
|
|
36
|
-
case 'integer':
|
|
37
|
-
case 'number':
|
|
38
|
-
return 'number';
|
|
39
|
-
case 'null':
|
|
40
|
-
return 'null';
|
|
41
|
-
case 'object':
|
|
42
|
-
return renderObjectType(schema);
|
|
43
|
-
case 'string':
|
|
44
|
-
return 'string';
|
|
45
|
-
default:
|
|
46
|
-
throw new Error(`Unsupported normalised schema type "${type}"`);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function renderSchemaType(schema) {
|
|
51
|
-
if (schema.booleanSchema === false) {
|
|
52
|
-
return 'never';
|
|
53
|
-
}
|
|
54
|
-
if (schema.booleanSchema === true) {
|
|
55
|
-
return 'unknown';
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const intersections = [];
|
|
59
|
-
|
|
60
|
-
if (schema.reference !== null) {
|
|
61
|
-
intersections.push(schema.reference);
|
|
62
|
-
}
|
|
63
|
-
if (schema.constValue !== undefined) {
|
|
64
|
-
intersections.push(renderLiteral(schema.constValue));
|
|
65
|
-
} else if (schema.enumValues !== null) {
|
|
66
|
-
intersections.push(schema.enumValues.map(renderLiteral).join(' | '));
|
|
67
|
-
} else if (schema.types !== null) {
|
|
68
|
-
intersections.push(
|
|
69
|
-
schema.types.map((type) => renderBasicType(type, schema)).join(' | ')
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
for (const child of schema.allOf) {
|
|
74
|
-
intersections.push(renderSchemaType(child));
|
|
75
|
-
}
|
|
76
|
-
if (schema.oneOf.length > 0) {
|
|
77
|
-
intersections.push(schema.oneOf.map(renderSchemaType).join(' | '));
|
|
78
|
-
}
|
|
79
|
-
if (schema.anyOf.length > 0) {
|
|
80
|
-
intersections.push(schema.anyOf.map(renderSchemaType).join(' | '));
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const meaningful = intersections.filter((type) => type !== 'unknown');
|
|
84
|
-
if (meaningful.length === 0) {
|
|
85
|
-
return 'unknown';
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return meaningful.map(parenthesise).join(' & ');
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export function emitRootTypes(model) {
|
|
92
|
-
const declarations = model.schemas.flatMap(({ name, schema }) => [
|
|
93
|
-
`export type ${name} = ${renderSchemaType(schema)};`,
|
|
94
|
-
`export type ShapeOf${name} = ${name};`,
|
|
95
|
-
]);
|
|
96
|
-
|
|
97
|
-
return `/**
|
|
98
|
-
* Generated by openapi-contract-kit.
|
|
99
|
-
* Do not edit directly.
|
|
100
|
-
*/
|
|
101
|
-
|
|
102
|
-
${declarations.join('\n\n')}
|
|
103
|
-
`;
|
|
104
|
-
}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { fileURLToPath } from 'node:url';
|
|
2
|
-
|
|
3
|
-
import { resolveGeneratorConfig } from './config.mjs';
|
|
4
|
-
import { emitEndpointModules } from './emitEndpoints.mjs';
|
|
5
|
-
import { emitSchemaMakers } from './emitMakers.mjs';
|
|
6
|
-
import { emitRootTypes } from './emitTypes.mjs';
|
|
7
|
-
import { buildOpenApiModel } from './model.mjs';
|
|
8
|
-
import { writeGeneratedOutput } from './writeOutput.mjs';
|
|
9
|
-
|
|
10
|
-
export async function generateOpenApiRuntime({
|
|
11
|
-
argv = [],
|
|
12
|
-
cwd = process.cwd(),
|
|
13
|
-
} = {}) {
|
|
14
|
-
const config = await resolveGeneratorConfig({ argv, cwd });
|
|
15
|
-
const model = await buildOpenApiModel(config.specPath);
|
|
16
|
-
const files = new Map([
|
|
17
|
-
['quickpay-api.ts', emitRootTypes(model)],
|
|
18
|
-
...emitSchemaMakers(model, config),
|
|
19
|
-
...emitEndpointModules(model, config),
|
|
20
|
-
]);
|
|
21
|
-
|
|
22
|
-
await writeGeneratedOutput(config.outDir, files, {
|
|
23
|
-
protectedPaths: [config.configPath, config.specPath],
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
return {
|
|
27
|
-
operationCount: model.operations.length,
|
|
28
|
-
operationNames: model.operations.map(({ name }) => name),
|
|
29
|
-
schemaCount: model.schemas.length,
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export async function main(argv = process.argv.slice(2)) {
|
|
34
|
-
try {
|
|
35
|
-
const result = await generateOpenApiRuntime({ argv });
|
|
36
|
-
process.stdout.write(
|
|
37
|
-
`Generated ${result.schemaCount} schemas and ${result.operationCount} endpoints.\n`
|
|
38
|
-
);
|
|
39
|
-
} catch (error) {
|
|
40
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
41
|
-
process.stderr.write(`OpenAPI generation failed: ${message}\n`);
|
|
42
|
-
process.exitCode = 1;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
47
|
-
await main();
|
|
48
|
-
}
|