openapi-contract-kit 0.0.1 → 0.0.4

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 (43) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +13 -5
  3. package/dist/bin/openapi-contract-kit.d.ts +2 -0
  4. package/dist/bin/openapi-contract-kit.js +3 -0
  5. package/dist/src/cli/formatOutput.d.ts +6 -0
  6. package/dist/src/cli/formatOutput.js +32 -0
  7. package/dist/src/generator/config.d.ts +3 -0
  8. package/dist/src/generator/config.js +72 -0
  9. package/dist/src/generator/documents.d.ts +11 -0
  10. package/dist/src/generator/documents.js +92 -0
  11. package/dist/src/generator/emitEndpoints.d.ts +2 -0
  12. package/{src/generator/emitEndpoints.mjs → dist/src/generator/emitEndpoints.js} +48 -80
  13. package/dist/src/generator/emitMakers.d.ts +2 -0
  14. package/dist/src/generator/emitMakers.js +328 -0
  15. package/dist/src/generator/emitTypes.d.ts +3 -0
  16. package/dist/src/generator/emitTypes.js +87 -0
  17. package/dist/src/generator/generateOpenApiRuntime.d.ts +3 -0
  18. package/dist/src/generator/generateOpenApiRuntime.js +41 -0
  19. package/dist/src/generator/model.d.ts +2 -0
  20. package/dist/src/generator/model.js +278 -0
  21. package/dist/src/generator/schemaModel.d.ts +9 -0
  22. package/dist/src/generator/schemaModel.js +408 -0
  23. package/dist/src/generator/types.d.ts +82 -0
  24. package/dist/src/generator/writeOutput.d.ts +5 -0
  25. package/dist/src/generator/writeOutput.js +141 -0
  26. package/dist/src/index.d.ts +2 -0
  27. package/dist/src/index.js +1 -0
  28. package/dist/src/runtime.d.ts +14 -0
  29. package/dist/src/runtime.js +1 -0
  30. package/package.json +30 -18
  31. package/bin/openapi-contract-kit.mjs +0 -5
  32. package/src/generator/config.mjs +0 -92
  33. package/src/generator/documents.mjs +0 -119
  34. package/src/generator/emitMakers.mjs +0 -459
  35. package/src/generator/emitTypes.mjs +0 -104
  36. package/src/generator/generateOpenApiRuntime.mjs +0 -48
  37. package/src/generator/model.mjs +0 -415
  38. package/src/generator/schemaModel.mjs +0 -497
  39. package/src/generator/writeOutput.mjs +0 -196
  40. package/src/index.d.ts +0 -16
  41. package/src/index.mjs +0 -4
  42. package/src/runtime.d.ts +0 -14
  43. /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
- }