@let-aurn/swagger-client 0.1.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/README.md +70 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +33 -0
- package/dist/core/generate.d.ts +9 -0
- package/dist/core/generate.js +36 -0
- package/dist/core/load-config.d.ts +9 -0
- package/dist/core/load-config.js +42 -0
- package/dist/core/names.d.ts +30 -0
- package/dist/core/names.js +87 -0
- package/dist/core/read-openapi.d.ts +10 -0
- package/dist/core/read-openapi.js +169 -0
- package/dist/core/schema-to-typescript.d.ts +17 -0
- package/dist/core/schema-to-typescript.js +92 -0
- package/dist/core/types.d.ts +89 -0
- package/dist/core/types.js +1 -0
- package/dist/generators/angular-client.d.ts +9 -0
- package/dist/generators/angular-client.js +210 -0
- package/dist/generators/endpoints.d.ts +11 -0
- package/dist/generators/endpoints.js +142 -0
- package/dist/generators/models.d.ts +8 -0
- package/dist/generators/models.js +18 -0
- package/dist/generators/react-client.d.ts +9 -0
- package/dist/generators/react-client.js +187 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/package.json +52 -0
package/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Swagger Client
|
|
2
|
+
|
|
3
|
+
Generate typed Angular and React clients from OpenAPI JSON files.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npx swagger-client generate
|
|
7
|
+
npx swagger-client generate --config=swagger-client.json
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Config
|
|
11
|
+
|
|
12
|
+
```json
|
|
13
|
+
{
|
|
14
|
+
"client": "angular",
|
|
15
|
+
"swaggers-directory": "path/to/swaggers",
|
|
16
|
+
"endpoint-name-strategy": "operationId",
|
|
17
|
+
"output": {
|
|
18
|
+
"api-models.ts": "src/app/api/generated/api-models.ts",
|
|
19
|
+
"api-endpoints.ts": "src/app/api/generated/api-endpoints.ts",
|
|
20
|
+
"client.ts": "src/app/api/generated/swagger-client.ts"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`client` can be `angular` or `react`.
|
|
26
|
+
|
|
27
|
+
`endpoint-name-strategy` is optional:
|
|
28
|
+
|
|
29
|
+
- `operationId` uses the OpenAPI `operationId` as-is. This is the default.
|
|
30
|
+
- `tagAndOperationId` prefixes endpoint exports with the first OpenAPI tag, unless the `operationId` already starts with that tag.
|
|
31
|
+
|
|
32
|
+
With `tagAndOperationId`, an operation tagged `SomeController` with `operationId: "someEndpoint"` is generated as:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
ApiEndpoints.SomeController_someEndpoint
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Angular
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
bootstrapApplication(AppComponent, {
|
|
42
|
+
providers: [
|
|
43
|
+
provideHttpClient(),
|
|
44
|
+
provideSwaggerClient({
|
|
45
|
+
baseUrl: 'https://api.example.com'
|
|
46
|
+
})
|
|
47
|
+
]
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Custom HTTP implementations are supported:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
provideSwaggerClient({
|
|
55
|
+
baseUrl: 'https://api.example.com',
|
|
56
|
+
httpClient: MySwaggerHttpClient
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## React
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
const client = useSwaggerClient(ApiEndpoints.PostsController_getPost, {
|
|
64
|
+
baseUrl: 'https://api.example.com'
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const post = await client.request({
|
|
68
|
+
pathVariables: { id: 123 }
|
|
69
|
+
});
|
|
70
|
+
```
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { generate } from "../core/generate.js";
|
|
4
|
+
import { loadConfig } from "../core/load-config.js";
|
|
5
|
+
async function main() {
|
|
6
|
+
const args = process.argv.slice(2);
|
|
7
|
+
const command = args[0];
|
|
8
|
+
if (command !== "generate") {
|
|
9
|
+
printHelp();
|
|
10
|
+
process.exit(command ? 1 : 0);
|
|
11
|
+
}
|
|
12
|
+
const configPath = readConfigPath(args) ?? "swagger-client.json";
|
|
13
|
+
const config = await loadConfig(resolve(configPath));
|
|
14
|
+
await generate(config);
|
|
15
|
+
console.log("Swagger client generated.");
|
|
16
|
+
}
|
|
17
|
+
function readConfigPath(args) {
|
|
18
|
+
const inline = args.find((arg) => arg.startsWith("--config="));
|
|
19
|
+
if (inline) {
|
|
20
|
+
return inline.slice("--config=".length);
|
|
21
|
+
}
|
|
22
|
+
const index = args.indexOf("--config");
|
|
23
|
+
return index === -1 ? undefined : args[index + 1];
|
|
24
|
+
}
|
|
25
|
+
function printHelp() {
|
|
26
|
+
console.log(`Usage:
|
|
27
|
+
swagger-client generate
|
|
28
|
+
swagger-client generate --config=swagger-client.json`);
|
|
29
|
+
}
|
|
30
|
+
main().catch((error) => {
|
|
31
|
+
console.error(error instanceof Error ? error.message : error);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { SwaggerClientConfig } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Generates TypeScript model, endpoint, and client files from a swagger-client configuration.
|
|
4
|
+
*
|
|
5
|
+
* @param config The resolved swagger-client generation configuration.
|
|
6
|
+
* @returns A promise that resolves when all generated files have been written.
|
|
7
|
+
* @throws Error when OpenAPI files cannot be read or generated files cannot be written.
|
|
8
|
+
*/
|
|
9
|
+
export declare function generate(config: SwaggerClientConfig): Promise<void>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, relative } from "node:path";
|
|
3
|
+
import { readOpenApiProject } from "./read-openapi.js";
|
|
4
|
+
import { emitModels } from "../generators/models.js";
|
|
5
|
+
import { emitEndpoints } from "../generators/endpoints.js";
|
|
6
|
+
import { emitAngularClient } from "../generators/angular-client.js";
|
|
7
|
+
import { emitReactClient } from "../generators/react-client.js";
|
|
8
|
+
/**
|
|
9
|
+
* Generates TypeScript model, endpoint, and client files from a swagger-client configuration.
|
|
10
|
+
*
|
|
11
|
+
* @param config The resolved swagger-client generation configuration.
|
|
12
|
+
* @returns A promise that resolves when all generated files have been written.
|
|
13
|
+
* @throws Error when OpenAPI files cannot be read or generated files cannot be written.
|
|
14
|
+
*/
|
|
15
|
+
export async function generate(config) {
|
|
16
|
+
const project = await readOpenApiProject(config["swaggers-directory"], config["endpoint-name-strategy"]);
|
|
17
|
+
const modelsPath = config.output["api-models.ts"];
|
|
18
|
+
const endpointsPath = config.output["api-endpoints.ts"];
|
|
19
|
+
const clientPath = config.output["client.ts"];
|
|
20
|
+
await writeGeneratedFile(modelsPath, emitModels(project));
|
|
21
|
+
await writeGeneratedFile(endpointsPath, emitEndpoints(project, {
|
|
22
|
+
modelsImportPath: toTypeScriptImportPath(endpointsPath, modelsPath)
|
|
23
|
+
}));
|
|
24
|
+
await writeGeneratedFile(clientPath, config.client === "angular"
|
|
25
|
+
? emitAngularClient({ endpointsImportPath: toTypeScriptImportPath(clientPath, endpointsPath) })
|
|
26
|
+
: emitReactClient({ endpointsImportPath: toTypeScriptImportPath(clientPath, endpointsPath) }));
|
|
27
|
+
}
|
|
28
|
+
async function writeGeneratedFile(path, content) {
|
|
29
|
+
await mkdir(dirname(path), { recursive: true });
|
|
30
|
+
await writeFile(path, `${content.trimEnd()}\n`, "utf8");
|
|
31
|
+
}
|
|
32
|
+
function toTypeScriptImportPath(fromFile, toFile) {
|
|
33
|
+
const withoutExtension = relative(dirname(fromFile), toFile).replace(/\.ts$/, "");
|
|
34
|
+
const normalized = withoutExtension.split("\\").join("/");
|
|
35
|
+
return normalized.startsWith(".") ? normalized : `./${normalized}`;
|
|
36
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { SwaggerClientConfig } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Loads and validates a swagger-client configuration file.
|
|
4
|
+
*
|
|
5
|
+
* @param configPath The path to the JSON configuration file.
|
|
6
|
+
* @returns The resolved swagger-client configuration.
|
|
7
|
+
* @throws Error when the configuration file is missing, invalid JSON, or structurally invalid.
|
|
8
|
+
*/
|
|
9
|
+
export declare function loadConfig(configPath: string): Promise<SwaggerClientConfig>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* Loads and validates a swagger-client configuration file.
|
|
5
|
+
*
|
|
6
|
+
* @param configPath The path to the JSON configuration file.
|
|
7
|
+
* @returns The resolved swagger-client configuration.
|
|
8
|
+
* @throws Error when the configuration file is missing, invalid JSON, or structurally invalid.
|
|
9
|
+
*/
|
|
10
|
+
export async function loadConfig(configPath) {
|
|
11
|
+
const absolutePath = resolve(configPath);
|
|
12
|
+
const raw = await readFile(absolutePath, "utf8");
|
|
13
|
+
const config = JSON.parse(raw);
|
|
14
|
+
validateConfig(config, absolutePath);
|
|
15
|
+
return {
|
|
16
|
+
...config,
|
|
17
|
+
"swaggers-directory": resolve(config["swaggers-directory"]),
|
|
18
|
+
output: {
|
|
19
|
+
"api-models.ts": resolve(config.output["api-models.ts"]),
|
|
20
|
+
"api-endpoints.ts": resolve(config.output["api-endpoints.ts"]),
|
|
21
|
+
"client.ts": resolve(config.output["client.ts"])
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function validateConfig(config, configPath) {
|
|
26
|
+
if (config.client !== "angular" && config.client !== "react") {
|
|
27
|
+
throw new Error(`${configPath}: "client" must be "angular" or "react".`);
|
|
28
|
+
}
|
|
29
|
+
if (!config["swaggers-directory"]) {
|
|
30
|
+
throw new Error(`${configPath}: "swaggers-directory" is required.`);
|
|
31
|
+
}
|
|
32
|
+
if (config["endpoint-name-strategy"] !== undefined &&
|
|
33
|
+
config["endpoint-name-strategy"] !== "operationId" &&
|
|
34
|
+
config["endpoint-name-strategy"] !== "tagAndOperationId") {
|
|
35
|
+
throw new Error(`${configPath}: "endpoint-name-strategy" must be "operationId" or "tagAndOperationId".`);
|
|
36
|
+
}
|
|
37
|
+
for (const key of ["api-models.ts", "api-endpoints.ts", "client.ts"]) {
|
|
38
|
+
if (!config.output?.[key]) {
|
|
39
|
+
throw new Error(`${configPath}: output["${key}"] is required.`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts a value to a valid PascalCase TypeScript identifier.
|
|
3
|
+
*
|
|
4
|
+
* @param value The value to convert.
|
|
5
|
+
* @returns A PascalCase identifier.
|
|
6
|
+
*/
|
|
7
|
+
export declare function toPascalCase(value: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* Ensures a value is a valid TypeScript identifier.
|
|
10
|
+
*
|
|
11
|
+
* @param value The value to sanitize.
|
|
12
|
+
* @param fallback The prefix to use when the value cannot start an identifier.
|
|
13
|
+
* @returns A valid TypeScript identifier.
|
|
14
|
+
*/
|
|
15
|
+
export declare function ensureIdentifier(value: string, fallback: string): string;
|
|
16
|
+
/**
|
|
17
|
+
* Returns a name that is unique within a mutable set of used names.
|
|
18
|
+
*
|
|
19
|
+
* @param baseName The preferred name.
|
|
20
|
+
* @param usedNames The set of names already reserved.
|
|
21
|
+
* @returns The unique name that was added to the set.
|
|
22
|
+
*/
|
|
23
|
+
export declare function uniqueName(baseName: string, usedNames: Set<string>): string;
|
|
24
|
+
/**
|
|
25
|
+
* Serializes a string as a TypeScript string literal.
|
|
26
|
+
*
|
|
27
|
+
* @param value The string value to quote.
|
|
28
|
+
* @returns A quoted string literal.
|
|
29
|
+
*/
|
|
30
|
+
export declare function quote(value: string): string;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
const RESERVED_WORDS = new Set([
|
|
2
|
+
"break",
|
|
3
|
+
"case",
|
|
4
|
+
"catch",
|
|
5
|
+
"class",
|
|
6
|
+
"const",
|
|
7
|
+
"continue",
|
|
8
|
+
"debugger",
|
|
9
|
+
"default",
|
|
10
|
+
"delete",
|
|
11
|
+
"do",
|
|
12
|
+
"else",
|
|
13
|
+
"export",
|
|
14
|
+
"extends",
|
|
15
|
+
"finally",
|
|
16
|
+
"for",
|
|
17
|
+
"function",
|
|
18
|
+
"if",
|
|
19
|
+
"import",
|
|
20
|
+
"in",
|
|
21
|
+
"instanceof",
|
|
22
|
+
"new",
|
|
23
|
+
"return",
|
|
24
|
+
"super",
|
|
25
|
+
"switch",
|
|
26
|
+
"this",
|
|
27
|
+
"throw",
|
|
28
|
+
"try",
|
|
29
|
+
"typeof",
|
|
30
|
+
"var",
|
|
31
|
+
"void",
|
|
32
|
+
"while",
|
|
33
|
+
"with",
|
|
34
|
+
"yield"
|
|
35
|
+
]);
|
|
36
|
+
/**
|
|
37
|
+
* Converts a value to a valid PascalCase TypeScript identifier.
|
|
38
|
+
*
|
|
39
|
+
* @param value The value to convert.
|
|
40
|
+
* @returns A PascalCase identifier.
|
|
41
|
+
*/
|
|
42
|
+
export function toPascalCase(value) {
|
|
43
|
+
const words = value
|
|
44
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
45
|
+
.split(/[^A-Za-z0-9]+/)
|
|
46
|
+
.filter(Boolean);
|
|
47
|
+
const name = words.map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
|
|
48
|
+
return ensureIdentifier(name || "GeneratedType", "GeneratedType");
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Ensures a value is a valid TypeScript identifier.
|
|
52
|
+
*
|
|
53
|
+
* @param value The value to sanitize.
|
|
54
|
+
* @param fallback The prefix to use when the value cannot start an identifier.
|
|
55
|
+
* @returns A valid TypeScript identifier.
|
|
56
|
+
*/
|
|
57
|
+
export function ensureIdentifier(value, fallback) {
|
|
58
|
+
const cleaned = value.replace(/[^A-Za-z0-9_$]/g, "");
|
|
59
|
+
const withStart = /^[A-Za-z_$]/.test(cleaned) ? cleaned : `${fallback}${cleaned}`;
|
|
60
|
+
return RESERVED_WORDS.has(withStart) ? `${withStart}Value` : withStart;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Returns a name that is unique within a mutable set of used names.
|
|
64
|
+
*
|
|
65
|
+
* @param baseName The preferred name.
|
|
66
|
+
* @param usedNames The set of names already reserved.
|
|
67
|
+
* @returns The unique name that was added to the set.
|
|
68
|
+
*/
|
|
69
|
+
export function uniqueName(baseName, usedNames) {
|
|
70
|
+
let name = baseName;
|
|
71
|
+
let index = 2;
|
|
72
|
+
while (usedNames.has(name)) {
|
|
73
|
+
name = `${baseName}${index}`;
|
|
74
|
+
index += 1;
|
|
75
|
+
}
|
|
76
|
+
usedNames.add(name);
|
|
77
|
+
return name;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Serializes a string as a TypeScript string literal.
|
|
81
|
+
*
|
|
82
|
+
* @param value The string value to quote.
|
|
83
|
+
* @returns A quoted string literal.
|
|
84
|
+
*/
|
|
85
|
+
export function quote(value) {
|
|
86
|
+
return JSON.stringify(value);
|
|
87
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { EndpointNameStrategy, ProjectModel } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Reads OpenAPI JSON files from a directory and converts them to the generator project model.
|
|
4
|
+
*
|
|
5
|
+
* @param swaggerDirectory The directory containing one or more OpenAPI JSON files.
|
|
6
|
+
* @param endpointNameStrategy The strategy used to name generated endpoint constants.
|
|
7
|
+
* @returns The normalized schemas and endpoints discovered in the OpenAPI files.
|
|
8
|
+
* @throws Error when a JSON file cannot be read or parsed.
|
|
9
|
+
*/
|
|
10
|
+
export declare function readOpenApiProject(swaggerDirectory: string, endpointNameStrategy?: EndpointNameStrategy): Promise<ProjectModel>;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { join, relative } from "node:path";
|
|
3
|
+
import { schemaToType } from "./schema-to-typescript.js";
|
|
4
|
+
import { ensureIdentifier, toPascalCase, uniqueName } from "./names.js";
|
|
5
|
+
const HTTP_METHODS = new Set(["get", "post", "put", "patch", "delete", "head", "options"]);
|
|
6
|
+
/**
|
|
7
|
+
* Reads OpenAPI JSON files from a directory and converts them to the generator project model.
|
|
8
|
+
*
|
|
9
|
+
* @param swaggerDirectory The directory containing one or more OpenAPI JSON files.
|
|
10
|
+
* @param endpointNameStrategy The strategy used to name generated endpoint constants.
|
|
11
|
+
* @returns The normalized schemas and endpoints discovered in the OpenAPI files.
|
|
12
|
+
* @throws Error when a JSON file cannot be read or parsed.
|
|
13
|
+
*/
|
|
14
|
+
export async function readOpenApiProject(swaggerDirectory, endpointNameStrategy = "operationId") {
|
|
15
|
+
const files = await findJsonFiles(swaggerDirectory);
|
|
16
|
+
const schemas = [];
|
|
17
|
+
const endpoints = [];
|
|
18
|
+
const usedSchemaNames = new Set();
|
|
19
|
+
const usedEndpointNames = new Set();
|
|
20
|
+
for (const file of files) {
|
|
21
|
+
const document = JSON.parse(await readFile(file, "utf8"));
|
|
22
|
+
const sourceFile = relative(swaggerDirectory, file);
|
|
23
|
+
const schemaNameMap = collectSchemas(document, schemas, usedSchemaNames);
|
|
24
|
+
endpoints.push(...collectEndpoints(document, sourceFile, schemaNameMap, usedEndpointNames, endpointNameStrategy));
|
|
25
|
+
}
|
|
26
|
+
return { schemas, endpoints };
|
|
27
|
+
}
|
|
28
|
+
async function findJsonFiles(directory) {
|
|
29
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
30
|
+
const files = [];
|
|
31
|
+
for (const entry of entries) {
|
|
32
|
+
const path = join(directory, entry.name);
|
|
33
|
+
if (entry.isDirectory()) {
|
|
34
|
+
files.push(...await findJsonFiles(path));
|
|
35
|
+
}
|
|
36
|
+
else if (entry.isFile() && entry.name.endsWith(".json")) {
|
|
37
|
+
files.push(path);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return files.sort();
|
|
41
|
+
}
|
|
42
|
+
function collectSchemas(document, schemas, usedSchemaNames) {
|
|
43
|
+
const schemaNameMap = new Map();
|
|
44
|
+
const sourceSchemas = document.components?.schemas ?? document.definitions ?? {};
|
|
45
|
+
for (const rawName of Object.keys(sourceSchemas)) {
|
|
46
|
+
const generatedName = uniqueName(toPascalCase(rawName), usedSchemaNames);
|
|
47
|
+
schemaNameMap.set(rawName, generatedName);
|
|
48
|
+
}
|
|
49
|
+
for (const [rawName, schema] of Object.entries(sourceSchemas)) {
|
|
50
|
+
const generatedName = schemaNameMap.get(rawName);
|
|
51
|
+
if (!generatedName) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
schemas.push({
|
|
55
|
+
name: generatedName,
|
|
56
|
+
schema: rewriteRefs(schema, schemaNameMap)
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return schemaNameMap;
|
|
60
|
+
}
|
|
61
|
+
function collectEndpoints(document, sourceFile, schemaNameMap, usedEndpointNames, endpointNameStrategy) {
|
|
62
|
+
const endpoints = [];
|
|
63
|
+
for (const [path, pathItem] of Object.entries(document.paths ?? {})) {
|
|
64
|
+
if (!isRecord(pathItem)) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
for (const [method, operation] of Object.entries(pathItem)) {
|
|
68
|
+
if (!HTTP_METHODS.has(method) || !isOperation(operation)) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const operationId = operation.operationId ?? fallbackOperationId(method, path);
|
|
72
|
+
const endpointName = uniqueName(endpointNameForOperation(operation, operationId, endpointNameStrategy), usedEndpointNames);
|
|
73
|
+
const parameters = operation.parameters ?? [];
|
|
74
|
+
endpoints.push({
|
|
75
|
+
name: endpointName,
|
|
76
|
+
method: method.toUpperCase(),
|
|
77
|
+
path,
|
|
78
|
+
operationId,
|
|
79
|
+
sourceFile,
|
|
80
|
+
pathVariables: variablesFor(parameters, "path", schemaNameMap),
|
|
81
|
+
queryVariables: variablesFor(parameters, "query", schemaNameMap),
|
|
82
|
+
headerVariables: variablesFor(parameters, "header", schemaNameMap),
|
|
83
|
+
bodyType: requestBodyType(operation, schemaNameMap),
|
|
84
|
+
responseType: responseType(operation.responses, schemaNameMap)
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return endpoints;
|
|
89
|
+
}
|
|
90
|
+
function endpointNameForOperation(operation, operationId, strategy) {
|
|
91
|
+
const operationName = ensureIdentifier(operationId, "endpoint");
|
|
92
|
+
if (strategy !== "tagAndOperationId") {
|
|
93
|
+
return operationName;
|
|
94
|
+
}
|
|
95
|
+
const tag = operation.tags?.[0];
|
|
96
|
+
if (!tag) {
|
|
97
|
+
return operationName;
|
|
98
|
+
}
|
|
99
|
+
const tagName = ensureIdentifier(toPascalCase(tag), "endpoint");
|
|
100
|
+
if (operationName === tagName || operationName.startsWith(`${tagName}_`)) {
|
|
101
|
+
return operationName;
|
|
102
|
+
}
|
|
103
|
+
return `${tagName}_${operationName}`;
|
|
104
|
+
}
|
|
105
|
+
function variablesFor(parameters, location, schemaNameMap) {
|
|
106
|
+
return parameters
|
|
107
|
+
.filter((parameter) => parameter.in === location)
|
|
108
|
+
.map((parameter) => ({
|
|
109
|
+
name: parameter.name,
|
|
110
|
+
type: schemaToType(rewriteRefs(parameter.schema ?? { type: "string" }, schemaNameMap)),
|
|
111
|
+
required: parameter.required === true || location === "path"
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
function requestBodyType(operation, schemaNameMap) {
|
|
115
|
+
const schema = preferredContentSchema(operation.requestBody?.content);
|
|
116
|
+
return schema ? schemaToType(rewriteRefs(schema, schemaNameMap)) : null;
|
|
117
|
+
}
|
|
118
|
+
function responseType(responses, schemaNameMap) {
|
|
119
|
+
const response = responses?.["200"] ?? responses?.["201"] ?? responses?.["204"] ?? firstSuccessResponse(responses);
|
|
120
|
+
const schema = preferredContentSchema(response?.content);
|
|
121
|
+
if (!schema || response === responses?.["204"]) {
|
|
122
|
+
return "void";
|
|
123
|
+
}
|
|
124
|
+
return schemaToType(rewriteRefs(schema, schemaNameMap));
|
|
125
|
+
}
|
|
126
|
+
function firstSuccessResponse(responses) {
|
|
127
|
+
return Object.entries(responses ?? {}).find(([status]) => status.startsWith("2"))?.[1];
|
|
128
|
+
}
|
|
129
|
+
function preferredContentSchema(content) {
|
|
130
|
+
return content?.["application/json"]?.schema ?? Object.values(content ?? {})[0]?.schema;
|
|
131
|
+
}
|
|
132
|
+
function rewriteRefs(schema, schemaNameMap) {
|
|
133
|
+
if (!schema) {
|
|
134
|
+
return schema;
|
|
135
|
+
}
|
|
136
|
+
const clone = structuredClone(schema);
|
|
137
|
+
rewriteRefsInPlace(clone, schemaNameMap);
|
|
138
|
+
return clone;
|
|
139
|
+
}
|
|
140
|
+
function rewriteRefsInPlace(schema, schemaNameMap) {
|
|
141
|
+
if (schema.$ref) {
|
|
142
|
+
const rawName = schema.$ref.split("/").at(-1);
|
|
143
|
+
const mappedName = rawName ? schemaNameMap.get(rawName) : undefined;
|
|
144
|
+
if (mappedName) {
|
|
145
|
+
schema.$ref = `#/components/schemas/${mappedName}`;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
for (const child of [
|
|
149
|
+
...Object.values(schema.properties ?? {}),
|
|
150
|
+
schema.items,
|
|
151
|
+
...(schema.allOf ?? []),
|
|
152
|
+
...(schema.oneOf ?? []),
|
|
153
|
+
...(schema.anyOf ?? []),
|
|
154
|
+
typeof schema.additionalProperties === "object" ? schema.additionalProperties : undefined
|
|
155
|
+
]) {
|
|
156
|
+
if (child) {
|
|
157
|
+
rewriteRefsInPlace(child, schemaNameMap);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function fallbackOperationId(method, path) {
|
|
162
|
+
return `${method}_${path.replace(/[{}]/g, "").replace(/[^A-Za-z0-9]+/g, "_")}`;
|
|
163
|
+
}
|
|
164
|
+
function isOperation(value) {
|
|
165
|
+
return isRecord(value);
|
|
166
|
+
}
|
|
167
|
+
function isRecord(value) {
|
|
168
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
169
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { OpenApiSchema } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Converts an OpenAPI schema to a TypeScript type expression.
|
|
4
|
+
*
|
|
5
|
+
* @param schema The OpenAPI schema to convert.
|
|
6
|
+
* @param fallback The type to use when the schema cannot be inferred.
|
|
7
|
+
* @returns A TypeScript type expression.
|
|
8
|
+
*/
|
|
9
|
+
export declare function schemaToType(schema: OpenApiSchema | undefined, fallback?: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* Emits a TypeScript declaration for a named OpenAPI schema.
|
|
12
|
+
*
|
|
13
|
+
* @param name The TypeScript declaration name.
|
|
14
|
+
* @param schema The OpenAPI schema to emit.
|
|
15
|
+
* @returns A TypeScript interface or type alias declaration.
|
|
16
|
+
*/
|
|
17
|
+
export declare function emitSchemaInterface(name: string, schema: OpenApiSchema): string;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { quote, toPascalCase } from "./names.js";
|
|
2
|
+
/**
|
|
3
|
+
* Converts an OpenAPI schema to a TypeScript type expression.
|
|
4
|
+
*
|
|
5
|
+
* @param schema The OpenAPI schema to convert.
|
|
6
|
+
* @param fallback The type to use when the schema cannot be inferred.
|
|
7
|
+
* @returns A TypeScript type expression.
|
|
8
|
+
*/
|
|
9
|
+
export function schemaToType(schema, fallback = "unknown") {
|
|
10
|
+
if (!schema) {
|
|
11
|
+
return fallback;
|
|
12
|
+
}
|
|
13
|
+
let type = schemaToNonNullableType(schema, fallback);
|
|
14
|
+
if (schema.nullable) {
|
|
15
|
+
type = `${type} | null`;
|
|
16
|
+
}
|
|
17
|
+
return type;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Emits a TypeScript declaration for a named OpenAPI schema.
|
|
21
|
+
*
|
|
22
|
+
* @param name The TypeScript declaration name.
|
|
23
|
+
* @param schema The OpenAPI schema to emit.
|
|
24
|
+
* @returns A TypeScript interface or type alias declaration.
|
|
25
|
+
*/
|
|
26
|
+
export function emitSchemaInterface(name, schema) {
|
|
27
|
+
if (schema.type === "object" || schema.properties || schema.additionalProperties) {
|
|
28
|
+
return emitObjectInterface(name, schema);
|
|
29
|
+
}
|
|
30
|
+
return `export type ${name} = ${schemaToType(schema)};`;
|
|
31
|
+
}
|
|
32
|
+
function emitObjectInterface(name, schema) {
|
|
33
|
+
const required = new Set(schema.required ?? []);
|
|
34
|
+
const lines = [`export interface ${name} {`];
|
|
35
|
+
for (const [propertyName, propertySchema] of Object.entries(schema.properties ?? {})) {
|
|
36
|
+
const optional = required.has(propertyName) ? "" : "?";
|
|
37
|
+
lines.push(` ${propertyKey(propertyName)}${optional}: ${schemaToType(propertySchema)};`);
|
|
38
|
+
}
|
|
39
|
+
if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
|
|
40
|
+
lines.push(` [key: string]: ${schemaToType(schema.additionalProperties)};`);
|
|
41
|
+
}
|
|
42
|
+
else if (schema.additionalProperties === true) {
|
|
43
|
+
lines.push(" [key: string]: unknown;");
|
|
44
|
+
}
|
|
45
|
+
lines.push("}");
|
|
46
|
+
return lines.join("\n");
|
|
47
|
+
}
|
|
48
|
+
function schemaToNonNullableType(schema, fallback) {
|
|
49
|
+
if (schema.$ref) {
|
|
50
|
+
return toPascalCase(schema.$ref.split("/").at(-1) ?? fallback);
|
|
51
|
+
}
|
|
52
|
+
if (schema.enum) {
|
|
53
|
+
return schema.enum.map((value) => JSON.stringify(value)).join(" | ") || fallback;
|
|
54
|
+
}
|
|
55
|
+
if (schema.allOf) {
|
|
56
|
+
return schema.allOf.map((item) => schemaToType(item, fallback)).join(" & ") || fallback;
|
|
57
|
+
}
|
|
58
|
+
if (schema.oneOf || schema.anyOf) {
|
|
59
|
+
return (schema.oneOf ?? schema.anyOf ?? []).map((item) => schemaToType(item, fallback)).join(" | ") || fallback;
|
|
60
|
+
}
|
|
61
|
+
if (schema.type === "array") {
|
|
62
|
+
return `${schemaToType(schema.items, "unknown")}[]`;
|
|
63
|
+
}
|
|
64
|
+
if (schema.type === "object" || schema.properties) {
|
|
65
|
+
const entries = Object.entries(schema.properties ?? {});
|
|
66
|
+
if (entries.length === 0) {
|
|
67
|
+
if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
|
|
68
|
+
return `Record<string, ${schemaToType(schema.additionalProperties)}>`;
|
|
69
|
+
}
|
|
70
|
+
return "Record<string, unknown>";
|
|
71
|
+
}
|
|
72
|
+
const required = new Set(schema.required ?? []);
|
|
73
|
+
const props = entries.map(([name, propSchema]) => {
|
|
74
|
+
const optional = required.has(name) ? "" : "?";
|
|
75
|
+
return `${propertyKey(name)}${optional}: ${schemaToType(propSchema)}`;
|
|
76
|
+
});
|
|
77
|
+
return `{ ${props.join("; ")} }`;
|
|
78
|
+
}
|
|
79
|
+
if (schema.type === "integer" || schema.type === "number") {
|
|
80
|
+
return "number";
|
|
81
|
+
}
|
|
82
|
+
if (schema.type === "boolean") {
|
|
83
|
+
return "boolean";
|
|
84
|
+
}
|
|
85
|
+
if (schema.type === "string") {
|
|
86
|
+
return "string";
|
|
87
|
+
}
|
|
88
|
+
return fallback;
|
|
89
|
+
}
|
|
90
|
+
function propertyKey(name) {
|
|
91
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : quote(name);
|
|
92
|
+
}
|