openapi-contract-kit 0.0.1
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 +9 -0
- package/LICENSE +21 -0
- package/README.md +88 -0
- package/bin/openapi-contract-kit.mjs +5 -0
- package/package.json +58 -0
- package/src/generator/config.mjs +92 -0
- package/src/generator/documents.mjs +119 -0
- package/src/generator/emitEndpoints.mjs +182 -0
- package/src/generator/emitMakers.mjs +459 -0
- package/src/generator/emitTypes.mjs +104 -0
- package/src/generator/generateOpenApiRuntime.mjs +48 -0
- package/src/generator/model.mjs +415 -0
- package/src/generator/schemaModel.mjs +497 -0
- package/src/generator/writeOutput.mjs +196 -0
- package/src/index.d.ts +16 -0
- package/src/index.mjs +4 -0
- package/src/runtime.d.ts +14 -0
- package/src/runtime.mjs +1 -0
package/CHANGELOG.md
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ibeanu Hillary (ibeanuhillary@gmail.com)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# openapi-contract-kit
|
|
2
|
+
|
|
3
|
+
JSON-first OpenAPI 3.1 contract generation and runtime validation for TypeScript applications.
|
|
4
|
+
|
|
5
|
+
`openapi-contract-kit` reads a JSON OpenAPI document and generates:
|
|
6
|
+
|
|
7
|
+
- TypeScript schema types
|
|
8
|
+
- `ShapeOf*` compatibility aliases
|
|
9
|
+
- Runtime schema makers
|
|
10
|
+
- Request validators
|
|
11
|
+
- Status-aware response validators
|
|
12
|
+
- Standalone endpoint modules
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install --save-dev openapi-contract-kit
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Quick start
|
|
21
|
+
|
|
22
|
+
Create `openapi.config.json`:
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"specPath": "openapi.json",
|
|
27
|
+
"outDir": "src/api/generated",
|
|
28
|
+
"runtimeImport": "openapi-contract-kit/runtime"
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Add a generation script:
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"scripts": {
|
|
37
|
+
"openapi:generate": "openapi-contract-kit"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Generate contracts:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm run openapi:generate
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Generated validation
|
|
49
|
+
|
|
50
|
+
Schema makers return a discriminated result:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
const result = makeLoginRequest(input);
|
|
54
|
+
|
|
55
|
+
if (!result.ok) {
|
|
56
|
+
console.error(result.errors);
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Validation errors contain a path, keyword, and message. Successful validation returns the original input without mutating or cloning it.
|
|
61
|
+
|
|
62
|
+
## Supported input
|
|
63
|
+
|
|
64
|
+
The generator supports JSON OpenAPI 3.1 documents with objects, primitive types, nullable values, arrays, enums, constants, unions, local `$ref` references, additional-property rules, string and numeric constraints, and email format validation.
|
|
65
|
+
|
|
66
|
+
## Current limitations
|
|
67
|
+
|
|
68
|
+
- YAML documents are not supported.
|
|
69
|
+
- Remote `$ref` references are not supported.
|
|
70
|
+
- Unsupported schema keywords fail generation.
|
|
71
|
+
- Path and operation parameters are not currently generated.
|
|
72
|
+
- Generated output is application-specific and should not be published with this package.
|
|
73
|
+
|
|
74
|
+
## Development
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
npm install
|
|
78
|
+
npm test
|
|
79
|
+
npm run pack:check
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Versioning
|
|
83
|
+
|
|
84
|
+
The package follows semantic versioning. Changes to generated contracts, public exports, CLI flags, or validation behavior are documented in `CHANGELOG.md`.
|
|
85
|
+
|
|
86
|
+
## License
|
|
87
|
+
|
|
88
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "openapi-contract-kit",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Generate type-safe TypeScript contracts and runtime validators from JSON OpenAPI 3.1 documents.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=20.11"
|
|
9
|
+
},
|
|
10
|
+
"main": "./src/index.mjs",
|
|
11
|
+
"types": "./src/index.d.ts",
|
|
12
|
+
"bin": {
|
|
13
|
+
"openapi-contract-kit": "./bin/openapi-contract-kit.mjs"
|
|
14
|
+
},
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./src/index.d.ts",
|
|
18
|
+
"import": "./src/index.mjs"
|
|
19
|
+
},
|
|
20
|
+
"./runtime": {
|
|
21
|
+
"types": "./src/runtime.d.ts",
|
|
22
|
+
"import": "./src/runtime.mjs"
|
|
23
|
+
},
|
|
24
|
+
"./package.json": "./package.json"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"bin",
|
|
28
|
+
"src",
|
|
29
|
+
"CHANGELOG.md",
|
|
30
|
+
"LICENSE",
|
|
31
|
+
"README.md"
|
|
32
|
+
],
|
|
33
|
+
"scripts": {
|
|
34
|
+
"test": "node --test test/*.test.mjs",
|
|
35
|
+
"pack:check": "npm pack --dry-run"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"typescript": "^5.9.3"
|
|
39
|
+
},
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "https://github.com/Clinsmann/openapi-contract-kit.git"
|
|
43
|
+
},
|
|
44
|
+
"bugs": {
|
|
45
|
+
"url": "https://github.com/Clinsmann/openapi-contract-kit/issues"
|
|
46
|
+
},
|
|
47
|
+
"homepage": "https://github.com/Clinsmann/openapi-contract-kit#readme",
|
|
48
|
+
"keywords": [
|
|
49
|
+
"openapi",
|
|
50
|
+
"openapi3",
|
|
51
|
+
"typescript",
|
|
52
|
+
"code-generation",
|
|
53
|
+
"runtime-validation",
|
|
54
|
+
"api-contracts",
|
|
55
|
+
"json-schema",
|
|
56
|
+
"cli"
|
|
57
|
+
]
|
|
58
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, isAbsolute, resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
const CONFIG_KEYS = new Set(['specPath', 'outDir', 'runtimeImport']);
|
|
5
|
+
const FLAG_NAMES = new Map([
|
|
6
|
+
['--config', 'configPath'],
|
|
7
|
+
['--spec', 'specPath'],
|
|
8
|
+
['--out', 'outDir'],
|
|
9
|
+
['--runtime-import', 'runtimeImport'],
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
function isRecord(value) {
|
|
13
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function parseArguments(argv, cwd) {
|
|
17
|
+
const values = {};
|
|
18
|
+
|
|
19
|
+
for (let index = 0; index < argv.length; index += 2) {
|
|
20
|
+
const flag = argv[index];
|
|
21
|
+
const key = FLAG_NAMES.get(flag);
|
|
22
|
+
const value = argv[index + 1];
|
|
23
|
+
|
|
24
|
+
if (key === undefined) {
|
|
25
|
+
throw new Error(`Unknown argument "${flag}"`);
|
|
26
|
+
}
|
|
27
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
28
|
+
throw new Error(`Missing value for "${flag}"`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
values[key] = isAbsolute(value) ? value : resolve(cwd, value);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return values;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function requirePath(config, key, configPath) {
|
|
38
|
+
const value = config[key];
|
|
39
|
+
|
|
40
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
41
|
+
throw new Error(`Config "${configPath}" requires a non-empty "${key}"`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function resolveGeneratorConfig({
|
|
48
|
+
argv = [],
|
|
49
|
+
cwd = process.cwd(),
|
|
50
|
+
} = {}) {
|
|
51
|
+
const cli = parseArguments(argv, cwd);
|
|
52
|
+
const configPath = cli.configPath ?? resolve(cwd, 'openapi.config.json');
|
|
53
|
+
let config;
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
config = JSON.parse(await readFile(configPath, 'utf8'));
|
|
57
|
+
} catch (error) {
|
|
58
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
59
|
+
throw new Error(
|
|
60
|
+
`Unable to read OpenAPI config "${configPath}": ${message}`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!isRecord(config)) {
|
|
65
|
+
throw new Error(`OpenAPI config "${configPath}" must contain an object`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (const key of Object.keys(config)) {
|
|
69
|
+
if (!CONFIG_KEYS.has(key)) {
|
|
70
|
+
throw new Error(`Unknown OpenAPI config key "${key}"`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const configDirectory = dirname(configPath);
|
|
75
|
+
const resolveConfigPath = (key) => {
|
|
76
|
+
const value = requirePath(config, key, configPath);
|
|
77
|
+
return isAbsolute(value) ? value : resolve(configDirectory, value);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const runtimeImport =
|
|
81
|
+
cli.runtimeImport ?? requirePath(config, 'runtimeImport', configPath);
|
|
82
|
+
if (runtimeImport.includes('\\') || runtimeImport.startsWith('.')) {
|
|
83
|
+
throw new Error(`Config "${configPath}" requires a package runtimeImport`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
configPath,
|
|
88
|
+
specPath: cli.specPath ?? resolveConfigPath('specPath'),
|
|
89
|
+
outDir: cli.outDir ?? resolveConfigPath('outDir'),
|
|
90
|
+
runtimeImport,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, extname, resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
export function isRecord(value) {
|
|
5
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function escapePointerSegment(segment) {
|
|
9
|
+
return segment.replaceAll('~', '~0').replaceAll('/', '~1');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function pointerChild(pointer, segment) {
|
|
13
|
+
return `${pointer}/${escapePointerSegment(String(segment))}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function locationOf(documentPath, pointer) {
|
|
17
|
+
return `${documentPath}#${pointer}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function requireRecord(value, location, label) {
|
|
21
|
+
if (!isRecord(value)) {
|
|
22
|
+
throw new Error(`${label} at ${location} must be an object`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class DocumentStore {
|
|
29
|
+
#documents = new Map();
|
|
30
|
+
|
|
31
|
+
async load(documentPath) {
|
|
32
|
+
const absolutePath = resolve(documentPath);
|
|
33
|
+
const existing = this.#documents.get(absolutePath);
|
|
34
|
+
|
|
35
|
+
if (existing !== undefined) {
|
|
36
|
+
return existing;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const pending = this.#read(absolutePath);
|
|
40
|
+
this.#documents.set(absolutePath, pending);
|
|
41
|
+
return pending;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async #read(documentPath) {
|
|
45
|
+
let source;
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
source = await readFile(documentPath, 'utf8');
|
|
49
|
+
} catch (error) {
|
|
50
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
51
|
+
throw new Error(
|
|
52
|
+
`Unable to read OpenAPI document "${documentPath}": ${message}`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const extension = extname(documentPath).toLowerCase();
|
|
58
|
+
if (extension !== '.json') {
|
|
59
|
+
throw new Error('Only JSON OpenAPI documents are supported');
|
|
60
|
+
}
|
|
61
|
+
const value = JSON.parse(source);
|
|
62
|
+
return requireRecord(value, documentPath, 'OpenAPI document');
|
|
63
|
+
} catch (error) {
|
|
64
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
65
|
+
throw new Error(
|
|
66
|
+
`Unable to parse OpenAPI document "${documentPath}": ${message}`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async resolveReference(reference, fromDocumentPath) {
|
|
72
|
+
if (typeof reference !== 'string' || reference.length === 0) {
|
|
73
|
+
throw new Error(`Invalid $ref at ${fromDocumentPath}`);
|
|
74
|
+
}
|
|
75
|
+
if (/^[A-Za-z][A-Za-z\d+.-]*:/u.test(reference)) {
|
|
76
|
+
throw new Error(`Remote $ref "${reference}" is unsupported`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const hashIndex = reference.indexOf('#');
|
|
80
|
+
const filePart =
|
|
81
|
+
hashIndex === -1 ? reference : reference.slice(0, hashIndex);
|
|
82
|
+
const fragment = hashIndex === -1 ? '' : reference.slice(hashIndex + 1);
|
|
83
|
+
const documentPath =
|
|
84
|
+
filePart.length === 0
|
|
85
|
+
? fromDocumentPath
|
|
86
|
+
: resolve(dirname(fromDocumentPath), decodeURIComponent(filePart));
|
|
87
|
+
const document = await this.load(documentPath);
|
|
88
|
+
const decodedFragment = decodeURIComponent(fragment);
|
|
89
|
+
|
|
90
|
+
if (decodedFragment.length > 0 && !decodedFragment.startsWith('/')) {
|
|
91
|
+
throw new Error(`Anchor $ref "${reference}" is unsupported`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let value = document;
|
|
95
|
+
for (const encodedSegment of decodedFragment.split('/').slice(1)) {
|
|
96
|
+
const segment = encodedSegment
|
|
97
|
+
.replaceAll('~1', '/')
|
|
98
|
+
.replaceAll('~0', '~');
|
|
99
|
+
if (!isRecord(value) && !Array.isArray(value)) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
`Unresolved $ref "${reference}" from ${fromDocumentPath}`
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
if (!Object.prototype.hasOwnProperty.call(value, segment)) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`Unresolved $ref "${reference}" from ${fromDocumentPath}`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
value = Reflect.get(value, segment);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
canonicalKey: locationOf(documentPath, decodedFragment),
|
|
114
|
+
documentPath,
|
|
115
|
+
pointer: decodedFragment,
|
|
116
|
+
value,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
|
|
3
|
+
function renderResponseType(responses) {
|
|
4
|
+
if (responses.length === 0) {
|
|
5
|
+
return 'never';
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
return responses
|
|
9
|
+
.map(({ schemaName, status }) => {
|
|
10
|
+
const bodyType = schemaName ?? 'null';
|
|
11
|
+
return `{ readonly status: ${status}; readonly body: ${bodyType} }`;
|
|
12
|
+
})
|
|
13
|
+
.join(' | ');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function renderStatusCases(responses) {
|
|
17
|
+
const lines = [];
|
|
18
|
+
|
|
19
|
+
for (const response of responses) {
|
|
20
|
+
lines.push(` case ${response.status}: {`);
|
|
21
|
+
if (response.schemaName === null) {
|
|
22
|
+
lines.push(
|
|
23
|
+
` if (parts.body !== null) {`,
|
|
24
|
+
` return failure(['body'], 'nullBody', 'Expected a null response body');`,
|
|
25
|
+
` }`,
|
|
26
|
+
` return { ok: true, value: { status: ${response.status}, body: null } };`
|
|
27
|
+
);
|
|
28
|
+
} else {
|
|
29
|
+
lines.push(
|
|
30
|
+
` const result = make${response.schemaName}(parts.body);`,
|
|
31
|
+
` if (!result.ok) {`,
|
|
32
|
+
` return result;`,
|
|
33
|
+
` }`,
|
|
34
|
+
` return { ok: true, value: { status: ${response.status}, body: result.value } };`
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
lines.push(' }');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
lines.push(
|
|
41
|
+
` default:`,
|
|
42
|
+
` return failure(['status'], 'status', 'Undocumented response status');`
|
|
43
|
+
);
|
|
44
|
+
return lines.join('\n');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function emitEndpoint(operation, { outDir, runtimeImport }) {
|
|
48
|
+
const successResponses = operation.responses.filter(
|
|
49
|
+
(response) => response.isSuccess
|
|
50
|
+
);
|
|
51
|
+
const errorResponses = operation.responses.filter(
|
|
52
|
+
(response) => !response.isSuccess
|
|
53
|
+
);
|
|
54
|
+
const schemaNames = new Set();
|
|
55
|
+
|
|
56
|
+
if (operation.request.schemaName !== null) {
|
|
57
|
+
schemaNames.add(operation.request.schemaName);
|
|
58
|
+
}
|
|
59
|
+
for (const response of operation.responses) {
|
|
60
|
+
if (response.schemaName !== null) {
|
|
61
|
+
schemaNames.add(response.schemaName);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const sortedSchemaNames = [...schemaNames].sort();
|
|
66
|
+
const typeImport =
|
|
67
|
+
sortedSchemaNames.length === 0
|
|
68
|
+
? ''
|
|
69
|
+
: `import type { ${sortedSchemaNames.join(', ')} } from '../quickpay-api';\n`;
|
|
70
|
+
const makerImports = sortedSchemaNames
|
|
71
|
+
.map((name) => `import { make${name} } from '../schemas/${name}';`)
|
|
72
|
+
.join('\n');
|
|
73
|
+
const requestType = operation.request.schemaName ?? 'null';
|
|
74
|
+
const requestMaker =
|
|
75
|
+
operation.request.schemaName === null
|
|
76
|
+
? `export function makeRequest(input: unknown): Result<null> {
|
|
77
|
+
if (input !== null) {
|
|
78
|
+
return failure([], 'nullBody', 'Expected null');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { ok: true, value: null };
|
|
82
|
+
}`
|
|
83
|
+
: `export const makeRequest = make${operation.request.schemaName};`;
|
|
84
|
+
|
|
85
|
+
return `/**
|
|
86
|
+
* Generated by openapi-contract-kit.
|
|
87
|
+
* Do not edit directly.
|
|
88
|
+
*/
|
|
89
|
+
|
|
90
|
+
import type { Result, ValidationPath } from ${JSON.stringify(runtimeImport)};
|
|
91
|
+
${typeImport}${makerImports}${makerImports.length > 0 ? '\n' : ''}
|
|
92
|
+
export const URL = ${JSON.stringify(operation.path)};
|
|
93
|
+
export const METHOD = ${JSON.stringify(operation.method)};
|
|
94
|
+
export const OPERATION_ID = ${JSON.stringify(operation.operationId)};
|
|
95
|
+
|
|
96
|
+
export type ShapeOfRequest = ${requestType};
|
|
97
|
+
export type ShapeOfSuccessResponse = ${renderResponseType(successResponses)};
|
|
98
|
+
export type ShapeOfErrorResponse = ${renderResponseType(errorResponses)};
|
|
99
|
+
export type ShapeOfResponse = ShapeOfSuccessResponse | ShapeOfErrorResponse;
|
|
100
|
+
|
|
101
|
+
type ResponseParts = {
|
|
102
|
+
readonly status: number;
|
|
103
|
+
readonly body: unknown;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
function failure<T>(
|
|
107
|
+
path: ValidationPath,
|
|
108
|
+
keyword: string,
|
|
109
|
+
message: string
|
|
110
|
+
): Result<T> {
|
|
111
|
+
return { ok: false, errors: [{ path, keyword, message }] };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function readResponse(input: unknown): ResponseParts | null {
|
|
115
|
+
if (typeof input !== 'object' || input === null || Array.isArray(input)) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
if (
|
|
119
|
+
!Object.prototype.hasOwnProperty.call(input, 'status') ||
|
|
120
|
+
!Object.prototype.hasOwnProperty.call(input, 'body')
|
|
121
|
+
) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const status = Reflect.get(input, 'status');
|
|
126
|
+
if (typeof status !== 'number' || !Number.isInteger(status)) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return { status, body: Reflect.get(input, 'body') };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
${requestMaker}
|
|
134
|
+
|
|
135
|
+
function makeSuccessResponseResult(input: unknown): Result<ShapeOfSuccessResponse> {
|
|
136
|
+
const parts = readResponse(input);
|
|
137
|
+
if (parts === null) {
|
|
138
|
+
return failure([], 'response', 'Expected { status, body }');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
switch (parts.status) {
|
|
142
|
+
${renderStatusCases(successResponses)}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function makeErrorResponseResult(input: unknown): Result<ShapeOfErrorResponse> {
|
|
147
|
+
const parts = readResponse(input);
|
|
148
|
+
if (parts === null) {
|
|
149
|
+
return failure([], 'response', 'Expected { status, body }');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
switch (parts.status) {
|
|
153
|
+
${renderStatusCases(errorResponses)}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function makeAnyResponse(input: unknown): Result<ShapeOfResponse> {
|
|
158
|
+
const parts = readResponse(input);
|
|
159
|
+
if (parts === null) {
|
|
160
|
+
return failure([], 'response', 'Expected { status, body }');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return parts.status >= 200 && parts.status <= 299
|
|
164
|
+
? makeSuccessResponseResult(input)
|
|
165
|
+
: makeErrorResponseResult(input);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export const makeResponse = Object.assign(makeAnyResponse, {
|
|
169
|
+
success: makeSuccessResponseResult,
|
|
170
|
+
error: makeErrorResponseResult,
|
|
171
|
+
});
|
|
172
|
+
`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function emitEndpointModules(model, config) {
|
|
176
|
+
return new Map(
|
|
177
|
+
model.operations.map((operation) => [
|
|
178
|
+
`endpoints/${operation.name}.ts`,
|
|
179
|
+
emitEndpoint(operation, config),
|
|
180
|
+
])
|
|
181
|
+
);
|
|
182
|
+
}
|